data/error.log 에서 확인 할 수 있습니다.
name: dxcms-skin-dev
description: >
DXCMS v8.1.0 게시판 스킨 개발 전문 스킬.
스킨 파일 구조, DB 스키마, API 패턴, 여분 필드, 디자인 패턴을 포함한다.
달력, 갤러리, Q&A, 쇼핑, 포트폴리오 등 다양한 스킨 유형에 적용 가능.
list.php / _list_rows.php / view.php / write.php 작성 시 반드시 이 스킬을 먼저 읽을 것.
license: Proprietary
이 부분에 description에 내가 만들고 싶은 내용을 입력하세요.최대한 내가 만들고 싶은 내용을 입력하는 것이 좋습니다.
다음은 프롬프트입니다.
클로드 Sonnet4.6으로 테스트 해봤습니다.
참고로 버전이 높다고 좋은 것은 아닙니다. 프롬프트 어떻게 작성하느냐에 결과물은 다 다릅니다.
위에 내용과 아래 프롬프트를 같이 넣어주면 됩니다.
## 1. 개발 환경
- **PHP 5.6 ~ 8.x** — 클로저(function(){}) 사용 금지, `array()` 표기 필수
- **MySQL 5.6+ / MariaDB 10.1+** — `ADD COLUMN IF NOT EXISTS` 미지원
- **스킨 위치**: `boards/skins/{스킨명}/`
- **필수 파일**: `skin.json` (없으면 스킨 인식 안 됨)
- **기본 전략**: `themes/default/board/basic/` 원본을 복사 후 최소한만 수정
### skin.json 필수 형식
```json
{
"name": "스킨키",
"label": "관리자 드롭다운 표시명",
"version": "1.0.0",
"author": "작성자",
"description": "설명",
"actions": ["list", "view", "write"],
"theme": "default"
}
```
---
## 2. 파일별 역할 — 절대 혼동 금지
| 파일 | 역할 | 주의 |
|---|---|---|
| `list.php` | 헤더·검색·정렬·글쓰기 버튼 UI | POST 처리 코드 절대 금지 |
| `_list_rows.php` | 공지행 + 일반글행 실제 렌더링 | list.php와 view.php 양쪽에서 include됨 |
| `view.php` | 게시글 상세 + 댓글 렌더링 | POST 처리는 파일 맨 위에서만 |
| `write.php` | 글쓰기/수정 폼 | 여분 필드 renderWriteForm 추가 정도 |
### list.php → _list_rows.php include 패턴
```php
// list.php 하단
$_ltNotices = !empty($notices) ? $notices : array();
$_ltPosts = !empty($posts) ? $posts : array();
$_ltTotal = (int)$total;
$_ltPage = (int)$page;
$_ltPerPage = (int)$perPage;
$_ltSearch = $search;
$_ltSf = $searchField;
$_ltCat = isset($currentCategory) ? $currentCategory : '';
$_ltTag = isset($tag) ? $tag : '';
$_ltIsAdm = $_isAdm;
$_ltCurPostId = 0;
$_ltPaginationBase = '';
$_ltShowPagination = false;
include __DIR__ . '/_list_rows.php';
```
---
## 3. ⚠️ DB 스키마 — 반드시 숙지
### 3-1. dx_posts (게시글)
```
id BIGINT(20) UNSIGNED -- 밀리초 타임스탬프 ID (BIGINT!)
board_id INT(11) UNSIGNED -- boards.id
parent_id BIGINT(20) UNSIGNED -- 기본 0
member_id INT(11) UNSIGNED -- 0=비회원
author_name VARCHAR(100) -- 비회원 작성자명
title VARCHAR(191)
content LONGTEXT
category VARCHAR(100)
category_slug VARCHAR(100)
tags VARCHAR(191)
thumbnail VARCHAR(191)
view_count INT(11)
like_count INT(11)
comment_count INT(11)
is_notice TINYINT(1)
is_secret TINYINT(1)
status TINYINT(1) -- 1=정상
popular_score INT(11)
created_at DATETIME
updated_at DATETIME
```
**⚠️ `member_name` 컬럼 없음** — 작성자명은 `dx_members.name` JOIN 필요:
```sql
LEFT JOIN `dx_members` m ON m.id = p.member_id
-- m.name AS member_name
```
### 3-2. dx_members (회원)
```
id INT(11) UNSIGNED AUTO_INCREMENT -- members는 INT, posts는 BIGINT!
login_id VARCHAR(50)
name VARCHAR(100) -- 닉네임/이름
email VARCHAR(191)
role ENUM('member','manager','admin')
status TINYINT(1)
profile_img VARCHAR(500)
point INT(11)
exp INT(11)
level SMALLINT(5)
```
### 3-3. dx_comments (댓글)
```
id BIGINT(20) UNSIGNED -- 밀리초 타임스탬프
post_id BIGINT(20) UNSIGNED
parent_id BIGINT(20) UNSIGNED -- 0=최상위
member_id INT(11) UNSIGNED
author_name VARCHAR(100)
content TEXT
depth TINYINT(3) -- 댓글 깊이
status TINYINT(1)
created_at DATETIME
```
### 3-4. dx_post_meta (여분 필드 값)
```
id BIGINT AUTO_INCREMENT
post_id BIGINT(20) UNSIGNED
field_key VARCHAR(64)
value TEXT
UNIQUE KEY (post_id, field_key)
```
### 3-5. dx_board_fields (여분 필드 정의)
```
id BIGINT AUTO_INCREMENT
board_id INT(11)
field_key VARCHAR(64)
field_label VARCHAR(128)
field_type VARCHAR(32) -- text/textarea/number/select/checkbox/datetime/...
field_options TEXT -- select 옵션 등
is_required TINYINT(1)
is_list TINYINT(1) -- 목록에 표시
is_view TINYINT(1) -- 뷰에 자동 렌더링
sort_order SMALLINT(5)
status TINYINT(1)
```
### 3-6. dx_likes (좋아요)
```
id INT AUTO_INCREMENT
target_type ENUM('post','comment')
target_id BIGINT(20) UNSIGNED
member_id INT(11)
ip VARCHAR(45)
created_at DATETIME
UNIQUE KEY (target_type, target_id, member_id)
```
**커스텀 활용**: `target_type`을 임의 문자열로 확장 가능 (`'qa_eval'`, `'apply'` 등)
---
## 4. ⚠️ BIGINT ID 처리 — 가장 중요한 규칙
```php
// ❌ 절대 금지 — 32bit PHP에서 오버플로우
$postId = (int)$_POST['post_id'];
$postId = (int)$post['id'];
// ✅ 올바른 방법 — 항상 string
$postId = dx_post('post_id', '0', 'bigint'); // string 반환
$postId = isset($_POST['post_id']) ? trim($_POST['post_id']) : '';
// ✅ DB 파라미터도 반드시 string 캐스팅
$db->rows("SELECT ... WHERE id = ?", array((string)$postId));
$db->rows("SELECT ... WHERE id = ? AND board_id = ?",
array((string)$postId, (string)$board['id']));
// ✅ 비교 시
if ((string)$post['member_id'] !== (string)$uid) { ... }
```
---
## 5. DB 쿼리 패턴
### 기본 조회
```php
$db = Database::getInstance();
// 복수 행
$rows = $db->rows("SELECT ... WHERE board_id = ?", array((string)$board['id']));
// 단건 — row() 메서드가 없을 수 있음, rows()[0] 사용
$result = $db->rows("SELECT ... WHERE id = ? LIMIT 1", array((string)$id));
$row = !empty($result) ? $result[0] : null;
// 단일 값
$count = $db->value("SELECT COUNT(*) FROM ...", array());
// INSERT/UPDATE/DELETE
$db->query("UPDATE ... SET ... WHERE id = ?", array((string)$id));
```
### ⚠️ 복잡한 JOIN 쿼리 금지
DXCMS `Database::rows()`는 **LEFT JOIN 다중(4개 이상)** 또는 **혼합 타입 파라미터**에서 오류 발생.
**대신 2단계 분리 패턴 사용**:
```php
// Step 1: post_id 목록만 조회 (단순 쿼리)
$pidRows = $db->rows(
"SELECT post_id FROM `{$db->table('post_meta')}`
WHERE field_key = ? AND value >= ? AND value <= ?",
array('event_start', $from, $to)
);
// Step 2: post_id별 단건 조회 + getMeta()
foreach ($pidRows as $r) {
$pid = (string)$r['post_id'];
$posts = $db->rows(
"SELECT p.id, p.title, p.member_id, p.author_name,
p.view_count, p.comment_count, p.created_at,
m.name AS member_name
FROM `{$db->table('posts')}` p
LEFT JOIN `{$db->table('members')}` m ON m.id = p.member_id
WHERE p.id = ? AND p.status = 1 LIMIT 1",
array($pid) // 파라미터는 string 하나만
);
if (empty($posts)) continue;
$meta = BoardFields::getInstance()->getMeta($pid);
}
```
### 테이블명
```php
$db->table('posts') // → dx_posts
$db->table('members') // → dx_members
$db->table('comments') // → dx_comments
$db->table('post_meta') // → dx_post_meta
$db->table('board_fields') // → dx_board_fields
$db->table('likes') // → dx_likes
$db->table('boards') // → dx_boards
```
---
## 6. API(POST) 처리 — view.php 상단 패턴
```php
// view.php 최상단 (다른 코드보다 먼저)
if (dx_method('POST') && isset($_POST['_sub'])) {
ob_clean(); // 필수 — ob_start() 버퍼 비우기
dx_csrf_check(); // 필수 — CSRF 검증
$sub = trim($_POST['_sub']);
$postId = dx_post('post_id', '0', 'bigint'); // string
if ($sub === 'my_action') {
// 처리
dx_json(array('success' => true, 'message' => '완료'));
}
dx_json(array('success' => false, 'message' => '알 수 없는 요청'));
}
```
**JS 호출 패턴**:
```javascript
fetch(BASE + '/' + BOARD_KEY + '/view/' + POST_ID, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
body: '_sub=my_action&post_id=' + POST_ID + '&_csrf=' + CSRF_TOKEN
})
.then(r => r.json())
.then(d => { if (d.success) { ... } });
```
**⚠️ 커스텀 액션 URL 절대 금지**:
`/게시판키/qa_accept` 같은 URL → Router.php에 없으면 404. 항상 `view.php POST + _sub` 패턴 사용.
---
## 7. 여분 필드 (BoardFields)
```php
// 파일 상단 필수
if (!class_exists('BoardFields')) {
require_once DX_ROOT . '/core/BoardFields.php';
}
// 전체 메타 읽기
$meta = BoardFields::getInstance()->getMeta((string)$postId);
$val = isset($meta['field_key']) ? $meta['field_key'] : '';
// 단일 값 읽기
$val = dx_get_post_meta($postId, 'field_key', '기본값');
// 저장
dx_save_post_meta($postId, array('field_key' => $value));
// 뷰 자동 렌더링 (is_view=1 필드만)
echo BoardFields::getInstance()->renderView($board['id'], $postId);
// 글쓰기 폼 렌더링
echo BoardFields::getInstance()->renderWriteForm($board['id'], $savedMeta);
```
**write.php에서 여분 필드 저장**: 필드명을 `bf_` 접두어로 작성하면 자동 저장:
```html
<input type="text" name="bf_필드키" value="...">
<select name="bf_필드키">...</select>
<input type="checkbox" name="bf_필드키" value="1">
<input type="datetime-local" name="bf_필드키">
```
**datetime-local 값 변환** (JS):
```javascript
form.addEventListener('submit', function() {
var dtFields = this.querySelectorAll('input[type="datetime-local"]');
for (var i = 0; i < dtFields.length; i++) {
if (dtFields[i].value) {
dtFields[i].value = dtFields[i].value.replace('T', ' ') + ':00';
}
}
});
```
**is_view 설정**:
- 스킨이 직접 커스텀 렌더링하는 필드 → **OFF** (ON이면 renderView가 중복 출력)
- 일반 정보성 필드 → **ON** (자동 렌더링)
---
## 8. 댓글 처리 — 베이직 view.php 상속 패턴
댓글 로직은 `themes/default/board/basic/view.php` 안에 직접 포함되어 있음.
별도 `_comments.php` 파일 없음.
**권장 방식**: 베이직 view.php를 스킨 폴더에 복사 후, 커스텀 블록만 삽입:
```php
// 베이직 view.php 복사 후 상단에 추가
if (!defined('DX_CMS')) exit('Direct access not allowed.');
// 커스텀 여분 필드 로드
if (!class_exists('BoardFields')) {
require_once DX_ROOT . '/core/BoardFields.php';
}
$_meta = BoardFields::getInstance()->getMeta((string)$post['id']);
// ... 커스텀 변수 처리 ...
// 이후 베이직 view.php 원본 코드 유지
// → 댓글/좋아요/스크랩/실시간 소켓 자동 포함
```
**커스텀 UI 삽입 위치**: `<!-- 본문 카드 -->` 주석 바로 앞:
```php
// 커스텀 정보 블록 (이벤트 정보, Q&A 메타 등)
<?php if ($myCondition): ?>
<div style="...커스텀 블록..."></div>
<?php endif; ?>
<!-- 본문 카드 -->
<article class="dx-view-card ...">
```
---
## 9. 컨텍스트 변수
### list.php에서 사용 가능
```php
$board // 게시판 정보 배열 (board_key, board_name, id, write_level, use_comment ...)
$posts // 일반글 배열
$notices // 공지글 배열
$globalNotices // 전체 공지 배열
$total // 일반글 전체 수
$page // 현재 페이지
$perPage // 페이지당 행 수
$search // 검색어
$searchField // 검색 필드
$categories // 카테고리 배열
$currentCategory// 현재 카테고리
$tag // 현재 태그
```
### view.php에서 사용 가능
```php
$board // 게시판 정보
$post // 게시글 배열 (id는 BIGINT string)
$files // 첨부파일 배열
$comments // 댓글 배열
$postLinks // 관련 링크 배열
$prevPost // 이전 글
$nextPost // 다음 글
$categories // 카테고리 배열
$viewSearch // 검색어 (목록에서 넘어온)
$viewSf // 검색 필드
$viewCat // 카테고리
```
---
## 10. 주요 헬퍼 함수
```php
// URL
dx_base_url('path') // 절대 URL
dx_current_url() // 현재 URL
dx_redirect('url') // 리다이렉트
// 입력
dx_get('key', '기본값') // GET
dx_post('key', '기본값') // POST
dx_post('key', '0', 'bigint') // BIGINT → string 반환
dx_method('POST') // HTTP 메서드 확인
// 출력
dx_json($data) // JSON 응답 + exit
dx_error('메시지', 403) // 에러 + 중단
dx_csrf_field() // <input type="hidden" name="_csrf" ...>
dx_csrf_check() // CSRF 검증 (실패 시 403)
// 날짜
dx_date('2026-01-01 00:00', 'Y.m.d H:i') // 포맷 변환
dx_date($dt, 'Y년 m월 d일 (D) H:i') // 한국어 요일 포함
// 문자열
dx_mb_substr($str, 0, 1) // 멀티바이트 substr
dx_ip() // 클라이언트 IP
// Auth
$auth = Auth::getInstance();
$auth->isLoggedIn()
$auth->isAdmin()
$auth->user()['id'] // INT — member_id용
(string)$auth->user()['id'] // BIGINT 비교 시 string 캐스팅
```
---
## 11. 스킨 유형별 설계 패턴
### 11-1. 달력/스케줄러 스킨
- `list.php`에서 직접 달력 렌더링 (perPage 방식 미사용)
- `_list_rows.php`는 빈 파일로 유지
- 여분 필드: `event_start`, `event_end`, `event_type`, `event_status`, `color_label` 필수
- **데이터 로드 패턴**: post_meta에서 날짜 범위 post_id 추출 → 단건씩 posts 조회 → getMeta()
- `_ltNotices/Posts = array()` 어댑터 변수 설정 후 `_list_rows.php` include 생략 가능
### 11-2. 카드형 갤러리 스킨
- `_list_rows.php`에서 CSS Grid 카드 렌더링
- `thumbnail` 컬럼 활용 또는 여분 필드 `cover_image` 추가
- 호버 효과: `transform: translateY(-3px)`, `box-shadow` 변화
- 카드 상단 컬러 바: `::before` 또는 상태별 배경색
### 11-3. Q&A 스킨
- 채택 기능: `view.php POST _sub=qa_accept` → `dx_post_meta` 저장
- 답변 여부 배지: `_list_rows.php`에서 `$_ltAcceptedIds` 배열로 처리
- 답변자 구분: `$post['member_id'] !== $post['parent_member_id']`
- `dx_likes` 재활용: `target_type='qa_eval'`로 평가 중복 방지
### 11-4. 포트폴리오/프로젝트 스킨
- 진행률 바: 여분 필드 `progress` (0~100)
- 상태 배지: `status` 필드 select (계획중/진행중/완료/보류)
- 기술 스택 태그: `tech_stack` 필드, 콤마 구분 → PHP `explode(',')`
- 공개 여부: `is_public` checkbox
### 11-5. 이벤트/모임 스킨
- 신청 기능: `require_apply` + `max_people` + `current_people` 여분 필드
- 마감 처리: `apply_deadline` datetime 필드, `strtotime()` 비교
- 인원 바: `current_people / max_people * 100`%
- 외부 신청 링크: `external_url` 필드
---
## 12. CSS/디자인 패턴
### 카드 공통 스타일 (CSS 변수 활용)
```css
.skin-card {
background: var(--bg-card, #fff);
border: 1.5px solid var(--border, #e2e8f0);
border-radius: 16px;
padding: 20px;
transition: border-color .18s, box-shadow .18s, transform .18s;
}
.skin-card:hover {
border-color: var(--p, #1a73e8);
box-shadow: 0 8px 28px rgba(26,115,232,.12);
transform: translateY(-3px);
}
```
### DXCMS CSS 변수
```css
var(--p) /* 주 색상 (파랑 계열) */
var(--bg-card) /* 카드 배경 */
var(--bg-body) /* 페이지 배경 */
var(--bg-sub) /* 서브 배경 (f8fafc) */
var(--border) /* 경계선 (e2e8f0) */
var(--text-main) /* 주 텍스트 (1e293b) */
var(--text-muted) /* 보조 텍스트 (94a3b8) */
```
### 상태 배지 패턴
```php
function _skin_status_style($status) {
$map = array(
'완료' => array('bg'=>'#dcfce7','color'=>'#15803d','dot'=>'#22c55e'),
'진행중' => array('bg'=>'#dbeafe','color'=>'#1d4ed8','dot'=>'#3b82f6'),
'대기' => array('bg'=>'#f1f5f9','color'=>'#475569','dot'=>'#94a3b8'),
'취소' => array('bg'=>'#fee2e2','color'=>'#b91c1c','dot'=>'#ef4444'),
);
return isset($map[$status]) ? $map[$status] : $map['대기'];
}
```
### 정보 카드 그리드 (뷰 페이지용)
```html
<div style="display:grid;grid-template-columns:1fr 1fr;gap:0">
<!-- 전체 폭 항목 -->
<div style="grid-column:1/-1;display:flex;align-items:flex-start;gap:12px;
padding:14px 20px;border-bottom:1px solid var(--border)">
<div style="width:34px;height:34px;border-radius:8px;
background:rgba(26,115,232,.1);color:#1d4ed8;
display:flex;align-items:center;justify-content:center;font-size:.85rem">
<i class="fa-regular fa-calendar-days"></i>
</div>
<div>
<div style="font-size:.68rem;font-weight:700;color:var(--text-muted);
margin-bottom:3px;letter-spacing:.03em">라벨</div>
<div style="font-size:.88rem;font-weight:600;color:var(--text-main)">값</div>
</div>
</div>
</div>
```
---
## 13. 체크리스트
스킨 코드 작성 전 반드시 확인:
- [ ] `require_once DX_ROOT . '/core/BoardFields.php'` 추가했는가?
- [ ] **post_id를 string으로 처리**하는가? `(int)` 캐스팅 없는가?
- [ ] **DB 파라미터 모두 string** 캐스팅했는가? `(string)$id`
- [ ] `posts` 테이블에 `member_name` 없음 → `dx_members` JOIN 필요
- [ ] `row()` 메서드 미지원 → `rows()[0]` 패턴 사용
- [ ] 복잡한 다중 JOIN 대신 **2단계 분리 쿼리** 사용했는가?
- [ ] POST API를 **view.php 상단 `_sub` 패턴**으로만 처리하는가?
- [ ] `ob_clean()` → `dx_csrf_check()` 순서가 맞는가?
- [ ] list.php에 렌더링/API 코드 없는가? (공지 중복 원인)
- [ ] 커스텀 액션 URL(`/게시판키/custom`) 사용하지 않는가?
- [ ] PHP 5.6 호환: `array()`, 클로저 대신 일반 함수, `usort('함수명')` 형식
- [ ] `skin.json`의 `actions` 배열에 표준 액션만 있는가?
- [ ] 댓글 필요 시 **베이직 view.php 복사 후 커스텀 블록 삽입** 방식 사용했는가?
- [ ] `datetime-local` 값을 submit 시 공백 형식으로 변환했는가?
- [ ] is_view OFF 필요한 여분 필드를 안내했는가?
---
## 14. 오류 디버깅
| 증상 | 원인 | 해결 |
|---|---|---|
| DB 500 오류 | BIGINT 파라미터 int 캐스팅 | 모든 파라미터 `(string)` 캐스팅 |
| DB 500 오류 | `member_name` 컬럼 없음 | `dx_members` LEFT JOIN 추가 |
| DB 500 오류 | 다중 LEFT JOIN | 2단계 분리 쿼리로 교체 |
| DB 500 오류 | `row()` 미지원 | `rows()[0]` 패턴으로 교체 |
| JSON 파싱 실패 | `ob_clean()` 누락 | view.php POST 분기 최상단에 추가 |
| 404 커스텀 액션 | Router.php 미등록 | view.php `_sub` 패턴으로 변경 |
| 공지 2번 출력 | list.php에 렌더링 코드 | list.php 베이직으로 복원 |
| syntax error `}` | 파일 조합 시 중복 | 해당 라인 전후 블록 중복 제거 |
| 댓글 없음 | 독자 view.php 구현 | 베이직 view.php 복사 후 커스텀 삽입 |
| 카드 좌우 여백 없음 | `.pj-grid` padding 미설정 | `padding: 16px 20px` 추가 |
| 전체공지 여백 줄어듦 | 래퍼에 padding 적용 | 래퍼 padding 제거, 그리드만 적용 |