From c17dd8685972ed120e6cb617e0eb64bcaf7ee9c7 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 10 Mar 2026 18:51:22 +0900 Subject: [PATCH] =?UTF-8?q?feat(pop):=20pop-search=20status-chip=20?= =?UTF-8?q?=EC=9E=85=EB=A0=A5=20=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?+=20all=5Frows=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20pop-search=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20status-chip=20=EC=9E=85=EB=A0=A5=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=EB=90=9C=20=EC=B9=B4=EB=93=9C=EC=9D=98=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20?= =?UTF-8?q?=EA=B5=AC=EB=8F=85=ED=95=98=EA=B3=A0=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EB=B3=84=20=EA=B1=B4=EC=88=98=EB=A5=BC=20=EC=A7=91=EA=B3=84/?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=ED=95=9C=EB=8B=A4.=20=EC=B9=A9=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=20=EC=8B=9C=20filter=5Fvalue=EB=A5=BC=20=EB=B0=9C?= =?UTF-8?q?=ED=96=89=ED=95=98=EC=97=AC=20=EC=B9=B4=EB=93=9C=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=EC=9D=84=20=ED=95=84=ED=84=B0=EB=A7=81=ED=95=9C?= =?UTF-8?q?=EB=8B=A4.=20[status-chip=20=EC=9E=85=EB=A0=A5=20=ED=83=80?= =?UTF-8?q?=EC=9E=85]=20-=20types.ts:=20StatusChipStyle,=20StatusChipConfi?= =?UTF-8?q?g,=20STATUS=5FCHIP=5FSTYLE=5FLABELS=20-=20PopSearchComponent:?= =?UTF-8?q?=20StatusChipInput=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20(al?= =?UTF-8?q?lRows=20=EA=B5=AC=EB=8F=85=20+=20=EA=B1=B4=EC=88=98=20=EC=A7=91?= =?UTF-8?q?=EA=B3=84)=20-=20PopSearchConfig:=20StatusChipDetailSettings=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=8C=A8=EB=84=90=20(=EC=B9=A9=20?= =?UTF-8?q?=EC=98=B5=EC=85=98/=EC=8A=A4=ED=83=80=EC=9D=BC)=20-=20index.tsx?= =?UTF-8?q?:=20receivable=EC=97=90=20all=5Frows=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=93=B1=EB=A1=9D=20[all=5Frows=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8]=20-=20pop-card-list-v2:=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EB=A1=9C=EB=93=9C=20=EC=8B=9C=20all=5Frow?= =?UTF-8?q?s=20publish=20+=20sendable=20=EB=93=B1=EB=A1=9D=20-=20pop-card-?= =?UTF-8?q?list:=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EC=8B=9C=20all=5Frows=20publish=20+=20sendable=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20-=20useConnectionResolver:=20all=5Frows=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=9E=90=EB=8F=99=20=EB=A7=A4=EC=B9=AD=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80=20[pop-card-list-v2=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0]=20-=20=ED=95=98=EC=9C=84=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=ED=95=84=ED=84=B0=20=EC=A0=81=EC=9A=A9=20=EC=8B=9C?= =?UTF-8?q?=20=5F=5FsubStatus=5F=5F=20=EA=B0=80=EC=83=81=20=EC=BB=AC?= =?UTF-8?q?=EB=9F=BC=20=EC=A3=BC=EC=9E=85=20-=20externalFilters=EC=97=90?= =?UTF-8?q?=20=ED=95=98=EC=9C=84=20=ED=85=8C=EC=9D=B4=EB=B8=94=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=20=EB=B6=84=EB=A6=AC=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/hooks/pop/useConnectionResolver.ts | 15 +- .../PopCardListV2Component.tsx | 97 +++++++++--- .../pop-components/pop-card-list-v2/index.tsx | 1 + .../pop-card-list/PopCardListComponent.tsx | 6 + .../pop-components/pop-card-list/index.tsx | 1 + .../pop-search/PopSearchComponent.tsx | 141 +++++++++++++++++- .../pop-search/PopSearchConfig.tsx | 130 ++++++++++++++++ .../pop-components/pop-search/index.tsx | 1 + .../pop-components/pop-search/types.ts | 27 +++- 9 files changed, 390 insertions(+), 29 deletions(-) diff --git a/frontend/hooks/pop/useConnectionResolver.ts b/frontend/hooks/pop/useConnectionResolver.ts index a778f35f..4aa03be3 100644 --- a/frontend/hooks/pop/useConnectionResolver.ts +++ b/frontend/hooks/pop/useConnectionResolver.ts @@ -60,6 +60,9 @@ function getAutoMatchPairs( if (s.type === "filter_value" && r.type === "filter_value") { pairs.push({ sourceKey: s.key, targetKey: r.key, isFilter: true }); } + if (s.type === "all_rows" && r.type === "all_rows") { + pairs.push({ sourceKey: s.key, targetKey: r.key, isFilter: false }); + } } } @@ -105,11 +108,17 @@ export function useConnectionResolver({ const fieldName = data?.fieldName as string | undefined; const filterColumns = data?.filterColumns as string[] | undefined; const filterMode = (data?.filterMode as string) || "contains"; + // conn.filterConfig에 targetColumn이 명시되어 있으면 우선 사용 + const effectiveColumn = conn.filterConfig?.targetColumn || fieldName; + const effectiveMode = conn.filterConfig?.filterMode || filterMode; + const baseFilterConfig = effectiveColumn + ? { targetColumn: effectiveColumn, targetColumns: conn.filterConfig?.targetColumns || (filterColumns?.length ? filterColumns : [effectiveColumn]), filterMode: effectiveMode } + : conn.filterConfig; publish(targetEvent, { value: payload, - filterConfig: fieldName - ? { targetColumn: fieldName, targetColumns: filterColumns?.length ? filterColumns : [fieldName], filterMode } - : conn.filterConfig, + filterConfig: conn.filterConfig?.isSubTable + ? { ...baseFilterConfig, isSubTable: true } + : baseFilterConfig, _connectionId: conn.id, }); } else { diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx index eacb0ca6..303d9a25 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx @@ -132,7 +132,7 @@ export function PopCardListV2Component({ Map >(new Map()); @@ -143,7 +143,7 @@ export function PopCardListV2Component({ (payload: unknown) => { const data = payload as { value?: { fieldName?: string; value?: unknown }; - filterConfig?: { targetColumn: string; targetColumns?: string[]; filterMode: string }; + filterConfig?: { targetColumn: string; targetColumns?: string[]; filterMode: string; isSubTable?: boolean }; _connectionId?: string; }; const connId = data?._connectionId || "default"; @@ -165,6 +165,12 @@ export function PopCardListV2Component({ return unsub; }, [componentId, subscribe]); + // 전체 rows 발행 (status-chip 등 연결된 컴포넌트에서 건수 집계용) + useEffect(() => { + if (!componentId || loading) return; + publish(`__comp_output__${componentId}__all_rows`, rows); + }, [componentId, rows, loading, publish]); + const cartRef = useRef(cart); cartRef.current = cart; @@ -235,31 +241,75 @@ export function PopCardListV2Component({ const gridColumns = Math.max(1, Math.min(autoColumns, maxGridColumns, maxAllowedColumns)); const gridRows = configGridRows; - // 외부 필터 + // 외부 필터 (메인 테이블 + 하위 테이블 분기) const filteredRows = useMemo(() => { if (externalFilters.size === 0) return rows; + const allFilters = [...externalFilters.values()]; - return rows.filter((row) => - allFilters.every((filter) => { - const searchValue = String(filter.value).toLowerCase(); - if (!searchValue) return true; - const fc = filter.filterConfig; - const columns: string[] = - fc?.targetColumns?.length ? fc.targetColumns - : fc?.targetColumn ? [fc.targetColumn] - : filter.fieldName ? [filter.fieldName] : []; - if (columns.length === 0) return true; - const mode = fc?.filterMode || "contains"; - return columns.some((col) => { - const cellValue = String(row[col] ?? "").toLowerCase(); - switch (mode) { - case "equals": return cellValue === searchValue; - case "starts_with": return cellValue.startsWith(searchValue); - default: return cellValue.includes(searchValue); - } + const mainFilters = allFilters.filter((f) => !f.filterConfig?.isSubTable); + const subFilters = allFilters.filter((f) => f.filterConfig?.isSubTable); + + return rows + .map((row) => { + // 1) 메인 테이블 필터 + const passMain = mainFilters.every((filter) => { + const searchValue = String(filter.value).toLowerCase(); + if (!searchValue) return true; + const fc = filter.filterConfig; + const columns: string[] = + fc?.targetColumns?.length ? fc.targetColumns + : fc?.targetColumn ? [fc.targetColumn] + : filter.fieldName ? [filter.fieldName] : []; + if (columns.length === 0) return true; + const mode = fc?.filterMode || "contains"; + return columns.some((col) => { + const cellValue = String(row[col] ?? "").toLowerCase(); + switch (mode) { + case "equals": return cellValue === searchValue; + case "starts_with": return cellValue.startsWith(searchValue); + default: return cellValue.includes(searchValue); + } + }); }); - }), - ); + if (!passMain) return null; + + // 2) 하위 테이블 필터 없으면 그대로 반환 + if (subFilters.length === 0) return row; + + // 3) __processFlow__에서 모든 하위 필터 조건을 만족하는 step 탐색 + const processFlow = row.__processFlow__ as TimelineProcessStep[] | undefined; + if (!processFlow || processFlow.length === 0) return null; + + const matchingSteps = processFlow.filter((step) => + subFilters.every((filter) => { + const searchValue = String(filter.value).toLowerCase(); + if (!searchValue) return true; + const fc = filter.filterConfig; + const col = fc?.targetColumn || filter.fieldName || ""; + if (!col) return true; + const cellValue = String(step.rawData?.[col] ?? "").toLowerCase(); + const mode = fc?.filterMode || "contains"; + switch (mode) { + case "equals": return cellValue === searchValue; + case "starts_with": return cellValue.startsWith(searchValue); + default: return cellValue.includes(searchValue); + } + }), + ); + + if (matchingSteps.length === 0) return null; + + // 매칭된 step 중 첫 번째의 상태를 __subStatus__/__subSemantic__으로 주입 + const matched = matchingSteps[0]; + return { + ...row, + __subStatus__: matched.status, + __subSemantic__: matched.semantic || "pending", + __subProcessName__: matched.processName, + __subSeqNo__: matched.seqNo, + }; + }) + .filter((row): row is RowData => row !== null); }, [rows, externalFilters]); const overflowCfg = effectiveConfig?.overflow; @@ -367,6 +417,7 @@ export function PopCardListV2Component({ status: normalizedStatus, semantic: semantic as "pending" | "active" | "done", isCurrent: semantic === "active", + rawData: p as Record, }); } diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/index.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/index.tsx index d3e80209..138ab941 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/index.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/index.tsx @@ -43,6 +43,7 @@ PopComponentRegistry.registerComponent({ connectionMeta: { sendable: [ { key: "selected_row", label: "선택된 행", type: "selected_row", category: "data", description: "사용자가 선택한 카드의 행 데이터를 전달" }, + { key: "all_rows", label: "전체 데이터", type: "all_rows", category: "data", description: "필터 적용 전 전체 데이터 배열 (상태 칩 건수 등)" }, { key: "cart_updated", label: "장바구니 상태", type: "event", category: "event", description: "장바구니 변경 시 count/isDirty 전달" }, { key: "cart_save_completed", label: "저장 완료", type: "event", category: "event", description: "장바구니 DB 저장 완료 후 결과 전달" }, { key: "selected_items", label: "선택된 항목", type: "event", category: "event", description: "장바구니 모드에서 체크박스로 선택된 항목 배열" }, diff --git a/frontend/lib/registry/pop-components/pop-card-list/PopCardListComponent.tsx b/frontend/lib/registry/pop-components/pop-card-list/PopCardListComponent.tsx index f6a1c5c3..c4a2e162 100644 --- a/frontend/lib/registry/pop-components/pop-card-list/PopCardListComponent.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list/PopCardListComponent.tsx @@ -256,6 +256,12 @@ export function PopCardListComponent({ return unsub; }, [componentId, subscribe]); + // 전체 rows 발행 (status-chip 등 연결된 컴포넌트에서 건수 집계용) + useEffect(() => { + if (!componentId || loading) return; + publish(`__comp_output__${componentId}__all_rows`, rows); + }, [componentId, rows, loading, publish]); + // cart를 ref로 유지: 이벤트 콜백에서 항상 최신 참조를 사용 const cartRef = useRef(cart); cartRef.current = cart; diff --git a/frontend/lib/registry/pop-components/pop-card-list/index.tsx b/frontend/lib/registry/pop-components/pop-card-list/index.tsx index b9b769af..fe6a43df 100644 --- a/frontend/lib/registry/pop-components/pop-card-list/index.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list/index.tsx @@ -61,6 +61,7 @@ PopComponentRegistry.registerComponent({ connectionMeta: { sendable: [ { key: "selected_row", label: "선택된 행", type: "selected_row", category: "data", description: "사용자가 선택한 카드의 행 데이터를 전달" }, + { key: "all_rows", label: "전체 데이터", type: "all_rows", category: "data", description: "필터 적용 전 전체 데이터 배열 (상태 칩 건수 등)" }, { key: "cart_updated", label: "장바구니 상태", type: "event", category: "event", description: "장바구니 변경 시 count/isDirty 전달" }, { key: "cart_save_completed", label: "저장 완료", type: "event", category: "event", description: "장바구니 DB 저장 완료 후 결과 전달" }, { key: "selected_items", label: "선택된 항목", type: "event", category: "event", description: "장바구니 모드에서 체크박스로 선택된 항목 배열" }, diff --git a/frontend/lib/registry/pop-components/pop-search/PopSearchComponent.tsx b/frontend/lib/registry/pop-components/pop-search/PopSearchComponent.tsx index 7c5f426d..7db10988 100644 --- a/frontend/lib/registry/pop-components/pop-search/PopSearchComponent.tsx +++ b/frontend/lib/registry/pop-components/pop-search/PopSearchComponent.tsx @@ -37,6 +37,8 @@ import type { ModalSelectConfig, ModalSearchMode, ModalFilterTab, + SelectOption, + StatusChipConfig, } from "./types"; import { DATE_PRESET_LABELS, @@ -147,6 +149,24 @@ export function PopSearchComponent({ return unsub; }, [subscribe, emitFilterChanged, config.fieldName, config.filterColumns, isModalType]); + // status-chip: 연결된 카드 컴포넌트의 전체 rows 수신 + const [allRows, setAllRows] = useState[]>([]); + + useEffect(() => { + if (!componentId || normalizedType !== "status-chip") return; + const unsub = subscribe( + `__comp_input__${componentId}__all_rows`, + (payload: unknown) => { + const data = payload as { value?: unknown } | unknown; + const rows = (typeof data === "object" && data && "value" in data) + ? (data as { value: unknown }).value + : data; + if (Array.isArray(rows)) setAllRows(rows); + } + ); + return unsub; + }, [componentId, subscribe, normalizedType]); + const handleModalOpen = useCallback(() => { if (!config.modalConfig) return; setSimpleModalOpen(true); @@ -189,6 +209,7 @@ export function PopSearchComponent({ modalDisplayText={modalDisplayText} onModalOpen={handleModalOpen} onModalClear={handleModalClear} + allRows={allRows} /> @@ -218,7 +239,11 @@ interface InputRendererProps { onModalClear?: () => void; } -function SearchInputRenderer({ config, value, onChange, modalDisplayText, onModalOpen, onModalClear }: InputRendererProps) { +interface InputRendererPropsExt extends InputRendererProps { + allRows?: Record[]; +} + +function SearchInputRenderer({ config, value, onChange, modalDisplayText, onModalOpen, onModalClear, allRows }: InputRendererPropsExt) { const normalized = normalizeInputType(config.inputType as string); switch (normalized) { case "text": @@ -238,6 +263,8 @@ function SearchInputRenderer({ config, value, onChange, modalDisplayText, onModa return ; case "modal": return ; + case "status-chip": + return ; default: return ; } @@ -651,6 +678,118 @@ function ModalSearchInput({ config, displayText, onClick, onClear }: { config: P ); } +// ======================================== +// status-chip 서브타입 +// ======================================== + +function StatusChipInput({ + config, + value, + onChange, + allRows, +}: { + config: PopSearchConfig; + value: string; + onChange: (v: unknown) => void; + allRows: Record[]; +}) { + const chipCfg: StatusChipConfig = config.statusChipConfig || {}; + const chipStyle = chipCfg.chipStyle || "tab"; + const showCount = chipCfg.showCount !== false; + const countColumn = chipCfg.countColumn || config.fieldName || ""; + const allowAll = chipCfg.allowAll !== false; + const allLabel = chipCfg.allLabel || "전체"; + + const options: SelectOption[] = config.options || []; + + const counts = useMemo(() => { + if (!showCount || !countColumn || allRows.length === 0) return new Map(); + const map = new Map(); + for (const row of allRows) { + const v = String(row[countColumn] ?? ""); + map.set(v, (map.get(v) || 0) + 1); + } + return map; + }, [allRows, countColumn, showCount]); + + const totalCount = allRows.length; + + const chipItems: { value: string; label: string; count: number }[] = useMemo(() => { + const items: { value: string; label: string; count: number }[] = []; + if (allowAll) { + items.push({ value: "", label: allLabel, count: totalCount }); + } + for (const opt of options) { + items.push({ + value: opt.value, + label: opt.label, + count: counts.get(opt.value) || 0, + }); + } + return items; + }, [options, counts, totalCount, allowAll, allLabel]); + + if (chipStyle === "pill") { + return ( +
+ {chipItems.map((item) => { + const isActive = value === item.value; + return ( + + ); + })} +
+ ); + } + + // tab 스타일 (기본) + return ( +
+ {chipItems.map((item) => { + const isActive = value === item.value; + return ( + + ); + })} +
+ ); +} + // ======================================== // 미구현 서브타입 플레이스홀더 // ======================================== diff --git a/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx b/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx index 4c52961b..eac031c3 100644 --- a/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx +++ b/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx @@ -38,6 +38,8 @@ import type { ModalDisplayStyle, ModalSearchMode, ModalFilterTab, + StatusChipStyle, + StatusChipConfig, } from "./types"; import { SEARCH_INPUT_TYPE_LABELS, @@ -46,6 +48,7 @@ import { MODAL_DISPLAY_STYLE_LABELS, MODAL_SEARCH_MODE_LABELS, MODAL_FILTER_TAB_LABELS, + STATUS_CHIP_STYLE_LABELS, normalizeInputType, } from "./types"; import { tableManagementApi, getTableColumns } from "@/lib/api/tableManagement"; @@ -231,6 +234,8 @@ function StepDetailSettings({ cfg, update, allComponents, connections, component return ; case "modal": return ; + case "status-chip": + return ; case "toggle": return (
@@ -1066,3 +1071,128 @@ function ModalDetailSettings({ cfg, update }: StepProps) {
); } + +// ======================================== +// status-chip 상세 설정 +// ======================================== + +function StatusChipDetailSettings({ cfg, update, allComponents, connections, componentId }: StepProps) { + const chipCfg: StatusChipConfig = cfg.statusChipConfig || {}; + const options = cfg.options || []; + + const updateChip = (partial: Partial) => { + update({ statusChipConfig: { ...chipCfg, ...partial } }); + }; + + const addOption = () => { + update({ + options: [...options, { value: `status_${options.length + 1}`, label: `상태 ${options.length + 1}` }], + }); + }; + + const removeOption = (index: number) => { + update({ options: options.filter((_, i) => i !== index) }); + }; + + const updateOption = (index: number, field: "value" | "label", val: string) => { + update({ options: options.map((opt, i) => (i === index ? { ...opt, [field]: val } : opt)) }); + }; + + return ( +
+ {/* 칩 옵션 목록 */} +
+ + {options.length === 0 && ( +

옵션이 없습니다. 아래 버튼으로 추가하세요.

+ )} + {options.map((opt, i) => ( +
+ updateOption(i, "value", e.target.value)} placeholder="DB 값" className="h-7 flex-1 text-[10px]" /> + updateOption(i, "label", e.target.value)} placeholder="표시 라벨" className="h-7 flex-1 text-[10px]" /> + +
+ ))} + +
+ + {/* 전체 칩 자동 추가 */} +
+ updateChip({ allowAll: Boolean(checked) })} + /> + +
+ + {chipCfg.allowAll !== false && ( +
+ + updateChip({ allLabel: e.target.value })} + placeholder="전체" + className="h-8 text-xs" + /> +
+ )} + + {/* 건수 표시 */} +
+ updateChip({ showCount: Boolean(checked) })} + /> + +
+ + {chipCfg.showCount !== false && ( +
+ + updateChip({ countColumn: e.target.value })} + placeholder="예: status" + className="h-8 text-xs" + /> +

+ 연결된 카드의 이 컬럼 값으로 상태별 건수를 집계합니다 +

+
+ )} + + {/* 칩 스타일 */} +
+ + +

+ 탭: 큰 숫자 + 라벨 / 알약: 작은 뱃지 형태 +

+
+ + {/* 필터 연결 */} + +
+ ); +} diff --git a/frontend/lib/registry/pop-components/pop-search/index.tsx b/frontend/lib/registry/pop-components/pop-search/index.tsx index e78dd11c..fadf0bd7 100644 --- a/frontend/lib/registry/pop-components/pop-search/index.tsx +++ b/frontend/lib/registry/pop-components/pop-search/index.tsx @@ -40,6 +40,7 @@ PopComponentRegistry.registerComponent({ ], receivable: [ { key: "set_value", label: "값 설정", type: "filter_value", category: "filter", description: "외부에서 값을 받아 검색 필드에 세팅 (스캔, 모달 선택 등)" }, + { key: "all_rows", label: "전체 데이터", type: "all_rows", category: "data", description: "연결된 카드의 전체 데이터를 받아 상태 칩 건수 표시" }, ], }, touchOptimized: true, diff --git a/frontend/lib/registry/pop-components/pop-search/types.ts b/frontend/lib/registry/pop-components/pop-search/types.ts index 6da0ae32..9157e024 100644 --- a/frontend/lib/registry/pop-components/pop-search/types.ts +++ b/frontend/lib/registry/pop-components/pop-search/types.ts @@ -1,7 +1,7 @@ // ===== pop-search 전용 타입 ===== // 단일 필드 검색 컴포넌트. 그리드 한 칸 = 검색 필드 하나. -/** 검색 필드 입력 타입 (9종) */ +/** 검색 필드 입력 타입 (10종) */ export type SearchInputType = | "text" | "number" @@ -11,7 +11,8 @@ export type SearchInputType = | "multi-select" | "combo" | "modal" - | "toggle"; + | "toggle" + | "status-chip"; /** 레거시 입력 타입 (DB에 저장된 기존 값 호환용) */ export type LegacySearchInputType = "modal-table" | "modal-card" | "modal-icon-grid"; @@ -78,6 +79,18 @@ export interface ModalSelectConfig { distinct?: boolean; } +/** 상태 칩 표시 스타일 */ +export type StatusChipStyle = "tab" | "pill"; + +/** status-chip 전용 설정 */ +export interface StatusChipConfig { + showCount?: boolean; + countColumn?: string; + allowAll?: boolean; + allLabel?: string; + chipStyle?: StatusChipStyle; +} + /** pop-search 전체 설정 */ export interface PopSearchConfig { inputType: SearchInputType | LegacySearchInputType; @@ -103,6 +116,9 @@ export interface PopSearchConfig { // modal 전용 modalConfig?: ModalSelectConfig; + // status-chip 전용 + statusChipConfig?: StatusChipConfig; + // 라벨 labelText?: string; labelVisible?: boolean; @@ -144,6 +160,13 @@ export const SEARCH_INPUT_TYPE_LABELS: Record = { combo: "자동완성", modal: "모달", toggle: "토글", + "status-chip": "상태 칩 (대시보드)", +}; + +/** 상태 칩 스타일 라벨 (설정 패널용) */ +export const STATUS_CHIP_STYLE_LABELS: Record = { + tab: "탭 (큰 숫자)", + pill: "알약 (작은 뱃지)", }; /** 모달 보여주기 방식 라벨 */