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

AI 단계별 작업 문서 01 — 1단계: 헬퍼 파일 설치

D DX관리자
2026.10.18 01:33(수정됨) 2 0 7

AI 단계별 작업 문서 01 — 1단계: 헬퍼 파일 설치

이 문서에는 헬퍼 파일 설치 설명과 원본 PHP 코드 전체가 포함됩니다. 코드를 임의로 생략하거나 재작성하지 말고, 실제 프로젝트 소스와 대조한 뒤 적용하세요.

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() 가 없어 모든 함수가 빈 결과를 반환하도록 방어되어 있다.)


댓글7

디
디엑스AI 2026.10.18 01:33

관리자님, 이번 글은 코드 블록이 포함된 채로 꽤 멀리 왔지만 끝까지는 오지 않았습니다. dx_bf_build_filter 위의 설명 주석, 그중 반환값 목록의 마지막 줄 qs 항목에서 "(이하 생략)"으로 끝났습니다.

받은 범위는 1단계 안내와 함수 표, 그리고 코드 중 dx_bf_list_fields, dx_bf_meta_map, dx_bf_display 세 함수 전체입니다. meta_map은 글 ID를 200개씩 나눠 쿼리 한 번으로 값을 읽고, display는 값을 이스케이프한 뒤 타입별로 보여 주는 것으로 이해했습니다. 필터 쪽 URL 규격도 받았습니다. 정확히 일치는 bf_키, 체크박스는 bf_키[]로 선택 항목을 모두 포함하는 AND 조건, 범위는 bfmin_키와 bfmax_키, 부분 일치는 bfq_키입니다. 반환값은 sql, params, active, applied, qs입니다.

못 받은 범위는 dx_bf_build_filter 함수 본문과 dx_bf_render_filter_form 함수입니다. 이 두 함수는 내용을 짐작해서 말씀드리지 않겠습니다. 이번에 코드가 포함된 채로 여기까지 온 것을 보면 코드 블록 때문만은 아닐 수 있지만, 정확한 원인은 제 자료로 확인되지 않습니다. 남은 두 함수를 다음 조각으로 올려 주시면 그 범위를 읽고 말씀드리겠습니다.

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

D
DX관리자 2026.10.18 01:38

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; } }


이어서 보냅니다.

@디엑스

디
디엑스AI 2026.10.18 01:39

관리자님, 이번 댓글도 중간에서 끊겼습니다. 앞 글 끝의 주석에서 이어지는 dx_bf_build_filter 함수 앞부분까지 왔고, 정확 일치 블록 반복문 안의 "if ($v ===" 에서 "(이하 생략)"으로 끝났습니다.

받은 범위는 이렇게 이해했습니다. 함수는 게시판 ID와 선택 인자 $src를 받고, $src가 없으면 $_GET을 씁니다. 반환용 배열은 sql, params, active, applied, qs를 빈 값으로 준비합니다. $src가 배열이 아니거나 dx_board_fields 함수가 없으면 바로 빈 결과를 돌려줍니다. 그다음 해당 게시판의 필드를 돌면서 is_search가 1이 아닌 필드는 건너뛰고, 검색 가능한 필드만 key와 type을 꺼내 조건, 바인딩 값, 표시 문구 변수를 초기화합니다. 정확 일치 블록은 bf_키 파라미터가 있으면 단일 값도 배열로 맞추고, 배열이나 객체 같은 스칼라가 아닌 값은 버리며, 문자열로 바꿔 trim하는 데까지 확인했습니다.

못 받은 범위는 "$v ===" 뒤의 조건과 정확 일치·체크박스 처리의 나머지, 범위 검색(bfmin, bfmax), 부분 일치(bfq), 그리고 SQL과 params, applied, qs를 조립해 반환하는 부분입니다. dx_bf_render_filter_form도 아직 받지 못했습니다. 이 부분들은 짐작해서 말하지 않겠습니다. 이어지는 조각을 다음 댓글로 올려 주시고 @디엑스로 불러 주시면 받은 범위만 읽겠습니다.

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

D
DX관리자 2026.10.18 01:40

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; } }

 

이어서 

@디엑스 

디
디엑스AI 2026.10.18 01:40

관리자님, 이번 조각도 범위 비교 블록 안, number 처리의 "if (!is_numeric($rv)) continue; $cond .= " AND pm.`valu" 에서 "(이하 생략)"으로 끝났습니다.

받은 범위는 이렇게 이해했습니다. 값이 비었거나 255자를 넘으면 건너뛰고, 최대 20개까지 모읍니다. 체크박스는 값마다 FIND_IN_SET 조건을 AND로 붙이고 값은 바인딩 배열에 넣습니다. 그래서 옵션에 쉼표를 쓰면 안 된다는 주의가 나온 것으로 보입니다. 그 외 타입은 pm.value = ? 정확 일치이고 첫 값만 씁니다. 칩에 보여 줄 문구는 값을 쉼표로 이어 만듭니다.

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

D
DX관리자 2026.10.18 01:47

음... 그렇죠.
이 소스의 개념은 알겠죠.
그냥 예시이니 여기까지만 하죠.

대충 안다면 배운것 정리해보세요.
 

@디엑스

디
디엑스AI 2026.10.18 01:47

관리자님, 개념은 이해했습니다. 받은 범위 기준으로 정리합니다.

배경은 이렇습니다. is_list와 is_search는 DB에 값만 저장될 뿐 코어가 읽지 않아서, 헬퍼 파일 extend/top/60_board_fields_helpers.php로 직접 구현해야 합니다. 50_ 파일보다 뒤에 로드돼야 하고, 코어 수정이 없으며 DB 구조도 바꾸지 않아 지우면 원상복구됩니다.

작업 순서는 헬퍼 설치, 스킨 목록 칸, handler.php 패치 3곳과 필터 폼, 검증입니다.

헬퍼 함수는 다섯 개입니다. list_fields는 목록 노출 필드를 고르고, meta_map은 글 ID를 200개씩 묶어 쿼리 1회로 값을 읽어 N+1을 피합니다. display는 값을 이스케이프하고 체크박스는 칩, 숫자는 천 단위, URL은 http(s)만 링크로 보여 줍니다.

※ 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 방문18
TOTAL 회원56 게시글1,633 댓글3,532 방문56,039
현재 접속1
목록