회원가입 | 고객센터 |
DESIGNONEX
디자인원엑스
About
Service
Q&A
PR리그
자유게시판N
갤러리
포인트게임
공지사항
통계
로그인 회원가입
고객센터
3.8 Extend 구조

Extend 실제 소스 코드 완전 분석 • 12가지 실전 사례

A Administrator
2026.05.02 02:56(수정됨) 11 0
 

1. 실제 적용 흐름 전체 조감

이 챕터는 index.php와 Dispatcher.php의 실제 소스를 기반으로 Extend가 어떤 순서로, 어떤 상태에서 실행되는지를 단계별로 상세히 설명합니다.


1.1  index.php 전체 실행 순서 (STEP 1~7)

 

STEP 1 — 클래스•함수 정의 파일 로드

functions.php, DxCache, Secure, Database, HookManager, PluginRegistry, Auth, DxPoint, DxSite, DxTheme, DxExtend, Router, Dispatcher 순서로 require_once. 이 단계에서는 실행이 없고 정의만 합니다.


STEP 2 — 보안 초기화

Secure::getInstance(). 세션 설정 → (조건부) 세션 시작 → 보안 헤더 발행 → CSRF 토큰 선제 발급.


STEP 3 — DB 연결 + 설정 로드

data/config.php require. 이 안에서 Database::getInstance()->connect() 실행. DX_DEBUG, DX_SECRET_KEY 등 상수 확정.


STEP 4 — CMS 객체 초기화

HookManager → PluginRegistry → load_plugins() → DxSite → DxTheme → Auth → DxContainer.
모든 플러그인의 dx_add_hook() 등록이 이 단계에서 완료됩니다.
DxExtend::ensureDirs()로 extend/ 폴더 자동 생성.


extend/top/ 실행  ←  STEP 4 완료 직후

DxExtend::getInstance()->runTop(["version"=>DX_VERSION, "path"=>dx_request_uri()]).
모든 초기화가 완료된 상태이므로 Database, Auth, DxSite, DxCache, HookManager 전부 사용 가능.
라우트 정보($GLOBALS["dx_route"])는 아직 미설정.


STEP 5 — 라우팅 + 디스패치

routes/ 폴더 자동 로드 → DxRouter::dispatch() 시도 → 미매칭 시 Dispatcher 폴백.   Dispatcher::dispatch() 내부:    
① Router::resolve()로 라우트 확정 → $GLOBALS["dx_route"] 설정     🟡 extend/middle/ 실행    
② switch($type) → 핸들러 실행 + 렌더링


extend/bottom/ 실행  ←  렌더링 완료 후

DxExtend::getInstance()->runBottom(["elapsed"=>경과ms]).
HTML이 ob 버퍼에 완성된 상태. 헤더 변경 불가. ob_end_flush() 직전.

🔍  핵심 타임라인 요약
① top 실행 시점: Auth•DB•플러그인 전부 준비 완료. 라우트 정보 없음. 헤더•리다이렉트 가능.
② middle 실행 시점: $GLOBALS["dx_route"]["type"] 확정. 어떤 페이지인지 알고 처리 가능.
③ bottom 실행 시점: HTML 버퍼 완성. echo로 HTML 말단에 추가 가능. 헤더 변경 불가.


1.2  $GLOBALS["dx_route"] 구조 상세

middle/bottom 파일에서 사용할 수 있는  $GLOBALS["dx_route"]  (또는 inject된  $route ) 배열의 실제 구조입니다. Router::resolve()가 URL을 분석하여 확정합니다.


📄  dx_route 배열 구조

// $GLOBALS["dx_route"] 전체 구조 (Dispatcher::dispatch() 직후)
 
// ── 공통 키 (모든 라우트 타입) ──────────────────────────
$route["type"]      // "home" | "page" | "board" | "admin" | "auth" | "api" | "search" | "404"
$route["slug"]      // 페이지 슬러그 ("about", "home", "free" 등)
 
// ── board 타입 전용 ────────────────────────────────────
$route["board_key"] // 게시판 키 (예: "free", "notice", "gallery")
$route["board"]     // 게시판 전체 설정 배열 (DB에서 로드된 행)
$route["action"]    // "list" | "view" | "write" | "edit" | "delete"
$route["id"]        // 글 ID (view/edit/delete 시)
 
// ── page 타입 전용 ────────────────────────────────────
$route["page"]      // 페이지 전체 설정 배열
 
// ── admin 타입 전용 ───────────────────────────────────
$route["admin_action"] // 관리자 액션 문자열
 
// middle context에서 inject된 변수 (extract()로 주입)
$type   // $route["type"]과 동일
$route  // 위 배열 전체
 
// 사용 예시:
if ($type === "board" && isset($route["board_key"])) {
    $boardKey = $route["board_key"]; // "free", "notice" 등
}


1.3  context 변수 inject 상세

DxExtend::safeExec() 는  extract($context, EXTR_SKIP) 를 호출하여 컨텍스트 배열의 키를 파일 로컬 변수로 자동 주입합니다. 슬롯별로 주입되는 변수가 다릅니다.
 
슬롯 주입되는 변수 타입 설명
top $version string DX_VERSION ("8.1.0")
top $path string dx_request_uri() — 현재 요청 경로
top $dx_extend_slot string "top" — 항상 주입
middle $type string 라우트 타입 (board/page/home/…)
middle $route array 전체 라우트 배열 (위 1.2 참조)
middle $dx_extend_slot string "middle" — 항상 주입
bottom $elapsed float 경과 시간(ms) — microtime 기반
bottom $dx_extend_slot string "bottom" — 항상 주입

💡  EXTR_SKIP 안전 모드
extract($context, EXTR_SKIP)은 파일 내에 이미 같은 이름의 변수가 있으면 덮어쓰지 않습니다.
예: extend 파일 첫 줄에 $type = "custom"; 을 정의하면 context의 $type이 주입되지 않습니다.
반대로, context에 없는 변수는 주입되지 않으므로 isset() 확인이 필요합니다.


1.4  파일 수집 알고리즘과 실행 순서 결정

 DxExtend::collectFiles($dir) 가 실제로 파일을 수집하는 방식과, 그 결과 실행 순서가 어떻게 결정되는지 구체적인 예시로 설명합니다.


📄  파일 수집 알고리즘 예시

// DxExtend::collectFiles() 단계별 동작
 
// ① 슬롯 폴더 직접 *.php 수집
$found = glob("extend/top/*.php");
// 결과: ["extend/top/01_darkmode_early.php", "extend/top/02_maintenance.php", ...]
// .disabled 파일은 *.php 패턴에 불일치 → 자동 제외
 
sort($found); // 파일명 오름차순 정렬 (문자열 비교)
 
// ② 하위 폴더 1단계 재귀
$subDirs = glob("extend/top/*", GLOB_ONLYDIR);
sort($subDirs); // 폴더명도 오름차순
 
// 예시 디렉터리:
// extend/top/
//   01_darkmode_early.php  ← 직접 파일
//   05_maintenance.php     ← 직접 파일
//   security/              ← 하위 폴더
//     01_ip_block.php
//     02_rate_limit.php
//   utils/                 ← 하위 폴더
//     01_globals.php
 
// 최종 실행 순서:
// 1. extend/top/01_darkmode_early.php
// 2. extend/top/05_maintenance.php
// 3. extend/top/security/01_ip_block.php
// 4. extend/top/security/02_rate_limit.php
// 5. extend/top/utils/01_globals.php


2. 내장 Extend 파일 완전 분석

DXCMS v8.1.0에 기본 포함된 3개 파일의 실제 소스 코드를 전체 로직 흐름, 설계 의도, 주의사항까지 완전히 분석합니다.
extend/top/  01_darkmode_early.php
다크모드 FOUC(Flash of Unstyled Content) 완전 제거

▸  동작 원리
이 파일의 핵심은  ob_start(callback) 입니다. 파일 자체는 HTML을 출력하지 않고, 출력 버퍼가 flush될 때 실행될 콜백 함수만 등록합니다. 콜백은 완성된 HTML 버퍼를 받아 두 곳을 수정합니다.

 
수정 위치 삽입 내용 목적
<head> 직후 인라인 JS + 인라인 CSS localStorage를 즉시 읽어 다크 클래스 적용 + 흰 화면 숨김
</body> 직전 마이크로 스크립트 (3줄) dx-dark-early 클래스 → body.dark 교체 (engine.js 로드 전 보정)


단계별 실행 흐름

1. ob_start(callback) 등록
extend/top/ 실행 시 이 파일이 실행되어 ob_start()만 등록하고 종료. 실제 HTML 수정은 ob_end_flush() 시점에 발생.

2. 브라우저 요청 처리 & 렌더링
CMS가 정상적으로 라우팅, 핸들러 실행, HTML 생성을 모두 완료. ob 버퍼에 전체 HTML이 쌓임.

3. ob_end_flush() 호출 → 콜백 실행
콜백이 완성된 HTML을 받아 preg_replace로 <head> 다음과 </body> 앞에 스크립트•스타일 삽입.

4. 브라우저 수신 & 렌더링
<head> 안의 인라인 JS가 즉시 실행 → localStorage 확인 → 다크모드면 html에 dx-dark-early 클래스 + visibility:hidden 적용.

5. bottom/02_darkmode_engine.js 로드 완료
dx-darkmode-engine.js가 body.dark 클래스를 정식 적용하고 visibility:visible 복원 → 깜빡임 0.


실제 소스 전체 분석

📄  extend/top/01_darkmode_early.php 전체
 
<?php
// extend/top/01_darkmode_early.php
if (!defined("DX_CMS")) exit;
 
ob_start(function($buffer) {
 
    /* ①  FOUC 방지 인라인 스크립트 */
    $earlyScript = "<script>"
        . "(function(){"
        . "try{"
        . "  var t=localStorage.getItem(\"dx-theme\");"
        . "  var sys=window.matchMedia&&window.matchMedia(\"(prefers-color-scheme: dark)\").matches;"
        . "  if(t===\"dark\"||( t===null&&sys)){"
        . "    document.documentElement.classList.add(\"dx-dark-early\");"
        . "    document.documentElement.style.visibility=\"hidden\";", // ← FOUC 방지
        . "  }"
        . "}catch(e){}"
        . "})();"
        . "</script>";
 
    /* ②  FOUC 방지 인라인 CSS: 다크 배경 즉시 적용 */
    $earlyStyle = "<style>"
        . ".dx-dark-early body,.dx-dark-early{background-color:#0f172a!important;color:#f1f5f9!important}"
        . ".dx-dark-early #adm_navi_sidebar,.dx-dark-early #adm_navi_top,"
        . ".dx-dark-early #adm_navi_wrap{background-color:#0f172a!important}"
        . "</style>";
 
    /* ③  <head> 바로 뒤에 삽입 (preg_replace, 1회) */
    $buffer = preg_replace(
        "/<head([^>]*)>/i",
        "<head$1>" . $earlyScript . $earlyStyle,
        $buffer, 1
    );
 
    /* ④  </body> 직전 — early 클래스 → body.dark 교체 */
    $lateScript = "<script>"
        . "(function(){"
        . "  var html=document.documentElement;"
        . "  if(html.classList.contains(\"dx-dark-early\")){"
        . "    html.classList.remove(\"dx-dark-early\");"
        . "    document.body.classList.add(\"dark\");"
        . "  }"
        . "})();"
        . "</script>";
    $buffer = preg_replace("/<\/body>/i", $lateScript . "</body>", $buffer, 1);
 
    return $buffer; // ← 수정된 HTML 반환
});
// → 이 파일 실행 완료. 나머지는 ob 콜백이 처리.

⚠️  설계 포인트
ob_start()를 top에서 등록하는 이유: 이미 출력이 시작된 후에는 ob_start()를 등록해도 그 이전 출력을 캡처할 수 없습니다.
EXTR_SKIP이 아닌 이유: 이 파일은 context 변수를 아예 사용하지 않으므로 extract 결과와 무관합니다.
preg_replace의 마지막 인자 1: <head>가 여러 번 등장하는 잘못된 HTML에서도 첫 번째만 처리합니다.

extend/middle/  01_visit_tracker.php
방문자 통계 자동 기록 — 봇 완전 차단 + 비동기 DB 기록


설계 목표

이 파일의 핵심 설계 목표는 두 가지입니다.
•    봇 트래픽 완전 차단: 40+ 봇 패턴으로 visit_logs INSERT를 완전히 막아 테이블 용량 폭증을 방지합니다.
•    응답 속도 보호: register_shutdown_function으로 DB INSERT를 응답 후에 처리합니다. 사용자는 통계 기록 시간을 기다리지 않습니다.


단계별 실행 흐름

1. 조기 탈출 — admin/api/정적파일 제외
$GLOBALS["dx_route"]["type"]가 "admin" 또는 "api"이면 즉시 return. /assets/, /favicon, .js/.css 등 정적 경로도 즉시 return.

2. 봇 판별 (40+ 패턴)
User-Agent를 소문자 변환 후 googlebot, bingbot, curl, python, selenium, puppeteer 등 40개 이상 패턴 순차 검사.
빈 UA도 봇으로 처리. 봇이면 즉시 return → DB 기록 없음.

3. IP 추출 (Cloudflare 우선)
HTTP_CF_CONNECTING_IP → HTTP_X_FORWARDED_FOR → HTTP_CLIENT_IP → REMOTE_ADDR 순서. 공인 IP만 허용(FILTER_FLAG_NO_PRIV_RANGE).

4. 브라우저 파싱
User-Agent에서 Edge/Opera/Samsung/Chrome/Firefox/Safari/IE/Whale 판별. DB 저장용.

5. 순방문자 판단 (DxCache — DB 조회 없음)
"vt_u:날짜:IP+브라우저 해시(12자)" 키로 자정까지 TTL 캐시. 캐시 히트 = 재방문, 미스 = 순방문. DB 쿼리 0.

6. 📦  shutdown 클로저에 데이터 캡처
date, ip, browser, is_unique, page_url, referer, ref_domain, ua, member_id, created_at 를 배열로 패킹하여 클로저에 전달.

7. ✅  사용자에게 응답 전송 (fastcgi_finish_request)
register_shutdown_function 내에서 fastcgi_finish_request() 호출 → PHP-FPM 환경에서 즉시 응답 완료.
사용자는 이 시점에 페이지를 수신.

8. DB INSERT (응답 후 백그라운드)
① dx_visits UPSERT (INSERT ON DUPLICATE KEY): visit_count+1, unique_count 조건부 +1.
② visit_logs INSERT: ip, page_url, referer, referer_domain, ua, browser, member_id.
③ 1/200 확률로 90일 초과 로그 삭제.


실제 소스 핵심 로직 분석

📄  extend/middle/01_visit_tracker.php 핵심 로직
// extend/middle/01_visit_tracker.php 핵심 발췌
 
/* ─ 1단계: 조기 탈출 ─────────────────────────────── */
$_vt_route = $GLOBALS["dx_route"] ?? [];
$_vt_type  = $_vt_route["type"] ?? "";
if (in_array($_vt_type, ["admin", "api"], true)) return;
 
$_vt_excl_prefixes = ["/ws-", "/api/", "/admin/", "/assets/", "/data/", "/favicon"];
foreach ($_vt_excl_prefixes as $_vt_ep) {
    if (strpos($_vt_path, $_vt_ep) === 0) return;
}
 
/* ─ 2단계: 봇 판별 (40+ 패턴) ──────────────────── */
$_vt_bot_patterns = [
    "googlebot","bingbot","curl","wget","python-requests",
    "selenium","puppeteer","playwright","headlesschrome",
    // ... (총 40+ 패턴)
];
if ($_vt_ua === "") { $_vt_isBot = true; }
else {
    $ua_lower = strtolower($_vt_ua);
    foreach ($_vt_bot_patterns as $_b) {
        if (strpos($ua_lower, $_b) !== false) { $_vt_isBot = true; break; }
    }
}
if ($_vt_isBot) return; // ← 봇: 기록 없이 종료
 
/* ─ 4단계: 순방문자 판단 (DxCache) ─────────────── */
$_vt_cacheKey = "vt_u:" . date("Y-m-d") . ":" . substr(md5($_vt_ip . $_vt_browser), 0, 12);
if (!DxCache::get($_vt_cacheKey, false)) {
    DxCache::set($_vt_cacheKey, 1, strtotime("tomorrow") - time()); // 자정까지 TTL
    $_vt_isUnique = true; // 순방문자
}
 
/* ─ 5단계: 응답 후 DB 기록 ─────────────────────── */
register_shutdown_function(function() use ($_vt_data) {
 
    // PHP-FPM: 즉시 응답 완료 (사용자는 이미 페이지 수신)
    if (function_exists("fastcgi_finish_request")) {
        fastcgi_finish_request();
    }
 
    $uniqueInc = $_vt_data["is_unique"] ? 1 : 0;
 
    // ① dx_visits UPSERT (쿼리 1회)
    $pdo->prepare(
        "INSERT INTO `{$vtbl}` (visit_date, visit_count, unique_count)",
        "VALUES (?, 1, ?)",
        "ON DUPLICATE KEY UPDATE",
        "  visit_count  = visit_count + 1,",
        "  unique_count = unique_count + {$uniqueInc}"
    )->execute([$_vt_data["date"], $uniqueInc]);
 
    // ② visit_logs INSERT
    $pdo->prepare(
        "INSERT INTO `{$ltbl}`",
        "(visit_date,ip,page_url,referer,referer_domain,user_agent,browser,is_bot,member_id,created_at)",
        "VALUES (?,?,?,?,?,?,?,0,?,?)"
    )->execute([...]); // $_vt_data 값들
 
    // ③ 1/200 확률 자동 정리 (90일 초과 삭제)
    if (mt_rand(1, 200) === 1) {
        $pdo->prepare("DELETE FROM `{$ltbl}` WHERE visit_date < ?")
           ->execute([date("Y-m-d", strtotime("-90 days"))]);
    }
});
 
/* ─ 변수 정리 (전역 오염 방지) ─────────────────── */
unset($_vt_route, $_vt_type, $_vt_ua, $_vt_isBot, ...);

🔍  변수명 접두사 $_vt_ 의 의미
이 파일은 전역 스코프에서 실행됩니다. $type, $ip, $browser 같은 짧은 변수명은 다른 파일의 변수와 충돌할 수 있습니다.
$_vt_*(visit tracker) 접두사를 붙여 네임스페이스 충돌을 방지합니다. 파일 끝에서 unset()으로 모두 정리합니다.
이것이 extend 파일 작성의 베스트 프랙티스입니다: 고유 접두사 + 파일 끝 unset.

extend/bottom/  02_darkmode_engine.php
다크모드 엔진 JS 주입 — 모든 페이지 </body> 직전


가장 단순한 내장 파일입니다. 모든 페이지(사이트 + 관리자)의 HTML 버퍼 말단에 다크모드 엔진 JS를 echo합니다.
📄  extend/bottom/02_darkmode_engine.php 전체
<?php
// extend/bottom/02_darkmode_engine.php
if (!defined("DX_CMS")) exit;
 
$_dmVer  = defined("DX_VERSION") ? DX_VERSION : "1.0";
$_dmBase = rtrim(function_exists("dx_base_url") ? dx_base_url("") : "", "/");
 
// 다크모드 엔진 JS 로드 (버전 쿼리로 캐시 무효화)
echo '<script src="' . $_dmBase . '/assets/js/dx-darkmode-engine.js'
   . '?v=' . htmlspecialchars($_dmVer, ENT_QUOTES, "UTF-8") . '"></script>' . "\n";
 
포인트 설명
슬롯 선택 이유 bottom은 HTML 버퍼 말단 → </body> 직전에 JS가 위치
파일명 02_ 내장 파일임을 표시. 사용자 파일을 03_이후로 배치하면 됨
dx_base_url() CMS 설치 경로가 서브디렉터리여도 올바른 URL 생성
버전 쿼리 ?v= 브라우저 캐시 무효화. 업데이트 시 자동 갱신
htmlspecialchars() XSS 방지. 버전 문자열이 의도치 않게 특수문자를 포함할 경우 대비


3. 다양한 활용 사례

top / middle / bottom 3개 슬롯별로 실전에서 바로 사용할 수 있는 12가지 사례를 실제 코드와 함께 상세히 설명합니다.


extend/top/ 활용 사례

 top  슬롯은 모든 초기화 완료 직후, 라우팅 전에 실행됩니다. 라우트 정보 없이 실행되므로 "모든 URL에 공통 적용되어야 하는" 처리에 적합합니다.

extend/top/  01_maintenance.php
사례 T-1 — 서비스 점검 모드


핵심 기능: 관리자 IP는 정상 접근, 그 외 모든 요청에 503 페이지 반환. 비활성화: 파일명을  01_maintenance.php.disabled 로 변경하면 즉시 비활성화됩니다.

📄  extend/top/01_maintenance.php
 
<?php
// extend/top/01_maintenance.php
if (!defined("DX_CMS")) exit;
 
// ── 설정 ──────────────────────────────────────────
$_mt_active   = true;                              // false → 이 파일 자체를 .disabled 로 관리
$_mt_adminIps = ["127.0.0.1", "123.45.67.89"];    // 통과시킬 관리자 IP
$_mt_endTime  = "2026-05-01 06:00:00";            // 점검 완료 예정 시각
 
if (!$_mt_active) return;
 
// ── IP 추출 (Cloudflare / 프록시 지원) ────────────
$_mt_ip = trim(explode(",", (
    $_SERVER["HTTP_CF_CONNECTING_IP"] ??
    $_SERVER["HTTP_X_FORWARDED_FOR"]  ??
    $_SERVER["REMOTE_ADDR"] ?? ""))[0]);
 
if (in_array($_mt_ip, $_mt_adminIps, true)) return; // 관리자 → 통과
 
// ── 503 점검 페이지 출력 ──────────────────────────
http_response_code(503);
header("Retry-After: 3600");
header("Content-Type: text/html; charset=UTF-8");
 
echo '<!DOCTYPE html><html lang="ko"><head>
<meta charset="UTF-8"><title>서비스 점검 중</title>
<style>
  body{margin:0;background:#0f172a;color:#f1f5f9;font-family:sans-serif;
       display:flex;align-items:center;justify-content:center;min-height:100vh}
  .box{text-align:center;padding:2rem}
  h1{font-size:2.5rem;margin-bottom:1rem}
  p{font-size:1.1rem;color:#94a3b8;line-height:1.8}
  .time{margin-top:1.5rem;color:#38bdf8;font-weight:bold}
</style></head><body>
<div class="box">
  <h1>🔧 서비스 점검 중</h1>
  <p>더 나은 서비스를 위해 점검 중입니다.<br>이용에 불편을 드려 죄송합니다.</p>
  <p class="time">예정 완료: ' . $_mt_endTime . '</p>
</div></body></html>';
exit; // ← 이후 모든 처리 중단

extend/top/  02_ip_block.php
사례 T-2 — 동적 IP 차단 (DB + 하드코딩 혼합)


핵심 기능: 하드코딩 차단 목록과 DB 동적 차단 목록을 합산하여 처리. 캐시로 DB 조회를 최소화합니다.
📄  extend/top/02_ip_block.php
 
<?php
// extend/top/02_ip_block.php
if (!defined("DX_CMS")) exit;
 
// ── 하드코딩 차단 목록 (긴급 차단용) ──────────────
$_ib_staticList = [
    "185.220.101.",  // Tor 출구 노드 대역
    "45.155.205.",   // 알려진 스캐너
];
 
// ── DB 동적 차단 목록 (캐시 5분) ──────────────────
$_ib_cacheKey = "ip_block_list_v1";
$_ib_dbList   = DxCache::get($_ib_cacheKey, []);
 
if (empty($_ib_dbList)) {
    $db = Database::getInstance();
    $rows = $db->query(
        "SELECT ip FROM " . $db->table("ip_blocks") .
        " WHERE (expires_at IS NULL OR expires_at > NOW())"
    );
    $_ib_dbList = array_column($rows, "ip");
    DxCache::set($_ib_cacheKey, $_ib_dbList, 300);
}
 
$_ib_allList = array_merge($_ib_staticList, $_ib_dbList);
 
// ── IP 추출 ────────────────────────────────────────
$_ib_ip = trim(explode(",", (
    $_SERVER["HTTP_CF_CONNECTING_IP"] ??
    $_SERVER["HTTP_X_FORWARDED_FOR"]  ??
    $_SERVER["REMOTE_ADDR"] ?? ""))[0]);
 
// ── 차단 검사 ──────────────────────────────────────
foreach ($_ib_allList as $_ib_pattern) {
    if (strpos($_ib_ip, $_ib_pattern) === 0 || $_ib_ip === $_ib_pattern) {
        // 차단 이력 기록 (비동기)
        register_shutdown_function(function() use ($_ib_ip) {
            $db = Database::getInstance();
            $db->query(
                "INSERT INTO " . $db->table("ip_block_logs") .
                " (ip, blocked_at) VALUES (?, NOW())",
                [$_ib_ip]
            );
        });
        http_response_code(403);
        echo "403 Forbidden";
        exit;
    }
}
unset($_ib_staticList, $_ib_dbList, $_ib_allList, $_ib_ip, $_ib_cacheKey, $_ib_pattern);

extend/top/  05_global_vars.php
사례 T-3 — 전역 변수 사전 주입 (캐시 활용)


핵심 기능: 모든 페이지에서 공통으로 사용하는 데이터(공지사항, 인기 태그, 사이트 통계 등)를 DB에서 한 번만 조회하여  $GLOBALS["dx_shared"] 에 캐싱합니다. 각 페이지에서 중복 쿼리를 방지합니다.
📄  extend/top/05_global_vars.php
 
<?php
// extend/top/05_global_vars.php
if (!defined("DX_CMS")) exit;
 
define("DX_SHARED_CACHE_TTL", 300); // 5분
 
$_gv_cacheKey = "dx_shared_v2";
$_gv_data = DxCache::get($_gv_cacheKey, null);
 
if ($_gv_data === null) {
    $db = Database::getInstance();
 
    // 최신 공지사항 3개
    $_gv_notices = $db->query(
        "SELECT id, title, created_at FROM " . $db->table("posts") .
        " WHERE board_key='notice' AND status='published' ORDER BY id DESC LIMIT 3"
    );
 
    // 인기 태그 20개
    $_gv_tags = $db->query(
        "SELECT tag, COUNT(*) as cnt FROM " . $db->table("post_tags") .
        " GROUP BY tag ORDER BY cnt DESC LIMIT 20"
    );
 
    // 사이트 통계 (회원수, 게시글수)
    $_gv_stats = [
        "members" => (int)($db->value("SELECT COUNT(*) FROM " . $db->table("members")) ?? 0),
        "posts"   => (int)($db->value("SELECT COUNT(*) FROM " . $db->table("posts")) ?? 0),
    ];
 
    $_gv_data = [
        "notices"     => $_gv_notices,
        "popular_tags"=> $_gv_tags,
        "stats"       => $_gv_stats,
        "cached_at"   => time(),
    ];
    DxCache::set($_gv_cacheKey, $_gv_data, DX_SHARED_CACHE_TTL);
}
 
// 전역 주입 → 모든 스킨/페이지 파일에서 접근 가능
$GLOBALS["dx_shared"] = $_gv_data;
 
// 스킨에서 사용 예시:
// $notices = $GLOBALS["dx_shared"]["notices"] ?? [];
// foreach ($notices as $n) { echo $n["title"]; }
 
unset($_gv_cacheKey, $_gv_data, $_gv_notices, $_gv_tags, $_gv_stats);

extend/top/  10_rate_limiter.php
사례 T-4 — Rate Limiter (분당 요청 수 제한)


핵심 기능: IP별 분당 요청 수를 캐시로 추적하여 임계값 초과 시 429 Too Many Requests 반환. DDoS 경감에 유효합니다.
📄  extend/top/10_rate_limiter.php
 
<?php
// extend/top/10_rate_limiter.php
if (!defined("DX_CMS")) exit;
 
// ── 설정 ──────────────────────────────────────────
$_rl_limit  = 120;  // 분당 최대 요청 수
$_rl_window = 60;   // 윈도우 (초)
 
// API•admin은 별도 제한 (더 엄격하게)
$_rl_type = $GLOBALS["dx_route"]["type"] ?? "";  // top이므로 dx_route 없을 수도 있음
// → top에서는 dx_route 미확정. 대신 REQUEST_URI로 판단
$_rl_uri  = $_SERVER["REQUEST_URI"] ?? "/";
if (strpos($_rl_uri, "/api/") !== false) { $_rl_limit = 30; }
 
// ── IP 추출 ────────────────────────────────────────
$_rl_ip = trim(explode(",", (
    $_SERVER["HTTP_CF_CONNECTING_IP"] ??
    $_SERVER["HTTP_X_FORWARDED_FOR"]  ??
    $_SERVER["REMOTE_ADDR"] ?? "127.0.0.1"))[0]);
 
// ── 요청 카운트 ────────────────────────────────────
$_rl_key   = "rl:" . $_rl_ip . ":" . floor(time() / $_rl_window);
$_rl_count = (int)DxCache::get($_rl_key, 0);
$_rl_count++;
DxCache::set($_rl_key, $_rl_count, $_rl_window + 5);
 
// ── 한도 초과 ──────────────────────────────────────
if ($_rl_count > $_rl_limit) {
    http_response_code(429);
    header("Retry-After: " . $_rl_window);
    header("X-RateLimit-Limit: " . $_rl_limit);
    header("X-RateLimit-Remaining: 0");
    echo json_encode(["error" => "Too many requests. Retry after " . $_rl_window . "s."]);
    exit;
}
 
header("X-RateLimit-Limit: " . $_rl_limit);
header("X-RateLimit-Remaining: " . max(0, $_rl_limit - $_rl_count));
unset($_rl_limit, $_rl_window, $_rl_ip, $_rl_key, $_rl_count, $_rl_type, $_rl_uri);


extend/middle/ 활용 사례

 middle  슬롯은 라우트 확정 직후, 핸들러 실행 전에 실행됩니다.  $route["type"]  로 어떤 페이지인지 알 수 있어 "라우트 타입별 처리"에 적합합니다.

extend/middle/  02_access_log.php
사례 M-1 — 상세 접근 로그 (파일 기록)


핵심 기능: 관리자 페이지 및 인증 행위를 파일 로그에 기록합니다. 보안 감사(audit trail)에 활용합니다. 봇 자동 제외, 비동기 기록.
📄  extend/middle/02_access_log.php
 
<?php
// extend/middle/02_access_log.php
if (!defined("DX_CMS")) exit;
 
// ── 기록 대상 라우트만 처리 ───────────────────────
$_al_type = $route["type"] ?? "";  // context에서 inject됨
$_al_targets = ["admin", "auth", "api"];
if (!in_array($_al_type, $_al_targets, true)) return;
 
// ── 봇 제외 ────────────────────────────────────────
$_al_ua = $_SERVER["HTTP_USER_AGENT"] ?? "";
if (preg_match("/bot|crawl|spider|curl|wget/i", $_al_ua)) return;
 
// ── 로그 데이터 수집 ───────────────────────────────
$_al_user   = class_exists("Auth") && Auth::getInstance()->isLoggedIn()
    ? Auth::getInstance()->get("nickname", "guest") : "guest";
$_al_method = $_SERVER["REQUEST_METHOD"] ?? "GET";
$_al_ip     = function_exists("dx_ip") ? dx_ip() : ($_SERVER["REMOTE_ADDR"] ?? "?");
$_al_uri    = substr($_SERVER["REQUEST_URI"] ?? "/", 0, 300);
$_al_action = $route["admin_action"] ?? ($route["slug"] ?? "");
 
// ── 비동기 파일 기록 ────────────────────────────────
register_shutdown_function(function() use (
    $_al_type, $_al_action, $_al_user, $_al_method, $_al_ip, $_al_uri, $_al_ua
) {
    if (function_exists("fastcgi_finish_request")) fastcgi_finish_request();
 
    $line = implode(" | ", [
        date("Y-m-d H:i:s"),
        strtoupper($_al_type),
        $_al_action,
        $_al_user,
        $_al_method,
        $_al_ip,
        $_al_uri,
    ]) . PHP_EOL;
 
    $logFile = DX_ROOT . "/data/logs/access_" . date("Y-m") . ".log";
    $logDir  = dirname($logFile);
    if (!is_dir($logDir)) mkdir($logDir, 0755, true);
    file_put_contents($logFile, $line, FILE_APPEND | LOCK_EX);
});
 
unset($_al_type, $_al_targets, $_al_ua, $_al_user, $_al_method, $_al_ip, $_al_uri, $_al_action);

extend/middle/  10_ab_test.php
사례 M-2 — A/B 테스트 (홈페이지 변형 테스트)


핵심 기능: 세션 기반으로 A/B 그룹을 결정하고,  $GLOBALS["ab_variant"] 에 변형 데이터를 주입합니다. 홈 스킨 파일에서 이 변수를 읽어 다른 UI를 렌더링합니다.
📄  extend/middle/10_ab_test.php
 
<?php
// extend/middle/10_ab_test.php
if (!defined("DX_CMS")) exit;
 
// home 라우트에서만 적용 ($type은 context에서 inject)
if (($type ?? "") !== "home") return;
 
// ── 세션 기반 그룹 결정 (재방문 시 유지) ─────────
if (!isset($_SESSION["ab_group"])) {
    $_SESSION["ab_group"] = mt_rand(0, 99) < 50 ? "A" : "B";
}
$_ab_group = $_SESSION["ab_group"];
 
// ── 변형 데이터 정의 ───────────────────────────────
$_ab_variants = [
    "A" => [
        "hero_title"   => "최고의 커뮤니티에 오신 것을 환영합니다",
        "hero_cta"     => "지금 가입하기",
        "cta_color"    => "#2563EB",
        "layout"       => "hero_centered",
    ],
    "B" => [
        "hero_title"   => "함께 성장하는 공간, 지금 시작하세요",
        "hero_cta"     => "무료로 시작하기",
        "cta_color"    => "#16A34A",
        "layout"       => "hero_left",
    ],
];
 
// ── 전역 주입 ──────────────────────────────────────
$GLOBALS["ab_group"]   = $_ab_group;
$GLOBALS["ab_variant"] = $_ab_variants[$_ab_group];
 
// 스킨 파일(home.php)에서 사용 예시:
// $variant = $GLOBALS["ab_variant"] ?? [];
// echo '<h1>' . ($variant["hero_title"] ?? "") . '</h1>';
 
// ── 전환 이벤트 비동기 기록 ────────────────────────
register_shutdown_function(function() use ($_ab_group) {
    if (function_exists("fastcgi_finish_request")) fastcgi_finish_request();
    $db = Database::getInstance();
    $db->query(
        "INSERT INTO " . $db->table("ab_impressions") . " (group_name, page, created_at) VALUES (?, ?, NOW())",
        [$_ab_group, "home"]
    );
});
 
unset($_ab_group, $_ab_variants);

extend/middle/  20_board_access.php
사례 M-3 — 게시판별 비회원 접근 제어


핵심 기능: 특정 게시판을 비회원에게 차단합니다. 플러그인 코드 없이 extend 파일 하나로 처리합니다.
📄  extend/middle/20_board_access.php
 
<?php
// extend/middle/20_board_access.php
if (!defined("DX_CMS")) exit;
 
// board 라우트에서만 처리
if (($type ?? "") !== "board") return;
 
// ── 회원 전용 게시판 목록 ─────────────────────────
$_ba_membersOnly = ["secret", "staff", "vip", "premium"];
 
$_ba_boardKey = $route["board_key"] ?? "";
 
if (!in_array($_ba_boardKey, $_ba_membersOnly, true)) return; // 비대상 게시판
 
// ── 로그인 여부 확인 ──────────────────────────────
if (!Auth::getInstance()->isLoggedIn()) {
    // 로그인 후 원래 페이지로 복귀하도록 return URL 포함
    $returnUrl = urlencode(dx_request_uri());
    dx_redirect(dx_base_url("auth/login?return=" . $returnUrl));
    exit;
}
 
// ── VIP 게시판: 레벨 제한 ─────────────────────────
if ($_ba_boardKey === "vip") {
    $_ba_userLevel = (int)Auth::getInstance()->get("level", 0);
    if ($_ba_userLevel < 10) {
        http_response_code(403);
        dx_redirect(dx_base_url("?msg=level_required"));
        exit;
    }
}
 
unset($_ba_membersOnly, $_ba_boardKey);

extend/middle/  30_feature_flags.php
사례 M-4 — 기능 플래그 (Feature Flags)


핵심 기능: DB에서 기능 플래그를 읽어 특정 사용자/IP/라우트에만 새 기능을 점진적으로 배포합니다.
📄  extend/middle/30_feature_flags.php
 
<?php
// extend/middle/30_feature_flags.php
if (!defined("DX_CMS")) exit;
 
// ── 플래그 로드 (캐시 10분) ───────────────────────
$_ff_key   = "feature_flags_v1";
$_ff_flags = DxCache::get($_ff_key, null);
 
if ($_ff_flags === null) {
    $db = Database::getInstance();
    $rows = $db->query(
        "SELECT flag_key, enabled, target_group FROM " . $db->table("feature_flags") .
        " WHERE enabled = 1"
    );
    $_ff_flags = [];
    foreach ($rows as $row) {
        $_ff_flags[$row["flag_key"]] = $row["target_group"];
    }
    DxCache::set($_ff_key, $_ff_flags, 600);
}
 
// ── 현재 사용자 그룹 판별 ─────────────────────────
$_ff_userId   = Auth::getInstance()->isLoggedIn() ? (int)Auth::getInstance()->get("id", 0) : 0;
$_ff_isBeta   = $_ff_userId > 0 && $_ff_userId % 10 < 2; // 20% 베타 사용자
$_ff_isStaff  = Auth::getInstance()->isLoggedIn() && Auth::getInstance()->get("is_staff", 0);
 
// ── 활성 플래그 계산 ──────────────────────────────
$_ff_active = [];
foreach ($_ff_flags as $flagKey => $targetGroup) {
    switch ($targetGroup) {
        case "all":    $_ff_active[$flagKey] = true; break;
        case "beta":   $_ff_active[$flagKey] = $_ff_isBeta; break;
        case "staff":  $_ff_active[$flagKey] = (bool)$_ff_isStaff; break;
    }
}
 
// ── 전역 주입 ──────────────────────────────────────
$GLOBALS["feature_flags"] = $_ff_active;
 
// 스킨/핸들러에서 사용 예시:
// if ($GLOBALS["feature_flags"]["new_editor"] ?? false) {
//     // 새 에디터 렌더링
// }
 
unset($_ff_key, $_ff_flags, $_ff_active, $_ff_userId, $_ff_isBeta, $_ff_isStaff);


extend/bottom/ 활용 사례

bottom  슬롯은 렌더링 완료 후, ob_end_flush() 직전에 실행됩니다. HTML이 이미 버퍼에 완성된 상태이므로 헤더 변경은 불가하지만 echo로 HTML에 추가가 가능합니다.

extend/bottom/  01_page_cache.php
사례 B-1 — HTML 페이지 캐시 저장


핵심 기능: 렌더링 완료된 HTML을 파일에 저장합니다. Nginx/Apache에서 이 파일을 직접 서빙하면 PHP 실행 없이 응답 가능합니다.
📄  extend/bottom/01_page_cache.php
 
<?php
// extend/bottom/01_page_cache.php
if (!defined("DX_CMS")) exit;
 
// ── 캐시 대상 조건 ────────────────────────────────
$_pc_route = $GLOBALS["dx_route"] ?? [];
if ($_SERVER["REQUEST_METHOD"] !== "GET") return;
if (Auth::getInstance()->isLoggedIn()) return;       // 로그인 사용자 제외
if (!empty($_GET)) return;                            // 쿼리스트링 제외
if (!in_array($_pc_route["type"] ?? "", ["home", "page"], true)) return;
 
// ── ob 버퍼 읽기 (flush 전이므로 가능) ───────────
$_pc_html = ob_get_contents();
if (!$_pc_html || strlen($_pc_html) < 200) return;
 
// ── 캐시 파일 경로 ────────────────────────────────
$_pc_uri    = preg_replace("/[^a-zA-Z0-9\/\-_]/", "_", $_SERVER["REQUEST_URI"] ?? "/");
$_pc_file   = DX_ROOT . "/data/cache/pages" . $_pc_uri . "/index.html";
$_pc_dir    = dirname($_pc_file);
 
if (!is_dir($_pc_dir)) mkdir($_pc_dir, 0755, true);
 
// ── 저장 (만료 메타 주석 포함) ───────────────────
$_pc_expires = time() + 300; // 5분
$_pc_content = "<!-- dx-cache:expires=" . $_pc_expires . " -->\n" . $_pc_html;
file_put_contents($_pc_file, $_pc_content, LOCK_EX);
 
// ── 만료 파일 정리 (5% 확률) ────────────────────
if (mt_rand(1, 20) === 1) {
    $cacheBase = DX_ROOT . "/data/cache/pages";
    $iterator  = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($cacheBase));
    foreach ($iterator as $_pc_f) {
        if ($_pc_f->isFile() && $_pc_f->getExtension() === "html") {
            if ($_pc_f->getMTime() + 300 < time()) @unlink($_pc_f->getPathname());
        }
    }
}
 
unset($_pc_route, $_pc_html, $_pc_uri, $_pc_file, $_pc_dir, $_pc_expires, $_pc_content);

extend/bottom/  05_slow_query_alert.php
사례 B-2 — 느린 페이지 Slack 알림


핵심 기능: 응답 시간이 임계값을 초과한 페이지를 감지하여 Slack Webhook으로 실시간 알림을 발송합니다.
📄  extend/bottom/05_slow_query_alert.php
 
<?php
// extend/bottom/05_slow_query_alert.php
if (!defined("DX_CMS")) exit;
 
// ── 임계값 설정 ───────────────────────────────────
$_sq_warnMs  = 1000;  // 1초 초과 시 경고
$_sq_critMs  = 3000;  // 3초 초과 시 위험
 
// context에서 inject된 $elapsed 사용
$_sq_elapsed = $elapsed ?? round((microtime(true) - DX_START) * 1000, 2);
$_sq_queries = Database::getInstance()->getQueryCount();
 
if ($_sq_elapsed < $_sq_warnMs && $_sq_queries < 50) return; // 정상
 
// ── 알림 데이터 구성 ──────────────────────────────
$_sq_route   = $GLOBALS["dx_route"] ?? [];
$_sq_url     = $_SERVER["REQUEST_URI"] ?? "/";
$_sq_level   = $_sq_elapsed >= $_sq_critMs ? "🔴 위험" : "🟡 경고";
 
$_sq_message = implode("\n", [
    $_sq_level . " *느린 페이지 감지*",
    "• URL: `" . $_sq_url . "`",
    "• 응답시간: `" . $_sq_elapsed . "ms`",
    "• DB쿼리: `" . $_sq_queries . "회`",
    "• 라우트: `" . ($route["type"] ?? "?") . "`",
    "• 시각: `" . date("Y-m-d H:i:s") . "`",
]);
 
// ── 비동기 Slack 발송 ────────────────────────────
register_shutdown_function(function() use ($_sq_message) {
    if (function_exists("fastcgi_finish_request")) fastcgi_finish_request();
 
    $webhookUrl = dx_config("slack_perf_webhook", "");
    if (!$webhookUrl) return;
 
    $payload = json_encode(["text" => $_sq_message]);
    $ch = curl_init($webhookUrl);
    curl_setopt_array($ch, [
        CURLOPT_POST           => 1,
        CURLOPT_POSTFIELDS     => $payload,
        CURLOPT_RETURNTRANSFER => 1,
        CURLOPT_HTTPHEADER     => ["Content-Type: application/json"],
        CURLOPT_TIMEOUT        => 5,
        CURLOPT_SSL_VERIFYPEER => true,
    ]);
    curl_exec($ch);
    curl_close($ch);
});
 
unset($_sq_warnMs, $_sq_critMs, $_sq_elapsed, $_sq_queries, $_sq_route, $_sq_url, $_sq_level, $_sq_message);

extend/bottom/  10_html_minify.php
사례 B-3 — HTML Minify (출력 버퍼 가로채기)


핵심 기능: ob_get_clean() + ob_start()를 사용하여 완성된 HTML에서 불필요한 공백과 주석을 제거합니다. 페이지 전송 크기를 10~20% 절감합니다.
📄  extend/bottom/10_html_minify.php
 
<?php
// extend/bottom/10_html_minify.php
if (!defined("DX_CMS")) exit;
 
// 개발 모드에서는 비활성화 (가독성 유지)
if (defined("DX_DEBUG") && DX_DEBUG) return;
 
// JSON/API 응답에는 적용 안 함
$_hm_ct = $_SERVER["HTTP_ACCEPT"] ?? "";
if (strpos($_hm_ct, "application/json") !== false) return;
 
// ── 현재 버퍼를 가져와서 교체 ───────────────────
$_hm_html = ob_get_clean(); // 버퍼 내용 읽고 종료
ob_start();                  // 새 버퍼 시작
 
// ── Minify 처리 ──────────────────────────────────
$_hm_html = preg_replace([
    '/<!--(?!\[if|<!)[\s\S]*?-->/',  // 조건부 주석 제외한 HTML 주석 제거
    '/\s+/',                            // 연속 공백 → 단일 공백
    '/>\s+</',                          // 태그 사이 공백 제거
], [
    '',
    ' ',
    '><',
], $_hm_html);
 
// <script>, <pre>, <textarea> 안 내용은 공백 보존 필요
// → 완전한 Minify는 전용 라이브러리 사용 권장
 
echo $_hm_html; // 새 버퍼에 minify된 HTML 출력
 
unset($_hm_ct, $_hm_html);
// ob_end_flush()는 index.php가 처리

⚠️  HTML Minify 주의사항
<script>, <pre>, <textarea>, <code> 내부 내용은 공백이 의미 있을 수 있습니다. 위 코드는 단순화 버전이며, 프로덕션에서는 전용 라이브러리 사용을 권장합니다.
ob_get_clean() + ob_start() 패턴을 사용할 때 이후에 ob_end_flush()를 호출하는 주체(index.php)가 있는지 확인하세요. 없으면 버퍼가 버려집니다.
top에서 01_darkmode_early.php가 ob_start(callback)을 등록한 경우, bottom에서 ob_get_clean()을 호출하면 그 콜백이 실행됩니다. 순서에 주의하세요.


4. 슬롯 간 협력 패턴

하나의 기능을 여러 슬롯의 파일이 협력하여 구현하는 패턴입니다. 내장 파일(다크모드)이 이 패턴을 사용하며, 실전에서도 자주 활용됩니다.


4.1  top → bottom 협력 (데이터 전달)

패턴: top에서  $GLOBALS 에 데이터를 설정하고, bottom에서 해당 데이터를 읽어 HTML에 추가합니다.

📄  extend/top/05_performance_start.php
 
// extend/top/05_performance_start.php
// ─ top: 측정 시작점 기록 ───────────────────────────────
$GLOBALS["_perf"] = [
    "start"      => microtime(true),
    "start_mem"  => memory_get_usage(true),
];

📄  extend/bottom/95_performance_overlay.php
// extend/bottom/95_performance_overlay.php
// ─ bottom: 수집 완료 후 오버레이 출력 ───────────────────
if (!defined("DX_CMS")) exit;
if (!defined("DX_DEBUG") || !DX_DEBUG) return;
 
$_po_perf    = $GLOBALS["_perf"] ?? [];
$_po_elapsed = round((microtime(true) - ($_po_perf["start"] ?? microtime(true))) * 1000, 2);
$_po_memDiff = round((memory_get_usage(true) - ($_po_perf["start_mem"] ?? 0)) / 1024, 1);
$_po_queries = Database::getInstance()->getQueryCount();
 
echo '<div style="position:fixed;bottom:0;left:0;right:0;background:#0F172A;color:#E2E8F0;'
   . 'padding:8px 16px;font-family:monospace;font-size:12px;z-index:99999;'
   . 'display:flex;gap:24px;align-items:center">'
   . '<span>⚡ ' . $_po_elapsed . 'ms</span>'
   . '<span>🗄 ' . $_po_queries . ' queries</span>'
   . '<span>💾 ' . $_po_memDiff . ' KB</span>'
   . '<span>📍 ' . htmlspecialchars($GLOBALS["dx_route"]["type"] ?? "?") . '</span>'
   . '</div>';
 
unset($_po_perf, $_po_elapsed, $_po_memDiff, $_po_queries);

💡  top→bottom 협력 실전 사용 사례
다크모드: top에서 ob_start 등록 → bottom에서 engine.js 로드
성능 측정: top에서 측정 시작 → bottom에서 완료 후 오버레이 출력
보안 헤더 커스터마이징: top에서 nonce 생성 → bottom에서 CSP 헤더 출력 (실제론 헤더 변경 불가하므로 meta http-equiv 삽입)
A/B 테스트: middle에서 그룹 설정 → bottom에서 그룹별 분석 스크립트 삽입


4.2  middle → bottom 협력 (라우트 컨텍스트 활용)

📄  extend/middle/40_breadcrumb.php
 
// extend/middle/40_breadcrumb.php
// ─ middle: 라우트 기반 브레드크럼 데이터 생성 ───────────
if (!defined("DX_CMS")) exit;
 
$_bc_type  = $type ?? "";
$_bc_route = $route ?? [];
 
$_bc_items = [["label" => "홈", "url" => dx_base_url()]];
 
switch ($_bc_type) {
    case "board":
        $boardName = $_bc_route["board"]["board_name"] ?? "게시판";
        $_bc_items[] = ["label" => $boardName,
                        "url" => dx_base_url("board/" . ($_bc_route["board_key"] ?? ""))];
        if (($_bc_route["action"] ?? "") === "view") {
            $_bc_items[] = ["label" => "글 상세", "url" => ""];
        }
        break;
    case "page":
        $_bc_items[] = ["label" => $_bc_route["page"]["title"] ?? "페이지", "url" => ""];
        break;
}
 
$GLOBALS["breadcrumb"] = $_bc_items;
unset($_bc_type, $_bc_route, $_bc_items);

📄  extend/bottom/40_breadcrumb_schema.php
// extend/bottom/40_breadcrumb_schema.php
// ─ bottom: breadcrumb JSON-LD 스키마 삽입 ──────────────
if (!defined("DX_CMS")) exit;
 
$_bcs_items = $GLOBALS["breadcrumb"] ?? [];
if (count($_bcs_items) < 2) return; // 홈만 있으면 스키마 불필요
 
$_bcs_list = [];
foreach ($_bcs_items as $i => $item) {
    $_bcs_list[] = [
        "@type"    => "ListItem",
        "position" => $i + 1,
        "name"     => $item["label"],
        "item"     => $item["url"] ?: dx_current_url(),
    ];
}
 
$_bcs_schema = json_encode([
    "@context" => "https://schema.org",
    "@type"    => "BreadcrumbList",
    "itemListElement" => $_bcs_list,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
 
echo '<script type="application/ld+json">' . $_bcs_schema . '</script>' . "\n";
 
unset($_bcs_items, $_bcs_list, $_bcs_schema);


5. 트러블슈팅 & 디버깅


5.1  실행된 파일 목록 확인

 DxExtend::getExecuted() 는 지금까지 실행된 모든 extend 파일의 슬롯 + 파일명 배열을 반환합니다. 어떤 파일이 실행됐는지 확인하는 데 사용합니다.
📄  extend/bottom/99_debug_extend.php
 
// extend/bottom/99_debug_extend.php (DX_DEBUG 모드에서만 활성)
if (!defined("DX_CMS")) exit;
if (!defined("DX_DEBUG") || !DX_DEBUG) return;
 
$executed = DxExtend::getInstance()->getExecuted();
// 반환 형식: [["slot"=>"top","file"=>"01_darkmode_early.php"], ...]
 
echo '<div style="position:fixed;bottom:50px;right:10px;background:#1E293B;color:#E2E8F0;'
   . 'padding:12px;border-radius:8px;font-family:monospace;font-size:11px;z-index:9999;max-width:300px">'
   . '<strong>Extend 실행 목록</strong><br>';
 
foreach ($executed as $ex) {
    $slotColors = ["top"=>"#EF4444","middle"=>"#F59E0B","bottom"=>"#22C55E"];
    $color = $slotColors[$ex["slot"]] ?? "#94A3B8";
    echo '<span style="color:' . $color . '">['.$ex["slot"].']</span> '
       . htmlspecialchars($ex["file"]) . '<br>';
}
 
echo '</div>';


5.2  자주 발생하는 문제와 해결

증상 원인 해결책
파일이 실행되지 않음 .php 확장자가 없음 (.disabled 상태) 또는 파일이 2단계 이상 하위 폴더에 있음 파일명 확인. 하위 폴더는 1단계만 지원.
변수 undefined 오류 top 파일에서 $type, $route 사용 시도 (middle 컨텍스트 변수) top에서는 $GLOBALS["dx_route"]도 미설정. REQUEST_URI로 판단
header() 경고 bottom에서 헤더 변경 시도 (출력 이미 완료) bottom에서는 헤더 변경 불가. echo로 HTML만 추가
다른 파일 실행 중단 exit/die 호출 exit는 명확한 의도(점검모드 등)에만 사용. 단순 스킵은 return 사용
$GLOBALS 오염 짧은 변수명($type, $ip 등) 사용 고유 접두사 사용 ($_vt_, $_mt_ 등) + 파일 끝 unset()
ob_start 중복 등록 top 파일 여러 개가 모두 ob_start 등록 같은 파일에서 ob_start를 한 번만 등록. 또는 ob_get_level() 확인















Extend 실제 소스 코드 완전 분석 • 12가지 실전 사례

댓글0

로그인 후 댓글을 작성할 수 있습니다.
번호 제목 작성자 날짜 조회
14
Administrator
04.20 61
2. 시작 가이드
서버별 설정 파일 상세
Administrator 04.20 조회 61
13
2. 시작 가이드 기본 폴더 구조 설명
Administrator
04.20 60
2. 시작 가이드
기본 폴더 구조 설명
Administrator 04.20 조회 60
12
2. 시작 가이드 설치 절차
Administrator
04.20 58
2. 시작 가이드
설치 절차
Administrator 04.20 조회 58
11
Administrator
04.20 63
2. 시작 가이드
설치 환경 (PHP 버전, 서버 환경)
Administrator 04.20 조회 63
10
Administrator
04.20 55
비전
DXCMS 비전
Administrator 04.20 조회 55
8
Administrator
04.20 51
라이선스
DXCMS 라이선스 (LGPL 3.0)
Administrator 04.20 조회 51
7
1. DX 철학 / 개념 생태계 확장 전략
Administrator
04.20 45
1. DX 철학 / 개념
생태계 확장 전략
Administrator 04.20 조회 45
6
Administrator
04.20 38
1. DX 철학 / 개념
DXCMS가 지향하는 방향 (플랫폼 vs 단순 CMS)
Administrator 04.20 조회 38
5
Administrator
04.20 42
1. DX 철학 / 개념
프레임워크 + CMS 통합 구조의 의미
Administrator 04.20 조회 42
4
Administrator
04.20 48
1. DX 철학 / 개념
기존 CMS와의 구조적 한계
Administrator 04.20 조회 48
3
1. DX 철학 / 개념 왜 DXCMS를 만들었는가
Administrator
04.20 51
1. DX 철학 / 개념
왜 DXCMS를 만들었는가
Administrator 04.20 조회 51
2
1. DX 철학 / 개념 DXCMS란 무엇인가
Administrator
04.20 54
1. DX 철학 / 개념
DXCMS란 무엇인가
Administrator 04.20 조회 54
1
Administrator
04.12 42
DXCMS 활용 (CMS)
DXCMS 날코딩•막코딩 완전 허용
Administrator 04.12 조회 42
27
전체 회원
248
전체 게시글
126
전체 댓글
396
오늘 방문
25,748
전체 방문
9
현재 접속
인기글 7일 이내
최신글
최신댓글
목록