1. 개요 — DX 컨트롤러 구조의 설계 철학
DXCMS는 두 개의 컨트롤러 레이어를 병행 운영합니다. 하나는 파일 기반의 전통적인 핸들러 방식이고, 다른 하나는 v6.2.0에서 추가된 라라벨 스타일의 클래스 기반 컨트롤러입니다. 두 방식은 서로 대체 관계가 아니라 폴백(Fallback) 관계로 공존합니다.
핵심 원칙: DxRouter(클래스 기반)에 매칭된 라우트가 있으면 해당 컨트롤러를 실행하고, 없으면 Dispatcher(파일 기반) 방식으로 자동 폴백합니다. 기존 코드를 전혀 건드리지 않아도 됩니다.
1.1 두 가지 컨트롤러 방식 비교
| 구분 |
파일 기반 핸들러 |
클래스 기반 컨트롤러 |
| 도입 버전 |
CMS 최초 버전 |
v6.2.0 |
| 진입점 |
Dispatcher::dispatch() |
DxRouter::dispatch() |
| 파일 위치 |
boards/handler.php, core/auth/*.php 등 |
controllers/{이름}.php |
| 라우트 정의 |
URL 세그먼트 자동 파싱 |
routes/*.php에 명시적 선언 |
| 미들웨어 |
없음 (접근제어는 Dispatcher 내부) |
auth, admin, csrf, json, throttle |
| 의존성 주입 |
없음 (싱글턴 직접 호출) |
DxContainer가 자동 주입 |
| URL 파라미터 |
$_GET, $GLOBALS 직접 참조 |
{id}, {slug} 자동 추출 후 $params 전달 |
| 적합한 용도 |
게시판, 관리자, 인증 등 CMS 핵심 기능 |
커스텀 페이지, API, 플러그인 확장 |
2. 컨트롤러 실행 흐름 전체 구조
index.php에서 라우팅이 시작되는 순간부터 컨트롤러가 실행되고 렌더링이 완료될 때까지의 전체 흐름입니다.
HTTP 요청
│
▼
index.php [STEP 5]
│
├── routes/*.php 자동 로드 (DxRouter에 라우트 등록)
│
├── DxRouter::dispatch() ← 클래스 기반 라우터 우선 시도
│ │
│ ├── URI + METHOD 매칭
│ ├── {id}, {slug} 파라미터 추출 → $params[]
│ ├── 미들웨어 실행 (auth, csrf, json ...)
│ ├── DxContainer::call("Controller@method", $params)
│ │ └── loadController() → require controllers/{클래스}.php
│ │ └── build($class) → 생성자 의존성 자동 주입
│ │ └── $controller->method($params)
│ └── return true ← 매칭 완료, 이하 실행 안 함
│
└── (매칭 없음) Dispatcher(Router)::dispatch() ← 파일 기반 폴백
│
├── extend/middle/ 실행
├── switch(라우트 타입)
│ board → boards/handler.php
│ admin → admin/index.php
│ auth → core/auth/{action}.php
│ api → core/api/{action}.php
│ page → pages/{slug}.php + layout
│ home → theme/page/home.php
│ 404 → theme/page/404.php
└── 렌더링 완료
3. 클래스 기반 컨트롤러 — DxRouter + DxContainer
3.1 라우트 등록 — routes/ 폴더
index.php 실행 시 routes/ 폴더의 PHP 파일을 알파벳 순으로 자동 로드합니다. 이 파일 안에서 DxRouter의 정적 메서드로 라우트를 선언합니다.
// routes/web.php
// 기본 라우트 — GET
DxRouter::get('/mypage/dashboard', 'MemberController@dashboard')
->middleware('auth');
// POST + 다중 미들웨어
DxRouter::post('/api/member/update', 'MemberController@update')
->middleware(array('auth', 'csrf'));
// URL 파라미터 — {id} 자동 추출
DxRouter::get('/product/{id}', 'ShopController@show');
// 라우트 그룹 — prefix + 미들웨어 공통 적용
DxRouter::group(array('prefix' => '/shop', 'middleware' => 'auth'), function() {
DxRouter::get('/cart', 'ShopController@cart');
DxRouter::post('/order', 'ShopController@order')->middleware('csrf');
});
// 클로저 라우트 — 컨트롤러 없이 바로 처리
DxRouter::get('/health', function() {
header('Content-Type: application/json');
echo json_encode(array('status' => 'ok', 'version' => DX_VERSION));
exit;
});
// REST 리소스 — index/show/store/update/destroy 자동 등록
DxRouter::resource('/posts', 'PostController');
3.2 지원하는 HTTP 메서드
| DxRouter::get() |
GET 요청 전용 |
| DxRouter::post() |
POST 요청 전용 |
| DxRouter::put() |
PUT 요청 전용 |
| DxRouter::patch() |
PATCH 요청 전용 |
| DxRouter::delete() |
DELETE 요청 전용 |
| DxRouter::any() |
GET + POST 동시 등록 |
| DxRouter::resource() |
index/show/store/update/destroy 6개 라우트 자동 등록 |
3.3 URI 파라미터 추출
라우트 패턴에 {변수명}을 사용하면 실제 요청 URI에서 값을 자동 추출합니다. 추출된 파라미터는 연관 배열($params)로 컨트롤러 메서드에 전달됩니다.
// 라우트 선언
DxRouter::get('/product/{id}', 'ShopController@show');
DxRouter::get('/board/{slug}/{num}', 'BoardController@view');
// 실제 요청: GET /product/42
// 컨트롤러에 전달: $params = array('id' => '42')
// 실제 요청: GET /board/notice/5
// 컨트롤러에 전달: $params = array('slug' => 'notice', 'num' => '5')
3.4 미들웨어
라우트에 체이닝 방식으로 미들웨어를 적용합니다. 미들웨어는 컨트롤러 실행 전에 순서대로 실행됩니다.
| 미들웨어 |
처리 조건 |
처리 내용 |
| auth |
비로그인 요청 |
로그인 페이지로 리다이렉트 (redirect 파라미터 포함) |
| admin |
비관리자 요청 |
403 Forbidden 응답 |
| guest |
로그인 상태 요청 |
홈으로 리다이렉트 |
| csrf |
CSRF 토큰 불일치 |
403 + JSON 에러 응답 |
| json |
모든 요청 |
Content-Type: application/json 헤더 자동 설정 |
| throttle |
향후 확장 |
분당 요청 횟수 제한 (구현 예정) |
| 커스텀 |
클로저 또는 Class@handle |
DxContainer를 통해 실행 |
4. 컨트롤러 클래스 구조와 역할
4.1 컨트롤러 파일 위치
| 일반 컨트롤러 |
controllers/{클래스명}.php |
| 소문자 파일명 |
controllers/{소문자}.php (Controller 접미사 제거) |
| 코어 컨트롤러 |
core/controllers/{클래스명}.php |
| 플러그인 컨트롤러 |
plugins/{플러그인명}/controllers/{클래스명}.php |
컨트롤러 자동 로드: DxContainer::loadController()가 위 경로를 순서대로 탐색합니다. 직접 require를 작성할 필요가 없습니다.
4.2 컨트롤러 클래스 기본 구조
컨트롤러는 일반 PHP 클래스입니다. 특별한 부모 클래스를 상속할 필요가 없으며, 각 퍼블릭 메서드가 하나의 라우트 액션에 대응합니다.
<?php
// controllers/MemberController.php
if (!defined('DX_CMS')) exit('Direct access not allowed.');
class MemberController
{
// 생성자 — DxContainer가 의존성을 자동 주입
public function __construct()
{
// 초기화 (필요한 경우)
}
/**
* GET /mypage/dashboard
* middleware: auth
*
* @param array $params URL 파라미터 (이 라우트는 없음)
*/
public function dashboard(array $params = array())
{
// 1. 서비스 인스턴스 획득
$auth = Auth::getInstance();
$user = $auth->user();
// 2. 데이터 조회
$recentPosts = dx_db('posts')
->where('member_id', $user['id'])
->where('status', 1)
->orderBy('id', 'desc')
->limit(5)
->get();
// 3. 뷰 렌더링
ob_start();
extract(compact('user', 'recentPosts'));
include DX_ROOT . '/pages/mypage/dashboard.php';
$dx_content = ob_get_clean();
// 4. 레이아웃 적용
$layoutFile = DxTheme::getInstance()->resolve('layout/main.php');
if ($layoutFile) include $layoutFile;
else echo $dx_content;
exit;
}
/**
* GET /product/{id}
*
* @param array $params array('id' => '42')
*/
public function show(array $params = array())
{
$id = isset($params['id']) ? (int)$params['id'] : 0;
if (!$id) { http_response_code(404); exit; }
$product = dx_db('products')
->where('id', $id)
->where('status', 1)
->first();
if (!$product) { http_response_code(404); exit; }
ob_start();
extract(compact('product'));
include DX_ROOT . '/pages/shop/product.php';
$dx_content = ob_get_clean();
$layoutFile = DxTheme::getInstance()->resolve('layout/main.php');
if ($layoutFile) include $layoutFile;
else echo $dx_content;
exit;
}
}
5. DxContainer — 의존성 주입(DI) 컨테이너
DxContainer는 라라벨의 서비스 컨테이너와 동일한 철학으로 설계된 경량 DI 컨테이너입니다. 기존의 싱글턴 패턴(getInstance())을 완전히 대체하는 것이 아니라, 그 위에 포장하는 방식으로 100% 하위 호환성을 유지합니다.
5.1 핵심 등록 방식
| bind() |
팩토리 바인딩. make() 호출마다 새 인스턴스 생성 |
| singleton() |
싱글턴 바인딩. 첫 make() 이후 동일 인스턴스 반환 |
| instance() |
이미 생성된 인스턴스를 직접 등록 (항상 싱글턴) |
| alias() |
별칭 등록. dx_app()->make('db')로 Database 반환 |
5.2 기본 등록된 서비스 (registerCoreServices)
CMS 초기화 완료 후 자동으로 핵심 서비스들이 컨테이너에 등록됩니다.
| 'db' / 'database' |
Database::getInstance() — PDO 래퍼 |
| 'auth' |
Auth::getInstance() — 세션 기반 인증 |
| 'secure' |
Secure::getInstance() — 보안 전담 |
| 'cache' |
'DxCache' (클래스명 문자열) — 파일/APCu 캐시 |
| 'hook' / 'hooks' |
HookManager::getInstance() — 훅 시스템 |
| 'seo' |
'DxSeo' (클래스명 문자열) — SEO 헬퍼 |
| 'site' |
DxSite::getInstance() — 멀티사이트 |
| 'theme' |
DxTheme::getInstance() — 테마 엔진 |
5.3 사용 예시 — 플러그인에서 서비스 등록
// plugins/my-plugin/plugin.php
// 팩토리 바인딩 (매번 새 인스턴스)
dx_app()->bind('mailer', function() {
return new MyMailer(dx_config('smtp_host'));
});
// 싱글턴 바인딩
dx_app()->singleton('sms', function() {
return new AlimtalkSMS(dx_config('alimtalk_key'));
});
// 컨트롤러에서 꺼내 쓰기
$mailer = dx_app()->make('mailer'); // 또는 dx_make('mailer')
$mailer->send('to@example.com', '제목', '내용');
5.4 컨트롤러 자동 로드 및 실행 흐름
DxContainer::call("클래스@메서드", $params) 호출 시 내부에서 다음 순서로 처리합니다.
// DxContainer::call() 내부 처리 순서
// 1. 클래스 존재 확인
if (!class_exists($class)) {
$this->loadController($class); // 파일 자동 탐색
}
// 2. loadController() 탐색 순서
// controllers/MemberController.php
// controllers/member.php
// core/controllers/MemberController.php
// plugins/*/controllers/MemberController.php
// 3. 인스턴스 생성 (build)
// ReflectionClass로 생성자 파라미터 분석
// 타입힌트 있으면 컨테이너에서 자동 주입
// 옵셔널 파라미터는 기본값 사용
// 4. 메서드 실행
call_user_func_array(array($controller, $method), $params);
6. 파일 기반 핸들러 — Dispatcher
DxRouter에 매칭된 라우트가 없을 때 실행되는 전통적인 컨트롤러 방식입니다. URL 세그먼트를 분석하여 적절한 핸들러 파일을 자동으로 찾아 실행합니다.
6.1 라우트 타입별 핸들러 파일
| 타입 |
핸들러 파일 |
역할 및 특이사항 |
| home |
themes/{테마}/page/home.php → pages/home.php |
우선순위 폴백 체인으로 홈 렌더링 |
| board |
boards/handler.php |
$GLOBALS에 board, action, id 주입 후 실행 |
| admin |
admin/index.php |
관리자 권한 확인 후 실행. $GLOBALS에 action, sub 주입 |
| auth |
core/auth/{action}.php |
소셜 콜백은 ob_start 버퍼 초기화 후 독립 실행 |
| api |
core/api/{action}.php |
JSON Content-Type 자동 설정 (일부 제외) |
| page |
pages/{slug}.php + layout/main.php |
에러 격리 모드로 실행. standalone 모드 지원 |
| search |
core/search/handler.php |
통합 검색 처리 |
| 404 |
themes/{테마}/page/404.php |
HTTP 404 코드 설정 후 렌더링 |
6.2 데이터 전달 방식 — $GLOBALS 주입
파일 기반 핸들러에서는 라우트 정보를 $GLOBALS를 통해 핸들러 파일에 전달합니다.
// Dispatcher::dispatchBoard() 내부
$GLOBALS['dx_board'] = $board; // 게시판 정보 (DB row)
$GLOBALS['dx_action'] = $action; // 액션 (list/view/write 등)
$GLOBALS['dx_board_skin'] = $boardSkin; // 스킨명
// boards/handler.php에서 수신
$board = $GLOBALS['dx_board'];
$action = $GLOBALS['dx_action'];
$boardSkin = $GLOBALS['dx_board_skin'];
// 관리자 핸들러
$GLOBALS['dx_admin_action'] = $action; // 관리 메뉴 (dashboard, boards 등)
$GLOBALS['dx_admin_sub'] = $sub; // 서브 메뉴
6.3 게시판 핸들러의 스킨 폴백 체인
boards/handler.php는 다음 우선순위로 뷰 파일을 탐색합니다.
// 스킨 독립 핸들러가 있으면 완전 위임
// 1. boards/skins/{스킨}/{액션}/handler.php
// 없으면 표준 뷰 탐색 (_brd_render 함수)
// 2. boards/skins/{스킨}/{액션}.php
// 3. themes/{테마}/board/{스킨}/{액션}.php
// 4. themes/{테마}/board/{액션}.php
// 5. themes/default/board/{스킨}/{액션}.php
// 6. themes/default/board/{액션}.php ← 최종 폴백
7. 데이터 전달 구조 상세
DXCMS에서 컨트롤러(핸들러)와 뷰 파일 사이에 데이터를 전달하는 방법은 세 가지가 있습니다. 각 방식은 사용 맥락에 따라 선택합니다.
7.1 방식 1 — extract()를 이용한 로컬 변수 주입
가장 일반적인 방식입니다. Dispatcher의 renderWithLayout()과 renderPageWithLayout()이 이 방식을 사용합니다.
// 핸들러(컨트롤러)에서
$user = Auth::getInstance()->user();
$posts = dx_db('posts')->where('status', 1)->get();
$context = array('user' => $user, 'posts' => $posts);
// Dispatcher::renderWithLayout() 내부
ob_start();
extract($context); // $user, $posts 변수로 풀림
require $contentFile; // 뷰 파일 실행
$dx_content = ob_get_clean();
extract($context);
require $layoutFile; // layout/main.php에서 $dx_content, $user 사용 가능
7.2 방식 2 — $GLOBALS를 통한 전역 공유
Dispatcher가 핸들러 파일에 데이터를 전달할 때, 또는 핸들러 내부에서 뷰 파일이 참조할 수 있도록 공유 데이터를 등록할 때 사용합니다.
// Dispatcher가 주입
$GLOBALS['dx_route'] = $route; // 현재 라우트 정보
$GLOBALS['dx_board'] = $board; // 게시판 row
$GLOBALS['dx_action'] = $action; // 현재 액션
$GLOBALS['dx_board_skin'] = $skin; // 스킨명
$GLOBALS['dx_handler_context'] = array(...); // 스킨 독립 핸들러 컨텍스트
// 뷰 파일에서 참조
$board = $GLOBALS['dx_board'];
$route = $GLOBALS['dx_route'];
7.3 방식 3 — ob_start/ob_get_clean 버퍼링
컨트롤러에서 뷰 파일의 출력을 캡처하여 $dx_content 변수에 저장하고, 레이아웃 파일(layout/main.php)에서 이를 출력하는 패턴입니다. DXCMS의 모든 페이지 렌더링이 이 구조를 따릅니다.
// 컨트롤러(클래스 기반)
ob_start();
extract(compact('user', 'posts'));
include DX_ROOT . '/pages/mypage.php'; // 뷰 파일 실행 → 버퍼에 쌓임
$dx_content = ob_get_clean(); // 버퍼 내용을 변수로 획득
// 레이아웃 파일 실행
$layoutFile = DxTheme::getInstance()->resolve('layout/main.php');
if ($layoutFile) {
include $layoutFile; // layout/main.php 안에서 echo $dx_content; 호출
}
exit;
// layout/main.php 구조
<!DOCTYPE html>
<html>
<head>...</head>
<body>
<?php include 'header.php'; ?>
<main>
<?php echo $dx_content; ?> // 뷰 파일 출력 결과
</main>
<?php include 'footer.php'; ?>
<?php dx_do_hook('dx_body_bottom'); ?> // 팝업, 스크립트 등
</body>
</html>
7.4 QueryBuilder — 데이터 조회 인터페이스
컨트롤러에서 DB 데이터를 조회할 때는 dx_db() 헬퍼 함수 또는 Database::getInstance()->table()을 통해 QueryBuilder를 사용합니다.
// dx_db() 헬퍼 — QueryBuilder 즉시 반환
$posts = dx_db('posts')
->select(array('id', 'title', 'created_at'))
->where('status', 1)
->where('member_id', '>=', 1)
->orderBy('id', 'desc')
->limit(10)
->get(); // array[] 반환
$post = dx_db('posts')
->where('id', $id)
->first(); // 단일 row 반환 (없으면 null)
// JOIN
$result = dx_db('posts')
->leftJoin('members', 'posts.member_id', '=', 'members.id')
->select(array('posts.*', 'members.nickname'))
->where('posts.status', 1)
->get();
// 페이지네이션
$result = dx_db('posts')
->where('status', 1)
->paginate(20); // array('data' => [], 'total' => N, 'per_page' => 20, ...)
8. extend/middle/ — 핸들러 실행 전 미들웨어 포인트
파일 기반 라우팅에서는 DxRouter의 미들웨어 대신 extend/middle/ 폴더를 통해 모든 요청에 대한 공통 처리를 수행합니다. 라우트 타입이 확정된 직후, 실제 핸들러가 실행되기 직전에 실행됩니다.
extend/middle/은 파일 기반 핸들러와 클래스 기반 컨트롤러 모두에 적용됩니다. DxRouter::dispatch()가 true를 반환하면 Dispatcher::dispatch()는 실행되지 않으므로, extend/middle/은 Dispatcher 내에서만 실행됩니다.
8.1 사용 예시
// extend/middle/01_visit_tracker.php
// $context 변수 사용 가능: type, route
$type = isset($type) ? $type : '';
$route = isset($route) ? $route : array();
// 게시판 페이지 방문만 통계 기록
if ($type === 'board') {
$boardKey = isset($route['slug']) ? $route['slug'] : '';
// 방문자 로그 기록 ...
}
// 점검 모드 (extend/top/에 넣어도 됨)
// if (dx_config('maintenance')) {
// http_response_code(503);
// exit('점검 중입니다.');
// }
9. 훅 시스템 — HookManager
컨트롤러와 뷰 사이, 또는 플러그인과 CMS 코어 사이의 확장 포인트는 훅(Hook) 시스템을 통해 제공됩니다. WordPress의 add_action/do_action과 동일한 개념입니다.
9.1 주요 훅 목록
| dx_after_login |
로그인 완료 직후. $args['user'] = 사용자 row |
| dx_after_logout |
로그아웃 직후. $args['user'] = 로그아웃된 사용자 row |
| dx_body_bottom |
레이아웃 </body> 직전. 팝업, 스크립트 주입 |
| dx_board_before |
게시판 핸들러 진입 직후. board, action, skin, id |
| dx_extend_top |
extend/top/ 실행 완료 후 |
| dx_extend_middle |
extend/middle/ 실행 완료 후 |
| dx_extend_bottom |
extend/bottom/ 실행 완료 후 |
9.2 훅 등록 및 실행
// 훅 등록 (plugins/my-plugin/plugin.php 등에서)
dx_add_hook('dx_body_bottom', function() {
echo '<script>console.log("loaded");</script>';
}, 20); // 20 = 우선순위 (낮을수록 먼저 실행)
// 훅 실행 (레이아웃 파일에서)
dx_do_hook('dx_body_bottom');
// 데이터 전달이 필요한 경우
dx_add_hook('dx_after_login', function($args) {
$user = $args['user'];
// 로그인 이력 기록 ...
}, 10);
dx_run_hook('dx_after_login', array('user' => $user));
10. 컨트롤러의 역할 — 무엇을 해야 하고, 무엇을 하면 안 되는가
10.1 컨트롤러가 해야 하는 일
- 요청 파라미터 수신 및 유효성 검사 ($params[], $_GET, $_POST)
- Auth::getInstance()로 인증/권한 확인
- QueryBuilder / Database로 데이터 조회 및 조작
- 비즈니스 로직 실행 (또는 서비스 클래스에 위임)
- 뷰 파일에 전달할 데이터 준비 (compact, extract)
- ob_start → include 뷰 → ob_get_clean → include 레이아웃 패턴으로 렌더링
- JSON API라면 header(Content-Type) 설정 후 json_encode 출력
- 리다이렉트: dx_redirect() 또는 header(Location:)
10.2 컨트롤러가 하면 안 되는 일
- HTML 마크업을 컨트롤러 파일 안에 직접 출력 — 반드시 뷰 파일로 분리
- 복잡한 비즈니스 로직을 컨트롤러에 모두 구현 — 서비스 클래스로 분리 권장
- 세션을 직접 조작 — Auth 클래스를 통해 처리
- 보안 헤더나 CSRF 검증을 직접 구현 — Secure 클래스 또는 미들웨어 사용
- exit 없이 컨트롤러 종료 — 렌더링 후 반드시 exit 또는 return으로 종료
권장 패턴: 컨트롤러는 얇게(thin controller) 유지하고, 복잡한 로직은 별도의 서비스 클래스나 헬퍼 함수로 분리합니다. 컨트롤러는 요청 수신 → 데이터 준비 → 뷰 렌더링 세 가지만 담당하는 것이 이상적입니다.
10.3 컨트롤러 유형별 역할 요약
| 컨트롤러 유형 |
대표 파일 |
핵심 역할 |
| 게시판 핸들러 |
boards/handler.php |
게시글 CRUD, 댓글, 좋아요, 파일 다운로드. 스킨 폴백 체인으로 뷰 결정 |
| 관리자 핸들러 |
admin/index.php |
관리자 권한 확인, 설정 저장, 회원/게시판/플러그인 관리 |
| 인증 핸들러 |
core/auth/*.php |
로그인/로그아웃/회원가입/마이페이지/소셜 로그인 |
| API 핸들러 |
core/api/*.php |
JSON 응답 전용. 댓글 CRUD, 파일 업로드, 좋아요, 알림 등 |
| 페이지 핸들러 |
Dispatcher::dispatchPage() |
접근 권한 확인, 뷰 파일 실행, 에러 격리 |
| 클래스 컨트롤러 |
controllers/*.php |
커스텀 기능. DI 컨테이너 활용, 미들웨어 지원 |
11. 에러 격리 — 컨트롤러의 방어 설계
DXCMS는 "막코딩 완전 지원"을 설계 목표 중 하나로 삼고 있습니다. 뷰 파일이나 페이지 파일에서 PHP 오류가 발생해도 레이아웃이 깨지지 않고 CMS 전체가 계속 동작하도록 에러 격리 메커니즘을 적용합니다.
11.1 에러 격리 동작 원리
// Dispatcher::renderPageWithLayout() 내부 에러 격리
$pageErrors = array();
// 에러 핸들러 등록 — 뷰 파일의 notice/warning을 로그에만 기록
set_error_handler(function($errno, $errstr, $errfile, $errline) use (&$pageErrors) {
dx_log('[Page] ' . $errstr, 'error'); // data/error.log에 기록
if (defined('DX_DEBUG') && DX_DEBUG) {
$pageErrors[] = '[' . $level . '] ' . $errstr;
}
return true; // PHP 기본 에러 핸들러 실행 방지
});
ob_start();
try {
extract($context, EXTR_SKIP);
include $contentFile; // 뷰 파일 실행
} catch (Exception $e) {
// Exception은 에러 박스로 표시
echo '<div class="dx-error">페이지 오류: ' . htmlspecialchars($e->getMessage()) . '</div>';
}
$dx_content = ob_get_clean();
restore_error_handler();
// 레이아웃은 정상 출력
require $layoutFile;
extend/middle/, extend/top/, extend/bottom/ 모두 동일한 에러 격리(set_error_handler + try-catch)를 적용합니다. 어떤 파일에서 오류가 발생해도 다른 파일의 실행은 계속됩니다.
12. 전체 구조 요약
+---------------------------------------------------------+
| HTTP 요청 |
+---------------------------------+-----------------------+
|
v
+---------------------------------------------------------+
| index.php [STEP 5] 라우팅 시작 |
| |
| routes/*.php 자동 로드 → DxRouter 라우트 등록 |
+---------------------------------+-----------------------+
|
+-----------------------+--------------------+
v v
DxRouter::dispatch() Dispatcher::dispatch()
(클래스 기반, 우선) (파일 기반, 폴백)
| |
URI + METHOD 매칭 Router::resolve()
파라미터 추출 $params 라우트 타입 결정
미들웨어 실행 extend/middle 실행
DxContainer::call() switch(type)
| |
loadController() 핸들러 파일 require
build() — 의존성 주입 boards/handler.php 등
$controller->method($params) |
| |
+--------------+-------------------+
|
데이터 조회
(QueryBuilder / Database)
|
뷰 파일 실행
ob_start → include → ob_get_clean
|
$dx_content 생성
|
layout/main.php 실행
(echo $dx_content + 헤더/푸터)
|
dx_body_bottom 훅
(팝업, 스크립트 등)
|
+------------------------v--------------------------------+
| HTTP 응답 전송 |
+---------------------------------------------------------+