"use client"; /** * ConditionalConfigPanel * * 비개발자도 쉽게 조건부 표시/숨김/활성화/비활성화를 설정할 수 있는 UI * * 사용처: * - 화면관리 > 상세설정 패널 * - 화면관리 > 속성 패널 */ import React, { useState, useEffect, useMemo } from "react"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Button } from "@/components/ui/button"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Zap, Plus, Trash2, HelpCircle, Check, ChevronsUpDown } from "lucide-react"; import { ConditionalConfig } from "@/types/unified-components"; import { cn } from "@/lib/utils"; // ===== 타입 정의 ===== interface FieldOption { id: string; label: string; type?: string; // text, number, select, checkbox, entity, code 등 options?: Array<{ value: string; label: string }>; // select 타입일 경우 옵션들 // 동적 옵션 로드를 위한 정보 entityTable?: string; entityValueColumn?: string; entityLabelColumn?: string; codeGroup?: string; } interface ConditionalConfigPanelProps { /** 현재 조건부 설정 */ config?: ConditionalConfig; /** 설정 변경 콜백 */ onChange: (config: ConditionalConfig | undefined) => void; /** 같은 화면에 있는 다른 필드들 (조건 필드로 선택 가능) */ availableFields: FieldOption[]; /** 현재 컴포넌트 ID (자기 자신은 조건 필드에서 제외) */ currentComponentId?: string; } // 연산자 옵션 const OPERATORS: Array<{ value: ConditionalConfig["operator"]; label: string; description: string }> = [ { value: "=", label: "같음", description: "값이 정확히 일치할 때" }, { value: "!=", label: "다름", description: "값이 일치하지 않을 때" }, { value: ">", label: "보다 큼", description: "값이 더 클 때 (숫자)" }, { value: "<", label: "보다 작음", description: "값이 더 작을 때 (숫자)" }, { value: "in", label: "포함됨", description: "여러 값 중 하나일 때" }, { value: "notIn", label: "포함 안됨", description: "여러 값 중 아무것도 아닐 때" }, { value: "isEmpty", label: "비어있음", description: "값이 없을 때" }, { value: "isNotEmpty", label: "값이 있음", description: "값이 있을 때" }, ]; // 동작 옵션 const ACTIONS: Array<{ value: ConditionalConfig["action"]; label: string; description: string }> = [ { value: "show", label: "표시", description: "조건 만족 시 이 필드를 표시" }, { value: "hide", label: "숨김", description: "조건 만족 시 이 필드를 숨김" }, { value: "enable", label: "활성화", description: "조건 만족 시 이 필드를 활성화" }, { value: "disable", label: "비활성화", description: "조건 만족 시 이 필드를 비활성화" }, ]; // ===== 컴포넌트 ===== export function ConditionalConfigPanel({ config, onChange, availableFields, currentComponentId, }: ConditionalConfigPanelProps) { // 로컬 상태 const [enabled, setEnabled] = useState(config?.enabled ?? false); const [field, setField] = useState(config?.field ?? ""); const [operator, setOperator] = useState(config?.operator ?? "="); const [value, setValue] = useState(String(config?.value ?? "")); const [action, setAction] = useState(config?.action ?? "show"); // 자기 자신을 제외한 필드 목록 const selectableFields = useMemo(() => { return availableFields.filter((f) => f.id !== currentComponentId); }, [availableFields, currentComponentId]); // 선택된 필드 정보 const selectedField = useMemo(() => { return selectableFields.find((f) => f.id === field); }, [selectableFields, field]); // 동적 옵션 로드 상태 const [dynamicOptions, setDynamicOptions] = useState>([]); const [loadingOptions, setLoadingOptions] = useState(false); // Combobox 열림 상태 const [comboboxOpen, setComboboxOpen] = useState(false); // 엔티티/공통코드 필드 선택 시 동적으로 옵션 로드 useEffect(() => { const loadDynamicOptions = async () => { if (!selectedField) { setDynamicOptions([]); return; } // 정적 옵션이 있으면 사용 if (selectedField.options && selectedField.options.length > 0) { setDynamicOptions([]); return; } // 엔티티 타입 (타입이 entity이거나, entityTable이 있으면 엔티티로 간주) if (selectedField.entityTable) { setLoadingOptions(true); try { const { apiClient } = await import("@/lib/api/client"); const valueCol = selectedField.entityValueColumn || "id"; const labelCol = selectedField.entityLabelColumn || "name"; const response = await apiClient.get(`/entity/${selectedField.entityTable}/options`, { params: { value: valueCol, label: labelCol }, }); if (response.data.success && response.data.data) { setDynamicOptions(response.data.data); } } catch (error) { console.error("엔티티 옵션 로드 실패:", error); setDynamicOptions([]); } finally { setLoadingOptions(false); } return; } // 공통코드 타입 (타입이 code이거나, codeGroup이 있으면 공통코드로 간주) if (selectedField.codeGroup) { setLoadingOptions(true); try { const { apiClient } = await import("@/lib/api/client"); // 올바른 API 경로: /common-codes/categories/:categoryCode/options const response = await apiClient.get(`/common-codes/categories/${selectedField.codeGroup}/options`); if (response.data.success && response.data.data) { setDynamicOptions( response.data.data.map((item: { value: string; label: string }) => ({ value: item.value, label: item.label, })) ); } } catch (error) { console.error("공통코드 옵션 로드 실패:", error); setDynamicOptions([]); } finally { setLoadingOptions(false); } return; } setDynamicOptions([]); }; loadDynamicOptions(); }, [selectedField?.id, selectedField?.entityTable, selectedField?.entityValueColumn, selectedField?.entityLabelColumn, selectedField?.codeGroup]); // 최종 옵션 (정적 + 동적) const fieldOptions = useMemo(() => { if (selectedField?.options && selectedField.options.length > 0) { return selectedField.options; } return dynamicOptions; }, [selectedField?.options, dynamicOptions]); // config prop 변경 시 로컬 상태 동기화 useEffect(() => { setEnabled(config?.enabled ?? false); setField(config?.field ?? ""); setOperator(config?.operator ?? "="); setValue(String(config?.value ?? "")); setAction(config?.action ?? "show"); }, [config]); // 설정 변경 시 부모에게 알림 const updateConfig = (updates: Partial) => { const newConfig: ConditionalConfig = { enabled: updates.enabled ?? enabled, field: updates.field ?? field, operator: updates.operator ?? operator, value: updates.value ?? value, action: updates.action ?? action, }; // enabled가 false이면 undefined 반환 (설정 제거) if (!newConfig.enabled) { onChange(undefined); } else { onChange(newConfig); } }; // 활성화 토글 const handleEnabledChange = (checked: boolean) => { setEnabled(checked); updateConfig({ enabled: checked }); }; // 조건 필드 변경 const handleFieldChange = (newField: string) => { setField(newField); setValue(""); // 필드 변경 시 값 초기화 updateConfig({ field: newField, value: "" }); }; // 연산자 변경 const handleOperatorChange = (newOperator: ConditionalConfig["operator"]) => { setOperator(newOperator); // 비어있음/값이있음 연산자는 value 필요 없음 if (newOperator === "isEmpty" || newOperator === "isNotEmpty") { setValue(""); updateConfig({ operator: newOperator, value: "" }); } else { updateConfig({ operator: newOperator }); } }; // 값 변경 const handleValueChange = (newValue: string) => { setValue(newValue); // 타입에 따라 적절한 값으로 변환 let parsedValue: unknown = newValue; if (selectedField?.type === "number") { parsedValue = Number(newValue); } else if (newValue === "true") { parsedValue = true; } else if (newValue === "false") { parsedValue = false; } updateConfig({ value: parsedValue }); }; // 동작 변경 const handleActionChange = (newAction: ConditionalConfig["action"]) => { setAction(newAction); updateConfig({ action: newAction }); }; // 값 입력 필드 렌더링 (필드 타입에 따라 다르게) const renderValueInput = () => { // 비어있음/값이있음은 값 입력 불필요 if (operator === "isEmpty" || operator === "isNotEmpty") { return (
(값 입력 불필요)
); } // 옵션 로딩 중 if (loadingOptions) { return (
옵션 로딩 중...
); } // 옵션이 있으면 검색 가능한 Combobox로 표시 if (fieldOptions.length > 0) { const selectedOption = fieldOptions.find((opt) => opt.value === value); return ( 검색 결과가 없습니다 {fieldOptions.map((opt) => ( { handleValueChange(opt.value); setComboboxOpen(false); }} className="text-xs" > {opt.label} ))} ); } // 체크박스 타입이면 true/false Select if (selectedField?.type === "checkbox" || selectedField?.type === "boolean") { return ( ); } // 숫자 타입 if (selectedField?.type === "number") { return ( handleValueChange(e.target.value)} placeholder="숫자 입력" className="h-8 text-xs" /> ); } // 기본: 텍스트 입력 return ( handleValueChange(e.target.value)} placeholder="값 입력" className="h-8 text-xs" /> ); }; return (
{/* 헤더 */}
조건부 표시
{/* 조건 설정 영역 */} {enabled && (
{/* 조건 필드 선택 */}

이 필드의 값에 따라 조건이 적용됩니다

{/* 연산자 선택 */}
{/* 값 입력 */}
{renderValueInput()}
{/* 동작 선택 */}

조건이 만족되면 이 필드를 {ACTIONS.find(a => a.value === action)?.label}합니다

{/* 미리보기 */} {field && (

설정 요약:

"{selectableFields.find(f => f.id === field)?.label || field}" 필드가{" "} {operator === "isEmpty" ? "비어있으면" : operator === "isNotEmpty" ? "값이 있으면" : `"${value}"${operator === "=" ? "이면" : operator === "!=" ? "이 아니면" : operator === ">" ? "보다 크면" : operator === "<" ? "보다 작으면" : operator === "in" ? "에 포함되면" : "에 포함되지 않으면"}`} {" "} → 이 필드를{" "} {action === "show" ? "표시" : action === "hide" ? "숨김" : action === "enable" ? "활성화" : "비활성화"}

)}
)}
); } export default ConditionalConfigPanel;