"use client"; import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { ChevronDown, ChevronUp, Plus, Trash2, RefreshCw, Loader2 } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { generateNumberingCode } from "@/lib/api/numberingRule"; import { useCascadingDropdown } from "@/hooks/useCascadingDropdown"; import { CascadingDropdownConfig } from "@/types/screen-management"; import { UniversalFormModalComponentProps, UniversalFormModalConfig, FormSectionConfig, FormFieldConfig, FormDataState, RepeatSectionItem, SelectOptionConfig, } from "./types"; import { defaultConfig, generateUniqueId } from "./config"; /** * πŸ”— 연쇄 λ“œλ‘­λ‹€μš΄ Select ν•„λ“œ μ»΄ν¬λ„ŒνŠΈ */ interface CascadingSelectFieldProps { fieldId: string; config: CascadingDropdownConfig; parentValue?: string | number | null; value?: string; onChange: (value: string) => void; placeholder?: string; disabled?: boolean; } const CascadingSelectField: React.FC = ({ fieldId, config, parentValue, value, onChange, placeholder, disabled, }) => { const { options, loading } = useCascadingDropdown({ config, parentValue, }); const getPlaceholder = () => { if (!parentValue) { return config.emptyParentMessage || "μƒμœ„ ν•­λͺ©μ„ λ¨Όμ € μ„ νƒν•˜μ„Έμš”"; } if (loading) { return config.loadingMessage || "λ‘œλ”© 쀑..."; } if (options.length === 0) { return config.noOptionsMessage || "선택 κ°€λŠ₯ν•œ ν•­λͺ©μ΄ μ—†μŠ΅λ‹ˆλ‹€"; } return placeholder || "μ„ νƒν•˜μ„Έμš”"; }; const isDisabled = disabled || !parentValue || loading; return ( ); }; /** * λ²”μš© 폼 λͺ¨λ‹¬ μ»΄ν¬λ„ŒνŠΈ * * μ„Ήμ…˜ 기반 폼 λ ˆμ΄μ•„μ›ƒ, μ±„λ²ˆκ·œμΉ™, 닀쀑 ν–‰ μ €μž₯을 μ§€μ›ν•©λ‹ˆλ‹€. */ export function UniversalFormModalComponent({ component, config: propConfig, isDesignMode = false, isSelected = false, className, style, initialData, onSave, onCancel, onChange, }: UniversalFormModalComponentProps) { // μ„€μ • 병합 const config: UniversalFormModalConfig = useMemo(() => { const componentConfig = component?.config || {}; return { ...defaultConfig, ...propConfig, ...componentConfig, modal: { ...defaultConfig.modal, ...propConfig?.modal, ...componentConfig.modal, }, saveConfig: { ...defaultConfig.saveConfig, ...propConfig?.saveConfig, ...componentConfig.saveConfig, multiRowSave: { ...defaultConfig.saveConfig.multiRowSave, ...propConfig?.saveConfig?.multiRowSave, ...componentConfig.saveConfig?.multiRowSave, }, afterSave: { ...defaultConfig.saveConfig.afterSave, ...propConfig?.saveConfig?.afterSave, ...componentConfig.saveConfig?.afterSave, }, }, }; }, [component?.config, propConfig]); // 폼 데이터 μƒνƒœ const [formData, setFormData] = useState({}); const [, setOriginalData] = useState>({}); // 반볡 μ„Ήμ…˜ 데이터 const [repeatSections, setRepeatSections] = useState<{ [sectionId: string]: RepeatSectionItem[]; }>({}); // μ„Ήμ…˜ μ ‘νž˜ μƒνƒœ const [collapsedSections, setCollapsedSections] = useState>(new Set()); // Select μ˜΅μ…˜ μΊμ‹œ const [selectOptionsCache, setSelectOptionsCache] = useState<{ [key: string]: { value: string; label: string }[]; }>({}); // 연동 ν•„λ“œ κ·Έλ£Ή 데이터 μΊμ‹œ (ν…Œμ΄λΈ”λ³„ 데이터) const [linkedFieldDataCache, setLinkedFieldDataCache] = useState<{ [tableKey: string]: Record[]; }>({}); // λ‘œλ”© μƒνƒœ const [saving, setSaving] = useState(false); // μ‚­μ œ 확인 λ‹€μ΄μ–Όλ‘œκ·Έ const [deleteDialog, setDeleteDialog] = useState<{ open: boolean; sectionId: string; itemId: string; }>({ open: false, sectionId: "", itemId: "" }); // μ΄ˆκΈ°ν™” useEffect(() => { initializeForm(); }, [config, initialData]); // ν•„λ“œ 레벨 linkedFieldGroup 데이터 λ‘œλ“œ useEffect(() => { const loadData = async () => { const tablesToLoad = new Set(); // λͺ¨λ“  μ„Ήμ…˜μ˜ ν•„λ“œμ—μ„œ linkedFieldGroup.sourceTable μˆ˜μ§‘ config.sections.forEach((section) => { section.fields.forEach((field) => { if (field.linkedFieldGroup?.enabled && field.linkedFieldGroup?.sourceTable) { tablesToLoad.add(field.linkedFieldGroup.sourceTable); } }); }); // 각 ν…Œμ΄λΈ” 데이터 λ‘œλ“œ for (const tableName of tablesToLoad) { if (!linkedFieldDataCache[tableName]) { console.log(`[UniversalFormModal] linkedFieldGroup 데이터 λ‘œλ“œ: ${tableName}`); await loadLinkedFieldData(tableName); } } }; loadData(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [config.sections]); // 폼 μ΄ˆκΈ°ν™” const initializeForm = useCallback(async () => { const newFormData: FormDataState = {}; const newRepeatSections: { [sectionId: string]: RepeatSectionItem[] } = {}; const newCollapsed = new Set(); // μ„Ήμ…˜λ³„ μ΄ˆκΈ°ν™” for (const section of config.sections) { // μ ‘νž˜ μƒνƒœ μ΄ˆκΈ°ν™” if (section.defaultCollapsed) { newCollapsed.add(section.id); } if (section.repeatable) { // 반볡 μ„Ήμ…˜ μ΄ˆκΈ°ν™” const minItems = section.repeatConfig?.minItems || 0; const items: RepeatSectionItem[] = []; for (let i = 0; i < minItems; i++) { items.push(createRepeatItem(section, i)); } newRepeatSections[section.id] = items; } else { // 일반 μ„Ήμ…˜ ν•„λ“œ μ΄ˆκΈ°ν™” for (const field of section.fields) { // κΈ°λ³Έκ°’ μ„€μ • let value = field.defaultValue ?? ""; // λΆ€λͺ¨μ—μ„œ 전달받은 κ°’ 적용 if (field.receiveFromParent && initialData) { const parentField = field.parentFieldName || field.columnName; if (initialData[parentField] !== undefined) { value = initialData[parentField]; } } newFormData[field.columnName] = value; } } } setFormData(newFormData); setRepeatSections(newRepeatSections); setCollapsedSections(newCollapsed); setOriginalData(initialData || {}); // μ±„λ²ˆκ·œμΉ™ μžλ™ 생성 await generateNumberingValues(newFormData); }, [config, initialData]); // 반볡 μ„Ήμ…˜ μ•„μ΄ν…œ 생성 const createRepeatItem = (section: FormSectionConfig, index: number): RepeatSectionItem => { const item: RepeatSectionItem = { _id: generateUniqueId("repeat"), _index: index, }; for (const field of section.fields) { item[field.columnName] = field.defaultValue ?? ""; } return item; }; // μ±„λ²ˆκ·œμΉ™ μžλ™ 생성 const generateNumberingValues = useCallback( async (currentFormData: FormDataState) => { const updatedData = { ...currentFormData }; let hasChanges = false; for (const section of config.sections) { if (section.repeatable) continue; for (const field of section.fields) { if ( field.numberingRule?.enabled && field.numberingRule?.generateOnOpen && field.numberingRule?.ruleId && !updatedData[field.columnName] ) { try { const response = await generateNumberingCode(field.numberingRule.ruleId); if (response.success && response.data?.generatedCode) { updatedData[field.columnName] = response.data.generatedCode; hasChanges = true; } } catch (error) { console.error(`μ±„λ²ˆκ·œμΉ™ 생성 μ‹€νŒ¨ (${field.columnName}):`, error); } } } } if (hasChanges) { setFormData(updatedData); } }, [config], ); // ν•„λ“œ κ°’ λ³€κ²½ ν•Έλ“€λŸ¬ const handleFieldChange = useCallback( (columnName: string, value: any) => { setFormData((prev) => { const newData = { ...prev, [columnName]: value }; // onChangeλŠ” λ Œλ”λ§ μ™ΈλΆ€μ—μ„œ ν˜ΈμΆœν•΄μ•Ό 함 (setTimeout μ‚¬μš©) if (onChange) { setTimeout(() => onChange(newData), 0); } return newData; }); }, [onChange], ); // 반볡 μ„Ήμ…˜ ν•„λ“œ κ°’ λ³€κ²½ ν•Έλ“€λŸ¬ const handleRepeatFieldChange = useCallback((sectionId: string, itemId: string, columnName: string, value: any) => { setRepeatSections((prev) => { const items = prev[sectionId] || []; const newItems = items.map((item) => (item._id === itemId ? { ...item, [columnName]: value } : item)); return { ...prev, [sectionId]: newItems }; }); }, []); // 반볡 μ„Ήμ…˜ μ•„μ΄ν…œ μΆ”κ°€ const handleAddRepeatItem = useCallback( (sectionId: string) => { const section = config.sections.find((s) => s.id === sectionId); if (!section) return; const maxItems = section.repeatConfig?.maxItems || 10; setRepeatSections((prev) => { const items = prev[sectionId] || []; if (items.length >= maxItems) { toast.error(`μ΅œλŒ€ ${maxItems}κ°œκΉŒμ§€λ§Œ μΆ”κ°€ν•  수 μžˆμŠ΅λ‹ˆλ‹€.`); return prev; } const newItem = createRepeatItem(section, items.length); return { ...prev, [sectionId]: [...items, newItem] }; }); }, [config], ); // 반볡 μ„Ήμ…˜ μ•„μ΄ν…œ μ‚­μ œ const handleRemoveRepeatItem = useCallback( (sectionId: string, itemId: string) => { const section = config.sections.find((s) => s.id === sectionId); if (!section) return; const minItems = section.repeatConfig?.minItems || 0; setRepeatSections((prev) => { const items = prev[sectionId] || []; if (items.length <= minItems) { toast.error(`μ΅œμ†Œ ${minItems}κ°œλŠ” μœ μ§€ν•΄μ•Ό ν•©λ‹ˆλ‹€.`); return prev; } const newItems = items.filter((item) => item._id !== itemId).map((item, index) => ({ ...item, _index: index })); return { ...prev, [sectionId]: newItems }; }); setDeleteDialog({ open: false, sectionId: "", itemId: "" }); }, [config], ); // μ„Ήμ…˜ μ ‘νž˜ ν† κΈ€ const toggleSectionCollapse = useCallback((sectionId: string) => { setCollapsedSections((prev) => { const newSet = new Set(prev); if (newSet.has(sectionId)) { newSet.delete(sectionId); } else { newSet.add(sectionId); } return newSet; }); }, []); // Select μ˜΅μ…˜ λ‘œλ“œ const loadSelectOptions = useCallback( async (fieldId: string, optionConfig: SelectOptionConfig): Promise<{ value: string; label: string }[]> => { // μΊμ‹œ 확인 if (selectOptionsCache[fieldId]) { return selectOptionsCache[fieldId]; } let options: { value: string; label: string }[] = []; try { if (optionConfig.type === "static") { options = optionConfig.staticOptions || []; } else if (optionConfig.type === "table" && optionConfig.tableName) { const response = await apiClient.get(`/table-management/tables/${optionConfig.tableName}/data`, { params: { limit: 1000 }, }); if (response.data?.success && response.data?.data) { options = response.data.data.map((row: any) => ({ value: String(row[optionConfig.valueColumn || "id"]), label: String(row[optionConfig.labelColumn || "name"]), })); } } else if (optionConfig.type === "code" && optionConfig.codeCategory) { const response = await apiClient.get(`/common-code/${optionConfig.codeCategory}`); if (response.data?.success && response.data?.data) { options = response.data.data.map((code: any) => ({ value: code.code_value || code.codeValue, label: code.code_name || code.codeName, })); } } // μΊμ‹œ μ €μž₯ setSelectOptionsCache((prev) => ({ ...prev, [fieldId]: options })); } catch (error) { console.error(`Select μ˜΅μ…˜ λ‘œλ“œ μ‹€νŒ¨ (${fieldId}):`, error); } return options; }, [selectOptionsCache], ); // 연동 ν•„λ“œ κ·Έλ£Ή 데이터 λ‘œλ“œ const loadLinkedFieldData = useCallback( async (sourceTable: string): Promise[]> => { // μΊμ‹œ 확인 - 이미 λ°°μ—΄λ‘œ μΊμ‹œλ˜μ–΄ 있으면 λ°˜ν™˜ if (Array.isArray(linkedFieldDataCache[sourceTable]) && linkedFieldDataCache[sourceTable].length > 0) { return linkedFieldDataCache[sourceTable]; } let data: Record[] = []; try { console.log(`[μ—°λ™ν•„λ“œ] ${sourceTable} 데이터 λ‘œλ“œ μ‹œμž‘`); // ν˜„μž¬ νšŒμ‚¬ κΈ°μ€€μœΌλ‘œ 데이터 쑰회 (POST λ©”μ„œλ“œ, autoFilter μ‚¬μš©) const response = await apiClient.post(`/table-management/tables/${sourceTable}/data`, { page: 1, size: 1000, autoFilter: { enabled: true, filterColumn: "company_code" }, // ν˜„μž¬ νšŒμ‚¬ κΈ°μ€€ μžλ™ 필터링 }); console.log(`[μ—°λ™ν•„λ“œ] ${sourceTable} API 응닡:`, response.data); if (response.data?.success) { // data ꡬ쑰 확인: { data: { data: [...], total, page, ... } } λ˜λŠ” { data: [...] } const responseData = response.data?.data; if (Array.isArray(responseData)) { // 직접 배열인 경우 data = responseData; } else if (responseData?.data && Array.isArray(responseData.data)) { // { data: [...], total: ... } ν˜•νƒœ (tableManagementService 응닡) data = responseData.data; } else if (responseData?.rows && Array.isArray(responseData.rows)) { // { rows: [...], total: ... } ν˜•νƒœ (λ‹€λ₯Έ API 응닡) data = responseData.rows; } console.log(`[μ—°λ™ν•„λ“œ] ${sourceTable} νŒŒμ‹±λœ 데이터 ${data.length}개:`, data.slice(0, 3)); } // μΊμ‹œ μ €μž₯ (빈 배열이라도 μ €μž₯ν•˜μ—¬ 쀑볡 μš”μ²­ λ°©μ§€) setLinkedFieldDataCache((prev) => ({ ...prev, [sourceTable]: data })); } catch (error) { console.error(`연동 ν•„λ“œ 데이터 λ‘œλ“œ μ‹€νŒ¨ (${sourceTable}):`, error); // μ‹€νŒ¨ν•΄λ„ 빈 λ°°μ—΄λ‘œ μΊμ‹œν•˜μ—¬ λ¬΄ν•œ μš”μ²­ λ°©μ§€ setLinkedFieldDataCache((prev) => ({ ...prev, [sourceTable]: [] })); } return data; }, [linkedFieldDataCache], ); // ν•„μˆ˜ ν•„λ“œ 검증 const validateRequiredFields = useCallback((): { valid: boolean; missingFields: string[] } => { const missingFields: string[] = []; for (const section of config.sections) { if (section.repeatable) continue; // 반볡 μ„Ήμ…˜μ€ 별도 검증 for (const field of section.fields) { if (field.required && !field.hidden && !field.numberingRule?.hidden) { const value = formData[field.columnName]; if (value === undefined || value === null || value === "") { missingFields.push(field.label || field.columnName); } } } } return { valid: missingFields.length === 0, missingFields }; }, [config.sections, formData]); // 단일 ν–‰ μ €μž₯ const saveSingleRow = useCallback(async () => { const dataToSave = { ...formData }; // 메타데이터 ν•„λ“œ 제거 Object.keys(dataToSave).forEach((key) => { if (key.startsWith("_")) { delete dataToSave[key]; } }); // μ €μž₯ μ‹œμ  μ±„λ²ˆκ·œμΉ™ 처리 for (const section of config.sections) { for (const field of section.fields) { if ( field.numberingRule?.enabled && field.numberingRule?.generateOnSave && field.numberingRule?.ruleId && !dataToSave[field.columnName] ) { const response = await generateNumberingCode(field.numberingRule.ruleId); if (response.success && response.data?.generatedCode) { dataToSave[field.columnName] = response.data.generatedCode; } } } } const response = await apiClient.post(`/table-management/tables/${config.saveConfig.tableName}/add`, dataToSave); if (!response.data?.success) { throw new Error(response.data?.message || "μ €μž₯ μ‹€νŒ¨"); } }, [config.sections, config.saveConfig.tableName, formData]); // 닀쀑 ν–‰ μ €μž₯ (겸직 λ“±) const saveMultipleRows = useCallback(async () => { const { multiRowSave } = config.saveConfig; if (!multiRowSave) return; let { commonFields = [], repeatSectionId = "" } = multiRowSave; const { typeColumn, mainTypeValue, subTypeValue, mainSectionFields = [] } = multiRowSave; // 곡톡 ν•„λ“œκ°€ μ„€μ •λ˜μ§€ μ•Šμ€ 경우, 기본정보 μ„Ήμ…˜μ˜ λͺ¨λ“  ν•„λ“œλ₯Ό 곡톡 ν•„λ“œλ‘œ μ‚¬μš© if (commonFields.length === 0) { const nonRepeatableSections = config.sections.filter((s) => !s.repeatable); commonFields = nonRepeatableSections.flatMap((s) => s.fields.map((f) => f.columnName)); console.log("[UniversalFormModal] 곡톡 ν•„λ“œ μžλ™ μ„€μ •:", commonFields); } // 반볡 μ„Ήμ…˜ IDκ°€ μ„€μ •λ˜μ§€ μ•Šμ€ 경우, 첫 번째 반볡 μ„Ήμ…˜ μ‚¬μš© if (!repeatSectionId) { const repeatableSection = config.sections.find((s) => s.repeatable); if (repeatableSection) { repeatSectionId = repeatableSection.id; console.log("[UniversalFormModal] 반볡 μ„Ήμ…˜ μžλ™ μ„€μ •:", repeatSectionId); } } // 디버깅: μ„€μ • 확인 console.log("[UniversalFormModal] 닀쀑 ν–‰ μ €μž₯ μ„€μ •:", { commonFields, repeatSectionId, mainSectionFields, typeColumn, mainTypeValue, subTypeValue, repeatSections, formData, }); // 반볡 μ„Ήμ…˜ 데이터 const repeatItems = repeatSections[repeatSectionId] || []; // μ €μž₯ν•  ν–‰λ“€ 생성 const rowsToSave: any[] = []; // 곡톡 데이터 (λͺ¨λ“  행에 적용) const commonData: any = {}; commonFields.forEach((fieldName) => { if (formData[fieldName] !== undefined) { commonData[fieldName] = formData[fieldName]; } }); // 메인 μ„Ήμ…˜ ν•„λ“œ 데이터 (메인 ν–‰μ—λ§Œ μ μš©λ˜λŠ” λΆ€μ„œ/직급 λ“±) const mainSectionData: any = {}; mainSectionFields.forEach((fieldName) => { if (formData[fieldName] !== undefined) { mainSectionData[fieldName] = formData[fieldName]; } }); console.log("[UniversalFormModal] 곡톡 데이터:", commonData); console.log("[UniversalFormModal] 메인 μ„Ήμ…˜ 데이터:", mainSectionData); console.log("[UniversalFormModal] 반볡 ν•­λͺ©:", repeatItems); // 메인 ν–‰ (곡톡 데이터 + 메인 μ„Ήμ…˜ ν•„λ“œ) const mainRow: any = { ...commonData, ...mainSectionData }; if (typeColumn) { mainRow[typeColumn] = mainTypeValue || "main"; } rowsToSave.push(mainRow); // 반볡 μ„Ήμ…˜ ν–‰λ“€ (곡톡 데이터 + 반볡 μ„Ήμ…˜ ν•„λ“œ) for (const item of repeatItems) { const subRow: any = { ...commonData }; // 반볡 μ„Ήμ…˜μ˜ ν•„λ“œ κ°’ μΆ”κ°€ const repeatSection = config.sections.find((s) => s.id === repeatSectionId); repeatSection?.fields.forEach((field) => { if (item[field.columnName] !== undefined) { subRow[field.columnName] = item[field.columnName]; } }); if (typeColumn) { subRow[typeColumn] = subTypeValue || "concurrent"; } rowsToSave.push(subRow); } // μ €μž₯ μ‹œμ  μ±„λ²ˆκ·œμΉ™ 처리 (메인 ν–‰λ§Œ) for (const section of config.sections) { if (section.repeatable) continue; for (const field of section.fields) { if (field.numberingRule?.enabled && field.numberingRule?.generateOnSave && field.numberingRule?.ruleId) { const response = await generateNumberingCode(field.numberingRule.ruleId); if (response.success && response.data?.generatedCode) { // λͺ¨λ“  행에 λ™μΌν•œ μ±„λ²ˆ κ°’ 적용 (곡톡 ν•„λ“œμΈ 경우) if (commonFields.includes(field.columnName)) { rowsToSave.forEach((row) => { row[field.columnName] = response.data?.generatedCode; }); } else { rowsToSave[0][field.columnName] = response.data?.generatedCode; } } } } } // λͺ¨λ“  ν–‰ μ €μž₯ console.log("[UniversalFormModal] μ €μž₯ν•  ν–‰λ“€:", rowsToSave); console.log("[UniversalFormModal] μ €μž₯ ν…Œμ΄λΈ”:", config.saveConfig.tableName); for (let i = 0; i < rowsToSave.length; i++) { const row = rowsToSave[i]; console.log(`[UniversalFormModal] ${i + 1}번째 ν–‰ μ €μž₯ μ‹œλ„:`, row); // 빈 객체 체크 if (Object.keys(row).length === 0) { console.warn(`[UniversalFormModal] ${i + 1}번째 행이 λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€. κ±΄λ„ˆλœλ‹ˆλ‹€.`); continue; } const response = await apiClient.post(`/table-management/tables/${config.saveConfig.tableName}/add`, row); if (!response.data?.success) { throw new Error(response.data?.message || `${i + 1}번째 ν–‰ μ €μž₯ μ‹€νŒ¨`); } } console.log(`[UniversalFormModal] ${rowsToSave.length}개 ν–‰ μ €μž₯ μ™„λ£Œ`); }, [config.sections, config.saveConfig, formData, repeatSections]); // μ»€μŠ€ν…€ API μ €μž₯ (사원+λΆ€μ„œ 톡합 μ €μž₯ λ“±) const saveWithCustomApi = useCallback(async () => { const { customApiSave } = config.saveConfig; if (!customApiSave) return; console.log("[UniversalFormModal] μ»€μŠ€ν…€ API μ €μž₯ μ‹œμž‘:", customApiSave.apiType); const saveUserWithDeptApi = async () => { const { mainDeptFields, subDeptSectionId, subDeptFields } = customApiSave; // 1. userInfo 데이터 ꡬ성 const userInfo: Record = {}; // λͺ¨λ“  ν•„λ“œμ—μ„œ user_info에 ν•΄λ‹Ήν•˜λŠ” 데이터 μΆ”μΆœ config.sections.forEach((section) => { if (section.repeatable) return; // 반볡 μ„Ήμ…˜μ€ μ œμ™Έ section.fields.forEach((field) => { const value = formData[field.columnName]; if (value !== undefined && value !== null && value !== "") { userInfo[field.columnName] = value; } }); }); // 2. mainDept 데이터 ꡬ성 let mainDept: { dept_code: string; dept_name?: string; position_name?: string } | undefined; if (mainDeptFields) { const deptCode = formData[mainDeptFields.deptCodeField || "dept_code"]; if (deptCode) { mainDept = { dept_code: deptCode, dept_name: formData[mainDeptFields.deptNameField || "dept_name"], position_name: formData[mainDeptFields.positionNameField || "position_name"], }; } } // 3. subDepts 데이터 ꡬ성 (반볡 μ„Ήμ…˜μ—μ„œ) const subDepts: Array<{ dept_code: string; dept_name?: string; position_name?: string }> = []; if (subDeptSectionId && repeatSections[subDeptSectionId]) { const subDeptItems = repeatSections[subDeptSectionId]; const deptCodeField = subDeptFields?.deptCodeField || "dept_code"; const deptNameField = subDeptFields?.deptNameField || "dept_name"; const positionNameField = subDeptFields?.positionNameField || "position_name"; subDeptItems.forEach((item) => { const deptCode = item[deptCodeField]; if (deptCode) { subDepts.push({ dept_code: deptCode, dept_name: item[deptNameField], position_name: item[positionNameField], }); } }); } // 4. API 호좜 console.log("[UniversalFormModal] 사원+λΆ€μ„œ μ €μž₯ 데이터:", { userInfo, mainDept, subDepts }); const { saveUserWithDept } = await import("@/lib/api/user"); const response = await saveUserWithDept({ userInfo: userInfo as any, mainDept, subDepts, isUpdate: !!initialData?.user_id, // 초기 데이터가 있으면 μˆ˜μ • λͺ¨λ“œ }); if (!response.success) { throw new Error(response.message || "사원 μ €μž₯ μ‹€νŒ¨"); } console.log("[UniversalFormModal] 사원+λΆ€μ„œ μ €μž₯ μ™„λ£Œ:", response.data); }; const saveWithGenericCustomApi = async () => { if (!customApiSave.customEndpoint) { throw new Error("μ»€μŠ€ν…€ API μ—”λ“œν¬μΈνŠΈκ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."); } const dataToSave = { ...formData }; // 메타데이터 ν•„λ“œ 제거 Object.keys(dataToSave).forEach((key) => { if (key.startsWith("_")) { delete dataToSave[key]; } }); // 반볡 μ„Ήμ…˜ 데이터 포함 if (Object.keys(repeatSections).length > 0) { dataToSave._repeatSections = repeatSections; } const method = customApiSave.customMethod || "POST"; const response = method === "PUT" ? await apiClient.put(customApiSave.customEndpoint, dataToSave) : await apiClient.post(customApiSave.customEndpoint, dataToSave); if (!response.data?.success) { throw new Error(response.data?.message || "μ €μž₯ μ‹€νŒ¨"); } }; switch (customApiSave.apiType) { case "user-with-dept": await saveUserWithDeptApi(); break; case "custom": await saveWithGenericCustomApi(); break; default: throw new Error(`μ§€μ›ν•˜μ§€ μ•ŠλŠ” API νƒ€μž…: ${customApiSave.apiType}`); } }, [config.sections, config.saveConfig, formData, repeatSections, initialData]); // μ €μž₯ 처리 const handleSave = useCallback(async () => { // μ»€μŠ€ν…€ API μ €μž₯ λͺ¨λ“œκ°€ μ•„λ‹Œ κ²½μš°μ—λ§Œ ν…Œμ΄λΈ”λͺ… 체크 if (!config.saveConfig.customApiSave?.enabled && !config.saveConfig.tableName) { toast.error("μ €μž₯ν•  ν…Œμ΄λΈ”μ΄ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."); return; } // ν•„μˆ˜ ν•„λ“œ 검증 const { valid, missingFields } = validateRequiredFields(); if (!valid) { toast.error(`ν•„μˆ˜ ν•­λͺ©μ„ μž…λ ₯ν•΄μ£Όμ„Έμš”: ${missingFields.join(", ")}`); return; } setSaving(true); try { const { multiRowSave, customApiSave } = config.saveConfig; // μ»€μŠ€ν…€ API μ €μž₯ λͺ¨λ“œ if (customApiSave?.enabled) { await saveWithCustomApi(); } else if (multiRowSave?.enabled) { // 닀쀑 ν–‰ μ €μž₯ await saveMultipleRows(); } else { // 단일 ν–‰ μ €μž₯ await saveSingleRow(); } // μ €μž₯ ν›„ λ™μž‘ if (config.saveConfig.afterSave?.showToast) { toast.success("μ €μž₯λ˜μ—ˆμŠ΅λ‹ˆλ‹€."); } if (config.saveConfig.afterSave?.refreshParent) { window.dispatchEvent(new CustomEvent("refreshParentData")); } // onSave μ½œλ°±μ€ μ €μž₯ μ™„λ£Œ μ•Œλ¦Όμš©μœΌλ‘œλ§Œ μ‚¬μš© // μ‹€μ œ μ €μž₯은 이미 μœ„μ—μ„œ μ™„λ£Œλ¨ (saveSingleRow λ˜λŠ” saveMultipleRows) // EditModal λ“± λΆ€λͺ¨ μ»΄ν¬λ„ŒνŠΈμ˜ μ €μž₯ 둜직이 λ‹€μ‹œ μ‹€ν–‰λ˜μ§€ μ•Šλ„λ‘ // _saveCompleted ν”Œλž˜κ·Έλ₯Ό ν¬ν•¨ν•˜μ—¬ 전달 if (onSave) { onSave({ ...formData, _saveCompleted: true }); } } catch (error: any) { console.error("μ €μž₯ μ‹€νŒ¨:", error); // axios μ—λŸ¬μ˜ 경우 μ„œλ²„ 응닡 λ©”μ‹œμ§€ μΆ”μΆœ const errorMessage = error.response?.data?.message || error.response?.data?.error?.details || error.message || "μ €μž₯에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."; toast.error(errorMessage); } finally { setSaving(false); } }, [config, formData, repeatSections, onSave, validateRequiredFields, saveSingleRow, saveMultipleRows, saveWithCustomApi]); // 폼 μ΄ˆκΈ°ν™” const handleReset = useCallback(() => { initializeForm(); toast.info("폼이 μ΄ˆκΈ°ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€."); }, [initializeForm]); // ν•„λ“œ μš”μ†Œ λ Œλ”λ§ (μž…λ ₯ μ»΄ν¬λ„ŒνŠΈλ§Œ) const renderFieldElement = ( field: FormFieldConfig, value: any, onChangeHandler: (value: any) => void, fieldKey: string, isDisabled: boolean, ) => { return (() => { switch (field.fieldType) { case "textarea": return (