DXCMS 게시판 여분 필드(Board Extra Fields) 완전 가이드
대상 버전: DXCMS v10.3.0 (소스 코드 직접 분석 기반) 대상 독자: DXCMS를 다루는 에이전트 AI / 개발자 핵심 파일:
core/BoardFields.php,extend/top/50_board_fields.php,admin/boards/index.php,admin/boards/fields.js,boards/handler.php
0. 한 줄 요약
게시판마다 관리자가 "가격", "지역", "연락처" 같은 입력 항목을 코드 수정 없이 추가할 수 있는 기능입니다. 정의는 dx_board_fields, 값은 dx_post_meta에 저장됩니다. (워드프레스의 custom field / post meta와 같은 구조)
dx_board_fields 테이블은 이 기능의 "필드 정의(설계도)" 테이블입니다. 실제 입력값은 여기에 들어가지 않습니다.
1. 전체 구조 (반드시 이해할 것)
┌───────────────────────┐ ┌──────────────────────────┐
│ dx_board_fields │ │ dx_post_meta │
│ (필드 정의 = 설계도) │ │ (필드 값 = 실제 데이터) │
│ │ │ │
│ board_id ───────────┼──┐ │ post_id (글 ID) │
│ field_key ──────────┼──┼────►│ field_key (정의의 key) │
│ field_label │ │ │ value (입력값 TEXT) │
│ field_type ... │ │ └──────────────────────────┘
└───────────────────────┘ │ ▲
│ │ post_id
┌────▼──────────────┴───┐
│ dx_posts │
│ id (BIGINT 밀리초 ID) │
│ board_id │
└───────────────────────┘
| 구분 | 테이블 | 역할 | 행 수 규모 |
|---|---|---|---|
| 정의 | dx_board_fields |
게시판별로 "어떤 항목이 있는지" | 게시판당 수 개 ~ 수십 개 |
| 값 | dx_post_meta |
글별로 "각 항목에 뭐라고 입력했는지" | 글 수 × 필드 수 |
- 테이블 접두사
dx_는 설치 시 바꿀 수 있습니다. 코드에서는 항상$db->table('board_fields')로 접두사를 붙입니다. 직접 SQL을 쓸 때는 실제 접두사를 확인하세요. - 이 구조는 EAV(Entity-Attribute-Value) 방식입니다. 컬럼을 추가(ALTER TABLE)하지 않고도 필드를 무한히 늘릴 수 있다는 장점이 있고, 대신 값으로 정렬·검색할 때 JOIN/EXISTS가 필요하다는 단점이 있습니다.
2. dx_board_fields 컬럼 상세
CREATE TABLE IF NOT EXISTS `dx_board_fields` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`board_id` int(11) unsigned NOT NULL,
`field_key` varchar(64) NOT NULL,
`field_label` varchar(128) NOT NULL,
`field_type` varchar(32) NOT NULL DEFAULT 'text',
`field_options` text,
`placeholder` varchar(255) NOT NULL DEFAULT '',
`default_value` varchar(255) NOT NULL DEFAULT '',
`is_required` tinyint(1) NOT NULL DEFAULT '0',
`is_list` tinyint(1) NOT NULL DEFAULT '0',
`is_search` tinyint(1) NOT NULL DEFAULT '0',
`is_view` tinyint(1) NOT NULL DEFAULT '1',
`sort_order` smallint(5) NOT NULL DEFAULT '0',
`status` tinyint(1) NOT NULL DEFAULT '1',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_board_field` (`board_id`,`field_key`),
KEY `idx_board` (`board_id`),
KEY `idx_sort` (`board_id`,`sort_order`)
);
| 컬럼 | 의미 | 규칙 / 주의 |
|---|---|---|
id |
필드 정의 PK | 자동 증가. 수정/삭제/토글 시 이 ID로 지정 |
board_id |
소속 게시판 (dx_boards.id) |
board_key(문자열)가 아니라 숫자 ID. 수정 시 변경 불가 |
field_key |
필드의 영문 식별자 | 아래 "key 규칙" 참고. 한 번 만들면 변경 불가 |
field_label |
화면에 보이는 이름 | 한글 가능. 최대 128자 (mb_substr로 잘림) |
field_type |
입력 형태 | 10가지 중 하나 (3장). 허용 목록 밖이면 text로 강제 |
field_options |
선택지 목록 | select/radio/checkbox 전용. 줄바꿈(Enter)으로 구분 |
placeholder |
입력창 안내 문구 | 최대 255자 |
default_value |
새 글 작성 시 기본값 | 최대 255자. 수정 화면에서는 저장값이 우선 |
is_required |
필수 여부 | HTML required 속성만 붙음 (서버 검증 없음, 8장 참고) |
is_list |
목록 노출 플래그 | 값만 저장될 뿐 코어에서 사용하는 코드 없음 → 스킨에서 직접 구현 |
is_search |
검색 필터 플래그 | 값만 저장될 뿐 코어에서 사용하는 코드 없음 → 스킨에서 직접 구현 |
is_view |
뷰(상세) 페이지 노출 | renderView() 가 이 값이 1인 필드만 출력 |
sort_order |
정렬 순서 | 작은 수가 먼저. 같으면 id 순 |
status |
활성/비활성 | 0이면 폼·뷰·저장 모두에서 제외 (기존 값은 DB에 남음) |
created_at |
생성 시각 | 자동 |
field_key 규칙 (가장 많이 실수하는 부분)
- 허용 문자: 영문 대소문자, 숫자, 언더스코어(
_) 만. 그 외 문자는saveField()에서 조용히 제거됩니다.- 예:
가격→ 빈 문자열 → 저장 실패(false) - 예:
price-won→pricewon(하이픈 제거됨)
- 예:
- 같은 게시판 안에서 중복 불가 (
UNIQUE (board_id, field_key)). - 다른 게시판끼리는 같은 key 사용 가능.
- 생성 후 수정 불가 (관리자 UI에서도 readOnly). key를 바꾸려면 새 필드를 만들고 값을 마이그레이션해야 합니다.
- 권장: 소문자 + 언더스코어 (
price,area_size,contact_tel). 이후 폼 name은bf_price처럼bf_접두사가 붙습니다.
3. 지원하는 필드 타입 (field_type)
| 타입 | 렌더링 | field_options |
저장 형식 | 비고 |
|---|---|---|---|---|
text |
<input type="text"> |
불필요 | 문자열 | 기본값 |
textarea |
<textarea rows=4> |
불필요 | 문자열 (줄바꿈 포함) | 뷰에서 nl2br 처리 안 됨 (한 줄로 출력) |
number |
<input type="number"> |
불필요 | 문자열로 저장 | 서버에서 숫자 검증 안 함 |
select |
<select> + -- 선택하세요 -- |
필수 (줄바꿈 구분) | 선택한 옵션 문자열 | |
radio |
<input type="radio"> 나열 |
필수 | 선택한 옵션 문자열 | |
checkbox |
<input type="checkbox" name="bf_키[]"> 나열 |
필수 | 콤마로 이어붙인 문자열 (A,B,C) |
옵션 안에 , 사용 금지 |
date |
<input type="date"> |
불필요 | YYYY-MM-DD |
|
datetime |
<input type="datetime-local"> |
불필요 | YYYY-MM-DDTHH:MM |
값 사이에 T 포함 |
url |
<input type="url"> |
불필요 | 문자열 | 뷰에서 클릭 가능한 링크로 출력 |
tel |
<input type="tel"> |
불필요 | 문자열 |
field_options 작성 예
field_options 컬럼에는 이렇게 저장합니다 (줄바꿈이 구분자).
서울
경기
부산
대구
- 앞뒤 공백은 trim 되고, 빈 줄은 무시됩니다.
- 옵션 문자열 자체가 저장값이 됩니다. 나중에 옵션 이름을 바꾸면 기존 글의 값과 불일치가 생기므로(체크/선택 상태가 풀림) 운영 중 옵션 이름 변경은 신중히 하세요.
4. 활성화 및 동작 흐름
4-1. 자동 로드
extend/top/50_board_fields.php 가 부팅 시 자동 실행됩니다.
core/BoardFields.php를require_onceBoardFields::getInstance()(싱글턴) 생성- 생성자에서
_ensureTables()→dx_board_fields,dx_post_meta를CREATE TABLE IF NOT EXISTS로 자동 생성 (멱등) - 생성자에서
_registerHooks()→ 아래 두 훅 등록
| 훅 | 호출 시점 | 콜백 | 하는 일 |
|---|---|---|---|
dx_board_after_save |
글 작성/수정 저장 직후 (boards/handler.php) |
_dx_bf_hook_after_save |
$_POST['bf_*'] 추출 → dx_post_meta upsert |
dx_board_after_delete |
글 삭제 직후 | _dx_bf_hook_after_delete |
해당 글의 meta 전부 삭제 |
extend/top/50_board_fields.php파일을 지우면 훅이 등록되지 않아 값이 저장되지 않습니다. (정의 화면은 관리자 쪽에서require_once하므로 열리지만, 저장·삭제 연동은 끊김)
4-2. 저장 흐름 (글쓰기 → DB)
[write.php 폼]
<input name="bf_price" value="1000"> ← renderWriteForm() 이 생성
│ POST
▼
[boards/handler.php] 글 저장(dx_posts INSERT/UPDATE)
│
▼ dx_run_hook('dx_board_after_save', post_id, board, ...)
[_dx_bf_hook_after_save]
│ extractFromPost($board['id'])
│ └ 활성(status=1) 필드만 순회하며 $_POST['bf_'.field_key] 수집
│ (checkbox는 배열 → implode(',') )
▼
[saveMeta($postId, $values)]
└ INSERT ... ON DUPLICATE KEY UPDATE (dx_post_meta, uk_post_field)
핵심: 필드 정의에 없는 bf_xxx 는 무시됩니다. 폼에 임의로 bf_abc 를 넣어도, abc 가 dx_board_fields 에 등록(활성)되어 있어야 저장됩니다.
4-3. 출력 흐름 (DB → 화면)
[view.php] renderView($board['id'], $post['id'])
├ getFields($boardId) 정의 조회 (status=1, 정렬순)
├ is_view=1 인 것만 필터
├ getMeta($postId) dx_post_meta 전체 조회 → ['key'=>'value']
└ 값이 빈 문자열이면 건너뛰고 <dl><dt>라벨</dt><dd>값</dd></dl> 출력
5. 관리자 화면에서 사용하는 방법 (GUI)
- 관리자 → 게시판 관리 목록
- 해당 게시판 행의 여분 필드 아이콘(표 모양
fa-table-list, 툴팁 "여분 필드") 클릭 - "여분 필드 관리" 모달이 열림 → 필드 추가
- 입력 항목:
- 필드 키(영문) / 필드명(라벨) / 타입 / 정렬 순서
- 타입이 select·radio·checkbox 면 "옵션" 입력란이 나타남 (한 줄에 하나)
- 플레이스홀더 / 기본값
- 체크박스: 필수 · 목록 노출 · 검색 필터 · 뷰 노출 · 활성화
- 저장하면 목록에 반영. 목록에서 수정 / 활성 토글 / 삭제 가능.
관리자 내부 AJAX 액션 (admin/boards/index.php)
act 값 |
동작 | 주요 파라미터 | 응답 |
|---|---|---|---|
bf_list |
필드 목록 (비활성 포함) | bf_board_id |
{ok:true, fields:[...]} |
bf_save |
필드 생성/수정 | bf_id(0이면 신규), bf_board_id, bf_field_key, bf_field_label, bf_field_type, bf_field_options, bf_placeholder, bf_default_value, bf_is_required, bf_is_list, bf_is_search, bf_is_view, bf_sort_order, bf_status |
JSON ok |
bf_toggle |
활성/비활성 전환 | bf_id |
JSON ok |
bf_delete |
필드 삭제 (저장값 포함) | bf_id |
JSON ok |
모든 요청은 관리자 로그인 + CSRF 토큰이 필요합니다. (프런트 로직은 admin/boards/fields.js)
게시판 단위 연동 동작
| 상황 | 필드 정의 | 필드 값 |
|---|---|---|
| 게시판 복사 (관리자) | 정의가 새 게시판으로 복사됨 (board_id 교체) | "데이터 포함 복사(copy_data=all)" 시 글과 함께 복제 |
| 게시판 삭제 | 정의 삭제 | 글 삭제 과정에서 post_meta 삭제 |
| 글 복사/이동 (handler) | — | post_meta 도 함께 복제 |
| 글 삭제 | — | dx_board_after_delete 훅 또는 완전삭제 루틴에서 삭제 |
| 필드 삭제 | 정의 삭제 | 해당 게시판 글들의 해당 key 값 전부 삭제 (복구 불가) |
| 필드 비활성(status=0) | 유지 | 값은 DB에 남아 있음, 화면·저장에서만 제외 |
6. PHP API 레퍼런스 (BoardFields 클래스)
전역 헬퍼:
dx_board_fields() // BoardFields 싱글턴
dx_get_post_meta($postId, $key, $default = '') // 단일 값 읽기
dx_save_post_meta($postId, array $values) // 값 일괄 저장
정의 관련
| 메서드 | 설명 |
|---|---|
isReady() |
테이블 준비 여부 (false면 모든 메서드가 빈 결과/false) |
getFields($boardId, $activeOnly = true) |
게시판의 필드 정의 배열. sort_order ASC, id ASC |
getField($id) |
정의 1건 (PK 기준) |
saveField(array $data) |
id>0 이면 UPDATE(단, board_id·field_key 는 변경 안 됨), 아니면 INSERT. 성공 true/실패 false |
deleteField($id) |
정의 + 해당 게시판 글들의 값 삭제 |
allowedTypes() |
허용 타입 배열 |
typeLabel($type) |
타입의 한글 라벨 |
값 관련
| 메서드 | 설명 |
|---|---|
getMeta($postId) |
['field_key' => 'value', ...] 전체 (정의와 무관하게 해당 글의 모든 meta) |
getMetaValue($postId, $key, $default='') |
단일 값. 내부적으로 getMeta()를 매번 호출 → 반복 호출 시 쿼리가 반복됨 |
saveMeta($postId, array $values) |
upsert. key는 [a-zA-Z0-9_] 외 제거 |
deleteMeta($postId) |
글 1건의 meta 전체 삭제 |
extractFromPost($boardId) |
$_POST['bf_*'] 를 읽어 ['key'=>'value'] 생성. 훅이 자동 호출하므로 직접 호출할 일은 거의 없음 |
렌더 관련
| 메서드 | 설명 |
|---|---|
renderWriteForm($boardId, array $savedMeta = []) |
작성/수정 폼 HTML 반환 (활성 필드만). 필드가 없으면 빈 문자열 |
renderView($boardId, $postId) |
상세 보기용 <dl> HTML 반환 (is_view=1 & 값이 있는 필드만) |
⚠️ README 의
dx_board_fields()->getValue(...)는 실제로 존재하지 않는 메서드입니다. 값 읽기는getMetaValue()또는dx_get_post_meta()를 사용하세요.
7. 스킨에 적용하는 방법 (실전)
중요: 번들 스킨(boards/skins/gallery)과 SKIN_GUIDE.md 에는 여분 필드 호출 코드가 들어 있지 않습니다. 필드를 정의했더라도 스킨의 write.php / view.php 에 아래 코드를 직접 추가해야 화면에 나타납니다. (저장 훅은 자동이지만 렌더는 수동)
스킨에서 쓸 수 있는 변수: write.php — $board, $action('write'|'edit'), $editPost(수정 시 글 배열) / view.php — $board, $post.
7-1. write.php — 글쓰기/수정 폼
<form> 안, 제출 버튼 위에 넣습니다. (<form id="dx-write-form" ... enctype="multipart/form-data"> 내부)
<?php
// 수정 모드면 저장된 값을 불러오고, 새 글이면 빈 배열 (기본값이 적용됨)
$_bfSaved = array();
if (!empty($editPost['id'])) {
$_bfSaved = dx_board_fields()->getMeta($editPost['id']);
}
echo dx_board_fields()->renderWriteForm($board['id'], $_bfSaved);
?>
- 반드시
<form>태그 안쪽이어야 값이 POST에 포함됩니다. - 파일 업로드가 있어도
bf_필드는 정상 전송됩니다.
7-2. view.php — 상세 보기
<?php echo dx_board_fields()->renderView($board['id'], $post['id']); ?>
본문 위/아래 원하는 위치에 넣으면 <dl> 형태로 "라벨: 값" 이 출력됩니다.
7-3. 단일 값만 꺼내 쓰기 (직접 디자인할 때)
$price = dx_get_post_meta($post['id'], 'price', '0');
echo number_format((int)$price) . '원';
// 여러 값이 필요하면 한 번에 가져오기 (쿼리 1회)
$meta = dx_board_fields()->getMeta($post['id']);
$area = isset($meta['area']) ? $meta['area'] : '';
$tel = isset($meta['tel']) ? $meta['tel'] : '';
출력할 때는 반드시 이스케이프하세요.
echo htmlspecialchars($area, ENT_QUOTES, 'UTF-8');
7-4. checkbox 값 다루기
$meta = dx_board_fields()->getMeta($post['id']);
$tags = !empty($meta['options']) ? explode(',', $meta['options']) : array();
foreach ($tags as $t) { echo '<span class="chip">'.htmlspecialchars($t,ENT_QUOTES,'UTF-8').'</span>'; }
7-5. list.php — 목록에 필드 표시 (is_list 직접 구현)
is_list 는 플래그일 뿐이라 목록 출력은 스킨에서 구현해야 합니다. 글마다 getMeta() 를 호출하면 N+1 쿼리가 되므로 한 번에 가져오세요.
<?php
// 1) 목록 노출 대상 필드 정의
$_listFields = array_filter(dx_board_fields()->getFields($board['id']), function($f){
return (int)$f['is_list'] === 1;
});
// 2) 현재 페이지 글들의 meta를 한 번에 조회
$_metaByPost = array();
if ($_listFields && !empty($posts)) {
$db = Database::getInstance();
$ids = array();
foreach ($posts as $p) { $ids[] = (string)$p['id']; }
$in = implode(',', array_fill(0, count($ids), '?'));
$rows = $db->rows(
"SELECT post_id, field_key, value FROM `{$db->table('post_meta')}` WHERE post_id IN ($in)",
$ids
);
foreach ($rows as $r) { $_metaByPost[$r['post_id']][$r['field_key']] = $r['value']; }
}
?>
<?php foreach ($posts as $p): $m = isset($_metaByPost[$p['id']]) ? $_metaByPost[$p['id']] : array(); ?>
<tr>
<td><?php echo htmlspecialchars($p['title'],ENT_QUOTES,'UTF-8'); ?></td>
<?php foreach ($_listFields as $f): ?>
<td><?php echo htmlspecialchars(isset($m[$f['field_key']]) ? $m[$f['field_key']] : '', ENT_QUOTES, 'UTF-8'); ?></td>
<?php endforeach; ?>
</tr>
<?php endforeach; ?>
7-6. 필드 값으로 검색/필터 (is_search 직접 구현)
핸들러가 필드 검색을 지원하지 않으므로, 글 목록 쿼리에 조건을 붙이는 코드가 필요합니다. 패턴은 EXISTS 입니다.
SELECT p.*
FROM dx_posts p
WHERE p.board_id = :board_id
AND p.status = 1
AND EXISTS (
SELECT 1 FROM dx_post_meta m
WHERE m.post_id = p.id
AND m.field_key = 'region'
AND m.value = '서울'
)
ORDER BY p.id DESC;
- checkbox 값(
A,B,C) 포함 검색은FIND_IN_SET('A', m.value)를 사용하세요. - 숫자 범위(가격 등)는
CAST(m.value AS UNSIGNED) BETWEEN 1000 AND 5000. (값이 문자열 TEXT이므로 인덱스를 타지 않아 대용량에서는 느립니다) - 문자열 완전일치 필터가 자주 필요하면
idx_key (field_key)인덱스를 활용하는 쿼리로 작성하세요.
8. 알아야 할 제약과 주의사항 (코드 분석 결과)
에이전트가 추측하지 말고 이 목록을 기준으로 판단해야 하는 항목입니다.
| # | 사실 | 영향 / 대응 |
|---|---|---|
| 1 | is_required 는 HTML required 속성만 부여. 서버 검증 없음 |
우회 가능. 중요한 필수값은 dx_board_after_save 이전 단계(별도 훅/핸들러)에서 서버 검증 필요 |
| 2 | checkbox 는 required 가 적용되지 않음 |
체크박스 필수 검사는 JS/서버에서 별도 구현 |
| 3 | 값 형식 검증 없음 (number, url, date, select 옵션 범위) | 직접 저장하는 코드를 만들 때 검증 로직 추가 |
| 4 | is_list, is_search 는 저장만 되고 코어가 사용하지 않음 |
7-5, 7-6 처럼 스킨에서 구현 |
| 5 | 번들 스킨·SKIN_GUIDE 에 renderWriteForm / renderView 호출 없음 |
스킨에 수동 삽입 |
| 6 | boards/handler.php 의 글 저장 경로 중 한 곳(약 1741행)은 dx_after_write 만 호출하고 dx_board_after_save 는 호출하지 않음 |
그 경로로 저장되는 글(별도 기능)은 여분 필드가 저장되지 않을 수 있음 → 해당 기능에서 쓰려면 dx_save_post_meta() 직접 호출 |
| 7 | api/ 와 core/api/ 쪽 글 작성 경로에서 dx_board_after_save 호출은 확인되지 않음 (삭제 API는 dx_board_after_delete 호출) |
API로 글을 만들면 meta는 안 생김 → dx_save_post_meta($postId, [...]) 를 직접 호출 |
| 8 | renderWriteForm 의 url 타입 placeholder 처리 코드(($ph?:' https://').$ph)에 버그가 있음 — placeholder를 지정하면 같은 문장이 두 번 이어붙여짐 |
url 필드는 placeholder를 비워 두거나, 코드를 수정 |
| 9 | renderView 는 모든 값을 htmlspecialchars 처리 → textarea 줄바꿈이 한 줄로 보임, checkbox는 A,B,C 그대로 |
줄바꿈/배지 형태가 필요하면 7-3, 7-4 처럼 직접 출력 |
| 10 | getMetaValue() 는 호출할 때마다 getMeta() 쿼리 실행 |
여러 값은 getMeta() 1회 후 배열 사용 |
| 11 | saveMeta 는 오류를 삼킴 (catch (Exception $e) {}) |
저장이 안 될 때 에러 메시지가 안 나옴 → DB 직접 확인 |
| 12 | saveField() 는 실패 시 이유 없이 false |
key 규칙(영문/숫자/_), 중복 key, board_id 유효성부터 점검 |
| 13 | post_id 는 밀리초 타임스탬프 BIGINT (자동 증가 아님) |
PHP 32bit 환경에서는 int 변환 시 깨짐 → 항상 문자열로 다룰 것 |
| 14 | 비활성(status=0) 필드는 폼에서 사라지므로 글 수정 시 그 key는 갱신/삭제되지 않음 |
값은 남아 있다가 다시 활성화하면 보임 |
| 15 | 필드를 삭제하면 저장값도 함께 삭제됨 | 삭제 전에 SELECT로 백업. 일시적으로 숨기려면 삭제 대신 비활성 |
| 16 | field_key 변경 기능 없음 |
아래 10-4 의 마이그레이션 SQL 사용 |
| 17 | datetime-local 값은 T 포함 형식 |
출력 시 str_replace('T',' ',$v) |
| 18 | 코드베이스가 PHP 5.6 호환 스타일 (클로저 대신 일반 함수, array()) |
새 코드도 []/?? 대신 array()/isset() 사용 권장 (서버 PHP 버전 확인 후 결정) |
9. 활용 시나리오 (복붙용)
9-1. 부동산 게시판
| field_key | 라벨 | 타입 | 옵션 | 목록 | 검색 | 필수 |
|---|---|---|---|---|---|---|
deal_type |
거래유형 | radio | 매매 / 전세 / 월세 | ✔ | ✔ | ✔ |
price |
가격(만원) | number | ✔ | ✔ | ||
area |
면적(㎡) | number | ✔ | |||
region |
지역 | select | 서울 / 경기 / 인천 … | ✔ | ✔ | ✔ |
facilities |
편의시설 | checkbox | 주차 / 엘리베이터 / 에어컨 | |||
contact_tel |
연락처 | tel | ✔ |
9-2. 상품/중고거래 게시판
| field_key | 라벨 | 타입 | 옵션 |
|---|---|---|---|
price |
판매가 | number | |
condition |
상태 | select | 새상품 / 거의 새것 / 사용감 있음 |
trade_method |
거래방식 | checkbox | 택배 / 직거래 |
purchase_url |
구매 링크 | url |
9-3. 구인구직 게시판
| field_key | 라벨 | 타입 | 옵션 |
|---|---|---|---|
company |
회사명 | text | |
job_type |
고용형태 | radio | 정규직 / 계약직 / 아르바이트 |
deadline |
마감일 | date | |
apply_url |
지원 링크 | url |
9-4. 행사/모임 게시판
| field_key | 라벨 | 타입 |
|---|---|---|
event_start |
시작 일시 | datetime |
event_place |
장소 | text |
capacity |
정원 | number |
note |
준비물 | textarea |
10. 에이전트 작업 레시피
10-1. "게시판에 필드 추가해줘" (권장: 관리자 GUI)
dx_boards에서 대상 게시판의 숫자id확인 (board_key아님)- 관리자 → 게시판 관리 → 해당 게시판의 여분 필드 아이콘 → 추가
- 스킨의
write.php/view.php에 7장 코드가 들어 있는지 확인, 없으면 추가 - 테스트 글 작성 →
dx_post_meta에 행이 생겼는지 확인
10-2. PHP로 프로그래밍 방식으로 필드 추가
$ok = dx_board_fields()->saveField(array(
'board_id' => 3,
'field_key' => 'price',
'field_label' => '가격(만원)',
'field_type' => 'number',
'placeholder' => '숫자만 입력',
'is_required' => 1,
'is_list' => 1,
'is_search' => 0,
'is_view' => 1,
'sort_order' => 10,
'status' => 1,
));
// $ok === false 이면: key 규칙 / 중복 key / board_id 확인
옵션이 있는 필드:
dx_board_fields()->saveField(array(
'board_id' => 3,
'field_key' => 'region',
'field_label' => '지역',
'field_type' => 'select',
'field_options' => "서울\n경기\n인천\n부산", // 줄바꿈 구분
'is_list' => 1,
'is_search' => 1,
));
10-3. SQL로 직접 필드 추가 (관리자 접근이 불가할 때)
INSERT INTO `dx_board_fields`
(board_id, field_key, field_label, field_type, field_options,
placeholder, default_value, is_required, is_list, is_search, is_view, sort_order, status)
VALUES
(3, 'region', '지역', 'select', '서울\n경기\n부산', '', '', 1, 1, 1, 1, 20, 1);
field_options의\n은 실제 줄바꿈 문자여야 합니다. (MySQL 문자열 리터럴의'\n'은 줄바꿈으로 해석되므로 위와 같이 써도 되지만, 클라이언트 도구에 따라 다를 수 있으니 저장 후HEX()또는 관리자 화면에서 확인)- 접두사가
dx_가 아닌 설치본이면 테이블명을 맞추세요.
10-4. field_key 이름 바꾸기 (마이그레이션)
START TRANSACTION;
-- 1) 정의의 key 변경
UPDATE dx_board_fields
SET field_key = 'price_won'
WHERE board_id = 3 AND field_key = 'price';
-- 2) 해당 게시판 글의 값 key 변경
UPDATE dx_post_meta m
JOIN dx_posts p ON p.id = m.post_id
SET m.field_key = 'price_won'
WHERE p.board_id = 3 AND m.field_key = 'price';
COMMIT;
스킨에서 dx_get_post_meta(..., 'price') 로 직접 읽던 코드도 새 key로 수정해야 합니다.
10-5. 기존 글에 값 일괄 입력
$ids = $db->rows("SELECT id FROM `{$db->table('posts')}` WHERE board_id=?", array(3));
foreach ($ids as $r) {
dx_save_post_meta((string)$r['id'], array('deal_type' => '매매'));
}
10-6. 값 조회 SQL
-- 특정 글의 모든 여분 필드 (라벨과 함께)
SELECT f.field_label, m.field_key, m.value
FROM dx_post_meta m
JOIN dx_posts p ON p.id = m.post_id
LEFT JOIN dx_board_fields f ON f.board_id = p.board_id AND f.field_key = m.field_key
WHERE m.post_id = 1760000000000
ORDER BY f.sort_order, f.id;
-- 한 게시판의 글 + 여분 필드를 가로 형태(피벗)로
SELECT p.id, p.title,
MAX(CASE WHEN m.field_key='region' THEN m.value END) AS region,
MAX(CASE WHEN m.field_key='price' THEN m.value END) AS price
FROM dx_posts p
LEFT JOIN dx_post_meta m ON m.post_id = p.id
WHERE p.board_id = 3
GROUP BY p.id, p.title
ORDER BY p.id DESC;
10-7. 고아 데이터 점검/정리
-- 정의가 없는 key의 값 (필드 삭제 이후 남은 것, 거의 없음)
SELECT m.field_key, COUNT(*) AS cnt
FROM dx_post_meta m
JOIN dx_posts p ON p.id = m.post_id
LEFT JOIN dx_board_fields f ON f.board_id = p.board_id AND f.field_key = m.field_key
WHERE f.id IS NULL
GROUP BY m.field_key;
-- 글이 이미 없는 meta
SELECT COUNT(*) FROM dx_post_meta m LEFT JOIN dx_posts p ON p.id = m.post_id WHERE p.id IS NULL;
삭제 쿼리는 반드시 SELECT로 건수 확인 + 백업 후 실행하세요.
11. 문제 해결 체크리스트
| 증상 | 점검 순서 |
|---|---|
| 글쓰기 폼에 필드가 안 보임 | ① 필드 status=1 인가 ② board_id 가 맞나 ③ 스킨 write.php 에 renderWriteForm 이 있나 ④ <form> 안쪽에 있나 ⑤ extend/top/50_board_fields.php 존재 여부 |
| 폼은 보이는데 저장이 안 됨 | ① 폼 name이 bf_키 인지(직접 만든 input일 때) ② 필드가 활성인지 ③ 50_board_fields.php 로 훅이 등록되는지 ④ 해당 저장 경로가 dx_board_after_save 를 호출하는지(8장 6·7번) ⑤ dx_post_meta 테이블 존재 |
| 수정 화면에서 값이 비어 있음 | renderWriteForm 두 번째 인자로 getMeta($editPost['id']) 를 넘겼는지 |
| 상세 페이지에 안 보임 | is_view=1 인가, 값이 비어 있지 않은가, 스킨 view.php 에 호출이 있나 |
| 체크박스가 수정 때 해제됨 | 옵션 이름을 바꾼 적이 있는지 (저장값과 옵션 문자열 불일치) |
| 필드 추가가 실패함 | key에 한글/하이픈 사용? 같은 게시판에 중복 key? board_id 가 0 아닌가? |
| 목록이 느려짐 | 글마다 getMeta 호출 중인지 → 7-5 방식(IN 한 번 조회)으로 변경 |
| 필드가 갑자기 사라짐 | 관리자에서 삭제하면 값까지 삭제됨. 비활성 처리 권장 |
| url 필드 placeholder가 두 번 표시됨 | 8장 8번 버그 |
12. 에이전트 행동 원칙 (요약)
- 정의와 값을 구분한다.
dx_board_fields= 설계도,dx_post_meta= 데이터. board_id는 숫자 ID,field_key는 영문/숫자/_ 만, 만든 뒤 변경 불가.- 저장은 자동(훅), 화면 출력은 수동(스킨 코드). 번들 스킨에는 렌더 코드가 없다.
is_list/is_search/is_required는 코어가 기능을 제공하지 않는다. 필요하면 직접 구현한다.- 삭제 대신 비활성. 필드 삭제는 값까지 영구 삭제한다.
post_id는 큰 정수(문자열로 취급), 값 출력은 항상htmlspecialchars.- 목록에서는 일괄 조회(IN)로 N+1을 피한다.
- 운영 DB를 수정하기 전에 SELECT로 대상 확인 → 백업 → 실행 순서를 지킨다.
- README의
getValue()같은 문서 오류가 있을 수 있으므로, 의심되면core/BoardFields.php소스를 우선한다.
부록 A. 관련 파일 지도
core/BoardFields.php ← 클래스 본체 (CRUD, 렌더, 훅 콜백)
extend/top/50_board_fields.php ← 부팅 시 자동 로더
admin/boards/index.php ← bf_list / bf_save / bf_toggle / bf_delete, 모달 HTML, 게시판 복사/삭제 연동
admin/boards/fields.js ← 관리자 모달 프런트 로직
boards/handler.php ← dx_board_after_save / dx_board_after_delete 훅 호출, 글 복사 시 post_meta 복제
core/api/post_delete.php ← 삭제 API (dx_board_after_delete 호출)
install/migrate.php ← 업그레이드 시 두 테이블 생성
install/schema.sql ← (참고)
boards/skins/SKIN_GUIDE.md ← 스킨 가이드 (여분 필드 항목 없음 — 7장 내용을 추가하면 좋음)
부록 B. 폼 name 규칙 요약
| 필드 타입 | HTML name | POST 수신 형태 | 저장값 |
|---|---|---|---|
| text, textarea, number, date, datetime, url, tel, select, radio | bf_{field_key} |
문자열 | trim 된 문자열 |
| checkbox | bf_{field_key}[] |
배열 | implode(',') 된 문자열 |
부록 C. 빠른 시작 (3줄 요약)
// write.php (form 안)
echo dx_board_fields()->renderWriteForm($board['id'], !empty($editPost['id']) ? dx_board_fields()->getMeta($editPost['id']) : array());
// view.php
echo dx_board_fields()->renderView($board['id'], $post['id']);
// 값 하나만
$v = dx_get_post_meta($post['id'], 'price', '');