1장. 스킨 시스템 개요
DXCMS의 스킨 시스템은 게시판 화면(HTML•CSS•JS)을 CMS 핵심 로직과 완전히 분리하는 구조입니다. boards/skins/ 폴더에 폴더 하나를 만들면 새로운 스킨이 완성되며, 관리자 화면의 드롭다운에 자동으로 등록됩니다. PHP Notice/Warning이 발생해도 CMS 전체는 계속 동작하는 막코딩 지원 환경입니다.
1.1 스킨 시스템의 핵심 특징
- 폴더 생성만으로 등록 — boards/skins/{이름}/ 폴더를 만들면 관리자 드롭다운에 자동 표시
- 6단계 폴백 체인 — 파일이 없으면 자동으로 상위 단계를 탐색 — 부분 구현만 해도 동작
- 막코딩 지원 — PHP 오류가 스킨에서 발생해도 CMS 전체 동작 유지
- 완전한 독립성 — 독립 핸들러 모드로 DB 쿼리부터 렌더까지 100% 직접 제어 가능
- 커스텀 URL — actions/ 폴더로 /게시판키/{액션} 형태의 완전히 새로운 URL 추가
- 설정 내장 — skin.json의 config 블록으로 스킨 자체 설정값 관리
1.2 스킨과 테마의 차이
| 구분 |
스킨 (boards/skins/) |
테마 (themes/) |
| 역할 |
게시판 화면만 담당 |
사이트 전체 레이아웃·디자인 담당 |
| 적용 범위 |
특정 게시판별로 선택 |
사이트 전체에 적용 |
| 필수 파일 |
skin.json 하나 |
theme.json + layout/main.php 등 |
| 레이아웃 |
DxTheme가 테마 레이아웃에 삽입 |
레이아웃 자체를 정의 |
| 변경 방법 |
게시판 설정에서 스킨 선택 |
관리자 → 테마 설정 |
2장. 스킨 파일 구조
2.1 디렉토리 구조 전체
스킨의 기본 폴더 위치는 boards/skins/{스킨이름}/ 입니다. 스킨 이름은 영문자•숫자•언더스코어•하이픈만 사용 가능합니다.
boards/skins/my-skin/
├── skin.json ← 스킨 메타 정보 (필수)
├── list.php ← 목록 뷰
├── view.php ← 상세 뷰
├── write.php ← 글쓰기/수정 폼
├── style.css ← 스킨 전용 스타일시트
├── parts/ ← 재사용 파셜 (공통 조각)
│ ├── row.php ← 목록 행 파셜
│ └── pagination.php ← 페이지네이션 파셜
├── actions/ ← 커스텀 액션 (표준 외 URL)
│ ├── cart.php ← /게시판키/cart URL
│ └── export.php ← /게시판키/export URL
└── assets/ ← CSS/JS/이미지 (URL 서빙 가능)
├── skin.css ← 에셋 스타일시트
└── skin.js ← 에셋 스크립트
2.2 파일별 역할과 필수 여부
| 파일 |
필수 여부 |
설명 |
| skin.json |
필수 |
스킨 메타 정보. 없으면 스킨 이름·버전·설정을 기본값으로 대체 |
| list.php |
권장 |
목록 뷰. 없으면 themes/default/board/list.php로 폴백 |
| view.php |
선택 |
상세 뷰. 없으면 기본 테마 view.php로 폴백 |
| write.php |
선택 |
글쓰기 폼. 없으면 기본 테마 write.php로 폴백 |
| style.css |
선택 |
스킨 전용 CSS. list.php 상단에서 link 태그로 직접 로드 |
| parts/*.php |
선택 |
재사용 조각. dx_skin_part() 함수로 호출 |
| actions/*.php |
선택 |
커스텀 액션. 파일명이 URL 경로가 됨 |
| assets/* |
선택 |
CSS/JS/이미지. dx_skin_asset() 함수로 URL 획득 |
💡 최소 구현
list.php + skin.json 두 파일만 있어도 완전히 동작하는 스킨이 됩니다.
view.php와 write.php는 기본 테마 파일로 자동 폴백되므로 목록만 커스텀할 때 유용합니다.
3장. skin.json 상세
3.1 skin.json 전체 형식
skin.json은 스킨의 메타 정보와 config 설정값을 담는 JSON 파일입니다. 관리자 드롭다운에 표시될 이름과 설명이 여기서 결정됩니다.
{
"name": "My 스킨", ← 관리자 드롭다운 표시 이름
"version": "1.0.0", ← 스킨 버전
"author": "작성자", ← 제작자 이름
"description": "스킨 설명", ← 관리자 화면 설명
"actions": ["list", "view", "write", "cart"], ← 이 스킨이 처리하는 액션
"config": { ← 스킨 자체 설정값
"date_format": "Y-m-d",
"columns": "3",
"per_page": "20",
"currency_symbol": "₩"
}
}
3.2 skin.json 각 필드 상세
| 필드 |
기본값 |
설명 |
| name |
(폴더명) |
관리자 드롭다운에 표시될 스킨 이름 |
| version |
"1.0.0" |
버전 표시 (기능에는 영향 없음) |
| author |
"" |
제작자 이름 |
| description |
"" |
스킨 설명 (관리자 드롭다운 툴팁) |
| actions |
["list","view","write"] |
이 스킨이 처리하는 액션 목록. 나머지는 기본 테마로 폴백 |
| config |
{} |
dx_skin_config() 함수로 읽을 수 있는 스킨 전용 설정값 |
3.3 actions 필드의 역할
actions 배열에 나열된 액션만 이 스킨이 처리합니다. 목록에 없는 액션은 기본 테마 폴백을 사용합니다. 커스텀 액션(actions/ 폴더)도 반드시 이 배열에 포함해야 URL이 활성화됩니다.
// list만 커스텀, view/write는 기본 테마 사용
"actions": ["list"]
// 전체 표준 액션 + 커스텀 액션 2개 처리
"actions": ["list", "view", "write", "cart", "order"]
3.4 config 설정값 활용
config에 정의한 값은 PHP 뷰 파일에서 dx_skin_config() 함수로 읽습니다. 관리자가 직접 수정할 수 있는 스킨 전용 파라미터를 여기에 담으면 코드를 변경하지 않고 동작을 바꿀 수 있습니다.
// skin.json의 config 읽기
$cols = (int)dx_skin_config("columns", "3"); // 없으면 3 반환
$sym = dx_skin_config("currency_symbol", "₩");
$dateFmt = dx_skin_config("date_format", "Y-m-d");
4장. 6단계 폴백(Fallback) 체인
DxBoardSkin 클래스는 요청된 액션의 뷰 파일을 아래 6단계 순서로 탐색합니다. 앞 단계에서 파일을 찾으면 뒤 단계는 탐색하지 않습니다. 최종 폴백(6단계)은 항상 존재하므로 어떤 스킨을 선택해도 500 오류 없이 동작합니다.
| 순위 |
탐색 경로 |
설명 |
| 1 |
boards/skins/{스킨}/{액션}/handler.php |
완전 독립 핸들러 — DB쿼리부터 렌더까지 직접 처리. 이 파일이 있으면 2~6은 탐색 안 함 |
| 2 |
boards/skins/{스킨}/{액션}.php |
스킨 독립 뷰 파일 (가장 일반적인 방식) |
| 3 |
themes/{현재테마}/board/{스킨}/{액션}.php |
현재 테마의 스킨 전용 뷰 |
| 4 |
themes/{현재테마}/board/{액션}.php |
현재 테마의 공통 뷰 |
| 5 |
themes/default/board/{스킨}/{액션}.php |
기본 테마의 스킨 전용 뷰 |
| 6 |
themes/default/board/{액션}.php |
최종 폴백 (항상 존재, 이것도 없으면 500 오류 박스 표시) |
4.1 폴백 체인 실전 예시
스킨 이름이 "gallery"이고 테마가 "default"일 때 list 액션 탐색 순서:
1. boards/skins/gallery/list/handler.php → 없음
2. boards/skins/gallery/list.php → 있음! ✅ → 여기서 탐색 종료
// 만약 2번도 없다면 계속 탐색:
3. themes/default/board/gallery/list.php → 없음
4. themes/default/board/list.php → 있음! ✅ → 기본 테마 목록 사용
💡 실전 활용 팁
목록만 바꾸고 싶을 때: list.php만 만들면 됩니다 (view, write는 자동 폴백)
레이아웃은 바꾸지 않고 스타일만 바꿀 때: style.css만 만들고 list.php에서 link 태그로 로드
완전히 새로운 UX가 필요할 때: list/handler.php로 독립 핸들러 사용
5장. 뷰 파일 변수 완전 레퍼런스
5.1 list.php — 목록 변수
handler.php가 DB 쿼리 후 $ctx 배열로 주입하는 변수들입니다. 스킨 list.php에서 바로 사용할 수 있습니다.
| 변수명 |
타입 |
설명 |
| $board |
array |
dx_boards 레코드 전체. board_key, board_name, write_level 등 포함 |
| $posts |
array |
현재 페이지 게시글 배열. member_name, member_profile_img도 포함 |
| $notices |
array |
공지글 배열 (use_notice=1일 때, 최대 5개) |
| $globalNotices |
array |
전체 공지 배열 (dx_global_notices, 기간 필터 적용) |
| $total |
int |
전체 게시글 수 (페이지네이션 계산용) |
| $page |
int |
현재 페이지 번호 (1부터 시작) |
| $perPage |
int |
페이지당 글 수 (board.per_page 값) |
| $search |
string |
검색어 (?s= 파라미터) |
| $searchField |
string |
검색 필드 ('both'/'title'/'content'/'author'/'like'/'popular') |
| $categories |
array |
카테고리 배열. depth, name, label, color, show_in_list 포함 |
| $currentCategory |
string |
현재 선택된 카테고리 이름 (?cat= 파라미터) |
| $currentTag |
string |
현재 선택된 태그 (?tag= 파라미터) |
| $sf |
string |
검색 필드 단축 (searchField의 별칭) |
| $cat |
string |
현재 카테고리 (currentCategory의 별칭) |
| $tag |
string |
현재 태그 (currentTag의 별칭) |
| $type |
string |
'board' 고정값 (레이아웃 사이드바 조건용) |
| $slug |
string |
게시판 키 (board_key와 동일) |
5.2 $posts 배열 각 요소의 필드
$posts의 각 게시글 배열에 포함된 필드입니다.
| 필드명 |
설명 |
| id |
게시글 ID (밀리초 타임스탬프, BIGINT) |
| title |
제목 |
| content |
본문 HTML |
| category |
카테고리 이름 (문자열) |
| tags |
쉼표 구분 태그 문자열 |
| thumbnail |
썸네일 이미지 경로 (data/uploads/ 기준) |
| view_count |
조회수 |
| like_count |
좋아요 수 |
| comment_count |
댓글 수 |
| is_notice |
공지 여부 (1=공지) |
| is_secret |
비밀글 여부 (1=비밀) |
| popular_score |
인기점수 (조회×1 + 좋아요×5 + 댓글×3 × 시간감쇠) |
| created_at |
작성일시 (Y-m-d H:i:s) |
| updated_at |
수정일시 |
| member_id |
작성 회원 ID (0=비회원) |
| member_name |
작성자 이름 (LEFT JOIN members) |
| member_profile_img |
프로필 이미지 경로 (LEFT JOIN members) |
| author_name |
비회원 작성자명 |
5.3 view.php 변수
| 변수명 |
설명 |
| $board |
dx_boards 레코드 |
| $post |
게시글 데이터 (member_name, member_profile_img 포함) |
| $files |
첨부파일 배열 (dx_post_files, id 오름차순) |
| $comments |
댓글 배열 (dx_comments + 작성자 정보, id 오름차순) |
| $postLinks |
다중 링크 배열 (dx_post_links, sort_order 오름차순) |
| $prevPost |
이전 글 (id, title, created_at) |
| $nextPost |
다음 글 (id, title, created_at) |
| $viewList / $viewListTotal |
뷰 하단 목록 (show_list_in_view=1일 때) |
| $survey / $surveyQuestions |
설문 데이터 (use_survey=1이고 설문 있을 때) |
| $surveyVoted / $surveyResults |
설문 참여 여부 / 결과 집계 |
| $categories |
카테고리 목록 (사이드바·모바일 탭용) |
| $viewSearch / $viewSf / $viewCat |
목록에서 넘어온 검색/카테고리 파라미터 (목록 버튼 URL 복원용) |
5.4 write.php 변수
| 변수명 |
설명 |
| $board |
dx_boards 레코드 |
| $action |
'write' 또는 'edit' |
| $editPost |
수정 시 기존 글 데이터 (신규 등록 시 null) |
| $editFiles |
수정 시 기존 첨부파일 배열 (신규 시 빈 배열) |
| $editLinks |
수정 시 기존 다중 링크 배열 |
| $categories |
카테고리 목록 (select 옵션용) |
| $editSurvey / $editSurveyQuestions |
수정 시 기존 설문 데이터 |
6장. 스킨 헬퍼 함수 완전 레퍼런스
6.1 스킨 전용 헬퍼
| 함수 시그니처 |
설명 및 반환값 |
| dx_skin_asset('skin.css') |
스킨 assets/ 폴더 파일의 URL 반환 예: .../boards/skins/my-skin/assets/skin.css |
| dx_skin_config('key', '기본값') |
skin.json config 값 읽기. 키가 없으면 기본값 반환 |
| dx_skin_part('row', ['post'=>$post]) |
parts/{이름}.php 파일을 include. 두 번째 인자 변수가 해당 파일에서 사용 가능 |
| dx_skin_meta() |
skin.json 전체를 배열로 반환 (name, version, config 등) |
| $GLOBALS['dx_board_skin'] |
현재 스킨 이름 문자열 (전역 변수) |
6.2 URL/경로 헬퍼
| 함수 시그니처 |
설명 및 반환값 |
| dx_base_url('path') |
사이트 기준 절대 URL 생성. 예: dx_base_url('free/list') → https://site.com/free/list |
| dx_upload_url('path') |
data/uploads/ 기준 파일 URL 반환 |
| dx_upload_exists('path') |
data/uploads/ 기준 파일 존재 여부 확인 (bool) |
| dx_current_url() |
현재 페이지 전체 URL 반환 |
| dx_redirect('url') |
지정 URL로 HTTP 302 리다이렉트 |
6.3 데이터 포맷 헬퍼
| 함수 시그니처 |
설명 및 반환값 |
| dx_date('2026-05-10 12:00', 'Y.m.d') |
날짜 형식 변환. PHP date() 포맷 사용 |
| dx_time_ago('2026-05-09 10:00') |
'1일 전', '3시간 전' 등 상대 시간 반환 |
| dx_filesize(1048576) |
'1.0MB' 등 사람이 읽기 좋은 파일 크기 반환 |
| dx_substr('문자열', 50, '...') |
멀티바이트 안전 문자열 자르기 (UTF-8) |
| dx_mb_substr('문자열', 0, 1) |
멀티바이트 substring (mb_substr 폴백 지원) |
6.4 요청/입력 헬퍼
| 함수 시그니처 |
설명 및 반환값 |
| dx_get('key', '기본값') |
$_GET 파라미터 안전 읽기 (XSS 방어 포함) |
| dx_post('key', '기본값') |
$_POST 파라미터 안전 읽기 |
| dx_method('POST') |
HTTP 메서드 확인 (bool 반환) |
| dx_csrf_field() |
CSRF hidden 필드 HTML 반환. 폼에 반드시 포함해야 함 |
| dx_csrf_check() |
CSRF 토큰 검증. 실패 시 403 오류 |
6.5 페이지네이션 헬퍼
// dx_pagination() 사용법
// urlPattern에 {page}를 넣으면 페이지 번호로 치환됨
echo dx_pagination(
$total, // 전체 글 수
$perPage, // 페이지당 글 수
$page, // 현재 페이지 번호
dx_base_url($board["board_key"] . "/list?page={page}"), // URL 패턴
5 // 좌우 표시할 페이지 수 (기본 5)
);
// 검색어•카테고리를 유지하는 경우:
$qs = "?page={page}";
if ($search) $qs .= "&s=" . urlencode($search) . "&sf=" . urlencode($searchField);
if ($cat) $qs .= "&cat=" . urlencode($cat);
echo dx_pagination($total, $perPage, $page, dx_base_url($board["board_key"] . "/list" . $qs));
6.6 에디터 렌더링
글쓰기 폼에서 에디터(CKEditor 등)를 사용하려면 dx_render_editor()를 호출합니다. 활성화된 에디터 플러그인이 없으면 일반 textarea로 렌더됩니다.
// 기본 에디터 렌더링 (content 필드)
dx_render_editor("content", "", ["board" => $board]);
// 수정 시 기존 내용 채우기
dx_render_editor("content",
isset($editPost["content"]) ? $editPost["content"] : "",
["board" => $board]
);
6.7 기타 유용한 함수
| 함수 시그니처 |
설명 |
| dx_config('key', '기본값') |
CMS 전역 설정값 읽기 (config 테이블) |
| dx_log('메시지', 'error') |
data/error.log에 로그 기록 (info/debug는 기록 안함) |
| dx_error('메시지', 403) |
에러 박스 출력 후 실행 중단 |
| dx_json(['key'=>'value'], 200) |
JSON 응답 출력 후 실행 중단 |
| dx_set_flash('저장되었습니다.') |
다음 페이지 로드 시 한 번만 표시되는 메시지 설정 |
| dx_is_ajax() |
AJAX 요청 여부 확인 (bool) |
| dx_run_hook('훅명', $args) |
훅 실행 — 플러그인/테마와 연동 |
7장. 커스텀 액션 (actions/)
7.1 커스텀 액션이란
boards/skins/{스킨}/actions/ 폴더에 PHP 파일을 만들면 /{게시판키}/{파일명} URL이 자동으로 생성됩니다. 표준 액션(list, view, write 등) 외의 완전히 새로운 기능 페이지를 만들 때 사용합니다
예시: boards/skins/shop/actions/cart.php
→ URL: /{게시판키}/cart
예시: boards/skins/erp/actions/inventory.php
→ URL: /{게시판키}/inventory
예시: boards/skins/report/actions/export.php
→ URL: /{게시판키}/export
7.2 커스텀 액션 기본 코드
커스텀 액션 파일에서는 $GLOBALS["dx_handler_context"]로 핵심 객체에 접근합니다.
<?php
// boards/skins/my-skin/actions/export.php
// URL: /{게시판키}/export
if (!defined("DX_CMS")) exit("Direct access not allowed.");
// 핵심 컨텍스트 수신
$db = $GLOBALS["dx_handler_context"]["db"];
$auth = $GLOBALS["dx_handler_context"]["auth"];
$board = $GLOBALS["dx_handler_context"]["board"];
$action= $GLOBALS["dx_handler_context"]["action"];
// 권한 체크
if (!$auth->isLoggedIn()) {
dx_redirect(dx_base_url("auth/login")); exit;
}
// 비즈니스 로직
$posts = $db->rows(
"SELECT * FROM `{$db->table("posts")}` WHERE board_id=? AND status=1",
[$board["id"]]
);
// 레이아웃에 삽입하여 출력
$dxth = DxTheme::getInstance();
$layoutFile = $dxth->resolve("layout/main.php");
ob_start();
// ... HTML 출력 ...
$dx_content = ob_get_clean();
if ($layoutFile) {
extract(["type"=>"board","slug"=>$board["board_key"],"board"=>$board]);
require $layoutFile;
} else {
echo $dx_content;
}
⚠️ 주의: actions 배열 등록 필수
skin.json의 "actions" 배열에 커스텀 액션 이름을 반드시 포함해야 URL이 활성화됩니다.
"actions": ["list", "view", "write", "cart", "export"]
배열에 없는 액션은 handler.php가 목록 페이지로 리다이렉트합니다.
8장. 독립 핸들러 (완전 자유 모드)
8.1 독립 핸들러란
boards/skins/{스킨}/{액션}/handler.php 파일이 존재하면, boards/handler.php의 표준 처리를 완전히 건너뜁니다. DB 쿼리 방식, 권한 체크, 페이지네이션, 렌더링까지 모든 것을 직접 구현할 수 있는 완전한 자유 모드입니다.
// 독립 핸들러 파일 위치
boards/skins/erp/list/handler.php ← list 액션 완전 독립 처리
boards/skins/erp/view/handler.php ← view 액션 완전 독립 처리
8.2 독립 핸들러 기본 구조
<?php
// boards/skins/my-skin/list/handler.php
// ⚠️ 표준 handler.php의 DB쿼리/권한체크를 완전히 대체합니다
if (!defined("DX_CMS")) exit("Direct access not allowed.");
// 컨텍스트 수신 (handler.php가 주입)
$db = $GLOBALS["dx_handler_context"]["db"];
$auth = $GLOBALS["dx_handler_context"]["auth"];
$board = $GLOBALS["dx_handler_context"]["board"];
$action = $GLOBALS["dx_handler_context"]["action"];
$id = $GLOBALS["dx_handler_context"]["id"];
$boardSkin= $GLOBALS["dx_handler_context"]["boardSkin"];
// 직접 DB 쿼리 — 완전히 자유롭게 구성
$page = max(1, (int)dx_get("page", 1));
$perPage = 12; // 갤러리용 12개
$offset = ($page - 1) * $perPage;
$tbl = $db->table("posts");
$total = (int)$db->value(
"SELECT COUNT(*) FROM `$tbl` WHERE board_id=? AND status=1",
[$board["id"]]
);
$posts = $db->rows(
"SELECT * FROM `$tbl` WHERE board_id=? AND status=1
ORDER BY id DESC LIMIT $perPage OFFSET $offset",
[$board["id"]]
);
// 직접 레이아웃 적용
$dxth = DxTheme::getInstance();
$dxSkin = DxBoardSkin::getInstance();
$skinFile = DX_BOARDS . "/skins/" . $boardSkin . "/list.php";
ob_start();
if (file_exists($skinFile)) {
extract(compact("board","posts","total","page","perPage"), EXTR_SKIP);
include $skinFile;
}
$dx_content = ob_get_clean();
$layoutFile = $dxth->resolve("layout/main.php");
if ($layoutFile) {
extract(["type"=>"board","slug"=>$board["board_key"],"board"=>$board]);
require $layoutFile;
} else {
echo $dx_content;
}
return; // handler.php에 제어 반환하지 않음
8.3 독립 핸들러 vs 일반 스킨 비교
| 항목 |
일반 스킨 (list.php) |
독립 핸들러 (list/handler.php) |
| DB 쿼리 |
표준 handler.php가 처리 |
직접 구현 (완전 자유) |
| 권한 체크 |
표준 handler.php가 처리 |
직접 구현 필요 |
| 캐시 |
handler.php 60초 캐시 자동 적용 |
직접 구현 필요 |
| 페이지네이션 |
handler.php가 계산 후 주입 |
직접 계산 필요 |
| 레이아웃 |
_brd_render()가 자동 적용 |
직접 require 필요 |
| 사용 시점 |
화면 구성만 바꿀 때 |
데이터 구조까지 바꿀 때 |
9장. 에셋 관리 (CSS/JS/이미지)
9.1 에셋 로드 방법
스킨의 CSS와 JS는 뷰 파일(list.php 등) 상단에서 link/script 태그로 직접 로드합니다. dx_skin_asset() 함수로 URL을 생성합니다.
<?php
// list.php 상단에서 스킨 CSS 로드
echo '<link rel="stylesheet" href="' . dx_skin_asset("skin.css") . '?v=' . DX_VERSION . '">';
// assets/ 하위 폴더 파일도 가능
echo '<script src="' . dx_skin_asset("js/skin.js") . '"></script>';
echo '<link rel="stylesheet" href="' . dx_skin_asset("css/dark.css") . '">';
9.2 에셋 URL 경로
dx_skin_asset()이 반환하는 URL은 boards/skins/{스킨}/assets/ 를 기준으로 합니다.
dx_skin_asset("skin.css")
→ https://site.com/boards/skins/my-skin/assets/skin.css
dx_skin_asset("img/logo.png")
→ https://site.com/boards/skins/my-skin/assets/img/logo.png
9.3 CSS 변수 (CSS Custom Properties)
DXCMS 테마는 CSS 변수로 색상•배경을 정의합니다. 스킨 CSS에서 이 변수를 사용하면 다크모드 전환이 자동으로 적용됩니다.
| CSS 변수 |
라이트 모드 기본값 |
용도 |
| var(--p) |
#1a73e8 |
주요 강조색 (블루) |
| var(--bg-card) |
#ffffff |
카드·패널 배경 |
| var(--bg-body) |
#f8fafc |
페이지 전체 배경 |
| var(--border) |
#e2e8f0 |
테두리·구분선 |
| var(--text-main) |
#0f172a |
주요 텍스트 |
| var(--text-muted) |
#64748b |
보조 텍스트 |
| var(--hover-row) |
#f8fafc |
호버 시 배경 |
/* 스킨 CSS에서 CSS 변수 활용 예 */ .my-card {
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-main);
}
.my-card:hover { background: var(--hover-row); }
.my-title { color: var(--p); }
/* 다크모드는 body.dark 선택자로 추가 정의 */
body.dark .my-card { box-shadow: 0 2px 8px rgba(0,0,0,.3); }
10장. 파셜(parts/) 활용
10.1 파셜이란
parts/ 폴더의 PHP 파일은 뷰 파일에서 dx_skin_part() 함수로 재사용 가능한 HTML 조각입니다. 목록 행 하나를 row.php로 분리하거나, 페이지네이션을 pagination.php로 만들어두면 코드가 깔끔해집니다.
10.2 파셜 만들기 및 호출
// 파일: boards/skins/my-skin/parts/row.php
// $post 변수가 주입됨
<?php if (!defined("DX_CMS")) exit; ?>
<div class="my-row">
<a href="<?php echo dx_base_url($board["board_key"]."/view/".$post["id"]); ?>">
<?php echo htmlspecialchars($post["title"], ENT_QUOTES, "UTF-8"); ?>
</a>
<span><?php echo dx_date($post["created_at"], "Y.m.d"); ?></span>
</div>
// list.php에서 호출:
<?php foreach ($posts as $post): ?>
<?php dx_skin_part("row", ["post" => $post, "board" => $board]); ?>
<?php endforeach; ?>
10.3 파셜 폴백
dx_skin_part()는 boards/skins/{스킨}/parts/{이름}.php를 찾고, 없으면 현재 테마의 parts/{이름}.php로 폴백합니다. 테마에서 제공하는 공통 파셜을 스킨에서 그대로 활용할 수 있습니다.
11장. 글쓰기 폼(write.php) 구현
11.1 write.php 필수 요소
커스텀 write.php를 만들 때 반드시 포함해야 하는 요소들입니다.
<?php
if (!defined("DX_CMS")) exit;
// 폼 액션 URL 설정 (신규/수정 자동 분기)
$isEdit = ($action === "edit" && $editPost);
$formAction = $isEdit
? dx_base_url($board["board_key"]."/edit/".$editPost["id"])
: dx_base_url($board["board_key"]."/write/");
?>
<form method="post" action="<?php echo $formAction; ?>"
enctype="multipart/form-data" id="dx-write-form">
<?php echo dx_csrf_field(); ?> <!-- ⚠️ 반드시 포함 -->
<!-- 제목 -->
<input type="text" name="title" required maxlength="191"
value="<?php echo $isEdit ? htmlspecialchars($editPost["title"],ENT_QUOTES) : ""; ?>">
<!-- 에디터 (플러그인 에디터 또는 textarea) -->
<?php dx_render_editor("content",
$isEdit ? $editPost["content"] : "",
["board" => $board]
); ?>
<!-- 카테고리 선택 (use_category=1일 때) -->
<?php if (!empty($board["use_category"]) && !empty($categories)): ?>
<select name="category">
<option value="">분류 선택</option>
<?php foreach ($categories as $cat): ?>
<option value="<?php echo htmlspecialchars($cat["name"],ENT_QUOTES); ?>"
<?php echo ($isEdit && $editPost["category"]===$cat["name"]) ? "selected" : ""; ?>>
<?php echo htmlspecialchars($cat["label"],ENT_QUOTES); ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
<!-- 파일 첨부 (use_file=1일 때) -->
<?php if (!empty($board["use_file"])): ?>
<input type="file" name="files[]" multiple
accept="*/*">
<?php endif; ?>
<button type="submit"><?php echo $isEdit ? "수정" : "등록"; ?></button>
</form>
11.2 기존 첨부파일 수정 처리
수정 모드에서 기존 첨부파일을 표시하고 삭제 체크박스를 제공하는 코드입니다.
<?php if ($isEdit && !empty($editFiles)): ?>
<div class="existing-files">
<h4>기존 첨부파일</h4>
<?php foreach ($editFiles as $f): ?>
<div>
<span><?php echo htmlspecialchars($f["orig_name"], ENT_QUOTES); ?></span>
<span>(<?php echo dx_filesize($f["file_size"]); ?>)</span>
<label>
<input type="checkbox" name="delete_files[]"
value="<?php echo (int)$f["id"]; ?>">
삭제
</label>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
12장. 실전 스킨 예제
12.1 카드형 목록 스킨 (최소 구현)
가장 간단한 카드형 목록 스킨 전체 코드입니다. skin.json + list.php 두 파일만으로 완성됩니다.
skin.json
{
"name": "카드형",
"version": "1.0.0",
"author": "나의스킨",
"description": "카드 그리드 목록 스킨",
"actions": ["list"],
"config": {
"columns": "3"
}
}
list.php
<?php if (!defined("DX_CMS")) exit; ?>
<?php
$cols = (int)dx_skin_config("columns", "3");
$bk = $board["board_key"];
$auth = Auth::getInstance();
$canWrite = ((int)$board["write_level"]===0)
|| ((int)$board["write_level"]===1 && $auth->isLoggedIn())
|| ((int)$board["write_level"]===9 && $auth->isAdmin());
?>
<style>
.card-grid { display:grid; grid-template-columns:repeat(<?php echo $cols; ?>,1fr); gap:16px; padding:16px; }
.card { background:var(--bg-card); border:1px solid var(--border); border-radius:12px; overflow:hidden; }
.card img { width:100%; aspect-ratio:16/9; object-fit:cover; }
.card-body { padding:12px; }
.card-title { font-size:.9rem; font-weight:700; color:var(--text-main); margin:0 0 8px; }
.card-meta { font-size:.75rem; color:var(--text-muted); }
@media(max-width:640px){ .card-grid{ grid-template-columns:1fr; } }
</style>
<div style="padding:16px 16px 0; display:flex; justify-content:space-between; align-items:center">
<h2 style="font-size:1.1rem;font-weight:800;color:var(--text-main)">
<?php echo htmlspecialchars($board["board_name"], ENT_QUOTES, "UTF-8"); ?>
<span style="font-size:.8rem;color:var(--text-muted);margin-left:8px"><?php echo number_format($total); ?>개</span>
</h2>
<?php if ($canWrite): ?>
<a href="<?php echo dx_base_url($bk."/write"); ?>"
style="padding:8px 16px;background:var(--p);color:#fff;border-radius:8px;text-decoration:none;font-size:.85rem;font-weight:700">글쓰기</a>
<?php endif; ?>
</div>
<div class="card-grid">
<?php if (empty($posts)): ?>
<div style="grid-column:1/-1;text-align:center;padding:60px 20px;color:var(--text-muted)">
등록된 글이 없습니다.
</div>
<?php else: ?>
<?php foreach ($posts as $post): ?>
<div class="card">
<?php if (!empty($post["thumbnail"])): ?>
<img src="<?php echo dx_upload_url($post["thumbnail"]); ?>"
alt="<?php echo htmlspecialchars($post["title"], ENT_QUOTES); ?>">
<?php endif; ?>
<div class="card-body">
<a href="<?php echo dx_base_url($bk."/view/".$post["id"]); ?>" style="text-decoration:none">
<h3 class="card-title"><?php echo htmlspecialchars($post["title"], ENT_QUOTES, "UTF-8"); ?></h3>
</a>
<div class="card-meta">
<?php echo dx_date($post["created_at"], "Y.m.d"); ?> |
조회 <?php echo number_format($post["view_count"]); ?>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<!-- 페이지네이션 -->
<div style="padding:16px;text-align:center">
<?php
$qs = "?page={page}";
if ($search) $qs .= "&s=".urlencode($search)."&sf=".urlencode($searchField);
if ($cat) $qs .= "&cat=".urlencode($cat);
echo dx_pagination($total, $perPage, $page, dx_base_url($bk."/list".$qs));
?>
</div>
12.2 ERP형 스킨 (커스텀 액션 포함)
사내 ERP 게시판 스킨으로, 재고 조회 커스텀 액션을 포함하는 예시입니다.
디렉토리 구조
boards/skins/erp/
├── skin.json
├── list.php ← 발주 목록 (테이블형)
├── write.php ← 발주 등록 폼 (커스텀 필드)
├── actions/
│ ├── inventory.php ← /board-key/inventory (재고 현황)
│ └── export.php ← /board-key/export (엑셀 내보내기)
└── assets/
└── erp.css
skin.json
{
"name": "ERP 발주 게시판",
"version": "2.0.0",
"author": "내부 개발팀",
"description": "발주•재고•내보내기 기능이 포함된 ERP 스킨",
"actions": ["list", "view", "write", "inventory", "export"],
"config": {
"company_name": "주식회사 예시",
"export_format": "xlsx"
}
}
actions/inventory.php
<?php
if (!defined("DX_CMS")) exit;
$db = $GLOBALS["dx_handler_context"]["db"];
$auth = $GLOBALS["dx_handler_context"]["auth"];
$board = $GLOBALS["dx_handler_context"]["board"];
if (!$auth->isAdmin()) {
dx_error("관리자만 접근 가능합니다.", 403);
}
$company = dx_skin_config("company_name", "회사명");
// 직접 DB 조회
$posts = $db->rows(
"SELECT * FROM `{$db->table("posts")}` WHERE board_id=? AND status=1 ORDER BY id DESC",
[$board["id"]]
);
$dxth = DxTheme::getInstance();
ob_start();
?>
<h2><?php echo $company; ?> 재고 현황</h2>
<table>...<tr>...</tr></table>
<?php
$dx_content = ob_get_clean();
if ($layoutFile = $dxth->resolve("layout/main.php")) {
extract(["type"=>"board","slug"=>$board["board_key"],"board"=>$board]);
require $layoutFile;
} else echo $dx_content;
13장. 기본 제공 스킨 분석
13.1 gallery 스킨
boards/skins/gallery/ — 썸네일 그리드 갤러리 스킨입니다.
| 파일 |
설명 |
| skin.json |
name=갤러리, actions=["list","view","write"] |
| list.php |
4열 썸네일 그리드, 7일 인기글 HOT 배지, 관리자 bulk 지원 |
| view.php |
기본 테마 view.php 그대로 사용 (갤러리 목록만 다름) |
| write.php |
기본 테마 write.php 사용 |
| style.css |
.gl55-grid, .gl55-card, .gl55-thumb-wrap 등 갤러리 전용 CSS |
| _list_rows.php |
view 하단 목록 partial (_list_rows 어댑터 패턴) |
주요 특징: board.thumb_w/thumb_h 설정값으로 썸네일 크기 동적 조정, CSS 변수로 다크모드 자동 대응, 7일 인기글 TOP9에 HOT 배지 표시
13.2 shop 스킨
boards/skins/shop/ — 쇼핑몰용 상품 목록 스킨입니다.
| 파일 |
설명 |
| skin.json |
columns(3), show_price(1), currency_symbol(₩) 등 config 활용 |
| list.php |
dx_skin_config()로 columns 읽어 CSS grid 동적 생성, 상품 카드형 출력 |
| style.css |
.shop-card, .shop-card-body, .price, .btn 등 쇼핑몰 전용 CSS |
| actions/cart.php |
/게시판키/cart — 장바구니 커스텀 액션 예제 |
주요 특징: config의 columns 값으로 그리드 열 수 조정, 커스텀 액션(cart)으로 장바구니 URL 구현, parts/ 폴더 구조 예시 포함
13.3 스킨 소스 위치 우선순위
DxBoardSkin::allSkins()가 관리자 드롭다운을 구성하는 순서입니다.
| 우선순위 |
소스 경로 |
레이블 |
| 1 |
boards/skins/{이름}/ |
커스텀 스킨 |
| 2 |
themes/{현재테마}/board/{이름}/ |
테마 스킨 ({테마명}) |
| 3 |
themes/default/board/{이름}/ |
기본 테마 스킨 |
| 4 |
(내장) |
내장 기본 (default) |
14장. 스킨에서 훅(Hook) 연동
14.1 스킨이 활용할 수 있는 훅
| 훅 이름 |
발생 시점 / 활용 방법 |
| dx_board_list_context |
$ctx 배열 확정 직전 — 목록에 추가 데이터 주입 가능 (참조 전달) |
| dx_board_view_context |
$ctx 배열 확정 직전 — 뷰에 추가 데이터 주입 |
| dx_board_write_context |
$ctx 배열 확정 직전 — 글쓰기 폼에 추가 필드 주입 |
| dx_board_before_save |
글 저장 직전 — $data에 커스텀 필드 추가 가능 |
| dx_after_write |
글 저장 완료 후 — 외부 시스템 연동, 알림 발송 등 |
| dx_board_after_save |
저장 후 redirect URL 변경 가능 |
| dx_after_comment |
댓글 등록 완료 후 |
14.2 훅 활용 예시
스킨 또는 플러그인의 plugin.php에서 dx_add_hook()으로 등록합니다.
// 목록 컨텍스트에 추가 데이터 주입 예시
dx_add_hook("dx_board_list_context", function($args) {
// $ctx를 참조로 받아 수정 가능
$args["context"]["my_extra"] = "추가 데이터";
// 이후 list.php에서 $my_extra 변수로 사용 가능
});
// 글 저장 전 커스텀 필드 추가
dx_add_hook("dx_board_before_save", function($args) {
// POST로 받은 커스텀 필드를 $data에 추가
$args["data"]["custom_field"] = isset($_POST["custom_field"])
? htmlspecialchars($_POST["custom_field"], ENT_QUOTES)
: "";
});
15장. 관리자에서 스킨 등록 및 선택
15.1 스킨 등록 절차
- boards/skins/ 폴더에 스킨 폴더 생성 (예: boards/skins/my-skin/)
- skin.json 파일 생성 (최소 name 필드 포함)
- list.php 등 필요한 뷰 파일 작성
- FTP/SFTP 또는 서버 파일 관리자로 서버에 업로드
- 관리자 → 게시판 관리 → 해당 게시판 수정 클릭
- 게시판 스킨 드롭다운에서 새 스킨 선택
- 저장 → 즉시 적용 확인
15.2 스킨 드롭다운 표시 조건
아래 조건을 만족하면 관리자 드롭다운에 자동으로 표시됩니다:
- boards/skins/ 하위 폴더 — 폴더명이 영문자•숫자•언더스코어•하이픈으로만 구성
- skin.json 존재 — 없어도 폴더 이름으로 표시되지만 이름/설명이 폴더명 그대로
- 폴더 권한 — 웹서버가 읽을 수 있는 권한 필요 (755 또는 644 이상)
15.3 스킨 개발 시 DX_DEBUG 활용
스킨 개발 중에는 config.php에서 DX_DEBUG를 true로 설정하면 PHP 오류가 화면에 표시됩니다:
// config.php (개발 시에만)
define('DX_DEBUG', true);
// DX_DEBUG=true 효과:
// - 스킨 파일의 PHP Notice/Warning이 화면 하단 파란 박스에 표시
// - Exception 발생 시 파일/라인 번호 포함한 상세 오류 표시
// - ⚠️ 프로덕션 환경에서는 반드시 false로 유지
15.4 자주 발생하는 문제와 해결
| 증상 |
원인 및 해결 |
| 드롭다운에 스킨이 안 보임 |
boards/skins/ 하위에 폴더가 있는지 확인. 폴더명에 한글이나 특수문자 없는지 확인 |
| 흰 화면 또는 스킨 오류 박스 |
DX_DEBUG=true로 오류 내용 확인. data/error.log 파일 확인 |
| CSS/JS가 적용 안 됨 |
dx_skin_asset() URL 확인. assets/ 폴더 경로/권한 확인. ?v=DX_VERSION 캐시 버스팅 추가 |
| 커스텀 액션 URL이 404 |
skin.json의 actions 배열에 액션 이름 포함 여부 확인. 파일명 일치 여부 확인 |
| 독립 핸들러가 작동 안 함 |
{액션}/handler.php 경로 확인. return 구문 마지막에 포함되어 있는지 확인 |
| 수정 시 파일 삭제 안 됨 |
write.php에 delete_files[] 체크박스 name 속성 정확한지 확인 |