"use client"; import { useMemo, useCallback } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { Eye, EyeOff, HelpCircle, Trash2, Plus } from "lucide-react"; import type { ComponentConfig, ConditionalRule } from "@/types/report"; import { useReportDesigner } from "@/contexts/ReportDesignerContext"; const TYPE_LABELS: Record = { text: "텍스트", label: "레이블", table: "테이블", image: "이미지", divider: "구분선", signature: "서명", stamp: "도장", pageNumber: "페이지 번호", card: "카드", calculation: "계산", barcode: "바코드", checkbox: "체크박스", }; interface OperatorDef { value: ConditionalRule["operator"]; symbol: string; label: string; summary: string; group: "compare" | "text" | "exist"; } const OPERATORS: OperatorDef[] = [ { value: "eq", symbol: "=", label: "같을 때", summary: "가 '$V'일 때", group: "compare" }, { value: "ne", symbol: "≠", label: "다를 때", summary: "가 '$V'이(가) 아닐 때", group: "compare" }, { value: "gt", symbol: ">", label: "보다 클 때", summary: "이(가) $V보다 클 때", group: "compare" }, { value: "lt", symbol: "<", label: "보다 작을 때", summary: "이(가) $V보다 작을 때", group: "compare" }, { value: "gte", symbol: "≥", label: "이상일 때", summary: "이(가) $V 이상일 때", group: "compare" }, { value: "lte", symbol: "≤", label: "이하일 때", summary: "이(가) $V 이하일 때", group: "compare" }, { value: "contains", symbol: "⊃", label: "포함할 때", summary: "에 '$V'이(가) 포함될 때", group: "text" }, { value: "notEmpty", symbol: "✓", label: "값이 있을 때", summary: "에 값이 있을 때", group: "exist" }, { value: "empty", symbol: "∅", label: "값이 없을 때", summary: "에 값이 없을 때", group: "exist" }, ]; const OPERATOR_GROUPS: { key: OperatorDef["group"]; label: string }[] = [ { key: "compare", label: "비교" }, { key: "text", label: "텍스트" }, { key: "exist", label: "값 유무" }, ]; const NO_VALUE_OPERATORS = ["notEmpty", "empty"]; interface CardColumn { column_name: string; data_type: string; } export interface CardColumnLabel { columnName: string; label: string; } interface Props { component: ComponentConfig; onConfigChange?: (updates: Partial) => void; cardColumns?: CardColumn[]; cardTableName?: string; cardColumnLabels?: CardColumnLabel[]; /** conditionalRules 대신 사용할 키 (격자 모드 분리용) */ rulesKey?: "conditionalRules" | "gridConditionalRules"; /** conditionalRule 대신 사용할 키 (격자 모드 분리용) */ ruleKey?: "conditionalRule" | "gridConditionalRule"; } const DEFAULT_RULE: ConditionalRule = { queryId: "", field: "", operator: "eq", value: "", action: "show", }; export function ConditionalProperties({ component, onConfigChange, cardColumns, cardTableName, cardColumnLabels, rulesKey = "conditionalRules", ruleKey = "conditionalRule", }: Props) { const { updateComponent, queries, getQueryResult } = useReportDesigner(); const componentLabel = TYPE_LABELS[component.type] ?? component.type; const isCardMode = !!cardColumns; const applyUpdate = useCallback( (updates: Partial) => { if (onConfigChange) onConfigChange(updates); else updateComponent(component.id, updates); }, [onConfigChange, updateComponent, component.id], ); /** 단일 rule → 배열로 정규화 (rulesKey/ruleKey 기반) */ const rules: ConditionalRule[] = useMemo(() => { const rulesArr = component[rulesKey] as ConditionalRule[] | undefined; const singleRule = component[ruleKey] as ConditionalRule | undefined; if (rulesArr && rulesArr.length > 0) { return rulesArr; } if (singleRule) { return [singleRule]; } return []; }, [component, rulesKey, ruleKey]); const action = rules.length > 0 ? rules[0].action : "show"; const syncRules = useCallback( (newRules: ConditionalRule[]) => { applyUpdate({ [rulesKey]: newRules, [ruleKey]: newRules.length > 0 ? newRules[0] : undefined, }); }, [applyUpdate, rulesKey, ruleKey], ); const getQueryFields = (queryId: string): string[] => { const result = getQueryResult(queryId); return result ? result.fields : []; }; const labelMap = useMemo(() => { const map = new Map(); cardColumnLabels?.forEach((cl) => map.set(cl.columnName, cl.label)); return map; }, [cardColumnLabels]); const addRule = useCallback(() => { const newRule: ConditionalRule = { ...DEFAULT_RULE, queryId: isCardMode ? "__card__" : (queries[0]?.id || ""), action, }; syncRules([...rules, newRule]); }, [rules, isCardMode, queries, action, syncRules]); const updateRule = useCallback( (index: number, patch: Partial) => { const updated = rules.map((r, i) => (i === index ? { ...r, ...patch } : r)); syncRules(updated); }, [rules, syncRules], ); const removeRule = useCallback( (index: number) => { const updated = rules.filter((_, i) => i !== index); syncRules(updated); }, [rules, syncRules], ); const removeAllRules = useCallback(() => { syncRules([]); }, [syncRules]); const updateAction = useCallback( (newAction: "show" | "hide") => { const updated = rules.map((r) => ({ ...r, action: newAction })); syncRules(updated); }, [rules, syncRules], ); const getFieldsForRule = useCallback( (rule: ConditionalRule): string[] => { if (isCardMode) { return cardColumnLabels?.map((cl) => cl.columnName) ?? []; } return rule.queryId ? getQueryFields(rule.queryId) : []; }, [isCardMode, cardColumnLabels], ); const getFieldDisplayName = useCallback( (fieldName: string): string => labelMap.get(fieldName) || fieldName, [labelMap], ); const summaryText = useMemo(() => { const validRules = rules.filter((r) => r.field && (isCardMode || r.queryId)); if (validRules.length === 0) return null; const parts = validRules.map((rule) => { const op = OPERATORS.find((o) => o.value === rule.operator); if (!op) return null; const fieldLabel = labelMap.get(rule.field) || rule.field; const condPart = op.summary.replace("$V", rule.value || "?"); return `${fieldLabel}${condPart}`; }).filter(Boolean); if (parts.length === 0) return null; const conditionStr = parts.join(", "); const actionPart = action === "show" ? `이 ${componentLabel}을(를) 보여줍니다` : `이 ${componentLabel}을(를) 숨깁니다`; if (parts.length === 1) { return `${conditionStr} ${actionPart}`; } return `다음 조건을 모두 만족할 때 ${actionPart}: ${conditionStr}`; }, [rules, isCardMode, labelMap, componentLabel, action]); const hasNoDataSource = isCardMode ? !cardTableName || (cardColumnLabels?.length ?? 0) === 0 : false; return (
{/* 헤더 */}
{action === "hide" ? ( ) : ( )} 표시 조건 {rules.length > 0 && ( {rules.length} )}

데이터 값에 따라 이 {componentLabel}을(를) 자동으로 보이거나 숨길 수 있습니다.

여러 조건 사용

조건을 여러 개 추가하면 모든 조건을 동시에 만족해야 결과가 적용됩니다.

{rules.length > 0 && ( )}
{/* 본문 */}
{hasNoDataSource ? (

{!cardTableName ? "데이터 연결 탭에서 테이블을 먼저 선택해주세요." : "레이아웃 구성 탭에서 데이터 항목을 먼저 추가해주세요."}

) : rules.length === 0 ? ( ) : ( <> {/* 결과 동작 (공통) */}

결과

{/* 조건 목록 */} {rules.map((rule, index) => ( 0} /> ))} {/* 조건 추가 버튼 */} {/* 요약 */} {summaryText && (

요약

{summaryText}

)} )}
); } /** 개별 조건 편집기 */ interface RuleEditorProps { index: number; rule: ConditionalRule; isCardMode: boolean; queries: { id: string; name: string }[]; fields: string[]; getFieldDisplayName: (fieldName: string) => string; onUpdate: (index: number, patch: Partial) => void; onRemove: (index: number) => void; showAndLabel: boolean; } function RuleEditor({ index, rule, isCardMode, queries, fields, getFieldDisplayName, onUpdate, onRemove, showAndLabel, }: RuleEditorProps) { const needsValue = !NO_VALUE_OPERATORS.includes(rule.operator); return (
{showAndLabel && (
동시 만족
)}

조건 {index + 1}

{!isCardMode && (
)}
{needsValue && (
onUpdate(index, { value: e.target.value })} placeholder="값 입력" className="h-8 text-xs" />
)}
); }