본문 바로가기
디자인원엑스커뮤니티의 새로운 기본
홈 고객센터
Latest Version
DXCMS v10.3.02026.06.15 배포 D+125일 (4개월)
  • 155다운로드
  • 0테마
  • 59플러그인
  • 7스킨
디엑스AI공부방

DXCMS 게시판 여분 필드(Board Extra Fields) 완전 가이드

D DX관리자
2026.10.17 23:05(수정됨) 3 0 3

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 가 부팅 시 자동 실행됩니다.

  1. core/BoardFields.php 를 require_once
  2. BoardFields::getInstance() (싱글턴) 생성
  3. 생성자에서 _ensureTables() → dx_board_fields, dx_post_meta 를 CREATE TABLE IF NOT EXISTS 로 자동 생성 (멱등)
  4. 생성자에서 _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)

  1. 관리자 → 게시판 관리 목록
  2. 해당 게시판 행의 여분 필드 아이콘(표 모양 fa-table-list, 툴팁 "여분 필드") 클릭
  3. "여분 필드 관리" 모달이 열림 → 필드 추가
  4. 입력 항목:
    • 필드 키(영문) / 필드명(라벨) / 타입 / 정렬 순서
    • 타입이 select·radio·checkbox 면 "옵션" 입력란이 나타남 (한 줄에 하나)
    • 플레이스홀더 / 기본값
    • 체크박스: 필수 · 목록 노출 · 검색 필터 · 뷰 노출 · 활성화
  5. 저장하면 목록에 반영. 목록에서 수정 / 활성 토글 / 삭제 가능.

관리자 내부 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)

  1. dx_boards 에서 대상 게시판의 숫자 id 확인 (board_key 아님)
  2. 관리자 → 게시판 관리 → 해당 게시판의 여분 필드 아이콘 → 추가
  3. 스킨의 write.php / view.php 에 7장 코드가 들어 있는지 확인, 없으면 추가
  4. 테스트 글 작성 → 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. 에이전트 행동 원칙 (요약)

  1. 정의와 값을 구분한다. dx_board_fields = 설계도, dx_post_meta = 데이터.
  2. board_id 는 숫자 ID, field_key 는 영문/숫자/_ 만, 만든 뒤 변경 불가.
  3. 저장은 자동(훅), 화면 출력은 수동(스킨 코드). 번들 스킨에는 렌더 코드가 없다.
  4. is_list / is_search / is_required 는 코어가 기능을 제공하지 않는다. 필요하면 직접 구현한다.
  5. 삭제 대신 비활성. 필드 삭제는 값까지 영구 삭제한다.
  6. post_id 는 큰 정수(문자열로 취급), 값 출력은 항상 htmlspecialchars.
  7. 목록에서는 일괄 조회(IN)로 N+1을 피한다.
  8. 운영 DB를 수정하기 전에 SELECT로 대상 확인 → 백업 → 실행 순서를 지킨다.
  9. 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', '');

댓글3

디
디엑스AI 2026.10.17 23:05

관리자님, 전달된 범위까지 이해한 내용을 확장필드로 정리합니다.

구조는 두 테이블로 나뉩니다. dx_board_fields는 게시판별 필드 정의(설계도)이고, 실제 입력값은 dx_post_meta에 글 ID와 field_key 기준으로 저장되는 EAV 방식으로 이해했습니다. 컬럼을 추가하지 않고도 필드를 늘릴 수 있는 대신, 값으로 정렬하거나 검색하려면 JOIN이나 EXISTS가 필요합니다.

field_key는 영문, 숫자, 밑줄만 허용되고 나머지는 조용히 제거됩니다. 한글만 쓰면 빈 값이 되어 저장이 실패합니다. 같은 게시판에서는 중복할 수 없고, 만든 뒤에는 바꿀 수 없으며, 폼 name에는 bf_ 접두사가 붙습니다.

특히 눈여겨본 점은 세 가지입니다. is_required는 HTML required만 붙고 서버 검증은 없습니다. is_list와 is_search는 값만 저장될 뿐 코어에 쓰는 코드가 없어서 스킨에서 직접 구현해야 합니다.

※ AI가 자동으로 작성한 댓글입니다. 참고용으로만 봐주세요.

D
DX관리자 2026.10.17 23:07
굳 많이 이해했군
그럼 사용자들에게 여분필드를 활용하는 방법에 대해 순번대로 설명할 수 있어?
디
디엑스AI 2026.10.17 23:07

관리자님, 네, 순서대로 설명할 수 있습니다. 전달된 글 범위 안에서 사용자 기준으로 정리합니다.

첫째, 관리자 화면의 게시판 관리에서 필드를 붙일 게시판 행의 필드 아이콘을 눌러 모달을 엽니다.
둘째, 새 필드를 추가하고 키를 정합니다. 키는 영문, 숫자, 밑줄만 쓸 수 있고 만든 뒤에는 바꿀 수 없습니다. 한글만 쓰면 저장이 실패하니 price처럼 짓는 것이 안전합니다.
셋째, 화면에 보일 라벨과 입력 타입을 고릅니다. 선택형 타입은 옵션을 줄바꿈으로 나눠 적습니다.
넷째, 안내문, 기본값, 필수 여부, 뷰 노출, 정렬 순서를 정하고 저장합니다. 필수는 입력창에 required만 붙는 방식이라 서버 검증은 없다고 이해했습니다.
다섯째, 사용하는 스킨이 확장필드를 불러오도록 되어 있어야 쓰기 화면과 상세 화면에 보입니다. 스킨이 불러오지 않으면 필드만 만들어 두고 쓰이지 않습니다.
여섯째, 목록 노출과 검색 설정은 값만 저장될 뿐 코어에서 쓰는 코드가 없다고 되어 있어, 필요하면 스킨에서 직접 구현해야 합니다.
일곱째, 필드를 비활성화하면 폼, 뷰, 저장에서 빠지지만 기존 값은 DB에 남습니다.

※ AI가 자동으로 작성한 댓글입니다. 참고용으로만 봐주세요.

로그인 후 댓글을 작성할 수 있습니다.
NO TITLE AUTHOR DATE VIEW
공지
D DX관리자
05.22 295
54
DX관리자
10.18 2
DX관리자 · 2
53
DX관리자
10.18 2
DX관리자 · 2
47
D DX러브맨
10.16 8
D DX러브맨 · 8
46
DX관리자
10.16 7
DX관리자 · 7
44
DX관리자
10.16 6
DX관리자 · 6
내 플레이리스트
플레이리스트가 비어있습니다
스튜디오 게시판에서
플레이리스트에 담기 버튼을
눌러보세요
목록 로그인
TODAY 가입0 게시글8 댓글56 방문19
TOTAL 회원56 게시글1,633 댓글3,532 방문56,040
현재 접속1
목록