회원가입 | 고객센터 |
DESIGNONEX
dxcms.kr
로그인 회원가입
고객센터
7. 테마

테마 제작

D DX
2026.05.01 01:36(수정됨) 128 0

1장. 테마 제작 준비

DXCMS 테마 제작은 PHP와 HTML/CSS 기본 지식만 있으면 시작할 수 있습니다. 복잡한 빌드 도구 없이 파일을 만들고 서버에 올리면 바로 동작합니다. 이 가이드는 처음부터 실제 동작하는 테마를 만드는 과정을 단계별로 설명합니다.


1.1 필요한 사전 지식

  • PHP 기본 — if/foreach/echo, 함수 호출, 변수 사용 정도면 충분
  • HTML/CSS — 페이지 마크업, Flexbox/Grid, CSS 변수(Custom Properties)
  • DXCMS 구조 이해 — 테마 구조 가이드의 폴백 체인, $dx_content, $context 개념


1.2 필수 파일 2개

테마를 만들려면 최소 2개 파일이 필요합니다. 이 2개만 있어도 완전히 동작합니다.
 
파일 역할
themes/{테마명}/theme.json 테마 메타 정보 (이름·버전·옵션 정의). 없으면 폴더명이 그대로 표시됨
themes/{테마명}/layout/main.php HTML 전체 골격 (DOCTYPE~</html>). $dx_content 위치가 본문 영역


1.3 테마 폴더 생성

  1. themes/ 폴더 안에 새 폴더 생성 (예: themes/my-theme/)
  2. 폴더명 규칙: 영소문자, 숫자, 하이픈, 언더스코어만 사용
  3. themes/my-theme/theme.json 파일 생성
  4. themes/my-theme/layout/ 폴더 생성
  5. themes/my-theme/layout/main.php 파일 생성
  6. 관리자 → 테마 관리 → 새 테마 활성화

💡 개발 환경 팁
로컬 환경에서 개발 후 FTP/SFTP로 서버에 업로드하는 방식이 가장 편리합니다.
PHP DX_DEBUG 모드를 켜두면 오류 메시지가 화면에 표시됩니다:
  config.php에 define("DX_DEBUG", true); 추가
VSCode + PHP Intelephense 확장을 사용하면 자동완성이 지원됩니다.


2장. theme.json 작성


2.1 기본 theme.json

{
    "name": "나의 테마",
    "version": "1.0.0",
    "author": "홍길동",
    "description": "DXCMS용 커스텀 테마",
    "preview": "assets/preview.png",
    "supports": ["board", "page", "gallery"]
}


2.2 options 추가

options 블록을 추가하면 관리자가 색상•텍스트 등을 직접 바꿀 수 있는 UI가 자동 생성됩니다.
{
    "name": "나의 테마",
    "version": "1.0.0",
    "author": "홍길동",
    "description": "관리자 옵션 지원 테마",
    "supports": ["board", "page"],
    "options": {
        "primary_color": {
            "type": "color",
            "label": "주요 색상",
            "default": "#3b82f6"
        },
        "logo_text": {
            "type": "text",
            "label": "로고 텍스트 (비우면 사이트 이름 표시)",
            "default": ""
        },
        "show_search": {
            "type": "checkbox",
            "label": "헤더 검색창 표시",
            "default": "on"
        },
        "footer_text": {
            "type": "textarea",
            "label": "푸터 문구",
            "default": "All rights reserved."
        }
    }
}


2.3 options 타입 완전 가이드

type 관리자 UI 저장 값 예시 및 읽기
"color" 색상 피커 (색상 선택기) "#3b82f6" → dx_theme_option("primary_color")
"text" 한 줄 텍스트 입력 "안녕하세요" → dx_theme_option("logo_text")
"textarea" 여러 줄 텍스트 "한 줄 두 줄" → dx_theme_option("footer_text")
"checkbox" 체크박스 (ON/OFF) "on" 또는 "" → dx_theme_option("show_search") === "on"
"select" 드롭다운 선택 "choices" 배열 별도 정의 필요 (아래 예시 참고)


select 타입 예시

"layout_style": {
    "type": "select",
    "label": "레이아웃 스타일",
    "default": "wide",
    "choices": {
        "wide":   "와이드 (1200px)",
        "normal": "일반 (960px)",
        "narrow": "좁게 (760px)"
    }
}

// 테마 파일에서 읽기:
$layout = dx_theme_option("layout_style", "wide");
$maxWidth = ($layout === "wide") ? "1200px" : (($layout === "narrow") ? "760px" : "960px");


2.4 options 값 읽기

theme.json에 정의한 옵션은 테마 PHP 파일 어디서든 dx_theme_option()으로 읽을 수 있습니다.
// layout/main.php 또는 page/home.php 등 테마 파일에서

// color 타입 읽기 → CSS 변수에 주입
$primaryColor = dx_theme_option("primary_color", "#3b82f6");

// text 타입 읽기 → HTML에 출력
$logoText = dx_theme_option("logo_text", "");
$siteName = dx_config("site_name", "DXCMS");
$displayName = $logoText ? $logoText : $siteName;

// checkbox 타입 읽기 → 조건 분기
$showSearch = dx_theme_option("show_search", "on") === "on";

// select 타입 읽기 → 분기 처리
$layout = dx_theme_option("layout_style", "wide");


3장. layout/main.php 단계별 구현

layout/main.php는 테마의 뼈대 파일입니다. 모든 페이지가 이 파일을 통해 렌더링됩니다. 각 섹션을 단계별로 구현하는 방법을 설명합니다.


3.1 파일 시작부 — PHP 변수 준비

<?php
/**
 * 나의 테마 — layout/main.php
 */
if (!defined("DX_CMS")) exit("Direct access not allowed.");

// ── 기본 사이트 정보 ──────────────────────────────
$siteName     = dx_config("site_name", "내 사이트");
$siteDesc     = dx_config("site_description", "");
$language     = dx_config("language", "ko");

// ── 테마 옵션 ────────────────────────────────────
$primaryColor = dx_theme_option("primary_color", "#3b82f6");
$logoText     = dx_theme_option("logo_text", "");
$showSearch   = dx_theme_option("show_search", "on") === "on";
$footerText   = dx_theme_option("footer_text", "All rights reserved.");

// ── 현재 페이지 컨텍스트 ─────────────────────────
$ctx      = isset($context) ? $context : array();
$pageType = isset($ctx["type"]) ? $ctx["type"] : "home";
$isBoard  = ($pageType === "board");

// ── 로그인 상태 ──────────────────────────────────
$isLogin = dx_is_login();
$isAdmin = dx_is_admin();
$authUser = $isLogin ? Auth::getInstance()->user() : null;
$userName = ($isLogin && $authUser) ? $authUser["name"] : "";

// ── SEO 타이틀 ──────────────────────────────────
$pageTitle = $siteName;
if (class_exists("DxSeo")) {
    if (!DxSeo::get("title")) DxSeo::build($pageType, $ctx);
    $pageTitle = DxSeo::get("title", $siteName);
}

// ── 메뉴 로드 ────────────────────────────────────
$db = Database::getInstance();
$menuGroup = dx_menu_group();
$menus = $db->findAll("menus",
    array("menu_group"=>$menuGroup,"parent_id"=>0,"status"=>1),
    "*", "sort_order ASC"
);
$allSubs = $db->findAll("menus",
    array("menu_group"=>$menuGroup,"status"=>1),
    "*", "sort_order ASC"
);
$subMap = array();
foreach ($allSubs as $s) {
    if ((int)$s["parent_id"] > 0)
        $subMap[(int)$s["parent_id"]][] = $s;
}
?>


3.2 HTML <head> 섹션

<!DOCTYPE html>
<html lang="<?php echo htmlspecialchars($language, ENT_QUOTES, "UTF-8"); ?>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title><?php echo htmlspecialchars($pageTitle, ENT_QUOTES, "UTF-8"); ?></title>

<?php if ($siteDesc): ?>
<meta name="description" content="<?php echo htmlspecialchars($siteDesc, ENT_QUOTES); ?>">
<?php endif; ?>

<?php if (class_exists("DxSeo")) DxSeo::renderHead(); ?>

<!-- ① 폰트 (Pretendard 한글 최적화 폰트) -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css">

<!-- ② Font Awesome 6 아이콘 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" crossorigin="anonymous">

<!-- ③ DX 유틸 CSS (dxb-css 대체, reflow 없음) -->
<link rel="stylesheet" href="<?php echo dx_base_url("assets/css/dx-utils.css"); ?>?v=<?php echo DX_VERSION; ?>">

<!-- ④ 테마 자체 CSS -->
<link rel="stylesheet" href="<?php echo dx_theme_asset("css/theme.css"); ?>?v=<?php echo DX_VERSION; ?>">

<!-- ⑤ CSS 변수 (--p 등 동적 색상은 PHP로 생성) -->
<style>
:root {
  --p:          <?php echo htmlspecialchars($primaryColor, ENT_QUOTES); ?>;
  --bg-body:    #f1f3f5;
  --bg-card:    #ffffff;
  --bg-header:  #ffffff;
  --text-main:  #1e293b;
  --text-muted: #64748b;
  --border:     #e2e8f0;
  --hover-row:  #f8faff;
  --font:       "Pretendard", -apple-system, BlinkMacSystemFont, sans-serif;
}
body.dark {
  --bg-body:    #0f172a;
  --bg-card:    #1e293b;
  --bg-header:  #1e293b;
  --text-main:  #f1f5f9;
  --text-muted: #94a3b8;
  --border:     #334155;
  --hover-row:  #243044;
}
body {
  font-family: var(--font);
  background: var(--bg-body);
  color: var(--text-main);
  min-height: 100vh;
}
</style>

<!-- ⑥ CSRF 토큰 (AJAX 요청에 필요) -->
<meta name="csrf-token" content="<?php echo dx_csrf_token(); ?>">

<!-- ⑦ 플러그인/확장 HEAD 훅 -->
<?php dx_run_hook("dx_head", isset($context) ? $context : array()); ?>

<!-- ⑧ Google Analytics (설정된 경우만) -->
<?php $ga = dx_config("google_analytics", ""); if ($ga): ?>
<script async src="https://www.googletagmanager.com/gtag/js?id=<?php echo htmlspecialchars($ga, ENT_QUOTES); ?>"></script>
<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}
gtag("js",new Date());gtag("config","<?php echo htmlspecialchars($ga, ENT_QUOTES); ?>");</script>
<?php endif; ?>

</head>


3.3 헤더 (네비게이션)

<body>

<!-- ── 헤더 ─────────────────────────────────────── -->
<header style="background:var(--bg-header);border-bottom:1px solid var(--border);
        position:sticky;top:0;z-index:100;box-shadow:0 1px 3px rgba(0,0,0,.06)">
  <div style="max-width:1200px;margin:0 auto;padding:0 20px;
       display:flex;align-items:center;gap:24px;height:60px">

    <!-- 로고 -->
    <a href="<?php echo dx_base_url(); ?>"
       style="font-size:1.2rem;font-weight:800;color:var(--p);text-decoration:none;white-space:nowrap">
      <?php echo htmlspecialchars($logoText ? $logoText : $siteName, ENT_QUOTES, "UTF-8"); ?>
    </a>

    <!-- 네비게이션 -->
    <nav style="display:flex;align-items:center;gap:4px;flex:1;overflow:hidden">
      <?php foreach ($menus as $menu):
        $menuId   = (int)$menu["id"];
        $menuUrl  = !empty($menu["url"]) ? $menu["url"] : "#";
        $menuSubs = isset($subMap[$menuId]) ? $subMap[$menuId] : array();
        $reqUri   = strtok($_SERVER["REQUEST_URI"], "?");
        $isActive = $menuUrl !== "#" && strpos($reqUri, rtrim($menuUrl, "/")) === 0;
      ?>
      <div style="position:relative">
        <a href="<?php echo dx_safe_url($menuUrl); ?>"
           style="display:flex;align-items:center;gap:4px;padding:6px 12px;border-radius:8px;
                  font-size:.875rem;font-weight:600;text-decoration:none;transition:all .15s;
                  color:<?php echo $isActive ? "var(--p)" : "var(--text-main)"; ?>;
                  background:<?php echo $isActive ? "rgba(59,130,246,.08)" : "transparent"; ?>">
          <?php echo htmlspecialchars($menu["title"], ENT_QUOTES, "UTF-8"); ?>
          <?php if (!empty($menuSubs)): ?><i class="fa-solid fa-chevron-down" style="font-size:.55rem;opacity:.5"></i><?php endif; ?>
        </a>
        <?php if (!empty($menuSubs)): ?>
        <div style="display:none;position:absolute;top:calc(100%+6px);left:0;
             background:var(--bg-card);border:1px solid var(--border);border-radius:10px;
             min-width:140px;box-shadow:0 8px 24px rgba(0,0,0,.1);z-index:200;
             overflow:hidden;padding:4px 0"
             class="nav-dropdown">
          <?php foreach ($menuSubs as $sub):
            $subUrl = !empty($sub["url"]) ? $sub["url"] : "#";
          ?>
          <a href="<?php echo dx_safe_url($subUrl); ?>"
             style="display:block;padding:9px 16px;font-size:.83rem;color:var(--text-main);
                    text-decoration:none;transition:background .12s"
            <?php echo htmlspecialchars($sub["title"], ENT_QUOTES, "UTF-8"); ?>
          </a>
          <?php endforeach; ?>
        </div>
        <?php endif; ?>
      </div>
      <?php endforeach; ?>
    </nav>

    <!-- 검색창 (옵션) -->
    <?php if ($showSearch): ?>
    <form action="<?php echo dx_base_url("search"); ?>" method="get"
          style="display:flex;align-items:center;gap:8px">
      <input type="text" name="q" placeholder="검색..."
             style="padding:7px 12px;border:1px solid var(--border);border-radius:8px;
                    font-size:.83rem;background:var(--bg-body);color:var(--text-main);
                    width:160px;outline:none">
      <button type="submit"
              style="background:var(--p);color:#fff;border:none;border-radius:8px;
                     padding:7px 12px;cursor:pointer">
        <i class="fa-solid fa-magnifying-glass fa-sm"></i>
      </button>
    </form>
    <?php endif; ?>

    <!-- 로그인/회원 버튼 -->
    <div style="display:flex;align-items:center;gap:8px;flex-shrink:0">
      <?php if ($isLogin): ?>
      <a href="<?php echo dx_base_url("auth/mypage"); ?>"
         style="font-size:.83rem;font-weight:600;color:var(--text-main);text-decoration:none">
        <?php echo htmlspecialchars($userName, ENT_QUOTES, "UTF-8"); ?>님
      </a>
      <a href="<?php echo dx_base_url("auth/logout"); ?>"
         style="font-size:.83rem;padding:6px 14px;border:1px solid var(--border);
                border-radius:8px;color:var(--text-muted);text-decoration:none">
        로그아웃
      </a>
      <?php if ($isAdmin): ?>
      <a href="<?php echo dx_base_url("admin"); ?>"
         style="font-size:.83rem;padding:6px 14px;background:var(--p);color:#fff;
                border-radius:8px;text-decoration:none;font-weight:600">
        관리자
      </a>
      <?php endif; ?>
      <?php else: ?>
      <a href="<?php echo dx_base_url("auth/login"); ?>"
         style="font-size:.83rem;padding:6px 14px;background:var(--p);color:#fff;
                border-radius:8px;text-decoration:none;font-weight:600">
        로그인
      </a>
      <?php endif; ?>
    </div>
  </div>
</header>


3.4 본문 영역 ($dx_content 출력)

본문은 $dx_content 변수를 출력하는 것이 핵심입니다. 사이드바 유무에 따라 레이아웃을 분기합니다.
<!-- ── 본문 ─────────────────────────────────────── -->
<main style="max-width:1200px;margin:0 auto;padding:24px 20px;flex:1">

  <?php
  // 사이드바 표시 여부 결정
  // 게시판 페이지이고 카테고리가 있으면 사이드바 표시
  $ctxCats = isset($ctx["categories"]) ? $ctx["categories"] : array();
  $showSidebar = $isBoard && !empty($ctxCats);
  ?>

  <?php if ($showSidebar): ?>
  <!-- 2컬럼 레이아웃 (본문 + 사이드바) -->
  <div style="display:flex;gap:24px;align-items:flex-start">

    <!-- 본문 -->
    <div style="flex:1;min-width:0">
      <?php echo isset($dx_content) ? $dx_content : ""; ?>
    </div>

    <!-- 사이드바 -->
    <aside style="width:220px;flex-shrink:0;position:sticky;top:80px;align-self:flex-start">

      <!-- 카테고리 탭 -->
      <?php if (!empty($ctxCats)): ?>
      <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:12px;overflow:hidden;margin-bottom:16px">
        <div style="padding:12px 16px;border-bottom:1px solid var(--border);font-size:.85rem;font-weight:700;color:var(--text-main)">
          <i class="fa-solid fa-tag" style="color:var(--p);margin-right:6px"></i>카테고리
        </div>
        <?php
        $ctxBk = isset($ctx["slug"]) ? $ctx["slug"] : "";
        $curCat = isset($ctx["currentCategory"]) ? $ctx["currentCategory"] : "";
        ?>
        <ul style="list-style:none;padding:4px 0;margin:0">
          <li>
            <a href="<?php echo dx_base_url($ctxBk."/list"); ?>"
               style="display:block;padding:8px 16px;font-size:.83rem;
                      color:<?php echo !$curCat?"var(--p)":"var(--text-main)"; ?>;
                      font-weight:<?php echo !$curCat?"700":"400"; ?>;
                      text-decoration:none">
              전체
            </a>
          </li>
          <?php foreach ($ctxCats as $cat):
            $catName = isset($cat["name"]) ? $cat["name"] : "";
            if (!$catName) continue;
            $catDepth = isset($cat["depth"]) ? (int)$cat["depth"] : 0;
            $catActive = ($curCat === $catName);
          ?>
          <li>
            <a href="<?php echo dx_base_url($ctxBk."/list")."?cat=".urlencode($catName); ?>"
               style="display:block;padding:8px 16px;padding-left:<?php echo (16+$catDepth*14); ?>px;
                      font-size:<?php echo $catDepth>0?".78":".83"; ?>rem;
                      color:<?php echo $catActive?"var(--p)":"var(--text-main)"; ?>;
                      font-weight:<?php echo $catActive?"700":"400"; ?>;
                      text-decoration:none">
              <?php echo htmlspecialchars($catName, ENT_QUOTES, "UTF-8"); ?>
            </a>
          </li>
          <?php endforeach; ?>
        </ul>
      </div>
      <?php endif; ?>

    </aside>
  </div>

  <?php else: ?>
  <!-- 단일 컬럼 레이아웃 -->
  <?php echo isset($dx_content) ? $dx_content : ""; ?>
  <?php endif; ?>

</main>


3.5 푸터

<!-- ── 푸터 ─────────────────────────────────────── -->
<footer style="border-top:1px solid var(--border);background:var(--bg-card);
        padding:32px 20px;margin-top:auto">
  <div style="max-width:1200px;margin:0 auto;text-align:center">

    <!-- 사이트 이름 -->
    <p style="font-size:1rem;font-weight:700;color:var(--text-main);margin:0 0 8px">
      <?php echo htmlspecialchars($siteName, ENT_QUOTES, "UTF-8"); ?>
    </p>

    <!-- 푸터 문구 -->
    <?php $footerText = dx_theme_option("footer_text", ""); if ($footerText): ?>
    <p style="font-size:.83rem;color:var(--text-muted);margin:0 0 16px;line-height:1.6">
      <?php echo htmlspecialchars($footerText, ENT_QUOTES, "UTF-8"); ?>
    </p>
    <?php endif; ?>

    <!-- 링크 -->
    <div style="display:flex;justify-content:center;gap:16px;flex-wrap:wrap;font-size:.8rem">
      <?php $termsUrl = dx_config("footer_terms_url", "#"); if ($termsUrl && $termsUrl !== "#"): ?>
      <a href="<?php echo htmlspecialchars($termsUrl, ENT_QUOTES); ?>"
         style="color:var(--text-muted);text-decoration:none">이용약관</a>
      <?php endif; ?>
      <?php $privacyUrl = dx_config("footer_privacy_url", "#"); if ($privacyUrl && $privacyUrl !== "#"): ?>
      <a href="<?php echo htmlspecialchars($privacyUrl, ENT_QUOTES); ?>"
         style="color:var(--text-muted);text-decoration:none">개인정보처리방침</a>
      <?php endif; ?>
    </div>

    <p style="font-size:.78rem;color:var(--text-muted);margin:12px 0 0">
      &copy; <?php echo date("Y"); ?> <?php echo htmlspecialchars($siteName, ENT_QUOTES, "UTF-8"); ?>.
      Powered by <a href="https://designonex.com" style="color:var(--p)">DXCMS</a>
    </p>
  </div>
</footer>


3.6 바디 하단 (스크립트 훅)

<!-- jQuery (전역에서 사용) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" crossorigin="anonymous"></script>

<!-- 드롭다운 메뉴 JS -->
<script>
// 마우스오버로 드롭다운 표시/숨김
document.querySelectorAll(".nav-dropdown").forEach(function(dd) {
  var item = dd.parentElement;
  item.addEventListener("mouseenter", function() { dd.style.display = "block"; });
  item.addEventListener("mouseleave", function() { dd.style.display = "none"; });
});
</script>

<!-- ① 플러그인 푸터 스크립트 훅 -->
<?php dx_run_hook("dx_footer_scripts", array()); ?>

<!-- ② 바디 최하단 훅 (분석코드, 채팅위젯 등) -->
<?php dx_run_hook("dx_body_bottom"); ?>

</body>
</html>


4장. CSS 변수 시스템 완전 활용

DXCMS 테마는 CSS Custom Properties(변수)로 색상과 배경을 관리합니다. 이 변수를 사용하면 다크모드 전환이 자동으로 처리되고, 관리자의 primary_color 옵션 변경도 즉시 반영됩니다.


4.1 필수 CSS 변수 목록

변수명 라이트 기본값 다크 기본값 용도
--p #1a73e8 (or 옵션) (동일) 주요 강조색. primary_color 옵션으로 변경
--bg-body #f1f3f5 #0f172a 페이지 전체 배경
--bg-card #ffffff #1e293b 카드·패널 배경색
--bg-header #ffffff #1e293b 헤더 배경색
--text-main #1e293b #f1f5f9 주요 본문 텍스트색
--text-muted #64748b #94a3b8 보조·설명 텍스트색
--border #e2e8f0 #334155 테두리·구분선 색
--hover-row #f8faff #243044 행 호버 배경색
--font Pretendard,... (동일) 기본 폰트 패밀리


4.2 CSS 변수 활용 예시

/* themes/my-theme/assets/css/theme.css */ 
/* 기본 리셋 */  *, *::before, *::after { box-sizing: border-box; }
body { margin: 0; font-family: var(--font); background: var(--bg-body); color: var(--text-main); }

/* 카드 컴포넌트 — 다크모드 자동 대응 */  .my-card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 20px;
  transition: background-color .3s, border-color .3s;
}

/* 강조 버튼 */  .my-btn-primary {
  background: var(--p);
  color: #ffffff;
  border: none;
  padding: 10px 20px;
  border-radius: 8px;
  cursor: pointer;
}
.my-btn-primary:hover { filter: brightness(1.1); }

/* 다크모드 전용 추가 스타일 */  body.dark .my-special-img {
  opacity: 0.85;
  filter: brightness(.9);
}

/* 반응형 (모바일) */  @media (max-width: 768px) {
  .my-sidebar { display: none; }
}


4.3 primary_color 옵션을 CSS에 적용

관리자가 primary_color를 변경하면 PHP가 CSS 변수 --p에 즉시 반영됩니다. 이 변수를 사용하는 모든 CSS가 자동으로 업데이트됩니다.
<!-- layout/main.php의 <style> 블록 -->
<style>
:root {
  --p: <?php echo htmlspecialchars(dx_theme_option("primary_color", "#3b82f6"), ENT_QUOTES); ?>;
  /* ... 다른 변수들 ... */
}
</style>

/* 이후 CSS에서 var(--p)를 사용하면 자동으로 반영됨 */


4.4 다크모드 지원

DXCMS는 localStorage의 "dx-theme" 키로 다크모드를 관리합니다. body 태그에 dark 클래스가 추가/제거됩니다. extend/top/01_darkmode_early.php 파일이 FOUC(깜빡임)를 방지합니다.
/* 다크모드 토글 버튼 예시 */  <button style="background:none;border:none;cursor:pointer;
        color:var(--text-main);font-size:1.1rem">
  <i class="fa-solid fa-moon"></i>
</button>

<script>
function toggleDarkMode() {
  var isDark = document.body.classList.toggle("dark");
  localStorage.setItem("dx-theme", isDark ? "dark" : "light");
}
// 초기 상태 적용
(function() {
  var saved = localStorage.getItem("dx-theme");
  var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
  if (saved === "dark" || (saved === null && prefersDark)) {
    document.body.classList.add("dark");
  }
})();
</script>


5장. 홈페이지 제작 (page/home.php)

themes/{테마}/page/home.php 파일이 있으면 홈페이지로 사용됩니다. 없으면 DB의 is_home=1 페이지가 사용됩니다. 이 파일 안에서 dx_board_latest()로 여러 게시판의 최신글을 자유롭게 배치합니다.


5.1 홈페이지 탐색 우선순위

순위 탐색 경로
1 themes/{현재테마}/page/home.php  ← 있으면 항상 이 파일 사용
2 DB에서 현재 도메인(site_domain) is_home=1 페이지
3 DB에서 전체 공통(site_domain='') is_home=1 페이지
4 pages/home.php 기본 폴백


5.2 실전 홈페이지 구현

<?php
// themes/my-theme/page/home.php
if (!defined("DX_CMS")) exit;
?>

<!-- ── 히어로 섹션 ─────────────────────────────── -->
<div style="background:linear-gradient(135deg,var(--p),#6366f1);
     color:#fff;padding:60px 24px;text-align:center;border-radius:16px;margin-bottom:32px">
  <h1 style="font-size:2rem;font-weight:800;margin:0 0 12px">
    <?php echo htmlspecialchars(dx_config("site_name",""), ENT_QUOTES, "UTF-8"); ?>
  </h1>
  <p style="font-size:1rem;opacity:.9;margin:0 0 24px">
    <?php echo htmlspecialchars(dx_config("site_description",""), ENT_QUOTES, "UTF-8"); ?>
  </p>
  <div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">
    <?php if (dx_is_login()): ?>
    <a href="<?php echo dx_base_url("auth/mypage"); ?>"
       style="padding:12px 24px;background:rgba(255,255,255,.2);color:#fff;
              border-radius:10px;text-decoration:none;font-weight:600">
      마이페이지
    </a>
    <?php else: ?>
    <a href="<?php echo dx_base_url("auth/login"); ?>"
       style="padding:12px 24px;background:#fff;color:var(--p);
              border-radius:10px;text-decoration:none;font-weight:700">
      로그인하기
    </a>
    <a href="<?php echo dx_base_url("auth/register"); ?>"
       style="padding:12px 24px;background:rgba(255,255,255,.2);color:#fff;
              border-radius:10px;text-decoration:none;font-weight:600">
      회원가입
    </a>
    <?php endif; ?>
  </div>
</div>

<!-- ── 최신글 위젯 영역 ──────────────────────────── -->
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;margin-bottom:32px">

  <!-- 공지사항 -->
  <div>
    <?php dx_board_latest("notice", 5, "simple", "공지사항", "📢"); ?>
  </div>

  <!-- 자유게시판 -->
  <div>
    <?php dx_board_latest("free", 8, "simple", "자유게시판", "💬"); ?>
  </div>

</div>

<!-- ── 갤러리 최신글 카드형 ─────────────────────── -->
<div style="margin-bottom:32px">
  <?php dx_board_latest("gallery", 6, "card", "갤러리", "🖼️"); ?>
</div>

<!-- ── 모바일 반응형 처리 -->
<style>
@media (max-width: 640px) {
  div[style*="grid-template-columns:1fr 1fr"] {
    grid-template-columns: 1fr !important;
  }
}
</style>


6장. 파셜(parts/) 제작 및 활용

파셜은 여러 파일에서 재사용되는 HTML 조각입니다. themes/{테마}/parts/ 폴더에 PHP 파일을 만들면 dx_include_part()로 어디서든 불러올 수 있습니다.


6.1 페이지네이션 파셜 (parts/pagination.php)

페이지네이션은 가장 자주 재사용되는 파셜입니다. default 테마에 이미 구현되어 있으며, 커스텀 테마에서 오버라이드할 수 있습니다.
<?php
// themes/my-theme/parts/pagination.php
if (!defined("DX_CMS")) exit;

// dx_include_part("pagination", [...])으로 전달받는 변수:
// $total    — 전체 글 수
// $page     — 현재 페이지
// $per_page — 페이지당 글 수 (기본 20)
// $url      — 링크 기본 URL
// $range    — 표시 페이지 수 (기본 10)

$total    = isset($total)    ? (int)$total    : 0;
$page     = isset($page)     ? (int)$page     : 1;
$per_page = isset($per_page) ? (int)$per_page : 20;
$url      = isset($url)      ? $url           : dx_current_url();
$range    = isset($range)    ? (int)$range    : 10;

if ($total <= 0 || $per_page <= 0) return;
$totalPages = (int)ceil($total / $per_page);
if ($totalPages <= 1) return;

$blockStart = (int)(floor(($page-1)/$range)*$range) + 1;
$blockEnd   = min($blockStart + $range - 1, $totalPages);

function _myPageUrl($base, $p) {
    $q = $_GET; unset($q["page"]);
    $qs = http_build_query($q);
    $sep = (strpos($base, "?") !== false) ? "&" : "?";
    return $base . ($qs ? $sep.$qs."&page=".$p : $sep."page=".$p);
}
?>
<nav style="display:flex;justify-content:center;gap:4px;padding:20px 0;flex-wrap:wrap">
  <?php if ($blockStart > 1): ?>
  <a href="<?php echo htmlspecialchars(_myPageUrl($url,1),ENT_QUOTES); ?>"
     style="<?php /* 버튼 공통 스타일 */ ?>
            display:inline-flex;align-items:center;justify-content:center;
            min-width:34px;height:34px;border-radius:8px;font-size:.82rem;
            background:var(--bg-card);border:1px solid var(--border);
            color:var(--text-muted);text-decoration:none">«</a>
  <?php endif; ?>

  <?php for ($i = $blockStart; $i <= $blockEnd; $i++): ?>
  <a href="<?php echo htmlspecialchars(_myPageUrl($url,$i),ENT_QUOTES); ?>"
     style="display:inline-flex;align-items:center;justify-content:center;
            min-width:34px;height:34px;border-radius:8px;font-size:.82rem;
            text-decoration:none;
            <?php echo $i===$page
              ? "background:var(--p);color:#fff;border:1px solid var(--p)"
              : "background:var(--bg-card);border:1px solid var(--border);color:var(--text-muted)";
            ?>"><?php echo $i; ?></a>
  <?php endfor; ?>

  <?php if ($blockEnd < $totalPages): ?>
  <a href="<?php echo htmlspecialchars(_myPageUrl($url,$totalPages),ENT_QUOTES); ?>"
     style="display:inline-flex;align-items:center;justify-content:center;
            min-width:34px;height:34px;border-radius:8px;font-size:.82rem;
            background:var(--bg-card);border:1px solid var(--border);
            color:var(--text-muted);text-decoration:none">»</a>
  <?php endif; ?>
</nav>


6.2 파셜 호출 방법

// board/list.php 또는 다른 테마 파일에서

// 기본 호출
dx_include_part("pagination", array(
    "total"    => $total,
    "page"     => $page,
    "per_page" => $perPage,
    "url"      => dx_base_url($board["board_key"]."/list"),
));

// 검색어•카테고리 유지하며 호출
$paginationUrl = dx_base_url($board["board_key"]."/list");
if ($search) $paginationUrl .= "?s=".urlencode($search)."&sf=".urlencode($searchField);
if ($cat)    $paginationUrl .= ($search ? "&" : "?")."cat=".urlencode($cat);

dx_include_part("pagination", array(
    "total"    => $total,
    "page"     => $page,
    "per_page" => $perPage,
    "url"      => $paginationUrl,
    "range"    => 5,  // 페이지 5개씩 표시
));


6.3 브레드크럼 파셜 (parts/breadcrumb.php)

<?php
// themes/my-theme/parts/breadcrumb.php
// dx_include_part("breadcrumb", array("items"=>[["label"=>"홈","url"=>"/"],[...]]]));
if (!defined("DX_CMS")) exit;

$items = isset($items) ? $items : array();
if (empty($items)) return;
?>
<nav style="display:flex;align-items:center;gap:6px;font-size:.8rem;
     color:var(--text-muted);margin-bottom:12px;flex-wrap:wrap">
  <?php foreach ($items as $i => $item):
    $isLast = ($i === count($items) - 1);
    $label = htmlspecialchars($item["label"], ENT_QUOTES, "UTF-8");
  ?>
  <?php if (!$isLast && !empty($item["url"])): ?>
  <a href="<?php echo htmlspecialchars($item["url"], ENT_QUOTES); ?>"
     style="color:var(--text-muted);text-decoration:none;
            transition:color .12s"
    <?php echo $label; ?>
  </a>
  <span style="font-size:.6rem;color:var(--border)">›</span>
  <?php else: ?>
  <span style="color:var(--text-main);font-weight:600"><?php echo $label; ?></span>
  <?php endif; ?>
  <?php endforeach; ?>
</nav>


7장. 오류 페이지 제작 (404•403)

page/404.php와 page/403.php 파일을 만들면 커스텀 오류 페이지가 표시됩니다. 없으면 Dispatcher가 간단한 기본 오류 메시지를 출력합니다.


7.1 404 오류 페이지 (page/404.php)

<?php
// themes/my-theme/page/404.php
if (!defined("DX_CMS")) exit;
?>
<div style="display:flex;flex-direction:column;align-items:center;
     justify-content:center;min-height:60vh;padding:40px;text-align:center">

  <!-- 큰 숫자 -->
  <div style="font-size:7rem;font-weight:900;
       color:var(--border);line-height:1;margin-bottom:16px">
    404
  </div>

  <!-- 메시지 -->
  <h1 style="font-size:1.5rem;font-weight:700;color:var(--text-main);margin:0 0 8px">
    페이지를 찾을 수 없습니다
  </h1>
  <p style="color:var(--text-muted);margin:0 0 32px;line-height:1.6">
    요청하신 페이지가 존재하지 않거나<br>주소가 변경되었을 수 있습니다.
  </p>

  <!-- 버튼들 -->
  <div style="display:flex;gap:12px;flex-wrap:wrap;justify-content:center">
    <a href="blocked:history.back()"
       style="padding:12px 24px;background:var(--bg-card);border:1px solid var(--border);
              border-radius:10px;color:var(--text-main);text-decoration:none;font-weight:600">
      ← 이전 페이지
    </a>
    <a href="<?php echo dx_base_url(); ?>"
       style="padding:12px 24px;background:var(--p);color:#fff;
              border-radius:10px;text-decoration:none;font-weight:600">
      홈으로 이동
    </a>
  </div>

</div>


7.2 403 접근 거부 페이지 (page/403.php)

<?php
// themes/my-theme/page/403.php
if (!defined("DX_CMS")) exit;

$isLogin = dx_is_login();
?>
<div style="display:flex;flex-direction:column;align-items:center;
     justify-content:center;min-height:60vh;padding:40px;text-align:center">

  <div style="font-size:4rem;margin-bottom:16px">🔒</div>

  <h1 style="font-size:1.5rem;font-weight:700;color:var(--text-main);margin:0 0 8px">
    접근이 거부되었습니다
  </h1>
  <p style="color:var(--text-muted);margin:0 0 32px">
    이 페이지에 접근할 권한이 없습니다.
  </p>

  <div style="display:flex;gap:12px;flex-wrap:wrap;justify-content:center">
    <?php if (!$isLogin): ?>
    <a href="<?php echo dx_base_url("auth/login")."?redirect=".urlencode(dx_current_url()); ?>"
       style="padding:12px 24px;background:var(--p);color:#fff;
              border-radius:10px;text-decoration:none;font-weight:600">
      로그인하기
    </a>
    <?php endif; ?>
    <a href="<?php echo dx_base_url(); ?>"
       style="padding:12px 24px;background:var(--bg-card);border:1px solid var(--border);
              border-radius:10px;color:var(--text-main);text-decoration:none;font-weight:600">
      홈으로 이동
    </a>
  </div>

</div>


8장. 게시판 뷰 오버라이드 (board/)

테마의 board/ 폴더에 파일을 만들면 기본 테마의 게시판 화면을 오버라이드합니다. 이 방법은 boards/skins/를 사용하지 않고 테마 안에서 게시판 레이아웃을 바꿀 때 유용합니다.


8.1 게시판 파일 폴백 체인

순위 경로 설명
1 boards/skins/{스킨명}/{액션}/handler.php 완전 독립 핸들러
2 boards/skins/{스킨명}/{액션}.php 스킨 독립 뷰
3 themes/{현재테마}/board/{스킨명}/{액션}.php 테마 내 스킨별 오버라이드
4 themes/{현재테마}/board/{액션}.php 테마 내 공통 오버라이드
5 themes/default/board/{스킨명}/{액션}.php default 테마 스킨 전용
6 themes/default/board/{액션}.php default 테마 공통 (최종 폴백)


8.2 테마 내 게시판 목록 커스텀 (board/list.php)

themes/my-theme/board/list.php를 만들면 모든 게시판의 목록 화면이 바뀝니다. 아래는 최소한의 커스텀 목록 구현입니다.
<?php
// themes/my-theme/board/list.php
if (!defined("DX_CMS")) exit;

// handler.php가 주입한 변수들
$auth     = Auth::getInstance();
$bk       = $board["board_key"];
$bn       = $board["board_name"];
$canWrite = ((int)$board["write_level"]===0)
          || ((int)$board["write_level"]===1 && $auth->isLoggedIn())
          || ((int)$board["write_level"]===9 && $auth->isAdmin());
?>

<!-- 게시판 헤더 -->
<div style="display:flex;align-items:center;justify-content:space-between;
     margin-bottom:16px;padding-bottom:16px;border-bottom:2px solid var(--p)">
  <h1 style="font-size:1.1rem;font-weight:700;color:var(--text-main);margin:0">
    <?php echo htmlspecialchars($bn, ENT_QUOTES, "UTF-8"); ?>
    <span style="font-size:.8rem;font-weight:400;color:var(--text-muted);margin-left:8px">
      총 <?php echo number_format($total); ?>개
    </span>
  </h1>
  <?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:600">
    글쓰기
  </a>
  <?php endif; ?>
</div>

<!-- 공지글 -->
<?php foreach ($notices as $notice): ?>
<div style="padding:12px 16px;background:rgba(59,130,246,.05);
     border-radius:10px;margin-bottom:8px;display:flex;align-items:center;gap:10px">
  <span style="font-size:.7rem;font-weight:700;color:var(--p);
       background:rgba(59,130,246,.1);padding:2px 8px;border-radius:4px">공지</span>
  <a href="<?php echo dx_base_url($bk."/view/".$notice["id"]); ?>"
     style="font-size:.9rem;color:var(--text-main);text-decoration:none;flex:1;
            white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
    <?php echo htmlspecialchars($notice["title"], ENT_QUOTES, "UTF-8"); ?>
  </a>
  <span style="font-size:.78rem;color:var(--text-muted);flex-shrink:0">
    <?php echo dx_date($notice["created_at"], "m.d"); ?>
  </span>
</div>
<?php endforeach; ?>

<!-- 일반 글 -->
<?php if (empty($posts)): ?>
<div style="padding:60px;text-align:center;color:var(--text-muted)">
  등록된 글이 없습니다.
</div>
<?php else: ?>
<?php foreach ($posts as $post): ?>
<div style="padding:12px 0;border-bottom:1px solid var(--border);
     display:flex;align-items:center;gap:10px"

  <!-- 카테고리 배지 -->
  <?php if (!empty($post["category"])): ?>
  <span style="font-size:.72rem;font-weight:600;color:var(--p);
       background:rgba(59,130,246,.1);padding:2px 8px;border-radius:4px;
       flex-shrink:0">
    <?php echo htmlspecialchars($post["category"], ENT_QUOTES, "UTF-8"); ?>
  </span>
  <?php endif; ?>

  <!-- 제목 -->
  <a href="<?php echo dx_base_url($bk."/view/".$post["id"]); ?>"
     style="flex:1;font-size:.9rem;color:var(--text-main);text-decoration:none;
            white-space:nowrap;overflow:hidden;text-overflow:ellipsis">
    <?php echo htmlspecialchars($post["title"], ENT_QUOTES, "UTF-8"); ?>
    <?php if ((int)$post["comment_count"] > 0): ?>
    <span style="font-size:.78rem;color:var(--p);margin-left:4px">
      [<?php echo (int)$post["comment_count"]; ?>]
    </span>
    <?php endif; ?>
  </a>

  <!-- 메타 정보 -->
  <div style="display:flex;gap:12px;font-size:.78rem;color:var(--text-muted);flex-shrink:0">
    <span><?php echo htmlspecialchars($post["member_name"] ?: $post["author_name"] ?: "익명", ENT_QUOTES); ?></span>
    <span><?php echo dx_date($post["created_at"], "Y.m.d"); ?></span>
    <span><i class="fa-regular fa-eye" style="font-size:.65rem"></i> <?php echo number_format($post["view_count"]); ?></span>
  </div>
</div>
<?php endforeach; ?>
<?php endif; ?>

<!-- 페이지네이션 -->
<?php
dx_include_part("pagination", array(
    "total"    => $total,
    "page"     => $page,
    "per_page" => $perPage,
    "url"      => dx_base_url($bk."/list"),
));
?>


9장. board_latest 위젯 스킨 직접 만들기


9.1 위젯 스킨 파일 위치

// 현재 테마 우선 (권장)
themes/my-theme/board_latest/timeline.php

// default 테마 (폴백)
themes/default/board_latest/timeline.php

// 호출
dx_board_latest("free", 5, "timeline", "최신글");


9.2 타임라인형 위젯 스킨

<?php
// themes/my-theme/board_latest/timeline.php
// 변수: $title, $icon, $more_url, $posts, $board_key, $show_excerpt
if (!defined("DX_CMS")) exit;
?>
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:14px;overflow:hidden">

  <!-- 헤더 -->
  <div style="display:flex;align-items:center;justify-content:space-between;
       padding:14px 18px;border-bottom:1px solid var(--border)">
    <span style="font-size:.9rem;font-weight:800;color:var(--text-main)">
      <?php if ($icon): ?><span style="margin-right:6px"><?php echo htmlspecialchars($icon, ENT_QUOTES); ?></span><?php endif; ?>
      <?php echo htmlspecialchars($title, ENT_QUOTES, "UTF-8"); ?>
    </span>
    <a href="<?php echo htmlspecialchars($more_url, ENT_QUOTES); ?>"
       style="font-size:.75rem;color:var(--p);text-decoration:none;font-weight:600">
      더보기 →
    </a>
  </div>

  <!-- 타임라인 목록 -->
  <?php if (empty($posts)): ?>
  <p style="padding:24px;text-align:center;color:var(--text-muted);font-size:.83rem">
    게시글이 없습니다.
  </p>
  <?php else: ?>
  <div style="padding:8px 0">
    <?php foreach ($posts as $post): ?>
    <div style="display:flex;gap:12px;padding:10px 18px;
         border-bottom:1px solid var(--border);align-items:flex-start">

      <!-- 날짜 (타임라인 축) -->
      <div style="flex-shrink:0;width:36px;text-align:center">
        <span style="font-size:.75rem;font-weight:700;color:var(--p)">
          <?php echo $post["date_short"]; ?>
        </span>
      </div>

      <!-- 세로선 -->
      <div style="flex-shrink:0;width:2px;background:var(--border);
           border-radius:1px;margin-top:3px"></div>

      <!-- 내용 -->
      <div style="flex:1;min-width:0">
        <a href="<?php echo htmlspecialchars($post["url"], ENT_QUOTES); ?>"
           style="font-size:.875rem;color:var(--text-main);text-decoration:none;
                  font-weight:500;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"
          <?php echo htmlspecialchars($post["title"], ENT_QUOTES, "UTF-8"); ?>
        </a>
        <div style="font-size:.75rem;color:var(--text-muted);margin-top:2px">
          <?php echo htmlspecialchars($post["author"], ENT_QUOTES); ?>
          <?php if ($post["comment_count"] > 0): ?>
          • 댓글 <?php echo $post["comment_count"]; ?>
          <?php endif; ?>
        </div>
      </div>
    </div>
    <?php endforeach; ?>
  </div>
  <?php endif; ?>

</div>


10장. 관리자에서 테마 관리


10.1 테마 활성화 단계

  1. themes/ 폴더에 새 테마 폴더 업로드 (theme.json + layout/main.php 필수)
  2. 관리자 → 테마 관리 메뉴 클릭
  3. 테마 목록에서 새 테마 확인 (themes/ 폴더 자동 스캔)
  4. "활성화" 버튼 클릭 → settings 테이블의 theme 키 저장
  5. DxTheme::reset() + DxCache 초기화 자동 실행
  6. 사이트에서 새 테마 즉시 확인


10.2 테마 옵션 설정

  1. 관리자 → 테마 관리 → 활성 테마의 "옵션 편집" 클릭
  2. theme.json의 options에 정의된 항목이 입력 UI로 표시
  3. 값 입력 후 "저장" 클릭
  4. settings 테이블에 theme_{테마명}_{키} 형식으로 저장
  5. dx_theme_option()으로 즉시 읽기 가능


10.3 자주 발생하는 문제 해결

증상 원인 및 해결
테마 목록에 안 보임 themes/ 하위 폴더인지 확인. 폴더명에 대문자·특수문자 없는지 확인
화면이 깨지거나 흰 화면 PHP 오류 발생. DX_DEBUG=true 설정 후 오류 메시지 확인
CSS가 적용 안 됨 dx_theme_asset() URL 확인. assets/ 경로·파일명 오타 확인. 브라우저 캐시 비우기
$dx_content가 비어있음 layout/main.php에 echo isset($dx_content) 구문 확인. ob_start() 충돌 여부 확인
다크모드 깜빡임 extend/top/01_darkmode_early.php가 있는지 확인. FOUC 방지 스크립트 필요
사이드바가 안 나옴 $context 배열에 categories가 있는지, showSidebar 조건 로직 확인
메뉴가 안 나옴 DB의 menus 테이블에 데이터 있는지 확인. menu_group 값 일치 여부 확인


11장. 완성 테마 체크리스트


11.1 필수 요소 체크리스트

  • [ ] themes/{테마명}/theme.json 존재 (name, version 포함)
  • [ ] themes/{테마명}/layout/main.php 존재
  • [ ] layout/main.php에 <!DOCTYPE html>~</html>완전한 HTML 구조
  • [ ] $dx_content 출력 구문 포함
  • [ ] <meta name="csrf-token" content="..."> 포함
  • [ ] dx_run_hook("dx_head", ...) 포함
  • [ ] dx_run_hook("dx_footer_scripts", ...) 포함
  • [ ] dx_run_hook("dx_body_bottom") 포함
  • [ ] CSS 변수(:root, body.dark) 정의


11.2 권장 요소 체크리스트

  • [ ] page/home.php 작성 (홈페이지 커스텀)
  • [ ] page/404.php 작성 (404 오류 페이지)
  • [ ] page/403.php 작성 (403 접근 거부 페이지)
  • [ ] parts/pagination.php 작성 (페이지네이션)
  • [ ] 다크모드 지원 (body.dark CSS)
  • [ ] 모바일 반응형 지원 (@media 쿼리)
  • [ ] assets/preview.png 추가 (관리자 미리보기 이미지)
  • [ ] theme.json에 options 정의 (관리자 커스텀 가능)


11.3 디버깅 체크리스트

  • [ ] config.php에 define("DX_DEBUG", true) 추가 (개발 시만)
  • [ ] data/error.log 파일 확인 (PHP 오류 로그)
  • [ ] 브라우저 DevTools → Network 탭에서 CSS/JS 로드 확인
  • [ ] PHP 구문 오류: php -l themes/my-theme/layout/main.php
  • [ ] DX_DEBUG 끄기 (프로덕션 배포 전 반드시)

댓글0

로그인 후 댓글을 작성할 수 있습니다.
7. 테마 DXCMS 테마 개발 AI 프롬프트 스킬과 멀티사이트 체험 2026.05.23 7. 테마 테마 DX마켓 등록 2026.05.01 7. 테마 테마 제작 2026.05.01 7. 테마 테마 구조 2026.05.01
31
전체 회원
503
전체 게시글
775
전체 댓글
442
오늘 방문
33,174
전체 방문
3
현재 접속
인기글 7일 이내
최신글
최신댓글
목록