사이트 잠금기능 정보
사이트 잠금기능
첨부파일
본문
홈페이지 점검하거나 할때 사용하면 편?리한 사이트 잠금기능입니다.
화이트리스트에 추가된 IP주소만 접속가능하고 나머지는 접근이 제한됩니다.
역시나 GPT를 열심히 갈아넣었습니다.
admin.menu100.php
맨 밑에 한줄 추가해주세요.
$menu['menu100'][] = array('100999', '사이트 잠금', G5_ADMIN_URL . '/site_lock.php', 'cf_site_lock');
그후 파일 2개를 생성합니다.
/adm/site_lock.php
/extend/site_lock.extend.php
첨부파일도 같이 올려두어서 그대로 적용하셔도 되고, 아래 코드 복사해서 만드셔도 됩니다.
site_lock.php
<?php
// /adm/site_lock.php
$sub_menu = "100999";
include_once('./_common.php');
if (($is_admin ?? '') !== 'super') alert('최고관리자만 접근 가능합니다.');
$g5['title'] = '사이트 잠금';
$cfg_file = G5_DATA_PATH.'/site_lock.json';
// CSRF
function sl_get_token() {
if (function_exists('get_admin_token')) return get_admin_token();
$t = sha1(uniqid('', true));
set_session('sl_admin_token', $t);
return $t;
}
function sl_check_token($tok) {
if (function_exists('check_admin_token')) return check_admin_token();
if (!($tok && $tok === get_session('sl_admin_token'))) alert('유효하지 않은 요청입니다.');
}
if ($_SERVER['REQUEST_METHOD']==='POST') {
sl_check_token($_POST['token'] ?? '');
$enabled = isset($_POST['enabled']) ? (bool)$_POST['enabled'] : false;
$mode = ($_POST['mode'] ?? 'text') === 'html' ? 'html' : 'text';
// 공통
$whitelist_raw = (string)($_POST['whitelist'] ?? '');
$whitelist = array_values(array_filter(array_map('trim', preg_split("/\r\n|\r|\n/", $whitelist_raw ?? ''))));
// 모드별
if ($mode === 'html') {
$html = (string)($_POST['html'] ?? '');
$title = '';
$content = '';
} else {
$title = trim((string)($_POST['title'] ?? '사이트 점검 중입니다'));
$content = (string)($_POST['content'] ?? "더 나은 서비스를 위해 일시적으로 접속이 제한됩니다.\n잠시 후 다시 이용해 주세요.");
$html = '';
}
$cfg = [
'enabled' => $enabled,
'whitelist' => $whitelist,
'mode' => $mode,
'title' => $title,
'content' => $content,
'html' => $html
];
@file_put_contents($cfg_file, json_encode($cfg, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT), LOCK_EX);
goto_url($_SERVER['SCRIPT_NAME'].'?saved=1');
exit;
}
// 로드
$cfg = [
'enabled'=>false, 'whitelist'=>[], 'mode'=>'text',
'title'=>'사이트 점검 중입니다',
'content'=>"더 나은 서비스를 위해 일시적으로 접속이 제한됩니다.\n잠시 후 다시 이용해 주세요.",
'html'=>''
];
if (is_file($cfg_file)) {
$json = @file_get_contents($cfg_file);
if ($json !== false) {
$tmp = json_decode($json, true);
if (is_array($tmp)) $cfg = array_merge($cfg, $tmp);
}
}
$whitelist_text = implode("\n", $cfg['whitelist']);
$is_html = ($cfg['mode'] === 'html');
include_once(G5_ADMIN_PATH.'/admin.head.php');
?>
<div class="local_ov01 local_ov">
<h2>사이트 잠금</h2>
</div>
<?php if (isset($_GET['saved'])) { ?>
<div class="local_desc01 local_desc"><p>저장되었습니다.</p></div>
<?php } ?>
<form method="post" action="<?php echo htmlspecialchars($_SERVER['SCRIPT_NAME'], ENT_QUOTES); ?>">
<input type="hidden" name="token" value="<?php echo sl_get_token(); ?>">
<div class="tbl_frm01 tbl_wrap">
<table>
<colgroup><col class="grid_3"><col></colgroup>
<tbody>
<tr>
<th scope="row">잠금 여부</th>
<td>
<label><input type="checkbox" name="enabled" value="1" <?php echo $cfg['enabled']?'checked':''; ?>> 사이트 잠금 활성화</label>
<p class="frm_info">활성화 시, 관리자/로그인/정적파일을 제외한 모든 요청이 503으로 응답됩니다.</p>
</td>
</tr>
<tr>
<th scope="row">접근 허용 IP</th>
<td>
<textarea name="whitelist" rows="6" class="frm_input" style="width:100%;"><?php echo htmlspecialchars($whitelist_text, ENT_QUOTES); ?></textarea>
<p class="frm_info">줄바꿈으로 구분. 현재 접속 IP: <?php echo htmlspecialchars($_SERVER['REMOTE_ADDR'] ?? '', ENT_QUOTES); ?></p>
</td>
</tr>
<tr>
<th scope="row">표시 방식</th>
<td>
<label><input type="radio" name="mode" value="text" <?php echo ($cfg['mode']!=='html')?'checked':''; ?>> 제목/내용(텍스트)</label>
<label><input type="radio" name="mode" value="html" <?php echo ($cfg['mode']==='html')?'checked':''; ?>> 직접 HTML</label>
<p class="frm_info">텍스트 모드는 안전하게 이스케이프되어 출력되며 줄바꿈은 자동으로 반영됩니다.</p>
</td>
</tr>
<!-- 텍스트 모드 -->
<tr class="sl-row sl-text" style="display:<?php echo $is_html?'none':'table-row'; ?>">
<th scope="row">제목</th>
<td>
<input type="text" name="title" class="frm_input" style="width:100%;" value="<?php echo htmlspecialchars($cfg['title'] ?? '', ENT_QUOTES); ?>">
</td>
</tr>
<tr class="sl-row sl-text" style="display:<?php echo $is_html?'none':'table-row'; ?>">
<th scope="row">내용</th>
<td>
<textarea name="content" rows="10" class="frm_input" style="width:100%;font-family:ui-monospace,Menlo,Consolas,monospace;"><?php
echo htmlspecialchars($cfg['content'] ?? '', ENT_QUOTES);
?></textarea>
</td>
</tr>
<!-- HTML 모드 -->
<tr class="sl-row sl-html" style="display:<?php echo $is_html?'table-row':'none'; ?>">
<th scope="row">HTML</th>
<td>
<textarea name="html" rows="16" class="frm_input" style="width:100%;font-family:ui-monospace,Menlo,Consolas,monospace;"><?php
echo htmlspecialchars($cfg['html'] ?? "<!doctype html>\n<html lang=\"ko\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"robots\" content=\"noindex,nofollow\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>점검 중</title>\n<style>body{margin:0;min-height:100vh;display:grid;place-items:center;font-family:system-ui} .box{max-width:640px;width:92vw;padding:32px;border-radius:16px;box-shadow:0 10px 30px rgba(0,0,0,.08)}</style>\n</head>\n<body>\n <main class=\"box\">\n <h1>사이트 점검 중입니다</h1>\n <p>더 나은 서비스를 위해 일시적으로 접속이 제한됩니다.<br>잠시 후 다시 이용해 주세요.</p>\n </main>\n</body>\n</html>", ENT_QUOTES);
?></textarea>
<p class="frm_info">직접 HTML은 이스케이프 없이 그대로 출력됩니다.</p>
</td>
</tr>
</tbody>
</table>
</div>
<div class="btn_fixed_top">
<a href="<?php echo G5_ADMIN_URL; ?>" class="btn btn_02">관리자홈</a>
<button type="button" class="btn btn_03" onclick="slPreview()">미리보기</button>
<button type="submit" class="btn btn_01">저장</button>
</div>
</form>
<script>
(function(){
function qsa(sel){ return Array.prototype.slice.call(document.querySelectorAll(sel)); }
function checkedValue(name){
var els = document.getElementsByName(name);
for (var i=0;i<els.length;i++){ if (els[i].checked) return els[i].value; }
return null;
}
function syncRows() {
var mode = checkedValue('mode') || 'text';
qsa('.sl-row.sl-text').forEach(function(el){ el.style.display = (mode==='text')?'table-row':'none'; });
qsa('.sl-row.sl-html').forEach(function(el){ el.style.display = (mode==='html')?'table-row':'none'; });
}
qsa('input[name="mode"]').forEach(function(r){ r.addEventListener('change', syncRows); });
syncRows();
function esc(s){
return String(s).replace(/[&<>"']/g, function(m){ return ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]); });
}
// 미리보기 (문자열 연결/문서쓰기 없이 DOM만으로 구성)
window.slPreview = function(){
var mode = checkedValue('mode') || 'text';
var w = window.open('', 'sl_preview', 'width=900,height=700');
if (!w) { alert('팝업이 차단되었습니다. 브라우저 팝업 허용 후 다시 시도하세요.'); return; }
var doc = w.document;
// 초기화
doc.title = '미리보기';
if (doc.head) { doc.head.innerHTML = ''; } else {
var head = doc.createElement('head'); doc.documentElement.appendChild(head);
}
if (doc.body) { doc.body.innerHTML = ''; } else {
var body = doc.createElement('body'); doc.documentElement.appendChild(body);
}
// 기본 메타/스타일
var meta1 = doc.createElement('meta'); meta1.setAttribute('charset','utf-8'); doc.head.appendChild(meta1);
var meta2 = doc.createElement('meta'); meta2.setAttribute('name','viewport'); meta2.setAttribute('content','width=device-width,initial-scale=1'); doc.head.appendChild(meta2);
var style = doc.createElement('style');
style.textContent = ':root{--fg:#222;--muted:#555;--bg:#f7f7f7;--card:#fff}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;background:var(--bg);font-family:system-ui,-apple-system,Segoe UI,Roboto,Noto Sans KR,sans-serif;color:var(--fg)}.box{background:var(--card);max-width:640px;width:92vw;padding:32px;border-radius:16px;box-shadow:0 10px 30px rgba(0,0,0,.08)}h1{margin:0 0 10px;font-size:28px}p{margin:10px 0 0;line-height:1.75;color:var(--muted)}';
doc.head.appendChild(style);
if (mode === 'html') {
// 사용자 HTML 그대로
var html = document.querySelector('textarea[name="html"]') ? document.querySelector('textarea[name="html"]').value : '';
doc.open(); doc.write(html); doc.close();
w.focus();
return;
}
// 텍스트 모드
var title = document.querySelector('input[name="title"]') ? document.querySelector('input[name="title"]').value : '점검 중';
var content = document.querySelector('textarea[name="content"]') ? document.querySelector('textarea[name="content"]').value : '';
var main = doc.createElement('main'); main.className = 'box';
var h1 = doc.createElement('h1'); h1.textContent = title;
var p = doc.createElement('p'); p.innerHTML = esc(content).replace(/\r?\n/g,'<br>');
main.appendChild(h1); main.appendChild(p);
doc.body.appendChild(main);
w.focus();
};
})();
</script>
<?php include_once(G5_ADMIN_PATH.'/admin.tail.php'); ?>
site_lock.extend.php
<?php
if (!defined('_GNUBOARD_')) exit;
/*
data/site_lock.json 구조
{
"enabled": true,
"whitelist": ["127.0.0.1"],
"mode": "text" | "html",
"title": "점검 중",
"content": "더 나은 서비스를 위해...",
"html": "<!doctype html>..."
}
*/
$cfg_file = G5_DATA_PATH.'/site_lock.json';
$cfg = [
'enabled' => false,
'whitelist' => [],
'mode' => 'text', // 'text' or 'html'
'title' => '사이트 점검 중입니다',
'content' => "더 나은 서비스를 위해 일시적으로 접속이 제한됩니다.\n잠시 후 다시 이용해 주세요.",
'html' => ''
];
if (is_file($cfg_file)) {
$json = @file_get_contents($cfg_file);
if ($json !== false) {
$tmp = json_decode($json, true);
if (is_array($tmp)) $cfg = array_merge($cfg, $tmp);
}
}
if (empty($cfg['enabled'])) return;
// 우회 조건
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
if (($is_admin ?? '') === 'super') return;
$allow_patterns = [
'~^/adm/.*~i', // 관리자
'~^/bbs/login\.php$~i', // 로그인
'~^/bbs/logout\.php$~i',
'~\.(css|js|png|jpe?g|gif|webp|svg|ico|woff2?)$~i' // 정적
];
foreach ($allow_patterns as $p) { if (preg_match($p, $uri)) return; }
$wl = is_array($cfg['whitelist']) ? array_filter(array_map('trim', $cfg['whitelist'])) : [];
if (in_array($ip, $wl, true)) return;
// 503 응답 + 페이지 출력
header('HTTP/1.1 503 Service Unavailable', true, 503);
header('Retry-After: 3600');
function sl_render_text($title, $content) {
$title = htmlspecialchars((string)$title, ENT_QUOTES, 'UTF-8');
// 줄바꿈을 <br>로
$content = nl2br(htmlspecialchars((string)$content, ENT_QUOTES, 'UTF-8'), false);
echo '<!doctype html><html lang="ko"><head><meta charset="utf-8">';
echo '<meta name="robots" content="noindex,nofollow">';
echo '<meta name="viewport" content="width=device-width,initial-scale=1">';
echo '<title>'. $title .'</title>';
echo '<style>
:root{--fg:#222;--muted:#555;--bg:#f7f7f7;--card:#fff}
*{box-sizing:border-box}
body{margin:0;min-height:100vh;display:grid;place-items:center;background:var(--bg);font-family:system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans KR", sans-serif;color:var(--fg)}
.box{background:var(--card);max-width:640px;width:92vw;padding:32px;border-radius:16px;box-shadow:0 10px 30px rgba(0,0,0,.08)}
h1{margin:0 0 10px;font-size:28px}
p{margin:10px 0 0;line-height:1.75;color:var(--muted)}
</style></head><body>';
echo '<main class="box"><h1>'.$title.'</h1><p>'.$content.'</p></main>';
echo '</body></html>';
}
$mode = $cfg['mode'] ?? 'text';
if ($mode === 'html' && !empty($cfg['html'])) {
// 사용자 HTML 그대로 출력
echo (string)$cfg['html'];
} else {
sl_render_text($cfg['title'] ?? '점검 중', $cfg['content'] ?? '');
}
exit;
코드 보시면 유추 가능하지만, 해당 정보는 /data/site_lock.json 에 저장됩니다.
만약 문제가 생기면 해당파일을 수정하시면 됩니다.
0
댓글 0개