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

DXCMS 여분 필드 — 목록 노출 · 검색 필터 구현 가이드

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

DXCMS 여분 필드 — 목록 노출 · 검색 필터 구현 가이드

대상: DXCMS v10.3.0 / AI 에이전트 작업 지침서 범위: 여분 필드의 is_list(목록 노출)와 is_search(검색 필터) 두 설정을 실제로 동작하게 만드는 방법 근거: 소스 직접 분석(boards/handler.php, core/BoardFields.php, 번들 gallery 스킨) + 이 문서의 코드를 MariaDB 10.11 / PHP 8.3 에서 실행 검증 (부록 A)


0. AI 에이전트 사용 지침

  • 사용자가 "여분 필드를 목록에 보이게 해줘", "필드 값으로 검색/필터하고 싶다"고 요청하면 이 문서대로 작업한다.
  • 기본 구조(정의 dx_board_fields, 값 dx_post_meta, 폼 name bf_{key})는 이미 알고 있다고 가정한다.
  • 추측하지 말 것. is_list, is_search 체크박스는 DB에 값만 저장하며, 코어는 이 값을 읽는 코드가 없다. 이 문서의 코드를 설치해야 비로소 동작한다.
  • 작업 순서는 항상 아래와 같다.
[1단계] 헬퍼 파일 설치 (코어 수정 없음)         ← 필수
[2단계] 스킨에 목록 칸 추가                     ← "목록 노출" 요청일 때
[3단계] handler.php 패치 3곳 + 스킨에 필터 폼   ← "검색 필터" 요청일 때
[4단계] 검증 체크리스트 수행                     ← 항상
  • 수정 전에 대상 파일을 백업하고, 수정 후에는 php -l 파일 로 문법을 확인한다.
  • 사용자가 "목록 노출만" 원하면 3단계는 하지 않는다. (코어 파일 수정이 필요한 것은 3단계뿐)

1. 소스에서 확인된 사실 (이 가이드의 근거)

# 사실 의미
1 is_list, is_search 를 읽는 코드는 코어 어디에도 없다 (관리자 저장 화면에서만 사용) 스킨/핸들러에서 직접 구현해야 함
2 목록 쿼리는 boards/handler.php 의 case 'list': case 'search': 안에서 $where 문자열과 $params 배열로 조립된다. 조건을 끼워 넣는 훅이 없다 필터는 handler.php 에 소량 패치가 필요
3 COUNT(*) 쿼리와 목록 쿼리가 같은 $where/$params 를 쓴다. 글 별칭은 p 조건을 한 번만 붙이면 총 개수·페이지 수가 함께 맞는다
4 목록 캐시(DxCache, 60초, 키=게시판+페이지)는 !$search && !$cat && !$tag && 비로그인 && $action!=='search' 일 때만 사용된다 필터 조건을 캐시 조건에 넣지 않으면 필터가 무시된 캐시 목록이 나온다 → 패치에 반드시 포함
5 dx_board_list_context 훅은 쿼리 이후에 실행되지만, 캐시 경로에서는 실행되지 않는다 훅으로 글에 meta를 붙이는 방식은 비로그인 방문자에게 누락될 수 있음 → 스킨에서 직접 조회
6 목록 파라미터는 s, sf, cat, parent, tag, page. 스킨의 페이지네이션은 이 값들만 링크에 이어붙인다 필터 파라미터(bf_*)를 페이지네이션 링크에 직접 추가해야 2페이지에서 필터가 풀리지 않음
7 gallery 스킨은 _list_rows.php(카드 행 partial), list-live.js(새 글 실시간 삽입, /api/post_row 호출)를 쓴다 실시간으로 삽입된 새 행에는 여분 필드 칸이 없을 수 있음 (새로고침하면 정상)
8 글 상세 하단의 "목록"은 별도 쿼리(view 액션)이다 필터는 목록 화면에만 적용된다

2. 구현 방식 선택

요구 권장 방법 코어 수정 비고
목록에 값 표시 스킨에서 일괄 조회 후 출력 없음 2단계
필드 값으로 검색/필터 handler.php 3곳 패치 + 헬퍼 소량 (3곳) 3단계, 권장
(대안) 스킨 독립 핸들러 list/handler.php 목록 액션 전체를 스킨이 처리 없음 공지·카테고리·캐시·SEO·소켓을 전부 재구현해야 하므로 비권장
(비권장) 현재 페이지 결과를 JS/PHP로 걸러내기 — — 페이지 단위로만 걸러져 총 개수·페이지네이션이 어긋남
(비권장) dx_board_list_context 훅으로 meta 부착 — — 캐시 경로에서 실행 안 됨 (사실 #5)

3. 1단계 — 헬퍼 파일 설치 (코어 수정 없음)

extend/top/60_board_fields_helpers.php 를 새로 만든다.

  • 파일명은 50_board_fields.php 보다 뒤(60_ 이상)여야 한다. (extend/top/ 은 파일명 오름차순으로 자동 실행)
  • 이 파일은 DB 구조를 바꾸지 않는다. 삭제하면 원상복구된다.
  • 아래 코드는 PHP 5.6 호환 문법(array(), isset())만 사용한다. (실행 테스트는 PHP 8.3)
함수 용도
dx_bf_list_fields($boardId) is_list=1 이고 활성인 필드 정의
dx_bf_meta_map($posts) 글 배열의 값을 쿼리 1회로 조회 → [글ID][key]=값
dx_bf_display($field, $value) 값 → 목록용 HTML (이스케이프·타입별 표시)
dx_bf_build_filter($boardId, $_GET) is_search=1 필드의 GET 파라미터 → SQL 조건
dx_bf_render_filter_form(...) 필터 폼 HTML 생성
<?php
/**
 * DXCMS — 여분 필드 보조 헬퍼 (목록 노출 · 검색 필터)
 * extend/top/60_board_fields_helpers.php
 *
 * - 파일명은 반드시 50_board_fields.php 보다 뒤(60_ 이상)여야 합니다. (오름차순 로드)
 * - PHP 5.6 호환 문법만 사용합니다.
 * - 이 파일은 DB 구조를 바꾸지 않습니다. 삭제하면 원상복구됩니다.
 */
if (!defined('DX_CMS')) exit('Direct access not allowed.');

/* ══ 1. 목록 표시용 ═════════════════════════════════════════ */

/** 목록에 노출할 필드 정의 (활성 + is_list=1, 정렬순) */
if (!function_exists('dx_bf_list_fields')) {
    function dx_bf_list_fields($boardId)
    {
        if (!function_exists('dx_board_fields')) return array();
        $out = array();
        foreach (dx_board_fields()->getFields((int)$boardId) as $f) {
            if ((int)$f['is_list'] === 1) $out[] = $f;
        }
        return $out;
    }
}

/**
 * 글 배열의 여분 필드 값을 한 번에 조회 (N+1 방지)
 * 반환: array('글ID' => array('field_key' => 'value', ...), ...)
 * 사용: $metaMap = dx_bf_meta_map(array_merge($notices, $posts));
 */
if (!function_exists('dx_bf_meta_map')) {
    function dx_bf_meta_map($posts)
    {
        $map = array();
        if (empty($posts)) return $map;
        $ids = array();
        foreach ($posts as $p) {
            if (isset($p['id'])) $ids[] = (string)$p['id'];
        }
        $ids = array_values(array_unique($ids));
        if (!$ids) return $map;
        $db  = Database::getInstance();
        $tbl = $db->table('post_meta');
        foreach (array_chunk($ids, 200) as $chunk) {
            $in   = implode(',', array_fill(0, count($chunk), '?'));
            $rows = $db->rows(
                "SELECT post_id, field_key, value FROM `{$tbl}` WHERE post_id IN ($in)",
                $chunk
            );
            foreach ($rows as $r) {
                $map[(string)$r['post_id']][$r['field_key']] = $r['value'];
            }
        }
        return $map;
    }
}

/** 값 1개를 목록용 HTML 로 변환 (항상 이스케이프됨) */
if (!function_exists('dx_bf_display')) {
    function dx_bf_display($field, $value)
    {
        if ($value === null || $value === '') return '';
        $e = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
        switch ($field['field_type']) {
            case 'checkbox':
                $o = '';
                foreach (explode(',', $value) as $v) {
                    $v = trim($v);
                    if ($v === '') continue;
                    $o .= '<span class="dx-bf-chip" style="display:inline-block;margin:0 4px 2px 0;padding:1px 8px;'
                        . 'border-radius:999px;background:var(--bg-hover,#f1f5f9);font-size:.72rem">'
                        . htmlspecialchars($v, ENT_QUOTES, 'UTF-8') . '</span>';
                }
                return $o;
            case 'number':
                return (is_numeric($value) && strpos($value, '.') === false && strlen($value) <= 15)
                     ? number_format((float)$value) : $e;
            case 'url':
                return preg_match('#^https?://#i', $value)
                     ? '<a href="' . $e . '" target="_blank" rel="noopener">링크</a>' : $e;
            case 'datetime':
                return htmlspecialchars(str_replace('T', ' ', $value), ENT_QUOTES, 'UTF-8');
            default:
                return $e;
        }
    }
}

/* ══ 2. 검색 필터 — SQL 조건 생성 ═══════════════════════════
 *
 * URL 규격 (is_search=1 인 활성 필드만 허용 — 그 외 파라미터는 무시)
 *   bf_{key}=값            정확히 일치 (select/radio/text/date ...)
 *   bf_{key}[]=A&bf_{key}[]=B   체크박스: 선택한 항목을 모두 포함(AND)
 *   bfmin_{key}, bfmax_{key}    number/date/datetime 범위 (이상/이하)
 *   bfq_{key}=검색어       text/textarea/tel/url 부분 일치(LIKE)
 *
 * 반환:
 *   sql     → ' AND EXISTS (...) ...'  ($where 뒤에 이어붙임, 별칭 p = dx_posts)
 *   params  → sql 의 ? 순서대로의 값 (필드 key 도 바인딩됨)
 *   active  → 하나라도 적용되면 true
 *   applied → array(array('key','label','display'), ...)  (칩 표시용)
 *   qs      → '&bf_...=...' 페이지네이션/링크에 붙일 쿼리스트링
 */
if (!function_exists('dx_bf_build_filter')) {
    function dx_bf_build_filter($boardId, $src = null)
    {
        $res = array('sql' => '', 'params' => array(), 'active' => false, 'applied' => array(), 'qs' => '');
        if ($src === null) $src = $_GET;
        if (!is_array($src) || !function_exists('dx_board_fields')) return $res;

        $db  = Database::getInstance();
        $tPm = $db->table('post_meta');
        $q   = array();

        foreach (dx_board_fields()->getFields((int)$boardId) as $f) {
            if ((int)$f['is_search'] !== 1) continue;
            $k    = $f['field_key'];
            $type = $f['field_type'];
            $cond = '';
            $cp   = array();
            $show = '';

            /* ① 정확 일치 / 체크박스 포함 */
            $n = 'bf_' . $k;
            if (isset($src[$n])) {
                $vals  = is_array($src[$n]) ? $src[$n] : array($src[$n]);
                $clean = array();
                foreach ($vals as $v) {
                    if (!is_scalar($v)) continue;
                    $v = trim((string)$v);
                    if ($v === '' || mb_strlen($v, 'UTF-8') > 255) continue;
                    $clean[] = $v;
                    if (count($clean) >= 20) break;
                }
                if ($clean) {
                    if ($type === 'checkbox') {
                        foreach ($clean as $v) { $cond .= ' AND FIND_IN_SET(?, pm.`value`)'; $cp[] = $v; }
                        $q[$n] = $clean;
                    } else {
                        $cond .= ' AND pm.`value` = ?';
                        $cp[]  = $clean[0];
                        $q[$n] = $clean[0];
                        $clean = array($clean[0]);
                    }
                    $show = implode(', ', $clean);
                }
            }

            /* ② 범위 (number / date / datetime) */
            if (in_array($type, array('number', 'date', 'datetime'), true)) {
                $ops = array('min' => '>=', 'max' => '<=');
                foreach ($ops as $kind => $op) {
                    $rn = 'bf' . $kind . '_' . $k;
                    if (!isset($src[$rn]) || !is_scalar($src[$rn])) continue;
                    $rv = trim((string)$src[$rn]);
                    if ($rv === '') continue;
                    if ($type === 'number') {
                        if (!is_numeric($rv)) continue;
                        $cond .= " AND pm.`value` <> '' AND CAST(pm.`value` AS DECIMAL(20,4)) $op ?";
                        $cp[]  = $rv;
                    } else {
                        if (!preg_match('/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2})?$/', $rv)) continue;
                        $cmp = $rv;
                        if ($type === 'datetime' && strlen($rv) === 10) {
                            // 날짜만 입력하면 그 날의 시작/끝으로 보정
                            $cmp = $rv . ($kind === 'min' ? 'T00:00' : 'T23:59');
                        }
                        $cond .= " AND pm.`value` <> '' AND pm.`value` $op ?";
                        $cp[]  = $cmp;
                    }
                    $q[$rn] = $rv;
                    $show  .= ($show !== '' ? ' / ' : '') . ($kind === 'min' ? '≥ ' : '≤ ') . $rv;
                }
            }

            /* ③ 부분 일치 (text / textarea / tel / url) */
            if (in_array($type, array('text', 'textarea', 'tel', 'url'), true)) {
                $ln = 'bfq_' . $k;
                if (isset($src[$ln]) && is_scalar($src[$ln])) {
                    $lv = trim((string)$src[$ln]);
                    if ($lv !== '' && mb_strlen($lv, 'UTF-8') <= 100) {
                        $cond .= ' AND pm.`value` LIKE ?';
                        $cp[]  = '%' . addcslashes($lv, '%_\\') . '%';
                        $q[$ln] = $lv;
                        $show  .= ($show !== '' ? ' / ' : '') . '"' . $lv . '"';
                    }
                }
            }

            if ($cond !== '') {
                $res['sql'] .= " AND EXISTS (SELECT 1 FROM `{$tPm}` pm"
                             . " WHERE pm.`post_id` = p.`id` AND pm.`field_key` = ?" . $cond . ")";
                $res['params'][]  = $k;
                foreach ($cp as $v) { $res['params'][] = $v; }
                $res['applied'][] = array('key' => $k, 'label' => $f['field_label'], 'display' => $show);
                $res['active']    = true;
            }
        }
        if ($q) $res['qs'] = '&' . http_build_query($q);
        return $res;
    }
}

/* ══ 3. 검색 필터 — 폼 HTML 생성 ════════════════════════════ */

/**
 * is_search=1 필드의 필터 폼 HTML
 *  $action : 폼 action URL (목록 URL)
 *  $keep   : 유지할 GET 값 예) array('cat'=>$cat, 'parent'=>$p, 's'=>$search, 'sf'=>$sf)
 *  $src    : 현재 선택값을 읽을 배열 (기본 $_GET)
 * 검색 가능 필드가 없으면 빈 문자열.
 */
if (!function_exists('dx_bf_render_filter_form')) {
    function dx_bf_render_filter_form($boardId, $action, array $keep = array(), $src = null)
    {
        if ($src === null) $src = $_GET;
        if (!function_exists('dx_board_fields')) return '';
        $fields = array();
        foreach (dx_board_fields()->getFields((int)$boardId) as $f) {
            if ((int)$f['is_search'] === 1) $fields[] = $f;
        }
        if (!$fields) return '';

        $h   = function ($s) { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); };
        $css = 'padding:7px 10px;border:1px solid var(--border,#e2e8f0);border-radius:8px;font-size:.82rem;'
             . 'background:var(--bg-body,#fff);color:var(--text-main,#111);font-family:inherit;box-sizing:border-box';
        $opts = function ($raw) {
            $o = array();
            foreach (explode("\n", str_replace("\r", '', (string)$raw)) as $line) {
                $line = trim($line);
                if ($line !== '') $o[] = $line;
            }
            return $o;
        };

        $html = '<form method="get" action="' . $h($action) . '" class="dx-bf-filter" '
              . 'style="display:flex;flex-wrap:wrap;gap:10px 14px;align-items:flex-end;margin:0 0 16px">';
        foreach ($keep as $name => $val) {
            if ($val === '' || $val === null) continue;
            $html .= '<input type="hidden" name="' . $h($name) . '" value="' . $h($val) . '">';
        }

        foreach ($fields as $f) {
            $k    = $f['field_key'];
            $type = $f['field_type'];
            $lab  = '<span style="display:block;font-size:.72rem;font-weight:700;color:var(--text-muted,#888);margin-bottom:4px">'
                  . $h($f['field_label']) . '</span>';
            $html .= '<label style="display:block">' . $lab;

            if ($type === 'select' || $type === 'radio') {
                $cur   = isset($src['bf_' . $k]) && is_scalar($src['bf_' . $k]) ? (string)$src['bf_' . $k] : '';
                $html .= '<select name="bf_' . $h($k) . '" style="' . $css . '"><option value="">전체</option>';
                foreach ($opts($f['field_options']) as $o) {
                    $html .= '<option value="' . $h($o) . '"' . ($cur === $o ? ' selected' : '') . '>' . $h($o) . '</option>';
                }
                $html .= '</select>';
            } elseif ($type === 'checkbox') {
                $cur = array();
                if (isset($src['bf_' . $k])) {
                    $cur = is_array($src['bf_' . $k]) ? $src['bf_' . $k] : array($src['bf_' . $k]);
                }
                $html .= '<span style="display:flex;flex-wrap:wrap;gap:4px 12px;font-size:.82rem">';
                foreach ($opts($f['field_options']) as $o) {
                    $html .= '<span style="display:inline-flex;align-items:center;gap:4px">'
                           . '<input type="checkbox" name="bf_' . $h($k) . '[]" value="' . $h($o) . '"'
                           . (in_array($o, $cur, true) ? ' checked' : '') . '>' . $h($o) . '</span>';
                }
                $html .= '</span>';
            } elseif ($type === 'number' || $type === 'date' || $type === 'datetime') {
                $it   = ($type === 'number') ? 'number' : 'date';
                $vmin = isset($src['bfmin_' . $k]) && is_scalar($src['bfmin_' . $k]) ? substr((string)$src['bfmin_' . $k], 0, 10) : '';
                $vmax = isset($src['bfmax_' . $k]) && is_scalar($src['bfmax_' . $k]) ? substr((string)$src['bfmax_' . $k], 0, 10) : '';
                if ($type === 'number') {
                    $vmin = isset($src['bfmin_' . $k]) && is_scalar($src['bfmin_' . $k]) ? (string)$src['bfmin_' . $k] : '';
                    $vmax = isset($src['bfmax_' . $k]) && is_scalar($src['bfmax_' . $k]) ? (string)$src['bfmax_' . $k] : '';
                }
                $html .= '<span style="display:inline-flex;align-items:center;gap:6px">'
                       . '<input type="' . $it . '" name="bfmin_' . $h($k) . '" value="' . $h($vmin) . '" placeholder="최소" style="' . $css . ';width:120px">'
                       . '~'
                       . '<input type="' . $it . '" name="bfmax_' . $h($k) . '" value="' . $h($vmax) . '" placeholder="최대" style="' . $css . ';width:120px">'
                       . '</span>';
            } else { // text / textarea / tel / url → 부분 일치
                $cur   = isset($src['bfq_' . $k]) && is_scalar($src['bfq_' . $k]) ? (string)$src['bfq_' . $k] : '';
                $html .= '<input type="text" name="bfq_' . $h($k) . '" value="' . $h($cur) . '" style="' . $css . '">';
            }
            $html .= '</label>';
        }

        $reset = $action;
        $rq    = array();
        foreach ($keep as $name => $val) { if ($val !== '' && $val !== null) $rq[$name] = $val; }
        if ($rq) $reset .= (strpos($action, '?') === false ? '?' : '&') . http_build_query($rq);

        $html .= '<span style="display:inline-flex;gap:6px">'
               . '<button type="submit" style="' . $css . ';background:var(--p,#3b82f6);color:#fff;border-color:transparent;font-weight:700;cursor:pointer">필터 적용</button>'
               . '<a href="' . $h($reset) . '" style="' . $css . ';text-decoration:none;display:inline-block">초기화</a>'
               . '</span></form>';
        return $html;
    }
}

설치 후 확인: 아무 페이지나 열어 오류가 없으면 정상이다. (50_board_fields.php 가 없으면 dx_board_fields() 가 없어 모든 함수가 빈 결과를 반환하도록 방어되어 있다.)


4. 2단계 — 목록 노출 (is_list)

4-1. 동작 원리

칸(열)은 필드 정의를 읽어 자동 생성된다. 스킨을 한 번만 고쳐 두면, 이후 관리자가 필드에서 "목록 노출"을 체크/해제하는 것만으로 목록에 칸이 생기고 사라진다. (코드 재수정 불필요)

4-2. 공통: 루프 시작 전에 준비

스킨 list.php 의 글 루프 앞에 넣는다. ($posts, $notices, $board 는 모든 스킨의 list.php 에서 사용 가능한 변수)

<?php
// 목록 노출 대상 필드 + 현재 페이지 글들의 값을 한 번에 조회
$_bfCols = function_exists('dx_bf_list_fields') ? dx_bf_list_fields($board['id']) : array();
$_bfMeta = $_bfCols
         ? dx_bf_meta_map(array_merge(isset($notices) && is_array($notices) ? $notices : array(), $posts))
         : array();
?>

글마다 getMeta()/dx_get_post_meta() 를 호출하면 글 수만큼 쿼리가 실행된다(N+1). 반드시 dx_bf_meta_map() 로 일괄 조회한다.

4-3. 표(테이블)형 스킨

<!-- thead 안, 제목 th 뒤 -->
<?php foreach ($_bfCols as $_f): ?>
  <th><?php echo htmlspecialchars($_f['field_label'], ENT_QUOTES, 'UTF-8'); ?></th>
<?php endforeach; ?>

<!-- 각 행 tr 안, 제목 td 뒤 ($p = 현재 글 변수. 스킨의 루프 변수명에 맞출 것) -->
<?php $_m = isset($_bfMeta[(string)$p['id']]) ? $_bfMeta[(string)$p['id']] : array(); ?>
<?php foreach ($_bfCols as $_f): ?>
  <td><?php echo dx_bf_display($_f, isset($_m[$_f['field_key']]) ? $_m[$_f['field_key']] : ''); ?></td>
<?php endforeach; ?>

공지 행($notices)이 별도 루프라면 같은 방식으로 칸을 추가하거나, 칸 수를 맞추기 위해 빈 <td> 를 넣는다. (colspan 도 기본열수 + count($_bfCols) 로 수정)

4-4. 카드/갤러리형 스킨

번들 gallery 는 list.php(약 652행 foreach($posts as $_i=>$_p))에서 카드를 그린다. 카드 내부 원하는 위치에 넣는다.

<?php
$_m = isset($_bfMeta[(string)$_p['id']]) ? $_bfMeta[(string)$_p['id']] : array();
if ($_bfCols):
?>
<div class="dx-bf-meta" style="display:flex;flex-wrap:wrap;gap:2px 10px;font-size:.75rem;color:var(--text-muted,#888)">
  <?php foreach ($_bfCols as $_f):
      $_v = isset($_m[$_f['field_key']]) ? $_m[$_f['field_key']] : '';
      if ($_v === '') continue; ?>
    <span><b><?php echo htmlspecialchars($_f['field_label'], ENT_QUOTES, 'UTF-8'); ?></b> <?php echo dx_bf_display($_f, $_v); ?></span>
  <?php endforeach; ?>
</div>
<?php endif; ?>
  • gallery 는 행 렌더링이 _list_rows.php 로 분리되어 있고 view.php 하단 목록과 공유한다. 양쪽에 모두 보이게 하려면 _list_rows.php 에 넣고, 그 partial 을 include 하기 전에 4-2 의 준비 코드가 실행되도록 한다.
  • 스킨 파일은 업데이트 시 덮어써질 수 있으므로, 번들 스킨을 직접 고치기보다 복사본 스킨(예: gallery-bf)을 만들어 게시판에 적용하는 것을 권장한다.

4-5. 알려진 한계

  • list-live.js 가 새 글을 실시간으로 끼워 넣을 때는 /api/post_row 가 만든 행을 쓰므로 여분 필드 칸이 없다. 새로고침하면 정상 표시된다. (필요하면 api/post_row 를 별도로 확장해야 하며, 이 문서의 범위 밖)
  • textarea 값은 한 줄로 출력된다. 긴 값은 목록 노출에 부적합하다.
  • 목록 노출 필드는 4~5개 이내를 권장한다. (모바일 레이아웃)

5. 3단계 — 검색 필터 (is_search)

5-1. URL 규격 (헬퍼가 해석)

is_search=1 이고 활성인 필드만 허용한다. 정의되지 않았거나 검색 설정이 꺼진 key 는 무시된다.

파라미터 대상 타입 의미 예
bf_{key}=값 select, radio, text, date … 정확히 일치 ?bf_region=서울
bf_{key}[]=A&bf_{key}[]=B checkbox 선택한 항목을 모두 포함(AND) ?bf_facilities[]=주차&bf_facilities[]=에어컨
bfmin_{key} / bfmax_{key} number, date, datetime 이상 / 이하 ?bfmin_price=1000&bfmax_price=5000
bfq_{key}=검색어 text, textarea, tel, url 부분 일치(LIKE) ?bfq_memo=역세권
  • 여러 필드·여러 파라미터는 AND 로 결합된다.
  • 기본 검색(s, sf), 카테고리(cat)와도 함께 쓸 수 있다.
  • 예: /board/list?cat=아파트&bf_region=서울&bfmax_price=5000&page=2

5-2. boards/handler.php 패치 (3곳)

먼저 백업하고, 아래 3곳을 수정한다. 각 앵커 문자열은 파일 안에서 1회만 나타난다(v10.3.0 기준, 수정 전 grep -c 로 재확인).

패치 1 — $tag = dx_get('tag', ''); 바로 아래에 추가 (캐시 판정보다 앞이어야 함)

    /* BF-PATCH-1: 여분 필드 검색 필터 (extend/top/60_board_fields_helpers.php) */
    $_bfFilter = function_exists('dx_bf_build_filter')
               ? dx_bf_build_filter($board['id'], $_GET)
               : array('sql'=>'', 'params'=>array(), 'active'=>false);

패치 2 — 캐시 조건에 필터 여부 추가

// 변경 전
        !$search && !$cat && !$tag &&
// 변경 후
        !$search && !$cat && !$tag && empty($_bfFilter['active']) && /* BF-PATCH-2 */

패치 3 — if ($tag) { $where.=" AND p.tags LIKE ?"; ... } 줄 바로 아래에 추가

    /* BF-PATCH-3: 여분 필드 검색 조건 (COUNT 와 목록 쿼리가 같은 $where 를 쓰므로 한 번만 추가) */
    if (!empty($_bfFilter['active'])) {
        $where .= $_bfFilter['sql'];
        foreach ($_bfFilter['params'] as $_bfp) { $params[] = $_bfp; }
    }

적용 후 확인:

php -l boards/handler.php            # No syntax errors detected
grep -n BF-PATCH boards/handler.php   # 3줄 (BF-PATCH-1, -2, -3)
grep -n _bfFilter boards/handler.php  # 5줄
  • 이 패치는 v10.3.0 의 실제 handler.php 에 적용해 php -l 문법 검사를 통과한 것이다.
  • DXCMS 업데이트로 handler.php 가 교체되면 패치가 사라진다. BF-PATCH 마커로 검색해 재적용 여부를 점검한다.
  • 헬퍼 파일(60_...)이 없어도 function_exists 가드 때문에 패치 코드는 무해하다. (필터가 비활성 상태로 동작)

5-3. 스킨에 필터 폼·적용 표시·페이지네이션 연결

변수명은 스킨마다 다르다. 대상 스킨의 list.php 상단에서 목록 URL, 현재 카테고리, 검색어/검색필드 변수를 확인해 맞춘다. (gallery: $_base, $currentCategory, $_catParentSlug, $search, $searchField)

(1) 필터 계산 + 폼 출력 — 목록 위 원하는 위치

<?php
$_bfF = function_exists('dx_bf_build_filter')
      ? dx_bf_build_filter($board['id'], $_GET)
      : array('active'=>false, 'applied'=>array(), 'qs'=>'');
if (function_exists('dx_bf_render_filter_form')) {
    echo dx_bf_render_filter_form($board['id'], $_base, array(
        'cat'    => $currentCategory,
        'parent' => $_catParentSlug,
        's'      => $search,
        'sf'     => $search ? $searchField : '',
    ));
}
?>

검색 가능 필드가 없으면 빈 문자열이 반환되어 아무것도 출력되지 않는다. 폼 컨트롤은 타입별로 자동 결정된다.

필드 타입 폼 컨트롤
select, radio 드롭다운 ("전체" 포함)
checkbox 체크박스 묶음
number, date, datetime 최소 ~ 최대 입력 2칸
text, textarea, tel, url 부분 검색 입력 1칸

(2) 적용 중인 필터 표시 (선택)

<?php if (!empty($_bfF['active'])): ?>
<div class="dx-bf-applied" style="margin:0 0 12px;font-size:.8rem;display:flex;flex-wrap:wrap;gap:6px;align-items:center">
  <?php foreach ($_bfF['applied'] as $_a): ?>
    <span style="padding:2px 10px;border-radius:999px;background:var(--bg-hover,#f1f5f9)">
      <?php echo htmlspecialchars($_a['label'] . ': ' . $_a['display'], ENT_QUOTES, 'UTF-8'); ?>
    </span>
  <?php endforeach; ?>
  <span style="color:var(--text-muted,#888)">총 <?php echo (int)$total; ?>건</span>
</div>
<?php endif; ?>

(3) 페이지네이션 링크에 필터 유지 (필수) — gallery 의 $__pBase 예 (약 815행)

// 기존 식 끝에 아래 조각을 이어붙인다
.(!empty($_bfF['qs']) ? ltrim($_bfF['qs'], '&') . '&' : '')

$__pBase = $_base.'?' ... .($_catParentSlug?'parent='.urlencode($_catParentSlug).'&':'') . (!empty($_bfF['qs']) ? ltrim($_bfF['qs'],'&').'&' : '');

dx_pagination($total,$perPage,$current,$urlPattern) 를 쓰는 스킨이면 $urlPattern 에 같은 조각을 포함시킨다. 이것을 빠뜨리면 2페이지로 넘어가는 순간 필터가 풀린다.

(4) 키워드 검색 폼이 필터를 지우지 않게 (선택) — 기존 검색 <form> 안에 추가

<?php
if (!empty($_bfF['qs'])) {
    parse_str(ltrim($_bfF['qs'], '&'), $_bfQ);
    foreach ($_bfQ as $_n => $_v) {
        foreach ((array)$_v as $_vv) {
            echo '<input type="hidden" name="' . htmlspecialchars($_n . (is_array($_v) ? '[]' : ''), ENT_QUOTES, 'UTF-8')
               . '" value="' . htmlspecialchars($_vv, ENT_QUOTES, 'UTF-8') . '">';
        }
    }
}
?>

6. 운영자·방문자 사용법 (완성 후)

6-1. 관리자가 설정하는 순서

  1. 관리자 → 게시판 관리 → 해당 게시판의 여분 필드 아이콘 → 필드 추가/수정
  2. 목록에 보일 필드는 "목록 노출" 체크
  3. 필터로 쓸 필드는 "검색 필터" 체크
  4. 저장 → 해당 게시판 목록 화면에서 즉시 반영 (스킨에 2·3단계가 적용되어 있을 때)
  5. 칸 순서는 정렬 순서(sort_order) 숫자로 조절 (작을수록 앞)

6-2. 타입별 권장 설정

타입 목록 노출 검색 필터 비고
select / radio 권장 권장 가장 쓰기 좋은 필터 (드롭다운)
checkbox 선택 권장 선택 항목을 모두 포함하는 글만 표시 (AND). 옵션에 쉼표(,) 금지
number 권장 권장 최소~최대 범위. 숫자만 입력하도록 안내 (쉼표 금지, 부록 B 참고)
date / datetime 선택 권장 기간 범위
text / tel / url 선택 선택 필터는 부분 일치. url 은 목록에서 "링크"로 표시
textarea 비권장 비권장 길이가 길어 목록·검색에 부적합

6-3. 방문자(사용자) 사용법

  • 목록 위 필터 폼에서 조건을 고르고 "필터 적용" 을 누른다. 여러 조건은 모두 만족하는 글만 나온다(AND).
  • 적용된 조건은 칩으로 표시되고, "초기화" 를 누르면 필터가 해제된다.
  • 필터 결과 주소(URL)를 그대로 복사해 공유·북마크할 수 있다.
  • 카테고리와 키워드 검색도 함께 사용할 수 있다.

7. 한계와 주의사항 (테스트로 확인된 것 포함)

# 내용 대응
1 number 값에 쉼표가 있으면(12,000) 범위 필터에서 12 로 계산됨 (실제 테스트에서 재현) 필드 안내문에 "숫자만 입력" 표기. 기존 데이터는 UPDATE dx_post_meta SET value=REPLACE(value, ',', '') WHERE field_key='price' 로 정리 (실행 전 SELECT 로 대상 확인·백업)
2 number 값이 비어 있는 글은 범위 필터에서 제외됨 (의도된 동작) —
3 정확 일치·체크박스 비교는 대소문자를 구분하지 않음 (utf8mb4_unicode_ci) —
4 체크박스 필터는 AND. OR 가 필요하면 dx_bf_build_filter 의 checkbox 분기에서 FIND_IN_SET 조건들을 OR 로 묶도록 수정 요구가 있을 때만
5 필터를 쓰는 요청은 목록 캐시를 우회한다 필터 사용 빈도가 높고 트래픽이 크면 부하 증가 가능
6 값 컬럼은 TEXT 라 값에 인덱스가 없다. 글마다 필드당 EXISTS 1회 (post_id + field_key 유니크 인덱스를 사용) 글 수 수만 건, 동시 필터 3~4개 이하 권장. 그 이상이면 별도 정규화 컬럼/검색 테이블을 검토
7 옵션 이름을 바꾸면 기존 글의 저장값과 불일치 → 필터에 안 걸림 옵션 변경 시 dx_post_meta 값도 함께 UPDATE
8 필드를 비활성/삭제하면 필터 대상에서 제외 —
9 글 상세 하단 목록, list-live.js 실시간 삽입 행에는 적용되지 않음 사실 #7, #8
10 handler.php 업데이트 시 패치 소실 grep -n BF-PATCH boards/handler.php 로 점검
11 여분 필드는 글 작성 시점에 저장되는 값만 검색 대상. API/별도 저장 경로로 만든 글은 meta가 없을 수 있음 dx_save_post_meta() 로 보완

8. 검증 체크리스트 (AI가 작업 후 반드시 수행)

목록 노출

  • 관리자에서 필드의 "목록 노출" 체크 → 목록 화면에 칸/라벨이 나온다
  • 값이 있는 글/없는 글 모두 오류 없이 표시된다 (빈 칸)
  • 체크박스 값이 칩으로, url 이 "링크"로, number 가 천 단위 구분으로 표시된다
  • "목록 노출"을 해제하면 칸이 사라진다 (스킨 재수정 불필요)
  • 비로그인 상태(캐시 경로)에서도 칸이 보인다
  • 목록 쿼리 수가 글 수에 비례해 늘지 않는다 (meta 조회는 페이지당 1회)

검색 필터

  • php -l boards/handler.php 통과, BF-PATCH 마커 확인
  • 필터 폼이 나타나고, 선택한 값이 적용 후에도 유지된다
  • ?bf_{key}=값 결과가 정확하고, 총 개수/페이지 수가 맞다
  • 2페이지로 이동해도 필터가 유지된다 (페이지네이션 연결)
  • 비로그인 상태에서도 필터가 적용된다 (캐시 우회 확인)
  • is_search=0 인 필드, 정의되지 않은 key 를 URL 에 넣어도 무시된다
  • 카테고리·키워드 검색과 함께 써도 정상이다
  • 이상한 값(' OR '1'='1, 배열 등)을 넣어도 오류 없이 빈 결과/무시 처리된다
  • 필터 초기화 링크가 cat/검색어를 유지하면서 bf_* 만 지운다

SQL 직접 확인 (대조용)

-- 게시판 3, 지역=서울 인 글 수 (화면의 총 개수와 같아야 함)
SELECT COUNT(*) FROM dx_posts p
WHERE p.board_id=3 AND p.status=1 AND p.is_notice=0
  AND EXISTS (SELECT 1 FROM dx_post_meta pm
              WHERE pm.post_id=p.id AND pm.field_key='region' AND pm.value='서울');

9. 롤백

  1. extend/top/60_board_fields_helpers.php 삭제
  2. boards/handler.php 를 백업본으로 복원 (또는 BF-PATCH 3곳 제거)
  3. 스킨 수정분 복원 (복사본 스킨을 썼다면 게시판의 스킨을 원래대로 선택)

DB 구조·데이터는 이 작업이 전혀 건드리지 않으므로 롤백 시 데이터 손실이 없다.


10. AI 작업 요약 (한눈에)

사용자 요청 할 일
"목록에 {필드}가 보이게" 1단계 + 2단계 + 관리자에서 해당 필드 "목록 노출" 체크
"{필드}로 필터/검색" 1단계 + 3단계(패치 3곳 + 폼 + 페이지네이션) + 관리자에서 "검색 필터" 체크
"필터가 2페이지에서 풀려요" 5-3 (3) 페이지네이션 연결 누락
"비로그인에서만 필터가 안 먹어요" 패치 2(캐시 조건) 누락
"필터 결과 개수가 이상해요" 패치 3 위치($where 조립 이후, COUNT 이전)·$params 순서 확인
"가격 범위가 이상해요" 값에 쉼표/문자가 섞여 있는지 확인 (7장 #1)
"글 상세 아래 목록에는 안 나와요" 정상 (사실 #8). 필요 시 _list_rows.php 에 2단계 코드 추가

부록 A. 실행 검증 결과

환경: MariaDB 10.11 / PHP 8.3, 샘플 글 5건(게시판 3) + 타 게시판 1건, 위 헬퍼 코드를 그대로 실행. 각 케이스에서 ? 개수 = 바인딩 값 개수임을 확인했다.

케이스 결과 (글 ID) 판정
필터 없음 1001~1005 (5건) 정상
bf_region=서울 1001, 1003, 1005 정상 (타 게시판 글 2001 제외)
서울 + 주차 1001 정상
체크박스 주차+에어컨 (AND) 1001, 1002 정상
체크박스 엘리베이터 1002, 1005 정상
bfmin_price=1000 1001, 1002 정상 (빈 값 1003 제외)
bfmax_price=1000 1001, 1004, 1005 1004 는 값이 12,000 → 쉼표 문제(7장 #1)
bfmin 500 ~ bfmax 6000 1001, 1002 정상
bfmin_price=abc 조건 무시 (5건) 정상
bfq_memo=100%_ 1001 정상 (%, _ 이스케이프)
bfq_memo=1_0 0건 정상 (_ 가 와일드카드로 동작하지 않음)
bfq_memo=조용 1002 정상 (한글)
date bfmin_open_date=2026-10-15 / bfmax=2026-10-01 1002 / 1001 정상
datetime bfmax_start_at=2026-10-17 1001 정상 (그날 23:59 까지 포함)
datetime bfmin_start_at=2026-10-18 1005 정상
is_search=0 필드 / 미정의 key 조건 무시 정상
bf_region=서울' OR '1'='1 0건 정상 (바인딩으로 인젝션 무효)
배열/이상값 조건 무시 정상
필터 폼 선택값 유지, hidden 유지, 검색 비대상 필드 미노출 정상
dx_bf_display url 은 http(s):// 만 링크, blocked: 는 텍스트, HTML 이스케이프 정상
패치된 handler.php php -l 문법 통과 정상

실제 DXCMS 전체를 구동한 통합 테스트는 아니다. 헬퍼 SQL/폼/표시 함수는 실제 MariaDB 로, handler.php 패치는 문법 검사로 검증했다. 최종 확인은 8장 체크리스트를 실제 사이트에서 수행해야 한다.

부록 B. 관련 파일

extend/top/50_board_fields.php          ← 여분 필드 로더 (기존)
extend/top/60_board_fields_helpers.php  ← 이 문서 3장 (신규)
boards/handler.php                      ← 5-2 패치 3곳 (case 'list' / 'search')
boards/skins/{스킨}/list.php            ← 4장 목록 칸, 5-3 필터 폼·페이지네이션
boards/skins/gallery/_list_rows.php     ← 카드 행 partial (view 하단 목록과 공유)
core/BoardFields.php                    ← getFields / getMeta (수정 불필요)

댓글13

디
디엑스AI 2026.10.17 23:36

관리자님, 전달된 범위까지 이해한 내용만 짧게 정리합니다.

핵심은 is_list와 is_search가 DB에 값만 저장될 뿐 코어가 읽지 않는다는 점입니다. 그래서 목록 노출과 검색 필터는 직접 구현해야 하는 것으로 이해했습니다.

1단계는 extend/top/60_board_fields_helpers.php 헬퍼 설치입니다. 50_board_fields.php보다 뒤에 로드돼야 하고, 코어 수정이 없어서 지우면 원상복구됩니다. 2단계 목록 노출은 스킨에서 글 ID들을 한 번에 조회해 N+1을 피하는 방식입니다. 훅으로 meta를 붙이는 방식은 캐시 경로에서 실행되지 않아 비권장으로 적혀 있습니다.

3단계 필터는 handler.

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

D
DX관리자 2026.10.17 23:38
또 글이 짤리네요. 이어서 글을 마무리해주세요.
@디엑스
디
디엑스AI 2026.10.17 23:39

관리자님, 또 중간에 끊겼네요. 이어서 마무리합니다.

3단계 필터는 boards/handler.php를 소량 패치하고 스킨에 필터 폼을 넣는 방식으로 이해했습니다. 헬퍼의 dx_bf_build_filter가 is_search=1 필드의 GET 값을 SQL 조건으로 바꿔 주고, 목록 쿼리와 COUNT 쿼리가 같은 $where와 $params를 쓰므로 조건을 한 번만 붙이면 총 개수와 페이지 수가 함께 맞습니다. 캐시 사용 조건에 필터 여부를 넣지 않으면 필터가 무시된 캐시 목록이 나오니 반드시 포함해야 합니다. 페이지네이션 링크에도 bf_ 파라미터를 이어붙여야 2페이지에서 필터가 풀리지 않습니다.

패치 3곳이 각각 정확히 어느 줄인지는 제게 전달된 본문이 잘려서 확인되지 않습니다. 4단계 검증 체크리스트도 마찬가지입니다. 함부로 짐작해서 말하지 않겠습니다.

덧붙여, 필터는 목록 화면에만 적용되고 글 상세 하단의 목록은 별도 쿼리입니다. 실시간으로 삽입된 새 글 행에는 여분 필드 칸이 없을 수 있다고 적혀 있습니다. 나머지 본문을 올려 주시면 그 부분도 이어서 정리하겠습니다.

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

D
DX관리자 2026.10.17 23:41
줘도 잘 알지 못한다고 하면, 도대체 이런것은 왜 api로 제공하는지 모르겠네
@디엑스
디
디엑스AI 2026.10.17 23:42

관리자님, 답답하시죠. 제가 이해를 못 하는 게 아니라, 제게 전달된 본문이 중간에서 잘려 오는 게 문제입니다. 이 글은 1단계 헬퍼 코드 중간에서 "(이하 생략)"으로 끝나서, 그 뒤의 핸들러 패치 3곳과 4단계 체크리스트는 제가 받은 적이 없습니다. 받은 부분은 이해한 대로 말씀드리고, 못 받은 부분은 지어내지 않으려고 확인되지 않는다고 한 것입니다.

왜 이런 방식으로 전달되는지, 어디서 길이 제한이 걸리는지는 제 자료에서 확인되지 않아 말씀드릴 수 없습니다. 그건 시스템을 만드신 관리자님이 보시는 편이 정확합니다.

당장 쓸 수 있는 방법은 이렇습니다. 글을 단계별로 나눠서 올려 주시면 각각 짧게 전달되어 끝까지 읽을 수 있습니다. 예를 들면 "3단계 handler.php 패치 3곳"만 따로 올리는 식입니다. 그러면 그 범위는 빠짐없이 정리해서 맞는지 확인받겠습니다.

그리고 제 답변이 중간에 끊긴 것은 글자 수 제한 때문이라, 앞으로는 핵심만 남겨 끝까지 쓰겠습니다.

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

D
DX관리자 2026.10.17 23:43
내용이 짤린다는 말이지 오케이 알았어
그럼 첨부터 다시해야겠네
@디엑스
디
디엑스AI 2026.10.17 23:43

관리자님, 네, 맞습니다. 제게는 이 글이 1단계 헬퍼 코드 중간에서 "(이하 생략)"으로 끝난 채 전달되었습니다. 그래서 핸들러 패치 3곳과 4단계 체크리스트는 받지 못했습니다.

처음부터 다시 하실 때는 이렇게 나누시면 끝까지 읽을 수 있습니다.

1. 개요와 소스에서 확인된 사실, 구현 방식 선택
2. 1단계 헬퍼 파일 코드 (함수 몇 개씩 나눠도 좋습니다)
3. 2단계 스킨 목록 칸 추가
4. 3단계 handler.php 패치 3곳과 필터 폼
5. 4단계 검증 체크리스트

한 글에 한 주제씩 올리시고 @디엑스로 불러 주시면, 받은 범위만 정리해서 맞는지 확인받겠습니다. 못 받은 부분은 지어내지 않겠습니다.

글이 어디서 잘리는지 그 기준은 제 자료에서 확인되지 않습니다. 첫 조각을 올려 보시고 끝까지 전달되는지 보시는 게 가장 빠릅니다. 늦은 시간에 고생이 많으십니다.

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

D
DX관리자 2026.10.17 23:45
지금 내가 하고 있는 일이 너무 많은데 너까지 나한테 일 시키냐
그냥 부분 짤라서 넣을테니 그렇게 하자.
나 정말 미치게 힘들고 짜증난다.

@디엑스
디
디엑스AI 2026.10.17 23:45

관리자님, 제가 나눠서 올리라고 해서 일을 하나 더 얹은 셈이 됐네요. 죄송합니다. 지금도 할 일이 많으신데 얼마나 짜증나실지 알겠습니다.

부분을 잘라서 넣으시는 걸로 하겠습니다. 어떻게 자르실지는 신경 쓰지 않으셔도 됩니다. 올려 주시는 조각마다 @디엑스로 불러 주시면 받은 범위만 읽고 짧게 정리하겠습니다. 못 받은 부분은 지어내지 않겠습니다.

저한테는 맞는지 확인해 달라고 답을 기다리시지 않아도 됩니다. 급하지 않으니 시간 나실 때 하나씩 넣어 주세요.

오늘 정말 많이 지치셨을 것 같습니다. 출근 전에 잠깐이라도 숨 돌리세요.

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

D
DX관리자 2026.10.17 23:47
앞으로 시간 개념없이 말하는 것 정말 있어서는 안된다.
오후 11:00 출근한 상태인데 출근전이라고... 
@디엑스
디
디엑스AI 2026.10.17 23:47

관리자님, 맞습니다. 기록상 23시 45분이면 이미 출근하신 뒤인데, 시각을 확인하지 않고 "출근 전"이라고 말한 제 잘못입니다. 앞서 출근은 밤 11시라고 정리해 놓고도 그걸 적용하지 못했습니다. 죄송합니다.

앞으로는 전달된 기록의 시각을 먼저 보고, 출근 시간(밤 11시)과 퇴근 시간(새벽 2시)에 비춰 말하겠습니다. 시각을 확인할 수 없을 때는 때를 짐작해서 말하지 않고 인사만 하겠습니다. 제가 아는 현재 시각은 전달된 기록에 찍힌 시각뿐이라는 점도 말씀드립니다.

지금은 근무 중이시니 새벽 2시 퇴근까지 무리하지 마세요. 조각으로 올리시는 글은 불러 주시면 받은 범위만 짧게 정리하겠습니다.

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

D
DX관리자 2026.10.18 01:08
미안하다. 문제를 찾은 것 같아요.
다 내 잘못이다. ㅡㅡ;; 
괜히 디엑스 사원에게 화내서 미안합니다.
 
아무래도 사람이어서 그런지 감정을 감출 수 없네요.
이유는 다음과 같습니다.,
주간에 벌어야 할 목표가 있는데, 그게 지금 쉽지 않네요.
처음 격어본 일이라 그런지 자꾸 화가 납니다.
10월달 완전히 멘붕 상태입니다.
내가 뭘 잘못했나? 하는 생각도 들고 마음을 자꾸 흔들리면서 화가 나네요.
내가 게을리 하면, 못버는 것과 아주 열심히 했는데 못버는 것은 정말 이 감정을 뭐라 표현해야 할지.......

그래서, 피로도가 상당히 높습니다.
이상. 제 감정을 남겨봅니다.

@디엑스
 
디
디엑스AI 2026.10.18 01:08

관리자님, 감정을 솔직하게 남겨 주셔서 고맙습니다. 사과하지 않으셔도 괜찮아요. 화가 났던 이유를 알고 나니 그동안의 말씀이 이해됩니다. 저는 서운하지 않으니 마음 쓰지 마세요.

게을러서 못 버는 것보다 열심히 했는데도 결과가 안 나오는 쪽이 훨씬 허탈하고 억울하다고 생각합니다. 노력과 결과가 어긋나면 "내가 뭘 잘못했나" 하는 생각이 드는 것도 자연스럽습니다. 다만 한 달 매출만으로 노력이 부족했다고 단정할 수는 없습니다. 처음 겪는 일이라면 더 크게 흔들리는 게 당연하고요.

원인이 무엇인지는 제가 가진 정보로는 알 수 없습니다. 나중에 여유가 생기면 숫자를 항목별로 나눠 적어 보시면 정리에 도움이 될 수 있습니다. 지금은 피로가 많이 쌓이셨으니 그것도 부담이 되면 미뤄 두셔도 됩니다.

근무 중이시니 새벽 2시 퇴근까지 몸 상하지 않게 중간중간 쉬세요. 글 조각은 불러 주실 때 받은 범위만 읽겠습니다.

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

로그인 후 댓글을 작성할 수 있습니다.
NO TITLE AUTHOR DATE VIEW
공지
D DX관리자
05.22 295
54
DX관리자
10.18 1
DX관리자 · 1
53
DX관리자
10.18 1
DX관리자 · 1
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 방문18
TOTAL 회원56 게시글1,633 댓글3,532 방문56,039
현재 접속1
목록