테스트 사이트 - 개발 중인 베타 버전입니다

사이트 잠금기능

· 2개월 전 · 402 · 2

홈페이지 점검하거나 할때 사용하면 편?리한 사이트 잠금기능입니다.

화이트리스트에 추가된 IP주소만 접속가능하고 나머지는 접근이 제한됩니다.

역시나 GPT를 열심히 갈아넣었습니다.

2038537583_1757102486.1021.png

 

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>

          &nbsp;&nbsp;

          <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 ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[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 에 저장됩니다.

만약 문제가 생기면 해당파일을 수정하시면 됩니다.

댓글 작성

댓글을 작성하시려면 로그인이 필요합니다.

로그인하기

댓글 2개

1개월 전

감사 합니다.

감사합니다.

게시글 목록

번호 제목
24318
24317
24315
24309
24294
24293
24277
24262
24260
24253
24251
24236
24233
24228
24226
24221
24214
24203
24201
24199
24196
24195
24194
24192
24191
24187
24185
24183
24172
24168