"use client"; import { useState, useEffect, useMemo, useCallback } from "react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Plus, Trash2, Loader2, AlertTriangle, RefreshCw } from "lucide-react"; import { getTableColumns } from "@/lib/api/tableManagement"; import { dataApi } from "@/lib/api/data"; import type { ColumnTypeInfo } from "@/lib/api/tableManagement"; import type { StatusBarConfig, StatusChipStyle, StatusChipOption } from "./types"; import { DEFAULT_STATUS_BAR_CONFIG, STATUS_CHIP_STYLE_LABELS } from "./types"; interface ConfigPanelProps { config: StatusBarConfig | undefined; onUpdate: (config: StatusBarConfig) => void; allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinitionV5[]; connections?: import("@/components/pop/designer/types/pop-layout").PopDataConnection[]; componentId?: string; } export function PopStatusBarConfigPanel({ config: rawConfig, onUpdate, allComponents, connections, componentId, }: ConfigPanelProps) { const cfg = { ...DEFAULT_STATUS_BAR_CONFIG, ...(rawConfig || {}) }; const update = (partial: Partial) => { onUpdate({ ...cfg, ...partial }); }; const options = cfg.options || []; const removeOption = (index: number) => { update({ options: options.filter((_, i) => i !== index) }); }; const updateOption = ( index: number, field: keyof StatusChipOption, val: string ) => { update({ options: options.map((opt, i) => i === index ? { ...opt, [field]: val } : opt ), }); }; // 연결된 카드 컴포넌트의 테이블 컬럼 가져오기 const connectedTableName = useMemo(() => { if (!componentId || !connections || !allComponents) return null; const targetIds = connections .filter((c) => c.sourceComponent === componentId) .map((c) => c.targetComponent); const sourceIds = connections .filter((c) => c.targetComponent === componentId) .map((c) => c.sourceComponent); const peerIds = [...new Set([...targetIds, ...sourceIds])]; for (const pid of peerIds) { const comp = allComponents.find((c) => c.id === pid); if (!comp?.config) continue; const compCfg = comp.config as Record; const ds = compCfg.dataSource as { tableName?: string } | undefined; if (ds?.tableName) return ds.tableName; } return null; }, [componentId, connections, allComponents]); const [targetColumns, setTargetColumns] = useState([]); const [columnsLoading, setColumnsLoading] = useState(false); // 집계 컬럼의 고유값 (옵션 선택용) const [distinctValues, setDistinctValues] = useState([]); const [distinctLoading, setDistinctLoading] = useState(false); useEffect(() => { if (!connectedTableName) { setTargetColumns([]); return; } let cancelled = false; setColumnsLoading(true); getTableColumns(connectedTableName) .then((res) => { if (cancelled) return; if (res.success && res.data?.columns) { setTargetColumns(res.data.columns); } }) .finally(() => { if (!cancelled) setColumnsLoading(false); }); return () => { cancelled = true; }; }, [connectedTableName]); const fetchDistinctValues = useCallback(async (tableName: string, column: string) => { setDistinctLoading(true); try { const res = await dataApi.getTableData(tableName, { page: 1, size: 9999 }); const vals = new Set(); for (const row of res.data) { const v = row[column]; if (v != null && String(v).trim() !== "") { vals.add(String(v)); } } const sorted = [...vals].sort(); setDistinctValues(sorted); return sorted; } catch { setDistinctValues([]); return []; } finally { setDistinctLoading(false); } }, []); // 집계 컬럼 변경 시 고유값 새로 가져오기 useEffect(() => { const col = cfg.countColumn; if (!connectedTableName || !col) { setDistinctValues([]); return; } fetchDistinctValues(connectedTableName, col); }, [connectedTableName, cfg.countColumn, fetchDistinctValues]); const handleAutoFill = useCallback(async () => { if (!connectedTableName || !cfg.countColumn) return; const vals = await fetchDistinctValues(connectedTableName, cfg.countColumn); if (vals.length === 0) return; const newOptions: StatusChipOption[] = vals.map((v) => { const existing = options.find((o) => o.value === v); return { value: v, label: existing?.label || v }; }); update({ options: newOptions }); }, [connectedTableName, cfg.countColumn, options, fetchDistinctValues]); const addOptionFromValue = (value: string) => { if (options.some((o) => o.value === value)) return; update({ options: [...options, { value, label: value }], }); }; return (
{/* --- 칩 옵션 목록 --- */}
{connectedTableName && cfg.countColumn && ( )}
{cfg.useSubCount && (

하위 필터 자동 전환이 켜져 있으면 런타임에 가상 컬럼으로 집계됩니다. DB 값과 다를 수 있으니 직접 입력을 권장합니다.

)} {options.length === 0 && (

{connectedTableName && cfg.countColumn ? "\"DB에서 자동 채우기\"를 클릭하거나 아래에서 추가하세요." : "옵션이 없습니다. 먼저 집계 컬럼을 선택한 후 추가하세요."}

)} {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]" />
))} {/* 고유값에서 추가 */} {distinctValues.length > 0 && (
{distinctValues .filter((dv) => !options.some((o) => o.value === dv)) .map((dv) => ( ))} {distinctValues.every((dv) => options.some((o) => o.value === dv)) && (

모든 값이 추가되었습니다

)}
)} {/* 수동 추가 */}
{/* --- 전체 보기 칩 --- */}
update({ allowAll: Boolean(checked) })} />

필터 해제용 칩을 옵션 목록 맨 앞에 자동 추가합니다

{cfg.allowAll !== false && (
update({ allLabel: e.target.value })} placeholder="전체" className="h-7 text-[10px]" />
)}
{/* --- 건수 표시 --- */}
update({ showCount: Boolean(checked) })} />
{cfg.showCount !== false && (
{columnsLoading ? (
컬럼 로딩...
) : targetColumns.length > 0 ? ( ) : ( update({ countColumn: e.target.value })} placeholder="예: status" className="h-8 text-xs" /> )}

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

)} {cfg.showCount !== false && (
update({ useSubCount: Boolean(checked) }) } />
{cfg.useSubCount && ( <>

연결된 카드의 하위 테이블 필터가 적용되면 집계 컬럼이 자동 전환됩니다

update({ hideUntilSubFilter: Boolean(checked) }) } />
{cfg.hideUntilSubFilter && (
update({ hiddenMessage: e.target.value })} placeholder="조건을 선택하면 상태별 현황이 표시됩니다" className="h-7 text-[10px]" />
)} )}
)} {/* --- 칩 스타일 --- */}

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

{/* --- 필터 컬럼 --- */}
{!connectedTableName && (

연결 탭에서 대상 카드 컴포넌트를 먼저 연결해주세요.

)} {connectedTableName && ( <> {columnsLoading ? (
컬럼 로딩...
) : targetColumns.length > 0 ? ( ) : ( update({ filterColumn: e.target.value })} placeholder="예: status" className="h-8 text-xs" /> )}

선택한 상태 칩 값으로 카드를 필터링할 컬럼 (비어있으면 집계 컬럼과 동일)

)}
); }