1. 관리자 라우팅 개요
DXCMS 관리자 시스템은 단일 레이아웃 파일(admin/index.php)과 모듈별 콘텐츠 파일(admin/{action}/index.php)로 구성됩니다. 모든 관리자 URL은 /admin/{action}/{sub} 패턴을 따르며, 일반 사용자는 접근 자체가 차단됩니다.
1.1 관리자 라우팅 전체 흐름
HTTP Request: GET /admin/boards/5
│
▼ index.php — 단일 진입점
[STEP 1~4] 인프라 초기화 (DB•세션•Auth)
│
▼
[Router::resolve()]
$first = 'admin' → TYPE_ADMIN
current['action'] = 'boards' ← $second
current['sub'] = '5' ← $third
│
▼
[Dispatcher::dispatchAdmin()]
① Auth::isAdmin() 확인
→ false: dx_redirect(/auth/login?redirect=...)
→ true: 계속 진행
② $GLOBALS['dx_admin_action'] = 'boards'
$GLOBALS['dx_admin_sub'] = '5'
③ require admin/index.php ← 레이아웃 파일
│
▼
[admin/index.php — 레이아웃]
$adminAction = 'boards' (from $GLOBALS)
$adminSub = '5' (from $GLOBALS)
$actionFile = DX_ROOT . '/admin/boards/index.php'
// 파일 없으면 → admin/dashboard/index.php 폴백
│
▼ HTML 렌더링
[사이드바 + 상단바 출력]
dx_run_hook('dx_admin_top', ['action'=>'boards'])
require admin/boards/index.php ← 모듈 콘텐츠
dx_run_hook('dx_admin_bottom', ['action'=>'boards'])
│
▼ HTTP Response (HTML)
2. 라우터(Router) — 관리자 URL 파싱
Router::parse()는 URL 세그먼트의 첫 번째 값이 'admin'인지 확인하고 TYPE_ADMIN으로 분류합니다. 두 번째 세그먼트가 action, 세 번째 세그먼트가 sub가 됩니다.
2.1 URL 세그먼트 파싱
// core/router/Router.php — parse() 내 관리자 처리
if ($first === 'admin') {
$this->current['type'] = self::TYPE_ADMIN; // 'admin'
$this->current['slug'] = 'admin';
$this->current['action'] = $second ? $second : 'dashboard';
$this->current['sub'] = $third;
return;
}
// URL 세그먼트 예시
// /admin → action='dashboard', sub=''
// /admin/boards → action='boards', sub=''
// /admin/boards/5 → action='boards', sub='5'
// /admin/members/add → action='members', sub='add'
// /admin/themes/clean → action='themes', sub='clean'
2.2 URL 패턴 — 액션/서브 조합
| URL |
action |
sub |
처리 내용 |
| /admin |
dashboard |
— |
대시보드 (action 없으면 dashboard 기본값) |
| /admin/boards |
boards |
— |
게시판 목록 |
| /admin/boards/5 |
boards |
5 |
게시판 ID=5 편집 폼 표시 (adminSub='5') |
| /admin/members |
members |
— |
회원 목록 |
| /admin/members/add |
members |
add |
회원 추가 폼 표시 (adminSub='add') |
| /admin/members/123 |
members |
123 |
회원 ID=123 편집 |
| /admin/members/123?tab=monitor |
members |
123 |
회원 모니터링 탭 |
| /admin/pages |
pages |
— |
페이지 목록 (GET: ?new=1 새페이지) |
| /admin/categories |
categories |
— |
카테고리 관리 (GET: ?board_id=X 필터) |
| /admin/menus |
menus |
— |
메뉴 관리 (GET: ?site=domain.com 멀티사이트) |
| /admin/menus/10 |
menus |
10 |
메뉴 ID=10 편집 |
| /admin/themes |
themes |
— |
테마 목록 |
| /admin/themes/clean |
themes |
clean |
'clean' 테마 옵션 편집 |
| /admin/settings |
settings |
— |
사이트 설정 |
| /admin/plugins |
plugins |
— |
플러그인 관리 |
| /admin/market |
market |
— |
DX 마켓 |
| /admin/socket |
socket |
— |
실시간 소켓 관리 |
| /admin/statistics |
statistics |
— |
방문자 통계 |
3. Dispatcher — 관리자 디스패치 처리
Dispatcher::dispatchAdmin()은 관리자 라우팅의 핵심입니다. 인증 확인 → 전역 변수 주입 → 레이아웃 파일 실행의 세 단계로 처리됩니다.
3.1 dispatchAdmin() 코드 분석
// core/router/Dispatcher.php
private function dispatchAdmin()
{
$auth = Auth::getInstance();
// ① 관리자 권한 확인 — role='admin' 필수
if (!$auth->isAdmin()) {
dx_redirect(
dx_base_url('auth/login')
. '?redirect=' . urlencode(dx_current_url())
);
return;
}
// ② 전역 변수 주입 — 레이아웃/모듈이 참조
$GLOBALS['dx_admin_action'] = $this->route['action']
? $this->route['action']
: 'dashboard';
$GLOBALS['dx_admin_sub'] = isset($this->route['sub'])
? $this->route['sub'] : '';
// ③ 레이아웃 파일 실행
$adminFile = DX_ROOT . '/admin/index.php';
if (!file_exists($adminFile)) { $this->dispatch404(); return; }
require $adminFile;
}
3.2 인증 차단 흐름
비관리자 접근 시 로그인 페이지로 리다이렉트되며, 로그인 완료 후 원래 요청 URL로 돌아옵니다. 이는 ?redirect= 쿼리스트링을 통해 구현됩니다.
// 비관리자 접근 시도 흐름
GET /admin/boards
│
▼ Auth::isAdmin() → false
│
▼ dx_redirect(
'/auth/login?redirect=%2Fadmin%2Fboards'
)
│
▼ 로그인 성공 → Auth::login()
│
▼ dx_redirect($_GET['redirect']) → /admin/boards
│
▼ dispatchAdmin() 재실행 → Auth::isAdmin() → true → 정상 접근
4. 관리자 레이아웃 — admin/index.php
admin/index.php는 관리자 UI의 공통 레이아웃을 담당합니다. 사이드바 내비게이션, 상단바, 모듈 콘텐츠 영역, 소켓 연동, 세션 감시 스크립트를 포함합니다.
4.1 레이아웃 구조
// admin/index.php 실행 흐름
$adminAction = $GLOBALS['dx_admin_action']; // 'boards'
$adminSub = $GLOBALS['dx_admin_sub']; // '5'
$actionFile = DX_ROOT . '/admin/' . $adminAction . '/index.php';
// 파일 없으면 대시보드로 폴백
if (!file_exists($actionFile))
$actionFile = DX_ROOT . '/admin/dashboard/index.php';
// $currentPath: 사이드바 active 상태 결정에 사용
$currentPath = strtok(dx_request_uri(), '?'); // ?쿼리 제거
// HTML 출력 순서
// 1) <head>: CSS(style.css, dxb-css.js), jQuery, 사이드바 JS
// 2) <aside>: 사이드바 (프로필 + 내비 + 하단)
// 3) <header>: 상단바 (햄버거 + 브레드크럼 + 날짜)
// 4) <main>:
// dx_run_hook('dx_admin_top', ['action' => $adminAction])
// require $actionFile ← 실제 모듈 콘텐츠
// dx_run_hook('dx_admin_bottom', ['action' => $adminAction])
// 5) <footer>: 버전 정보
// 6) <script>: dx.js, 세션가드, 다크모드 엔진
4.2 사이드바 내비게이션 구조
사이드바는 adm_nav() 헬퍼 함수로 각 메뉴 링크를 출력합니다. 현재 URL과 메뉴 경로를 비교하여 active 클래스를 자동으로 적용합니다.
// adm_nav() — 사이드바 링크 출력 헬퍼
function adm_nav($label, $path, $icon, $cur) {
// active 판정: 현재 URL이 이 경로로 시작하면 active
$active = (
rtrim($cur,'/') === rtrim($path,'/')
|| strpos($cur, $path.'/') === 0
);
$base = dx_base_url(ltrim($path,'/'));
$cls = 'adm-sb-link' . ($active ? ' active' : '');
echo '<a href="'.$base.'#adm-nav-pos" class="'.$cls.'"'
. ' data-nav-link="1">'
. '<span class="icon">'.$icon.'</span>'
. '<span>'.$label.'</span></a>';
}
// adm_section() — 섹션 구분자
function adm_section($label) {
echo '<div class="adm-sb-section">'.$label.'</div>';
}
4.3 사이드바 섹션 구성
| 섹션 |
메뉴 |
URL 경로 |
| 대시보드 |
대시보드 |
/admin |
| 콘텐츠 |
페이지 관리 |
/admin/pages |
| |
팝업 관리 |
/admin/popup |
| |
전체 공지 |
/admin/global_notices |
| |
게시판 관리 |
/admin/boards |
| |
게시판 그룹 |
/admin/board_groups |
| |
카테고리 |
/admin/categories |
| |
인기글 |
/admin/popular |
| |
메뉴 관리 |
/admin/menus |
| 회원 |
회원 목록 |
/admin/members |
| |
메일 보내기 |
/admin/sendmail |
| |
문자 보내기 |
/admin/sendsms |
| |
포인트 관리 |
/admin/points |
| |
레벨 관리 |
/admin/levels |
| |
포인트샵 |
/admin/shop |
| 사이트 |
회원 랭킹 |
/admin/ranking |
| |
통계 |
/admin/statistics |
| |
다운로드 통계 |
/admin/downloads |
| |
실시간 소켓 |
/admin/socket |
| |
플러그인 |
/admin/plugins |
| |
테마 |
/admin/themes |
| |
멀티사이트 |
/admin/sites |
| |
소셜 로그인 |
/admin/social |
| |
사이트 설정 |
/admin/settings |
| 마켓 |
DX 마켓 |
/admin/market |
4.4 브레드크럼 자동 생성
상단바의 브레드크럼은 $_admActionLabels 배열에서 현재 action에 해당하는 한글 레이블을 찾아 표시합니다. adminSub가 있으면 두 번째 뎁스로 추가됩니다.
// admin/index.php — 브레드크럼 렌더링
$_admActionLabels = [
'dashboard' => '대시보드',
'boards' => '게시판 관리',
'members' => '회원 목록',
'pages' => '페이지 관리',
'settings' => '사이트 설정',
'plugins' => '플러그인',
'themes' => '테마',
'socket' => '실시간 소켓',
// ... 25개 action 레이블
];
// 브레드크럼 HTML 출력
// 🏠 홈 아이콘 → 현재 action 레이블 [→ adminSub (있을 때만)]
// 예시: /admin/boards/5 접근 시
// 🏠 > 게시판 관리 > 5
// 예시: /admin/members/add 접근 시
// 🏠 > 회원 목록 > add
4.5 보안 기능 — .htaccess 직접 접근 차단
admin/ 폴더의 .htaccess는 PHP 파일에 대한 모든 직접 HTTP 접근을 차단합니다. 반드시 index.php(단일 진입점)를 통해서만 접근 가능합니다.
# admin/.htaccess
# admin/ 폴더 — PHP 직접 실행 차단, POST는 상위 index.php로
<FilesMatch "\.php$">
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order Allow,Deny
Deny from all
</IfModule>
</FilesMatch>
Options -Indexes
// 결과:
// GET https://example.com/admin/boards/index.php → 403 Forbidden
// GET https://example.com/admin/boards → 정상 (index.php 경유)
// 디렉터리 목록 열람도 차단 (Options -Indexes)
5. 모듈 시스템 — admin/{action}/index.php
각 관리자 기능은 admin/{action}/index.php 파일 하나에 구현됩니다. GET(목록/편집폼)과 POST(저장/삭제)를 같은 파일에서 처리하며, adminSub를 통해 세부 뷰를 분기합니다.
5.1 모듈 파일 처리 패턴
// admin/{module}/index.php 공통 처리 패턴
if (!defined('DX_CMS')) exit('Direct access not allowed.');
$db = Database::getInstance();
$adminSub = isset($GLOBALS['dx_admin_sub']) ? $GLOBALS['dx_admin_sub'] : '';
$msg = ['type' => '', 'text' => ''];
// ── POST 처리 (저장/삭제) ──────────────────────────
if (dx_method('POST')) {
dx_csrf_check(); // CSRF 토큰 검증 필수
$act = dx_post('act');
if ($act === 'add') { /* 추가 */ }
elseif ($act === 'edit') { /* 수정 */ }
elseif ($act === 'delete') { /* 삭제 */ }
dx_redirect(dx_base_url('admin/' . $module));
exit;
}
// ── GET 처리 (목록/편집폼) ─────────────────────────
if ($adminSub && is_numeric($adminSub)) {
$editItem = $db->find($table, ['id' => (int)$adminSub]);
}
// ── HTML 출력 ────────────────────────────────────────
// (admin/index.php가 require로 포함하므로 별도 HTML 헤더 불필요)
?>
<div class="adm-card"> ... </div>
5.2 adminSub 활용 패턴 — 뷰 분기
adminSub는 모듈 내에서 세부 뷰를 분기하는 데 사용됩니다. 숫자이면 해당 ID의 레코드를 편집, 특수 문자열(add 등)이면 해당 뷰를 표시합니다.
| 모듈 |
adminSub 값 |
처리 내용 |
| boards |
(빈값) |
게시판 목록 출력 |
| boards |
5 (숫자) |
$editBoard = $db->find('boards', ['id'=>5]) → 편집 폼 표시 |
| members |
(빈값) |
회원 목록 출력 (GET: ?s= 검색, ?page= 페이지) |
| members |
add |
회원 추가 폼 표시 |
| members |
123 (숫자) |
$editMember = $db->find('members', ['id'=>123]) → 편집 폼 |
| members |
123?tab=monitor |
회원 모니터링 탭 표시 |
| themes |
(빈값) |
테마 목록 출력 |
| themes |
clean (테마명) |
'clean' 테마 옵션 편집 폼 표시 |
| menus |
(빈값) |
메뉴 목록 (GET: ?site=domain.com 멀티사이트 필터) |
| menus |
10 (숫자) |
$editMenu = $db->find('menus', ['id'=>10]) → 편집 폼 |
| pages |
(빈값) |
페이지 목록 (GET: ?new=1 새 페이지 폼) |
6. 관리자 모듈 전체 목록 및 기능
DXCMS v8.1.0은 25개의 관리자 모듈을 제공합니다. 각 모듈은 admin/{module}/index.php 단일 파일로 구현됩니다.
6.1 콘텐츠 관리 모듈
| URL |
파일 크기 |
주요 기능 |
| /admin/dashboard |
23.3KB |
KPI 카드(게시글·회원·게시판·댓글 수), 최근 게시글·회원·댓글, 인기글, 검색어, 방문자 통계, 소켓 실시간 위젯(훅 연동) |
| /admin/boards |
69.8KB |
게시판 CRUD, 스킨 선택, 카테고리 스킨, 에디터 선택, 업로드 설정, 게시판 그룹 연결, 멀티사이트 site_domain, 썸네일 설정, 이동/복사 |
| /admin/board_groups |
14.1KB |
게시판 그룹 CRUD, 그룹별 게시판 목록 연결, 정렬 순서 |
| /admin/categories |
35.2KB |
카테고리 CRUD, 무한 계층(parent_id), 배지 색상, list/view 표시 on/off, 슬러그 자동 생성, 마이그레이션 전 폴백 |
| /admin/pages |
60.5KB |
페이지 CRUD, editor/file 타입, global/theme 위치, 홈 지정(is_home), standalone, 멀티사이트 site_domain, 에디터 콘텐츠 → DX_PAGES/ 파일 저장 |
| /admin/popup |
29.9KB |
팝업 CRUD, 노출 기간(start_at/end_at), 노출 조건(전체/회원/비회원), Z-index, 크기·위치 설정 |
| /admin/global_notices |
32KB |
전체 공지 CRUD, 노출 기간, 상태(active/inactive), 게시판 목록 상단 자동 표시 |
| /admin/menus |
50.2KB |
메뉴 CRUD, 드래그&드롭 정렬(AJAX JSON), 무한 계층, 멀티사이트별 독립 메뉴, 게시판·페이지 빠른 선택 UI |
| /admin/popular |
27.6KB |
인기글 관리, popular_score 기반 순위, 게시판별 필터, 수동 순위 조정 |
6.2 회원 관리 모듈
| URL |
파일 크기 |
주요 기능 |
| /admin/members |
39.8KB |
회원 CRUD, 검색(이름/아이디/이메일), 역할(admin/member) 변경, 상태 변경, 포인트/경험치 직접 수정, 모니터링 탭(last_seen, 접속 이력) |
| /admin/sendmail |
34.5KB |
전체 회원/특정 회원/레벨별 이메일 대량 발송, SMTP/Sendmail 드라이버, 발송 이력 |
| /admin/sendsms |
43.4KB |
SMS 대량 발송, 알리고/NCP/CoolSMS/Twilio 드라이버, 수신자 그룹 필터 |
| /admin/points |
15.1KB |
포인트 로그 조회, 회원별 포인트 내역, 수동 포인트 지급/차감 |
| /admin/levels |
28.5KB |
레벨 설정 CRUD(경험치 기준, 레벨명), DB dx_level_config 테이블 관리 |
| /admin/shop |
26.7KB |
포인트샵 상품 CRUD, 가격(포인트), 재고, 구매 이력 |
6.3 사이트 관리 모듈
| URL |
파일 크기 |
주요 기능 |
| /admin/ranking |
26.1KB |
회원 랭킹, 포인트/경험치/레벨 순위, 기간별 필터 |
| /admin/statistics |
48.5KB |
방문자 통계(일별/주별/월별), 페이지별 조회수, 유입 경로, 검색어 통계 |
| /admin/downloads |
23.3KB |
파일 다운로드 통계, 게시판/파일별 집계 |
| /admin/socket |
55.5KB |
소켓 서버 상태(온라인/오프라인), 실시간 접속자, 기능별 ON/OFF, 멀티도메인 소켓 설정 |
| /admin/plugins |
39.3KB |
플러그인 활성화/비활성화, PluginRegistry 타입별 선택, 플러그인별 설정 필드 |
| /admin/themes |
23.5KB |
테마 활성화, theme.json 메타 표시, 테마 옵션 편집 |
| /admin/sites |
17.5KB |
멀티사이트 CRUD, 도메인별 테마·메뉴그룹·언어·시간대 설정 |
| /admin/social |
14.9KB |
소셜 로그인 설정(카카오/네이버/구글/GitHub), OAuth 키 관리 |
| /admin/settings |
40.9KB |
사이트명·URL·테마·업로드·CAPTCHA·SEO·Analytics·robots.txt·푸터 등 전체 설정, 캐시 초기화, 세션 청소 |
| /admin/market |
34.5KB |
DX 마켓 플러그인/테마 탐색·설치(SHA-256 검증), 개발자 등록, 아이템 등록 |
7. 모듈 내부 요청 처리 패턴
각 관리자 모듈은 GET(목록/폼 표시)과 POST(데이터 처리)를 같은 파일에서 처리합니다. POST 처리 후에는 항상 PRG(Post-Redirect-Get) 패턴을 사용합니다.
7.1 PRG 패턴 (Post-Redirect-Get)
// POST 처리 후 Redirect → 새로고침 시 중복 제출 방지
if (dx_method('POST')) {
dx_csrf_check();
$act = dx_post('act');
if ($act === 'add') {
// 게시판 추가
$newId = $db->insertRow('boards', $data);
// 저장 성공 메시지를 URL 파라미터로 전달
dx_redirect(dx_base_url('admin/boards/' . $newId) . '?saved=1');
exit;
}
if ($act === 'edit') {
$db->updateRow('boards', $data, ['id' => $id]);
dx_redirect(dx_base_url('admin/boards/' . $id) . '?saved=1');
exit;
}
if ($act === 'delete') {
$db->updateRow('boards', ['status' => 0], ['id' => $id]);
dx_redirect(dx_base_url('admin/boards'));
exit;
}
}
// GET 처리 (목록 또는 편집 폼)
if ($adminSub && is_numeric($adminSub)) {
$editBoard = $db->find('boards', ['id' => (int)$adminSub]);
}
$boards = $db->rows("SELECT * FROM dx_boards ORDER BY sort_order");
7.2 AJAX/JSON POST 처리 패턴
메뉴 관리처럼 드래그&드롭 정렬이 필요한 모듈은 JSON 형식의 AJAX 요청을 처리합니다. Content-Type: application/json 헤더로 구분합니다.
// admin/menus/index.php — JSON AJAX 처리
if (
dx_method('POST') &&
!empty($_SERVER['CONTENT_TYPE']) &&
strpos($_SERVER['CONTENT_TYPE'], 'application/json') !== false
) {
header('Content-Type: application/json; charset=utf-8');
$raw = file_get_contents('php://input');
$data = @json_decode($raw, true);
// CSRF 검증 — JSON body의 _csrf를 세션 토큰과 비교
$ajaxCsrf = isset($data['_csrf']) ? $data['_csrf'] : '';
$sessionCsrf = Secure::getInstance()->csrfToken();
if (!$ajaxCsrf || $ajaxCsrf !== $sessionCsrf) {
echo json_encode(['success'=>false, 'error'=>'CSRF token mismatch']);
exit;
}
if ($data['act'] === 'sort_reorder') {
foreach ($data['items'] as $item) {
$db->updateRow('menus',
['sort_order'=>$item['sort_order'], 'parent_id'=>$item['parent_id']],
['id'=> (int)$item['id']]
);
}
echo json_encode(['success'=>true]);
exit;
}
}
7.3 설정 저장 패턴 — DB + 캐시 무효화
관리자 설정 저장은 INSERT ON DUPLICATE KEY UPDATE 패턴으로 누락된 설정도 자동 생성합니다. 저장 후 즉시 캐시를 무효화하고 현재 요청의 $dx_config도 갱신합니다.
// admin/settings/index.php — 설정 저장 패턴
foreach ($fields as $key) {
$val = dx_post($key, '');
// INSERT IGNORE + ON DUPLICATE KEY UPDATE
// → 행이 없으면 INSERT, 있으면 UPDATE
$db->query(
"INSERT INTO dx_settings (setting_key, setting_value, updated_at)"
. " VALUES (?, ?, NOW())"
. " ON DUPLICATE KEY UPDATE"
. " setting_value=VALUES(setting_value), updated_at=NOW()",
[$key, $val]
);
// ① 현재 요청 $dx_config 즉시 갱신 (이번 페이지 렌더링에 반영)
dx_set_config($key, $val);
}
// ② 캐시 무효화 (다음 요청부터 DB에서 새 값 로드)
if (class_exists('DxCache')) {
DxCache::delete('dx_settings');
}
// 설정 로드는 항상 DB 직접 조회 (캐시와 독립)
// → 저장 직후 화면에서 바로 저장된 값이 보임
$settings = _adm_load_settings($db);
// 캐시 전체 초기화 (수동 요청 시)
if (dx_post('cache_flush') === '1') {
DxCache::flush(); // 모든 캐시 삭제
}
7.4 마이그레이션 자동 대응 패턴
각 모듈은 DB 스키마가 구버전과 다를 수 있음을 고려하여 컬럼 존재 여부를 확인하고 없으면 자동으로 추가합니다.
// admin/pages/index.php — 컬럼 자동 추가
$_pgAllCols = array_column(
$db->rows("SHOW COLUMNS FROM dx_pages"),
'Field'
);
foreach ([
'page_type' => "VARCHAR(10) NOT NULL DEFAULT 'editor'",
'page_location' => "VARCHAR(10) NOT NULL DEFAULT 'global'",
'site_domain' => "VARCHAR(191) NOT NULL DEFAULT ''",
'is_standalone' => "TINYINT(1) NOT NULL DEFAULT 0",
] as $col => $def) {
if (!in_array($col, $_pgAllCols)) {
try {
$db->query("ALTER TABLE dx_pages ADD COLUMN `{$col}` {$def}");
$_pgAllCols[] = $col;
} catch (Exception $e) {}
}
}
// admin/categories/index.php — 테이블 없으면 migrate 안내 표시
try {
$db->rows("SELECT 1 FROM dx_categories LIMIT 1");
$_tblOk = true;
} catch (Exception $e) { $_tblOk = false; }
if (!$_tblOk) {
echo 'migrate.php 실행이 필요합니다.';
return;
}
8. 관리자 훅 시스템
관리자 시스템은 HookManager를 통해 외부 코드(플러그인, extend/)가 관리자 UI를 확장할 수 있도록 훅 포인트를 제공합니다.
8.1 관리자 훅 포인트
| 훅 이름 |
발생 위치 |
활용 예시 |
| dx_admin_top |
admin/index.php 본문 상단 |
관리자 페이지 공통 상단 알림, 점검 공지, 커스텀 버튼 추가 |
| dx_admin_bottom |
admin/index.php 본문 하단 |
관리자 페이지 공통 하단 스크립트, 로그 뷰어 추가 |
| dx_admin_dashboard_widgets |
대시보드 위젯 영역 |
dx-socket 플러그인이 실시간 접속 위젯을 대시보드에 삽입 |
// admin/index.php — 훅 실행 위치
<main>
<?php dx_run_hook('dx_admin_top', ['action' => $adminAction]); ?>
<?php require $actionFile; ?> // 모듈 콘텐츠
<?php dx_run_hook('dx_admin_bottom', ['action' => $adminAction]); ?>
</main>
// 플러그인에서 관리자 UI 확장 예시
dx_add_hook('dx_admin_top', function($args) {
if ($args['action'] === 'dashboard') {
echo '<div class="adm-card">실시간 접속자 위젯</div>';
}
}, 10);
// action별 분기 가능
dx_add_hook('dx_admin_top', function($args) {
if ($args['action'] === 'boards') {
echo '<div class="alert">게시판 설정 주의사항...</div>';
}
}, 5);
8.2 소켓 플러그인 관리자 연동
dx-socket 플러그인은 dx_admin_dashboard_widgets 훅을 통해 대시보드에 실시간 접속 위젯을 삽입하고, /admin/socket 모듈로 소켓 서버를 관리합니다.
// plugins/dx-socket/admin/widget.php
// dx_admin_dashboard_widgets 훅에서 include됨
// 소켓 연결 정보를 JavaScript에 주입
$_admSockHtml = '<script>var DX_SOCKET=DX_SOCKET||{};'
. 'DX_SOCKET.wsUrl="' . dx_config('plugin_dx-socket_ws_url','...') . '"'
. 'DX_SOCKET.isAdmin=true;</script>';
// admin/index.php에서 조건부 출력
if (dx_config('plugin_dx-socket_enabled','1')=='1'
&& function_exists('_dx_sock_group')) {
echo $_admSockHtml;
}
9. 관리자 보안 구조
관리자 시스템은 다층 보안 구조를 갖습니다. .htaccess 직접 접근 차단, Auth 인증 확인, CSRF 토큰 검증, 세션 감시 스크립트가 협력합니다.
9.1 보안 계층
| 계층 |
메커니즘 |
상세 |
| 1계층 (웹서버) |
admin/.htaccess |
.php 파일 직접 접근 403 차단. Options -Indexes로 디렉터리 목록 차단 |
| 2계층 (PHP) |
DX_CMS 상수 확인 |
모든 모듈 파일 첫 줄: if (!defined('DX_CMS')) exit('Direct access not allowed.') |
| 3계층 (인증) |
Auth::isAdmin() |
Dispatcher::dispatchAdmin()에서 role='admin' 확인. 실패 시 로그인 페이지로 리다이렉트 |
| 4계층 (CSRF) |
dx_csrf_check() |
모든 POST 처리 첫 번째 줄. 세션 토큰과 POST _csrf 필드 비교 (hash_equals) |
| 5계층 (세션감시) |
SESSION GUARD JS |
dx-session-guard.js가 30초마다 세션 유효성 확인. 만료 시 로그인 페이지로 자동 리다이렉트 |
9.2 세션 감시 스크립트 (SESSION GUARD)
// admin/index.php — 세션 감시 스크립트
window.DX_SESSION_GUARD = {
isLogin : true,
baseUrl : 'https://example.com',
loginUrl : 'https://example.com/auth/login'
};
// dx-session-guard.js 동작 원리
// 30초마다 /api/session_check 요청 → 세션 유효성 확인
// 응답: {logged_in: false} → 로그인 페이지로 자동 이동
// 응답: {logged_in: true} → 계속 유지
// 관리자 탭을 방치해도 세션 만료 시 자동 로그아웃
// 보안 강화: 오래된 세션으로 관리자 작업 방지
9.3 CSRF 이중 검증 — 폼/AJAX 공통
// ① 일반 HTML 폼 POST
// 폼에 hidden 필드 포함
echo dx_csrf_field();
// → <input type="hidden" name="_csrf" value="abc123...">
// 서버에서 검증
dx_csrf_check(); // POST _csrf와 세션 토큰 비교
// ② JSON AJAX POST
// JavaScript에서 토큰을 JSON body에 포함
const csrfToken = document.querySelector('[name=_csrf]').value;
fetch('/admin/menus', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({act: 'sort_reorder', _csrf: csrfToken, items: [...]})
});
// 서버에서 JSON body의 _csrf 직접 검증
$ajaxCsrf = $data['_csrf'] ?? '';
$sessionCsrf = Secure::getInstance()->csrfToken();
if (!hash_equals($sessionCsrf, $ajaxCsrf)) { ... 403 ... }
10. 관리자 프론트엔드 구조
관리자 UI는 dxb-css(Tailwind 호환 런타임 CSS 엔진) + Pretendard 폰트 + Font Awesome 6 아이콘으로 구성됩니다. 별도 빌드 도구 없이 동작합니다.
10.1 CSS/JS 로드 구조
| 리소스 |
역할 및 로드 이유 |
| Pretendard (CDN) |
한국어 최적화 폰트. 가독성 높은 관리자 UI |
| Space Grotesk (Google Fonts) |
숫자·영문 디스플레이 폰트. KPI 카드 수치 표시 |
| Font Awesome 6 (CDN) |
아이콘 라이브러리. 사이드바 메뉴 아이콘 |
| admin/style.css (로컬) |
★ 최우선 로드. 정적 pre-compile 유틸리티 CSS. dxb-css.js보다 먼저 로드해야 첫 paint 전에 스타일 적용 |
| assets/js/dxb-css.js |
★ 동적 CSS 엔진. style.css에 없는 동적 클래스를 JS 런타임에 생성. MutationObserver로 새 클래스 자동 감지 |
| jQuery 3.7.1 (CDN) |
사이드바 토글, 이벤트 핸들링 |
| assets/js/dx.js |
공통 JS. CSRF 토큰 관리, 플래시 메시지 등 |
| dx-session-guard.js |
세션 만료 감지 및 자동 로그아웃 |
| dx-darkmode-engine.js |
다크모드 토글 엔진 |
10.2 사이드바 스크롤 상태 유지
관리자 메뉴를 클릭하면 페이지가 이동되면서 사이드바 스크롤 위치가 초기화되는 문제를 sessionStorage로 해결합니다.
// admin/index.php <head> 인라인 스크립트
var NAV_KEY = 'adm_nav_scroll';
// 메뉴 클릭 시 현재 스크롤 위치 저장
function saveNavScroll() {
var nav = document.getElementById('adm_navi_nav');
if (nav) sessionStorage.setItem(NAV_KEY, nav.scrollTop);
}
// 페이지 로드 시 스크롤 복원
function restoreNavScroll() {
var nav = document.getElementById('adm_navi_nav');
var saved = sessionStorage.getItem(NAV_KEY);
if (saved !== null) nav.scrollTop = parseInt(saved) || 0;
}
// #adm-nav-pos 앵커로 인한 본문 점프 방지
// 링크 href의 #adm-nav-pos를 제거하고 이동
$("[data-nav-link='1']").on("click", function(e) {
saveNavScroll();
var href = this.href.replace(/#adm-nav-pos$/, '');
location.href = href;
e.preventDefault();
});
11. 관리자 라우팅 전체 흐름 요약
┌───────────────────────────────────────────────────────────────┐
│ HTTP Request: GET /admin/boards/5 │
└───────────────────────────────────────────────────────────────┘
│
▼ index.php (단일 진입점)
[STEP 1] Secure.php, Database, functions.php, HookManager 로드
[STEP 2] Secure::initSession(), sendSecurityHeaders()
[STEP 3] data/config.php (DB 연결)
[STEP 4] load_plugins(), DxSite, DxTheme, Auth 초기화
│
▼ [STEP 5] 라우팅
DxRouter::dispatch() → 매칭 없음 → Dispatcher 폴백
Router::resolve()
segments = ['admin', 'boards', '5']
type = TYPE_ADMIN
action = 'boards'
sub = '5'
│
▼
Dispatcher::dispatchAdmin()
┌─ Auth::isAdmin() → false
│ → dx_redirect('/auth/login?redirect=%2Fadmin%2Fboards%2F5')
│ → exit
│
└─ Auth::isAdmin() → true
$GLOBALS['dx_admin_action'] = 'boards'
$GLOBALS['dx_admin_sub'] = '5'
require admin/index.php
│
▼ admin/index.php (레이아웃)
$actionFile = DX_ROOT . '/admin/boards/index.php'
// 파일 없으면 admin/dashboard/index.php 폴백
│
▼ HTML 출력 시작
<head> CSS + JS 로드
<aside> 사이드바 (boards 메뉴 active 표시)
<header> 브레드크럼: 🏠 > 게시판 관리 > 5
<main>
dx_run_hook('dx_admin_top', ['action'=>'boards'])
│
▼ require admin/boards/index.php
$adminSub = '5' (from $GLOBALS)
// adminSub가 숫자 → 편집 폼 표시
$editBoard = $db->find('boards', ['id'=>5])
// POST 없음(GET) → 편집 폼 HTML 출력
│
dx_run_hook('dx_admin_bottom', ['action'=>'boards'])
</main>
<footer> 버전 정보
<script> dx.js, 세션가드, 다크모드
│
▼
┌───────────────────────────────────────────────────────────────┐
│ HTTP Response: HTML (게시판 편집 폼) │
└───────────────────────────────────────────────────────────────┘
✅ 관리자 라우팅 핵심 원칙 요약
1) 단일 레이아웃: admin/index.php가 모든 모듈의 공통 프레임 제공
2) 모듈 분리: admin/{action}/index.php 한 파일로 GET/POST 모두 처리
3) sub 파라미터: URL 세 번째 세그먼트 → 편집할 레코드 ID 또는 특수 문자열
4) 보안 5계층: .htaccess → DX_CMS → isAdmin() → CSRF → 세션가드
5) PRG 패턴: POST 처리 후 반드시 dx_redirect()로 이동 (중복 제출 방지)
6) 훅 확장: dx_admin_top/bottom으로 플러그인이 관리자 UI 확장 가능
7) 마이그레이션 자동 대응: SHOW COLUMNS로 컬럼 확인 후 ALTER TABLE
8) 폴백 안전성: 모듈 파일 없으면 dashboard로 자동 폴백