"use client"; import React, { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; import { Plus, Trash2, GripVertical, ChevronUp, ChevronDown, Settings, Database, Layout, Table } from "lucide-react"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { getNumberingRules } from "@/lib/api/numberingRule"; import { UniversalFormModalConfig, UniversalFormModalConfigPanelProps, FormSectionConfig, FormFieldConfig, MODAL_SIZE_OPTIONS, SECTION_TYPE_OPTIONS, } from "./types"; import { defaultSectionConfig, defaultTableSectionConfig, generateSectionId } from "./config"; // 모달 import import { FieldDetailSettingsModal } from "./modals/FieldDetailSettingsModal"; import { SaveSettingsModal } from "./modals/SaveSettingsModal"; import { SectionLayoutModal } from "./modals/SectionLayoutModal"; import { TableSectionSettingsModal } from "./modals/TableSectionSettingsModal"; // 도움말 텍스트 컴포넌트 const HelpText = ({ children }: { children: React.ReactNode }) => (

{children}

); // 부모 화면에서 전달 가능한 필드 타입 interface AvailableParentField { name: string; // 필드명 (columnName) label: string; // 표시 라벨 sourceComponent?: string; // 출처 컴포넌트 (예: "TableList", "SplitPanelLayout2") sourceTable?: string; // 출처 테이블명 } export function UniversalFormModalConfigPanel({ config, onChange, allComponents = [], }: UniversalFormModalConfigPanelProps) { // 테이블 목록 const [tables, setTables] = useState<{ name: string; label: string }[]>([]); const [tableColumns, setTableColumns] = useState<{ [tableName: string]: { name: string; type: string; label: string; inputType?: string }[]; }>({}); // 부모 화면에서 전달 가능한 필드 목록 const [availableParentFields, setAvailableParentFields] = useState([]); // 채번규칙 목록 const [numberingRules, setNumberingRules] = useState<{ id: string; name: string }[]>([]); // 모달 상태 const [saveSettingsModalOpen, setSaveSettingsModalOpen] = useState(false); const [sectionLayoutModalOpen, setSectionLayoutModalOpen] = useState(false); const [fieldDetailModalOpen, setFieldDetailModalOpen] = useState(false); const [tableSectionSettingsModalOpen, setTableSectionSettingsModalOpen] = useState(false); const [selectedSection, setSelectedSection] = useState(null); const [selectedField, setSelectedField] = useState(null); // 테이블 목록 로드 useEffect(() => { loadTables(); loadNumberingRules(); }, []); // allComponents에서 부모 화면에서 전달 가능한 필드 추출 useEffect(() => { const extractParentFields = async () => { if (!allComponents || allComponents.length === 0) { setAvailableParentFields([]); return; } const fields: AvailableParentField[] = []; for (const comp of allComponents) { // 컴포넌트 타입 추출 (여러 위치에서 확인) const compType = comp.componentId || comp.componentConfig?.type || comp.componentConfig?.id || comp.type; const compConfig = comp.componentConfig || {}; // 1. TableList / InteractiveDataTable - 테이블 컬럼 추출 if (compType === "table-list" || compType === "interactive-data-table") { const tableName = compConfig.selectedTable || compConfig.tableName; if (tableName) { // 테이블 컬럼 로드 try { const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); const columns = response.data?.data?.columns; if (response.data?.success && Array.isArray(columns)) { columns.forEach((col: any) => { const colName = col.columnName || col.column_name; const colLabel = col.displayName || col.columnComment || col.column_comment || colName; fields.push({ name: colName, label: colLabel, sourceComponent: "TableList", sourceTable: tableName, }); }); } } catch (error) { console.error(`테이블 컬럼 로드 실패 (${tableName}):`, error); } } } // 2. SplitPanelLayout2 - 데이터 전달 필드 및 소스 테이블 컬럼 추출 if (compType === "split-panel-layout2") { // dataTransferFields 추출 const transferFields = compConfig.dataTransferFields; if (transferFields && Array.isArray(transferFields)) { transferFields.forEach((field: any) => { if (field.targetColumn) { fields.push({ name: field.targetColumn, label: field.targetColumn, sourceComponent: "SplitPanelLayout2", sourceTable: compConfig.leftPanel?.tableName, }); } }); } // 좌측 패널 테이블 컬럼도 추출 const leftTableName = compConfig.leftPanel?.tableName; if (leftTableName) { try { const response = await apiClient.get(`/table-management/tables/${leftTableName}/columns`); const columns = response.data?.data?.columns; if (response.data?.success && Array.isArray(columns)) { columns.forEach((col: any) => { const colName = col.columnName || col.column_name; const colLabel = col.displayName || col.columnComment || col.column_comment || colName; // 중복 방지 if (!fields.some((f) => f.name === colName && f.sourceTable === leftTableName)) { fields.push({ name: colName, label: colLabel, sourceComponent: "SplitPanelLayout2 (좌측)", sourceTable: leftTableName, }); } }); } } catch (error) { console.error(`테이블 컬럼 로드 실패 (${leftTableName}):`, error); } } } // 3. 기타 테이블 관련 컴포넌트 if (compType === "card-display" || compType === "simple-repeater-table") { const tableName = compConfig.tableName || compConfig.initialDataConfig?.sourceTable; if (tableName) { try { const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); const columns = response.data?.data?.columns; if (response.data?.success && Array.isArray(columns)) { columns.forEach((col: any) => { const colName = col.columnName || col.column_name; const colLabel = col.displayName || col.columnComment || col.column_comment || colName; if (!fields.some((f) => f.name === colName && f.sourceTable === tableName)) { fields.push({ name: colName, label: colLabel, sourceComponent: compType, sourceTable: tableName, }); } }); } } catch (error) { console.error(`테이블 컬럼 로드 실패 (${tableName}):`, error); } } } // 4. 버튼 컴포넌트 - openModalWithData의 fieldMappings/dataMapping에서 소스 컬럼 추출 if (compType === "button-primary" || compType === "button" || compType === "button-secondary") { const action = compConfig.action || {}; // fieldMappings에서 소스 컬럼 추출 const fieldMappings = action.fieldMappings || []; fieldMappings.forEach((mapping: any) => { if (mapping.sourceColumn && !fields.some((f) => f.name === mapping.sourceColumn)) { fields.push({ name: mapping.sourceColumn, label: mapping.sourceColumn, sourceComponent: "Button (fieldMappings)", sourceTable: action.sourceTableName, }); } }); // dataMapping에서 소스 컬럼 추출 const dataMapping = action.dataMapping || []; dataMapping.forEach((mapping: any) => { if (mapping.sourceColumn && !fields.some((f) => f.name === mapping.sourceColumn)) { fields.push({ name: mapping.sourceColumn, label: mapping.sourceColumn, sourceComponent: "Button (dataMapping)", sourceTable: action.sourceTableName, }); } }); } } // 5. 현재 모달의 저장 테이블 컬럼도 추가 (부모에서 전달받을 수 있는 값들) const currentTableName = config.saveConfig?.tableName; if (currentTableName) { try { const response = await apiClient.get(`/table-management/tables/${currentTableName}/columns`); const columns = response.data?.data?.columns; if (response.data?.success && Array.isArray(columns)) { columns.forEach((col: any) => { const colName = col.columnName || col.column_name; const colLabel = col.displayName || col.columnComment || col.column_comment || colName; if (!fields.some((f) => f.name === colName)) { fields.push({ name: colName, label: colLabel, sourceComponent: "현재 폼 테이블", sourceTable: currentTableName, }); } }); } } catch (error) { console.error(`현재 테이블 컬럼 로드 실패 (${currentTableName}):`, error); } } // 중복 제거 (같은 name이면 첫 번째만 유지) const uniqueFields = fields.filter( (field, index, self) => index === self.findIndex((f) => f.name === field.name), ); setAvailableParentFields(uniqueFields); }; extractParentFields(); }, [allComponents, config.saveConfig?.tableName]); // 저장 테이블 변경 시 컬럼 로드 useEffect(() => { if (config.saveConfig.tableName) { loadTableColumns(config.saveConfig.tableName); } }, [config.saveConfig.tableName]); const loadTables = async () => { try { const response = await apiClient.get("/table-management/tables"); const data = response.data?.data; if (response.data?.success && Array.isArray(data)) { setTables( data.map( (t: { tableName?: string; table_name?: string; displayName?: string; tableLabel?: string; table_label?: string; }) => ({ name: t.tableName || t.table_name || "", // displayName 우선, 없으면 tableLabel, 그것도 없으면 테이블명 label: t.displayName || t.tableLabel || t.table_label || "", }), ), ); } } catch (error) { console.error("테이블 목록 로드 실패:", error); } }; const loadTableColumns = async (tableName: string) => { if (!tableName || (tableColumns[tableName] && tableColumns[tableName].length > 0)) return; try { const response = await apiClient.get(`/table-management/tables/${tableName}/columns`); // API 응답 구조: { success, data: { columns: [...], total, page, ... } } const columns = response.data?.data?.columns; if (response.data?.success && Array.isArray(columns)) { setTableColumns((prev) => ({ ...prev, [tableName]: columns.map( (c: { columnName?: string; column_name?: string; dataType?: string; data_type?: string; displayName?: string; columnComment?: string; column_comment?: string; inputType?: string; input_type?: string; }) => ({ name: c.columnName || c.column_name || "", type: c.dataType || c.data_type || "text", label: c.displayName || c.columnComment || c.column_comment || c.columnName || c.column_name || "", inputType: c.inputType || c.input_type || "text", }), ), })); } } catch (error) { console.error(`테이블 컬럼 로드 실패 (${tableName}):`, error); } }; const loadNumberingRules = async () => { try { const response = await getNumberingRules(); const data = response?.data; if (response?.success && Array.isArray(data)) { const rules = data.map( (r: { id?: string | number; ruleId?: string; rule_id?: string; name?: string; ruleName?: string; rule_name?: string; }) => ({ id: String(r.id || r.ruleId || r.rule_id || ""), name: r.name || r.ruleName || r.rule_name || "", }), ); setNumberingRules(rules); } } catch (error) { console.error("채번규칙 목록 로드 실패:", error); } }; // 설정 업데이트 헬퍼 const updateModalConfig = useCallback( (updates: Partial) => { onChange({ ...config, modal: { ...config.modal, ...updates }, }); }, [config, onChange], ); // 섹션 관리 const addSection = useCallback( (type: "fields" | "table" = "fields") => { const newSection: FormSectionConfig = { ...defaultSectionConfig, id: generateSectionId(), title: type === "table" ? `테이블 섹션 ${config.sections.length + 1}` : `섹션 ${config.sections.length + 1}`, type, fields: type === "fields" ? [] : undefined, tableConfig: type === "table" ? { ...defaultTableSectionConfig } : undefined, }; onChange({ ...config, sections: [...config.sections, newSection], }); }, [config, onChange], ); // 섹션 타입 변경 const changeSectionType = useCallback( (sectionId: string, newType: "fields" | "table") => { onChange({ ...config, sections: config.sections.map((s) => { if (s.id !== sectionId) return s; if (newType === "table") { return { ...s, type: "table", fields: undefined, tableConfig: { ...defaultTableSectionConfig }, }; } else { return { ...s, type: "fields", fields: [], tableConfig: undefined, }; } }), }); }, [config, onChange], ); // 테이블 섹션 설정 모달 열기 const handleOpenTableSectionSettings = (section: FormSectionConfig) => { setSelectedSection(section); setTableSectionSettingsModalOpen(true); }; const updateSection = useCallback( (sectionId: string, updates: Partial) => { onChange({ ...config, sections: config.sections.map((s) => (s.id === sectionId ? { ...s, ...updates } : s)), }); }, [config, onChange], ); const removeSection = useCallback( (sectionId: string) => { onChange({ ...config, sections: config.sections.filter((s) => s.id !== sectionId), }); }, [config, onChange], ); const moveSectionUp = useCallback( (index: number) => { if (index <= 0) return; const newSections = [...config.sections]; [newSections[index - 1], newSections[index]] = [newSections[index], newSections[index - 1]]; onChange({ ...config, sections: newSections }); }, [config, onChange], ); const moveSectionDown = useCallback( (index: number) => { if (index >= config.sections.length - 1) return; const newSections = [...config.sections]; [newSections[index], newSections[index + 1]] = [newSections[index + 1], newSections[index]]; onChange({ ...config, sections: newSections }); }, [config, onChange], ); // 필드 타입별 색상 const getFieldTypeColor = (fieldType: FormFieldConfig["fieldType"]): string => { switch (fieldType) { case "text": case "email": case "password": case "tel": return "text-blue-600 bg-blue-50 border-blue-200"; case "number": return "text-cyan-600 bg-cyan-50 border-cyan-200"; case "date": case "datetime": return "text-purple-600 bg-purple-50 border-purple-200"; case "select": return "text-green-600 bg-green-50 border-green-200"; case "checkbox": return "text-pink-600 bg-pink-50 border-pink-200"; case "textarea": return "text-orange-600 bg-orange-50 border-orange-200"; default: return "text-gray-600 bg-gray-50 border-gray-200"; } }; // 섹션 레이아웃 모달 열기 const handleOpenSectionLayout = (section: FormSectionConfig) => { setSelectedSection(section); setSectionLayoutModalOpen(true); }; // 필드 상세 설정 모달 열기 const handleOpenFieldDetail = (section: FormSectionConfig, field: FormFieldConfig) => { setSelectedSection(section); setSelectedField(field); setFieldDetailModalOpen(true); }; return (
{/* 모달 기본 설정 */}
모달 기본 설정
updateModalConfig({ title: e.target.value })} className="h-9 w-full max-w-full text-sm" /> 모달 상단에 표시될 제목입니다
모달 창의 크기를 선택하세요
{/* 저장 설정 */}
저장 설정

{config.saveConfig.tableName || "(미설정)"}

{config.saveConfig.customApiSave?.enabled && config.saveConfig.customApiSave?.multiTable?.enabled && ( 다중 테이블 모드 )}
데이터를 저장할 테이블과 방식을 설정합니다.
"저장 설정 열기"를 클릭하여 상세 설정을 변경하세요.
{/* 섹션 구성 */}
섹션 구성 {config.sections.length}개
{/* 섹션 추가 버튼들 */}