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

테마 제작

D DX
2026.05.01 01:36(수정됨) 126 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

로그인 후 댓글을 작성할 수 있습니다.
1. DX 철학 / 개념 왜 DXCMS를 만들었는가 2026.04.20 1. DX 철학 / 개념 DXCMS란 무엇인가 2026.04.20 DXCMS 활용 (CMS) DXCMS 날코딩•막코딩 완전 허용 2026.04.12
31
전체 회원
503
전체 게시글
770
전체 댓글
441
오늘 방문
33,173
전체 방문
2
현재 접속
인기글 7일 이내
최신글
최신댓글
목록