사이트 잠금기능 > 그누보드5 팁자료실

그누보드5 팁자료실

사이트 잠금기능 정보

사이트 잠금기능

첨부파일

gb_site_lock.zip (5.5K) 0회 다운로드 2025-09-06 05:09:05

본문

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

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

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

2038537583_1757103084.544.png

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>

            

          <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개

전체 2,679 |RSS
그누보드5 팁자료실 내용 검색

회원로그인

(주)에스아이알소프트 / 대표:홍석명 / (06211) 서울특별시 강남구 역삼동 707-34 한신인터밸리24 서관 1402호 / E-Mail: admin@sir.kr
사업자등록번호: 217-81-36347 / 통신판매업신고번호:2014-서울강남-02098호 / 개인정보보호책임자:김민섭(minsup@sir.kr)
© SIRSOFT