회원가입 | 고객센터 |
DESIGNONEX
dxcms.kr
로그인 회원가입
고객센터
6. 게시판

게시판 스킨 제작

D DX
2026.05.01 01:32(수정됨) 122 0

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 스킨 등록 절차

  1. boards/skins/ 폴더에 스킨 폴더 생성 (예: boards/skins/my-skin/)
  2. skin.json 파일 생성 (최소 name 필드 포함)
  3. list.php 등 필요한 뷰 파일 작성
  4. FTP/SFTP 또는 서버 파일 관리자로 서버에 업로드
  5. 관리자 → 게시판 관리 → 해당 게시판 수정 클릭
  6. 게시판 스킨 드롭다운에서 새 스킨 선택
  7. 저장 → 즉시 적용 확인


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 속성 정확한지 확인

댓글0

로그인 후 댓글을 작성할 수 있습니다.
3.8 Extend 구조 코어 수정 없이 CMS를 확장하는 방법 2026.05.02 8. 플러그인 플러그인 DX마켓 등록 2026.05.01 8. 플러그인 플러그인 제작 2026.05.01 8. 플러그인 플러그인 구조 2026.05.01 7. 테마 테마 DX마켓 등록 2026.05.01 7. 테마 테마 제작 2026.05.01 7. 테마 테마 구조 2026.05.01 6. 게시판 스킨 DX마켓 등록 2026.05.01 6. 게시판 게시판 스킨 제작 2026.05.01 6. 게시판 댓글 및 답글 구조 2026.05.01 6. 게시판 게시판 구조 2026.05.01 5. 관리자 기능 사용법 DX 마켓 2026.04.21 5. 관리자 기능 사용법 사이트 설정 2026.04.21 5. 관리자 기능 사용법 소셜 로그인 2026.04.21 5. 관리자 기능 사용법 멀티사이트 2026.04.21 5. 관리자 기능 사용법 테마 2026.04.21 5. 관리자 기능 사용법 플러그인 2026.04.21 5. 관리자 기능 사용법 실시간 소켓 2026.04.21 5. 관리자 기능 사용법 다운로드 통계 2026.04.21 5. 관리자 기능 사용법 통계 2026.04.21
31
전체 회원
503
전체 게시글
770
전체 댓글
441
오늘 방문
33,173
전체 방문
2
현재 접속
인기글 7일 이내
최신글
최신댓글
목록