"use client"; import React, { useState, useEffect, useCallback, useMemo, useRef } 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, ChevronRight, Plus, Trash2, RefreshCw, Loader2 } from "lucide-react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; import { generateNumberingCode, allocateNumberingCode, previewNumberingCode } from "@/lib/api/numberingRule"; import { useCascadingDropdown } from "@/hooks/useCascadingDropdown"; import { CascadingDropdownConfig } from "@/types/screen-management"; import { UniversalFormModalComponentProps, UniversalFormModalConfig, FormSectionConfig, FormFieldConfig, FormDataState, RepeatSectionItem, SelectOptionConfig, OptionalFieldGroupConfig, } from "./types"; import { defaultConfig, generateUniqueId } from "./config"; import { TableSectionRenderer } from "./TableSectionRenderer"; /** * ๐Ÿ”— ์—ฐ์‡„ ๋“œ๋กญ๋‹ค์šด 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: propInitialData, // DynamicComponentRenderer์—์„œ ์ „๋‹ฌ๋˜๋Š” props (DOM ์ „๋‹ฌ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด _ prefix ์‚ฌ์šฉ) _initialData, _originalData, _groupedData, onSave, onCancel, onChange, ...restProps // ๋‚˜๋จธ์ง€ props๋Š” DOM์— ์ „๋‹ฌํ•˜์ง€ ์•Š์Œ }: UniversalFormModalComponentProps & { _initialData?: any; _originalData?: any; _groupedData?: any }) { // initialData ์šฐ์„ ์ˆœ์œ„: ์ง์ ‘ ์ „๋‹ฌ๋œ prop > DynamicComponentRenderer์—์„œ ์ „๋‹ฌ๋œ prop const initialData = propInitialData || _initialData; // ์„ค์ • ๋ณ‘ํ•ฉ 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()); // ์˜ต์…”๋„ ํ•„๋“œ ๊ทธ๋ฃน ํ™œ์„ฑํ™” ์ƒํƒœ (์„น์…˜ID-๊ทธ๋ฃนID ์กฐํ•ฉ) const [activatedOptionalFieldGroups, setActivatedOptionalFieldGroups] = 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); // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ: ์›๋ณธ ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ (INSERT/UPDATE/DELETE ์ถ”์ ์šฉ) const [originalGroupedData, setOriginalGroupedData] = useState([]); const groupedDataInitializedRef = useRef(false); // ์‚ญ์ œ ํ™•์ธ ๋‹ค์ด์–ผ๋กœ๊ทธ const [deleteDialog, setDeleteDialog] = useState<{ open: boolean; sectionId: string; itemId: string; }>({ open: false, sectionId: "", itemId: "" }); // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฒˆ๋งŒ ์บก์ฒ˜ (์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ) const capturedInitialData = useRef | undefined>(undefined); const hasInitialized = useRef(false); // ๋งˆ์ง€๋ง‰์œผ๋กœ ์ดˆ๊ธฐํ™”๋œ ๋ฐ์ดํ„ฐ์˜ ID๋ฅผ ์ถ”์  (์ˆ˜์ • ๋ชจ๋‹ฌ์—์„œ ๋‹ค๋ฅธ ํ•ญ๋ชฉ ์„ ํƒ ์‹œ ์žฌ์ดˆ๊ธฐํ™” ํ•„์š”) const lastInitializedId = useRef(undefined); // ์ดˆ๊ธฐํ™” - ์ตœ์ดˆ ๋งˆ์šดํŠธ ์‹œ ๋˜๋Š” initialData๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ ์‹คํ–‰ useEffect(() => { // initialData์—์„œ ID ๊ฐ’ ์ถ”์ถœ (id, ID, objid ๋“ฑ) const currentId = initialData?.id || initialData?.ID || initialData?.objid; const currentIdString = currentId !== undefined ? String(currentId) : undefined; // ์ƒ์„ฑ ๋ชจ๋“œ์—์„œ ๋ถ€๋ชจ๋กœ๋ถ€ํ„ฐ ์ „๋‹ฌ๋ฐ›์€ ๋ฐ์ดํ„ฐ ํ•ด์‹œ (ID๊ฐ€ ์—†์„ ๋•Œ๋งŒ) const createModeDataHash = !currentIdString && initialData && Object.keys(initialData).length > 0 ? JSON.stringify(initialData) : undefined; // ์ด๋ฏธ ์ดˆ๊ธฐํ™”๋˜์—ˆ๊ณ , ID๊ฐ€ ๋™์ผํ•˜๊ณ , ์ƒ์„ฑ ๋ชจ๋“œ ๋ฐ์ดํ„ฐ๋„ ๋™์ผํ•˜๋ฉด ์Šคํ‚ต if (hasInitialized.current && lastInitializedId.current === currentIdString) { // ์ƒ์„ฑ ๋ชจ๋“œ์—์„œ ๋ฐ์ดํ„ฐ๊ฐ€ ์ƒˆ๋กœ ์ „๋‹ฌ๋œ ๊ฒฝ์šฐ๋Š” ์žฌ์ดˆ๊ธฐํ™” ํ•„์š” if (!createModeDataHash || capturedInitialData.current) { return; } } // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ: initialData์— ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด์„œ ID๊ฐ€ ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ ์žฌ์ดˆ๊ธฐํ™” if (hasInitialized.current && currentIdString && lastInitializedId.current !== currentIdString) { console.log("[UniversalFormModal] ID ๋ณ€๊ฒฝ ๊ฐ์ง€ - ์žฌ์ดˆ๊ธฐํ™”:", { prevId: lastInitializedId.current, newId: currentIdString, initialData: initialData, }); // ์ฑ„๋ฒˆ ํ”Œ๋ž˜๊ทธ ์ดˆ๊ธฐํ™” (์ƒˆ ํ•ญ๋ชฉ์ด๋ฏ€๋กœ) numberingGeneratedRef.current = false; isGeneratingRef.current = false; } // ์ตœ์ดˆ initialData ์บก์ฒ˜ (์ดํ›„ ๋ณ€๊ฒฝ๋˜์–ด๋„ ์ด ๊ฐ’ ์‚ฌ์šฉ) if (initialData && Object.keys(initialData).length > 0) { capturedInitialData.current = JSON.parse(JSON.stringify(initialData)); // ๊นŠ์€ ๋ณต์‚ฌ lastInitializedId.current = currentIdString; console.log("[UniversalFormModal] ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ์บก์ฒ˜:", capturedInitialData.current); } hasInitialized.current = true; initializeForm(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialData]); // initialData ์ „์ฒด ๋ณ€๊ฒฝ ์‹œ ์žฌ์ดˆ๊ธฐํ™” // config ๋ณ€๊ฒฝ ์‹œ์—๋งŒ ์žฌ์ดˆ๊ธฐํ™” (initialData ๋ณ€๊ฒฝ์€ ๋ฌด์‹œ) - ์ฑ„๋ฒˆ๊ทœ์น™ ์ œ์™ธ useEffect(() => { if (!hasInitialized.current) return; // ์ตœ์ดˆ ์ดˆ๊ธฐํ™” ์ „์ด๋ฉด ์Šคํ‚ต console.log("[useEffect config ๋ณ€๊ฒฝ] ์žฌ์ดˆ๊ธฐํ™” ์Šคํ‚ต (์ฑ„๋ฒˆ ์ค‘๋ณต ๋ฐฉ์ง€)"); // initializeForm(); // ์ฃผ์„ ์ฒ˜๋ฆฌ - config ๋ณ€๊ฒฝ ์‹œ ์žฌ์ดˆ๊ธฐํ™” ์•ˆ ํ•จ (์ฑ„๋ฒˆ ์ค‘๋ณต ๋ฐฉ์ง€) // eslint-disable-next-line react-hooks/exhaustive-deps }, [config]); // ์ปดํฌ๋„ŒํŠธ unmount ์‹œ ์ฑ„๋ฒˆ ํ”Œ๋ž˜๊ทธ ์ดˆ๊ธฐํ™” useEffect(() => { return () => { console.log("[์ฑ„๋ฒˆ] ์ปดํฌ๋„ŒํŠธ unmount - ํ”Œ๋ž˜๊ทธ ์ดˆ๊ธฐํ™”"); numberingGeneratedRef.current = false; isGeneratingRef.current = false; }; }, []); // ๐Ÿ†• beforeFormSave ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ - ButtonPrimary ์ €์žฅ ์‹œ formData๋ฅผ ์ „๋‹ฌ // ์„ค์ •๋œ ํ•„๋“œ(columnName)๋งŒ ๋ณ‘ํ•ฉํ•˜์—ฌ ์˜๋„์น˜ ์•Š์€ ๋ฎ์–ด์“ฐ๊ธฐ ๋ฐฉ์ง€ useEffect(() => { const handleBeforeFormSave = (event: Event) => { if (!(event instanceof CustomEvent) || !event.detail?.formData) return; // ์„ค์ •์— ์ •์˜๋œ ํ•„๋“œ columnName ๋ชฉ๋ก ์ˆ˜์ง‘ const configuredFields = new Set(); config.sections.forEach((section) => { (section.fields || []).forEach((field) => { if (field.columnName) { configuredFields.add(field.columnName); } }); }); console.log("[UniversalFormModal] beforeFormSave ์ด๋ฒคํŠธ ์ˆ˜์‹ "); console.log("[UniversalFormModal] ์„ค์ •๋œ ํ•„๋“œ ๋ชฉ๋ก:", Array.from(configuredFields)); // UniversalFormModal์— ์„ค์ •๋œ ํ•„๋“œ๋งŒ ๋ณ‘ํ•ฉ (์ฑ„๋ฒˆ ๊ทœ์น™ ํฌํ•จ) // ์™ธ๋ถ€ formData์— ์ด๋ฏธ ๊ฐ’์ด ์žˆ์–ด๋„ UniversalFormModal ๊ฐ’์œผ๋กœ ๋ฎ์–ด์”€ // (UniversalFormModal์ด ํ•ด๋‹น ํ•„๋“œ์˜ ์ฃผ์ธ์ด๋ฏ€๋กœ) for (const [key, value] of Object.entries(formData)) { // ์„ค์ •์— ์ •์˜๋œ ํ•„๋“œ ๋˜๋Š” ์ฑ„๋ฒˆ ๊ทœ์น™ ID ํ•„๋“œ๋งŒ ๋ณ‘ํ•ฉ const isConfiguredField = configuredFields.has(key); const isNumberingRuleId = key.endsWith("_numberingRuleId"); if (isConfiguredField || isNumberingRuleId) { if (value !== undefined && value !== null && value !== "") { event.detail.formData[key] = value; console.log(`[UniversalFormModal] ํ•„๋“œ ๋ณ‘ํ•ฉ: ${key} =`, value); } } } // ๋ฐ˜๋ณต ์„น์…˜ ๋ฐ์ดํ„ฐ๋„ ๋ณ‘ํ•ฉ (ํ•„์š”ํ•œ ๊ฒฝ์šฐ) if (Object.keys(repeatSections).length > 0) { for (const [sectionId, items] of Object.entries(repeatSections)) { const sectionKey = `_repeatSection_${sectionId}`; event.detail.formData[sectionKey] = items; console.log(`[UniversalFormModal] ๋ฐ˜๋ณต ์„น์…˜ ๋ณ‘ํ•ฉ: ${sectionKey}`, items); } } // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ: ์›๋ณธ ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์ „๋‹ฌ (UPDATE/DELETE ์ถ”์ ์šฉ) if (originalGroupedData.length > 0) { event.detail.formData._originalGroupedData = originalGroupedData; console.log(`[UniversalFormModal] ์›๋ณธ ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ๋ณ‘ํ•ฉ: ${originalGroupedData.length}๊ฐœ`); } }; window.addEventListener("beforeFormSave", handleBeforeFormSave as EventListener); return () => { window.removeEventListener("beforeFormSave", handleBeforeFormSave as EventListener); }; }, [formData, repeatSections, config.sections, originalGroupedData]); // ๐Ÿ†• ์ˆ˜์ • ๋ชจ๋“œ: _groupedData๊ฐ€ ์žˆ์œผ๋ฉด ํ…Œ์ด๋ธ” ์„น์…˜ ์ดˆ๊ธฐํ™” useEffect(() => { if (!_groupedData || _groupedData.length === 0) return; if (groupedDataInitializedRef.current) return; // ์ด๋ฏธ ์ดˆ๊ธฐํ™”๋จ // ํ…Œ์ด๋ธ” ํƒ€์ž… ์„น์…˜ ์ฐพ๊ธฐ const tableSection = config.sections.find((s) => s.type === "table"); if (!tableSection) { console.log("[UniversalFormModal] ํ…Œ์ด๋ธ” ์„น์…˜ ์—†์Œ - _groupedData ๋ฌด์‹œ"); return; } console.log("[UniversalFormModal] ์ˆ˜์ • ๋ชจ๋“œ - ํ…Œ์ด๋ธ” ์„น์…˜ ์ดˆ๊ธฐํ™”:", { sectionId: tableSection.id, itemCount: _groupedData.length, }); // ์›๋ณธ ๋ฐ์ดํ„ฐ ์ €์žฅ (์ˆ˜์ •/์‚ญ์ œ ์ถ”์ ์šฉ) setOriginalGroupedData(JSON.parse(JSON.stringify(_groupedData))); // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ์„ค์ • const tableSectionKey = `_tableSection_${tableSection.id}`; setFormData((prev) => ({ ...prev, [tableSectionKey]: _groupedData, })); groupedDataInitializedRef.current = true; }, [_groupedData, config.sections]); // ํ•„๋“œ ๋ ˆ๋ฒจ 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]) { await loadLinkedFieldData(tableName); } } }; loadData(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [config.sections]); // ํผ ์ดˆ๊ธฐํ™” const initializeForm = useCallback(async () => { console.log("[initializeForm] ์‹œ์ž‘"); // ์บก์ฒ˜๋œ initialData ์‚ฌ์šฉ (props๋กœ ์ „๋‹ฌ๋œ initialData๊ฐ€ ์•„๋‹Œ) const effectiveInitialData = capturedInitialData.current || initialData; console.log("[initializeForm] ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ:", { capturedInitialData: capturedInitialData.current, initialData: initialData, effectiveInitialData: effectiveInitialData, hasData: effectiveInitialData && Object.keys(effectiveInitialData).length > 0, }); const newFormData: FormDataState = {}; const newRepeatSections: { [sectionId: string]: RepeatSectionItem[] } = {}; const newCollapsed = new Set(); const newActivatedGroups = 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 if (section.type === "table") { // ํ…Œ์ด๋ธ” ์„น์…˜์€ ํ•„๋“œ ์ดˆ๊ธฐํ™” ์Šคํ‚ต (TableSectionRenderer์—์„œ ์ฒ˜๋ฆฌ) continue; } else { // ์ผ๋ฐ˜ ์„น์…˜ ํ•„๋“œ ์ดˆ๊ธฐํ™” for (const field of (section.fields || [])) { // ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • let value = field.defaultValue ?? ""; // ๋ถ€๋ชจ์—์„œ ์ „๋‹ฌ๋ฐ›์€ ๊ฐ’ ์ ์šฉ (receiveFromParent ๋˜๋Š” effectiveInitialData์— ํ•ด๋‹น ๊ฐ’์ด ์žˆ์œผ๋ฉด) if (effectiveInitialData) { const parentField = field.parentFieldName || field.columnName; if (effectiveInitialData[parentField] !== undefined) { // receiveFromParent๊ฐ€ true์ด๊ฑฐ๋‚˜, effectiveInitialData์— ๊ฐ’์ด ์žˆ์œผ๋ฉด ์ ์šฉ if (field.receiveFromParent || value === "" || value === undefined) { value = effectiveInitialData[parentField]; } } } newFormData[field.columnName] = value; } // ์˜ต์…”๋„ ํ•„๋“œ ๊ทธ๋ฃน ์ฒ˜๋ฆฌ if (section.optionalFieldGroups) { for (const group of section.optionalFieldGroups) { const key = `${section.id}-${group.id}`; // ์ˆ˜์ • ๋ชจ๋“œ: triggerField ๊ฐ’์ด triggerValueOnAdd์™€ ์ผ์น˜ํ•˜๋ฉด ๊ทธ๋ฃน ์ž๋™ ํ™œ์„ฑํ™” if (effectiveInitialData && group.triggerField && group.triggerValueOnAdd !== undefined) { const triggerValue = effectiveInitialData[group.triggerField]; if (triggerValue === group.triggerValueOnAdd) { newActivatedGroups.add(key); console.log(`[initializeForm] ์˜ต์…”๋„ ๊ทธ๋ฃน ์ž๋™ ํ™œ์„ฑํ™”: ${key}, triggerField=${group.triggerField}, value=${triggerValue}`); // ํ™œ์„ฑํ™”๋œ ๊ทธ๋ฃน์˜ ํ•„๋“œ๊ฐ’๋„ ์ดˆ๊ธฐํ™” for (const field of group.fields || []) { let value = field.defaultValue ?? ""; const parentField = field.parentFieldName || field.columnName; if (effectiveInitialData[parentField] !== undefined) { value = effectiveInitialData[parentField]; } newFormData[field.columnName] = value; } } } // ์‹ ๊ทœ ๋“ฑ๋ก ๋ชจ๋“œ: triggerValueOnRemove๋ฅผ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์„ค์ • if (group.triggerField && group.triggerValueOnRemove !== undefined) { // effectiveInitialData์— ํ•ด๋‹น ๊ฐ’์ด ์—†๋Š” ๊ฒฝ์šฐ์—๋งŒ ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • if (!effectiveInitialData || effectiveInitialData[group.triggerField] === undefined) { newFormData[group.triggerField] = group.triggerValueOnRemove; } } } } } } setFormData(newFormData); setRepeatSections(newRepeatSections); setCollapsedSections(newCollapsed); setActivatedOptionalFieldGroups(newActivatedGroups); setOriginalData(effectiveInitialData || {}); // ์ˆ˜์ • ๋ชจ๋“œ์—์„œ ์„œ๋ธŒ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋กœ๋“œ (๊ฒธ์ง ๋“ฑ) const multiTable = config.saveConfig?.customApiSave?.multiTable; if (multiTable && effectiveInitialData) { const pkColumn = multiTable.mainTable?.primaryKeyColumn; const pkValue = effectiveInitialData[pkColumn]; // PK ๊ฐ’์ด ์žˆ์œผ๋ฉด ์ˆ˜์ • ๋ชจ๋“œ๋กœ ํŒ๋‹จ if (pkValue) { console.log("[initializeForm] ์ˆ˜์ • ๋ชจ๋“œ - ์„œ๋ธŒ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹œ์ž‘"); for (const subTableConfig of multiTable.subTables || []) { // loadOnEdit ์˜ต์…˜์ด ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ์—๋งŒ ๋กœ๋“œ if (!subTableConfig.enabled || !subTableConfig.options?.loadOnEdit) { continue; } const { tableName, linkColumn, repeatSectionId, fieldMappings, options } = subTableConfig; if (!tableName || !linkColumn?.subColumn || !repeatSectionId) { continue; } try { // ์„œ๋ธŒ ํ…Œ์ด๋ธ”์—์„œ ๋ฐ์ดํ„ฐ ์กฐํšŒ const filters: Record = { [linkColumn.subColumn]: pkValue, }; // ์„œ๋ธŒ ํ•ญ๋ชฉ๋งŒ ๋กœ๋“œ (๋ฉ”์ธ ํ•ญ๋ชฉ ์ œ์™ธ) if (options?.loadOnlySubItems && options?.mainMarkerColumn) { filters[options.mainMarkerColumn] = options.subMarkerValue ?? false; } console.log(`[initializeForm] ์„œ๋ธŒ ํ…Œ์ด๋ธ” ${tableName} ์กฐํšŒ:`, filters); const response = await apiClient.get(`/table-management/tables/${tableName}/data`, { params: { filters: JSON.stringify(filters), page: 1, pageSize: 100, }, }); if (response.data?.success && response.data?.data?.items) { const subItems = response.data.data.items; console.log(`[initializeForm] ์„œ๋ธŒ ํ…Œ์ด๋ธ” ${tableName} ๋ฐ์ดํ„ฐ ${subItems.length}๊ฑด ๋กœ๋“œ๋จ`); // ์—ญ๋งคํ•‘: ์„œ๋ธŒ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ โ†’ ๋ฐ˜๋ณต ์„น์…˜ ๋ฐ์ดํ„ฐ const repeatItems: RepeatSectionItem[] = subItems.map((item: any, index: number) => { const repeatItem: RepeatSectionItem = { _id: generateUniqueId("repeat"), _index: index, _originalData: item, // ์›๋ณธ ๋ฐ์ดํ„ฐ ๋ณด๊ด€ (์ˆ˜์ • ์‹œ ํ•„์š”) }; // ํ•„๋“œ ๋งคํ•‘ ์—ญ๋ณ€ํ™˜ (targetColumn โ†’ formField) for (const mapping of fieldMappings || []) { if (mapping.formField && mapping.targetColumn) { repeatItem[mapping.formField] = item[mapping.targetColumn]; } } return repeatItem; }); // ๋ฐ˜๋ณต ์„น์…˜์— ๋ฐ์ดํ„ฐ ์„ค์ • newRepeatSections[repeatSectionId] = repeatItems; setRepeatSections({ ...newRepeatSections }); console.log(`[initializeForm] ๋ฐ˜๋ณต ์„น์…˜ ${repeatSectionId}์— ${repeatItems.length}๊ฑด ์„ค์ •`); } } catch (error) { console.error(`[initializeForm] ์„œ๋ธŒ ํ…Œ์ด๋ธ” ${tableName} ๋กœ๋“œ ์‹คํŒจ:`, error); } } } } // ์ฑ„๋ฒˆ๊ทœ์น™ ์ž๋™ ์ƒ์„ฑ console.log("[initializeForm] generateNumberingValues ํ˜ธ์ถœ"); await generateNumberingValues(newFormData); console.log("[initializeForm] ์™„๋ฃŒ"); // eslint-disable-next-line react-hooks/exhaustive-deps }, [config]); // initialData๋Š” ์˜์กด์„ฑ์—์„œ ์ œ๊ฑฐ (capturedInitialData.current ์‚ฌ์šฉ) // ๋ฐ˜๋ณต ์„น์…˜ ์•„์ดํ…œ ์ƒ์„ฑ 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 numberingGeneratedRef = useRef(false); const isGeneratingRef = useRef(false); // ์ง„ํ–‰ ์ค‘ ํ”Œ๋ž˜๊ทธ ์ถ”๊ฐ€ const generateNumberingValues = useCallback( async (currentFormData: FormDataState) => { // ์ด๋ฏธ ์ƒ์„ฑ๋˜์—ˆ๊ฑฐ๋‚˜ ์ง„ํ–‰ ์ค‘์ด๋ฉด ์Šคํ‚ต if (numberingGeneratedRef.current) { console.log("[์ฑ„๋ฒˆ] ์ด๋ฏธ ์ƒ์„ฑ๋จ - ์Šคํ‚ต"); return; } if (isGeneratingRef.current) { console.log("[์ฑ„๋ฒˆ] ์ƒ์„ฑ ์ง„ํ–‰ ์ค‘ - ์Šคํ‚ต"); return; } isGeneratingRef.current = true; // ์ง„ํ–‰ ์ค‘ ํ‘œ์‹œ console.log("[์ฑ„๋ฒˆ] ์ƒ์„ฑ ์‹œ์ž‘"); const updatedData = { ...currentFormData }; let hasChanges = false; for (const section of config.sections) { if (section.repeatable || section.type === "table") continue; for (const field of (section.fields || [])) { if ( field.numberingRule?.enabled && field.numberingRule?.generateOnOpen && field.numberingRule?.ruleId && !updatedData[field.columnName] ) { try { console.log(`[์ฑ„๋ฒˆ ๋ฏธ๋ฆฌ๋ณด๊ธฐ API ํ˜ธ์ถœ] ${field.columnName}, ruleId: ${field.numberingRule.ruleId}`); // generateOnOpen: ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋งŒ ํ‘œ์‹œ (DB ์‹œํ€€์Šค ์ฆ๊ฐ€ ์•ˆ ํ•จ) const response = await previewNumberingCode(field.numberingRule.ruleId); if (response.success && response.data?.generatedCode) { updatedData[field.columnName] = response.data.generatedCode; // ์ €์žฅ ์‹œ ์‹ค์ œ ํ• ๋‹น์„ ์œ„ํ•ด ruleId ์ €์žฅ (TextInput๊ณผ ๋™์ผํ•œ ํ‚ค ํ˜•์‹) const ruleIdKey = `${field.columnName}_numberingRuleId`; updatedData[ruleIdKey] = field.numberingRule.ruleId; hasChanges = true; numberingGeneratedRef.current = true; // ์ƒ์„ฑ ์™„๋ฃŒ ํ‘œ์‹œ console.log( `[์ฑ„๋ฒˆ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์™„๋ฃŒ] ${field.columnName} = ${response.data.generatedCode} (์ €์žฅ ์‹œ ์‹ค์ œ ํ• ๋‹น)`, ); console.log(`[์ฑ„๋ฒˆ ๊ทœ์น™ ID ์ €์žฅ] ${ruleIdKey} = ${field.numberingRule.ruleId}`); // ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์—๋„ ruleId ์ „๋‹ฌ (ModalRepeaterTable โ†’ ScreenModal) if (onChange) { onChange({ ...updatedData, [ruleIdKey]: field.numberingRule.ruleId, }); console.log(`[์ฑ„๋ฒˆ] ๋ถ€๋ชจ์—๊ฒŒ ruleId ์ „๋‹ฌ: ${ruleIdKey}`); } } } catch (error) { console.error(`์ฑ„๋ฒˆ๊ทœ์น™ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์‹คํŒจ (${field.columnName}):`, error); } } } } isGeneratingRef.current = false; // ์ง„ํ–‰ ์™„๋ฃŒ if (hasChanges) { setFormData(updatedData); } }, [config, onChange], ); // ํ•„๋“œ ๊ฐ’ ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ 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; }); }, []); // ์˜ต์…”๋„ ํ•„๋“œ ๊ทธ๋ฃน ํ™œ์„ฑํ™” const activateOptionalFieldGroup = useCallback((sectionId: string, groupId: string) => { const section = config.sections.find((s) => s.id === sectionId); const group = section?.optionalFieldGroups?.find((g) => g.id === groupId); if (!group) return; const key = `${sectionId}-${groupId}`; setActivatedOptionalFieldGroups((prev) => { const newSet = new Set(prev); newSet.add(key); return newSet; }); // ์—ฐ๋™ ํ•„๋“œ ๊ฐ’ ๋ณ€๊ฒฝ (์ถ”๊ฐ€ ์‹œ) if (group.triggerField && group.triggerValueOnAdd !== undefined) { handleFieldChange(group.triggerField, group.triggerValueOnAdd); } }, [config, handleFieldChange]); // ์˜ต์…”๋„ ํ•„๋“œ ๊ทธ๋ฃน ๋น„ํ™œ์„ฑํ™” const deactivateOptionalFieldGroup = useCallback((sectionId: string, groupId: string) => { const section = config.sections.find((s) => s.id === sectionId); const group = section?.optionalFieldGroups?.find((g) => g.id === groupId); if (!group) return; const key = `${sectionId}-${groupId}`; setActivatedOptionalFieldGroups((prev) => { const newSet = new Set(prev); newSet.delete(key); return newSet; }); // ์—ฐ๋™ ํ•„๋“œ ๊ฐ’ ๋ณ€๊ฒฝ (์ œ๊ฑฐ ์‹œ) if (group.triggerField && group.triggerValueOnRemove !== undefined) { handleFieldChange(group.triggerField, group.triggerValueOnRemove); } // ์˜ต์…”๋„ ํ•„๋“œ ๊ทธ๋ฃน ํ•„๋“œ ๊ฐ’ ์ดˆ๊ธฐํ™” (group.fields || []).forEach((field) => { handleFieldChange(field.columnName, field.defaultValue || ""); }); }, [config, handleFieldChange]); // 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) { // ํ…Œ์ด๋ธ” ์ฐธ์กฐ: POST ๋ฐฉ์‹์œผ๋กœ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์กฐํšŒ (autoFilter ํฌํ•จ) const response = await apiClient.post(`/table-management/tables/${optionConfig.tableName}/data`, { page: 1, size: 1000, autoFilter: { enabled: true, filterColumn: "company_code" }, }); // ์‘๋‹ต ๋ฐ์ดํ„ฐ ํŒŒ์‹ฑ let dataArray: any[] = []; if (response.data?.success) { const responseData = response.data?.data; if (responseData?.data && Array.isArray(responseData.data)) { dataArray = responseData.data; } else if (Array.isArray(responseData)) { dataArray = responseData; } else if (responseData?.rows && Array.isArray(responseData.rows)) { dataArray = responseData.rows; } } options = dataArray.map((row: any) => ({ value: String(row[optionConfig.valueColumn || "id"]), label: String(row[optionConfig.labelColumn || "name"]), })); } else if (optionConfig.type === "code" && optionConfig.categoryKey) { // ๊ณตํ†ต์ฝ”๋“œ(์นดํ…Œ๊ณ ๋ฆฌ ์ปฌ๋Ÿผ): table_column_category_values ํ…Œ์ด๋ธ”์—์„œ ์กฐํšŒ // categoryKey ํ˜•์‹: "tableName.columnName" const [categoryTable, categoryColumn] = optionConfig.categoryKey.split("."); if (categoryTable && categoryColumn) { const response = await apiClient.get( `/table-categories/${categoryTable}/${categoryColumn}/values` ); if (response.data?.success && response.data?.data) { // ๋ผ๋ฒจ๊ฐ’์„ DB์— ์ €์žฅ (ํ™”๋ฉด์— ํ‘œ์‹œ๋˜๋Š” ๊ฐ’ ๊ทธ๋Œ€๋กœ ์ €์žฅ) options = response.data.data.map((item: any) => ({ value: item.valueLabel || item.value_label, label: item.valueLabel || item.value_label, })); } } } // ์บ์‹œ ์ €์žฅ 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 || section.type === "table") 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 }; // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ์ถ”์ถœ (๋ณ„๋„ ์ €์žฅ์šฉ) const tableSectionData: Record = {}; // ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ•„๋“œ ์ œ๊ฑฐ (์ฑ„๋ฒˆ ๊ทœ์น™ ID๋Š” ์œ ์ง€ - buttonActions.ts์—์„œ ์‚ฌ์šฉ) Object.keys(dataToSave).forEach((key) => { if (key.startsWith("_tableSection_")) { // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ๋Š” ๋ณ„๋„๋กœ ์ €์žฅ const sectionId = key.replace("_tableSection_", ""); tableSectionData[sectionId] = dataToSave[key] || []; delete dataToSave[key]; } else if (key.startsWith("_") && !key.includes("_numberingRuleId")) { delete dataToSave[key]; } }); // ์ €์žฅ ์‹œ์  ์ฑ„๋ฒˆ๊ทœ์น™ ์ฒ˜๋ฆฌ (generateOnSave๋งŒ ์ฒ˜๋ฆฌ) for (const section of config.sections) { // ํ…Œ์ด๋ธ” ํƒ€์ž… ์„น์…˜์€ ๊ฑด๋„ˆ๋›ฐ๊ธฐ if (section.type === "table") continue; for (const field of (section.fields || [])) { if (field.numberingRule?.enabled && field.numberingRule?.generateOnSave && field.numberingRule?.ruleId) { const response = await allocateNumberingCode(field.numberingRule.ruleId); if (response.success && response.data?.generatedCode) { dataToSave[field.columnName] = response.data.generatedCode; console.log(`[์ฑ„๋ฒˆ ํ• ๋‹น] ${field.columnName} = ${response.data.generatedCode}`); } else { console.error(`[์ฑ„๋ฒˆ ์‹คํŒจ] ${field.columnName}:`, response.error); } } } } // ๋ณ„๋„ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•ด์•ผ ํ•˜๋Š” ํ…Œ์ด๋ธ” ์„น์…˜ ๋ชฉ๋ก const tableSectionsForSeparateTable = config.sections.filter( (s) => s.type === "table" && s.tableConfig?.saveConfig?.targetTable && s.tableConfig.saveConfig.targetTable !== config.saveConfig.tableName ); // ํ…Œ์ด๋ธ” ์„น์…˜์ด ์žˆ๊ณ  ๋ฉ”์ธ ํ…Œ์ด๋ธ”์— ํ’ˆ๋ชฉ๋ณ„๋กœ ์ €์žฅํ•˜๋Š” ๊ฒฝ์šฐ (๊ณตํ†ต + ๊ฐœ๋ณ„ ๋ณ‘ํ•ฉ ์ €์žฅ) // targetTable์ด ์—†๊ฑฐ๋‚˜ ๋ฉ”์ธ ํ…Œ์ด๋ธ”๊ณผ ๊ฐ™์€ ๊ฒฝ์šฐ const tableSectionsForMainTable = config.sections.filter( (s) => s.type === "table" && (!s.tableConfig?.saveConfig?.targetTable || s.tableConfig.saveConfig.targetTable === config.saveConfig.tableName) ); console.log("[saveSingleRow] ๋ฉ”์ธ ํ…Œ์ด๋ธ”:", config.saveConfig.tableName); console.log("[saveSingleRow] ๋ฉ”์ธ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•  ํ…Œ์ด๋ธ” ์„น์…˜:", tableSectionsForMainTable.map(s => s.id)); console.log("[saveSingleRow] ๋ณ„๋„ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•  ํ…Œ์ด๋ธ” ์„น์…˜:", tableSectionsForSeparateTable.map(s => s.id)); console.log("[saveSingleRow] ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ํ‚ค:", Object.keys(tableSectionData)); console.log("[saveSingleRow] dataToSave ํ‚ค:", Object.keys(dataToSave)); if (tableSectionsForMainTable.length > 0) { // ๊ณตํ†ต ์ €์žฅ ํ•„๋“œ ์ˆ˜์ง‘ (sectionSaveModes ์„ค์ •์— ๋”ฐ๋ผ) const commonFieldsData: Record = {}; const { sectionSaveModes } = config.saveConfig; // ํ•„๋“œ ํƒ€์ž… ์„น์…˜์—์„œ ๊ณตํ†ต ์ €์žฅ ํ•„๋“œ ์ˆ˜์ง‘ for (const section of config.sections) { if (section.type === "table") continue; const sectionMode = sectionSaveModes?.find((s) => s.sectionId === section.id); const defaultMode = "common"; // ํ•„๋“œ ํƒ€์ž… ์„น์…˜์˜ ๊ธฐ๋ณธ๊ฐ’์€ ๊ณตํ†ต ์ €์žฅ const sectionSaveMode = sectionMode?.saveMode || defaultMode; if (section.fields) { for (const field of section.fields) { const fieldOverride = sectionMode?.fieldOverrides?.find((f) => f.fieldName === field.columnName); const fieldSaveMode = fieldOverride?.saveMode || sectionSaveMode; if (fieldSaveMode === "common" && dataToSave[field.columnName] !== undefined) { commonFieldsData[field.columnName] = dataToSave[field.columnName]; } } } } // ๊ฐ ํ…Œ์ด๋ธ” ์„น์…˜์˜ ํ’ˆ๋ชฉ ๋ฐ์ดํ„ฐ์— ๊ณตํ†ต ํ•„๋“œ ๋ณ‘ํ•ฉํ•˜์—ฌ ์ €์žฅ for (const tableSection of tableSectionsForMainTable) { const sectionData = tableSectionData[tableSection.id] || []; if (sectionData.length > 0) { // ํ’ˆ๋ชฉ๋ณ„๋กœ ํ–‰ ์ €์žฅ for (const item of sectionData) { const rowToSave = { ...commonFieldsData, ...item }; // _sourceData ๋“ฑ ๋‚ด๋ถ€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ œ๊ฑฐ Object.keys(rowToSave).forEach((key) => { if (key.startsWith("_")) { delete rowToSave[key]; } }); const response = await apiClient.post( `/table-management/tables/${config.saveConfig.tableName}/add`, rowToSave ); if (!response.data?.success) { throw new Error(response.data?.message || "ํ’ˆ๋ชฉ ์ €์žฅ ์‹คํŒจ"); } } // ์ด๋ฏธ ์ €์žฅํ–ˆ์œผ๋ฏ€๋กœ ์•„๋ž˜ ๋กœ์ง์—์„œ ๋‹ค์‹œ ์ €์žฅํ•˜์ง€ ์•Š๋„๋ก ์ œ๊ฑฐ delete tableSectionData[tableSection.id]; } } // ํ’ˆ๋ชฉ์ด ์—†์œผ๋ฉด ๊ณตํ†ต ๋ฐ์ดํ„ฐ๋งŒ ์ €์žฅํ•˜์ง€ ์•Š์Œ (ํ’ˆ๋ชฉ์ด ํ•„์š”ํ•œ ํ™”๋ฉด์ด๋ฏ€๋กœ) // ๋‹ค๋ฅธ ํ…Œ์ด๋ธ” ์„น์…˜์ด ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ๋ฉ”์ธ ๋ฐ์ดํ„ฐ ์ €์žฅ const hasOtherTableSections = Object.keys(tableSectionData).length > 0; if (!hasOtherTableSections) { return; // ๋ฉ”์ธ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•  ํ’ˆ๋ชฉ์ด ์—†์œผ๋ฉด ์ข…๋ฃŒ } } // ๋ฉ”์ธ ๋ฐ์ดํ„ฐ ์ €์žฅ (ํ…Œ์ด๋ธ” ์„น์…˜์ด ์—†๊ฑฐ๋‚˜ ๋ณ„๋„ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•˜๋Š” ๊ฒฝ์šฐ) const response = await apiClient.post(`/table-management/tables/${config.saveConfig.tableName}/add`, dataToSave); if (!response.data?.success) { throw new Error(response.data?.message || "์ €์žฅ ์‹คํŒจ"); } // ํ…Œ์ด๋ธ” ์„น์…˜ ๋ฐ์ดํ„ฐ ์ €์žฅ (๋ณ„๋„ ํ…Œ์ด๋ธ”์—) for (const section of config.sections) { if (section.type === "table" && section.tableConfig?.saveConfig?.targetTable) { const sectionData = tableSectionData[section.id]; if (sectionData && sectionData.length > 0) { // ๋ฉ”์ธ ๋ ˆ์ฝ”๋“œ ID๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ (response.data์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ) const mainRecordId = response.data?.data?.id; // ๊ณตํ†ต ์ €์žฅ ํ•„๋“œ ์ˆ˜์ง‘: ๋‹ค๋ฅธ ์„น์…˜(ํ•„๋“œ ํƒ€์ž…)์—์„œ ๊ณตํ†ต ์ €์žฅ์œผ๋กœ ์„ค์ •๋œ ํ•„๋“œ ๊ฐ’ // ๊ธฐ๋ณธ๊ฐ’: ํ•„๋“œ ํƒ€์ž… ์„น์…˜์€ 'common', ํ…Œ์ด๋ธ” ํƒ€์ž… ์„น์…˜์€ 'individual' const commonFieldsData: Record = {}; const { sectionSaveModes } = config.saveConfig; // ๋‹ค๋ฅธ ์„น์…˜์—์„œ ๊ณตํ†ต ์ €์žฅ์œผ๋กœ ์„ค์ •๋œ ํ•„๋“œ ๊ฐ’ ์ˆ˜์ง‘ for (const otherSection of config.sections) { if (otherSection.id === section.id) continue; // ํ˜„์žฌ ํ…Œ์ด๋ธ” ์„น์…˜์€ ๊ฑด๋„ˆ๋›ฐ๊ธฐ const sectionMode = sectionSaveModes?.find((s) => s.sectionId === otherSection.id); // ๊ธฐ๋ณธ๊ฐ’: ํ•„๋“œ ํƒ€์ž… ์„น์…˜์€ 'common', ํ…Œ์ด๋ธ” ํƒ€์ž… ์„น์…˜์€ 'individual' const defaultMode = otherSection.type === "table" ? "individual" : "common"; const sectionSaveMode = sectionMode?.saveMode || defaultMode; // ํ•„๋“œ ํƒ€์ž… ์„น์…˜์˜ ํ•„๋“œ๋“ค ์ฒ˜๋ฆฌ if (otherSection.type !== "table" && otherSection.fields) { for (const field of otherSection.fields) { // ํ•„๋“œ๋ณ„ ์˜ค๋ฒ„๋ผ์ด๋“œ ํ™•์ธ const fieldOverride = sectionMode?.fieldOverrides?.find((f) => f.fieldName === field.columnName); const fieldSaveMode = fieldOverride?.saveMode || sectionSaveMode; // ๊ณตํ†ต ์ €์žฅ์ด๋ฉด formData์—์„œ ๊ฐ’์„ ๊ฐ€์ ธ์™€ ๋ชจ๋“  ํ’ˆ๋ชฉ์— ์ ์šฉ if (fieldSaveMode === "common" && formData[field.columnName] !== undefined) { commonFieldsData[field.columnName] = formData[field.columnName]; } } } // ๐Ÿ†• ์„ ํƒ์  ํ•„๋“œ ๊ทธ๋ฃน (optionalFieldGroups)๋„ ์ฒ˜๋ฆฌ if (otherSection.optionalFieldGroups && otherSection.optionalFieldGroups.length > 0) { for (const optGroup of otherSection.optionalFieldGroups) { if (optGroup.fields) { for (const field of optGroup.fields) { // ์„ ํƒ์  ํ•„๋“œ ๊ทธ๋ฃน์€ ๊ธฐ๋ณธ์ ์œผ๋กœ common ์ €์žฅ if (formData[field.columnName] !== undefined) { commonFieldsData[field.columnName] = formData[field.columnName]; } } } } } } console.log("[saveSingleRow] ๋ณ„๋„ ํ…Œ์ด๋ธ” ์ €์žฅ - ๊ณตํ†ต ํ•„๋“œ:", Object.keys(commonFieldsData)); for (const item of sectionData) { // ๊ณตํ†ต ํ•„๋“œ ๋ณ‘ํ•ฉ + ๊ฐœ๋ณ„ ํ’ˆ๋ชฉ ๋ฐ์ดํ„ฐ const itemToSave = { ...commonFieldsData, ...item }; // saveToTarget: false์ธ ์ปฌ๋Ÿผ์€ ์ €์žฅ์—์„œ ์ œ์™ธ const columns = section.tableConfig?.columns || []; for (const col of columns) { if (col.saveConfig?.saveToTarget === false && col.field in itemToSave) { delete itemToSave[col.field]; } } // _sourceData ๋“ฑ ๋‚ด๋ถ€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ œ๊ฑฐ Object.keys(itemToSave).forEach((key) => { if (key.startsWith("_")) { delete itemToSave[key]; } }); // ๋ฉ”์ธ ๋ ˆ์ฝ”๋“œ์™€ ์—ฐ๊ฒฐ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ if (mainRecordId && config.saveConfig.primaryKeyColumn) { itemToSave[config.saveConfig.primaryKeyColumn] = mainRecordId; } const saveResponse = await apiClient.post( `/table-management/tables/${section.tableConfig.saveConfig.targetTable}/add`, itemToSave ); if (!saveResponse.data?.success) { throw new Error(saveResponse.data?.message || `${section.title || "ํ…Œ์ด๋ธ” ์„น์…˜"} ์ €์žฅ ์‹คํŒจ`); } } } } } }, [config.sections, config.saveConfig.tableName, config.saveConfig.primaryKeyColumn, config.saveConfig.sectionSaveModes, 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)); } // ๋ฐ˜๋ณต ์„น์…˜ ID๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ, ์ฒซ ๋ฒˆ์งธ ๋ฐ˜๋ณต ์„น์…˜ ์‚ฌ์šฉ if (!repeatSectionId) { const repeatableSection = config.sections.find((s) => s.repeatable); if (repeatableSection) { repeatSectionId = repeatableSection.id; } } // ๋ฐ˜๋ณต ์„น์…˜ ๋ฐ์ดํ„ฐ 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]; } }); // ๋ฉ”์ธ ํ–‰ (๊ณตํ†ต ๋ฐ์ดํ„ฐ + ๋ฉ”์ธ ์„น์…˜ ํ•„๋“œ) 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 || section.type === "table") continue; for (const field of (section.fields || [])) { if (field.numberingRule?.enabled && field.numberingRule?.ruleId) { // generateOnSave ๋˜๋Š” generateOnOpen ๋ชจ๋‘ ์ €์žฅ ์‹œ ์‹ค์ œ ์ˆœ๋ฒˆ ํ• ๋‹น const shouldAllocate = field.numberingRule.generateOnSave || field.numberingRule.generateOnOpen; if (shouldAllocate) { const response = await allocateNumberingCode(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; } } } } } } // ๋ชจ๋“  ํ–‰ ์ €์žฅ for (let i = 0; i < rowsToSave.length; i++) { const row = rowsToSave[i]; // ๋นˆ ๊ฐ์ฒด ์ฒดํฌ if (Object.keys(row).length === 0) { 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}๋ฒˆ์งธ ํ–‰ ์ €์žฅ ์‹คํŒจ`); } } }, [config.sections, config.saveConfig, formData, repeatSections]); // ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ €์žฅ (๋ฒ”์šฉ) const saveWithMultiTable = useCallback(async () => { const { customApiSave } = config.saveConfig; if (!customApiSave?.multiTable) return; const { multiTable } = customApiSave; // 1. ๋ฉ”์ธ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ const mainData: Record = {}; config.sections.forEach((section) => { if (section.repeatable || section.type === "table") return; // ๋ฐ˜๋ณต ์„น์…˜ ๋ฐ ํ…Œ์ด๋ธ” ํƒ€์ž… ์ œ์™ธ (section.fields || []).forEach((field) => { const value = formData[field.columnName]; if (value !== undefined && value !== null && value !== "") { mainData[field.columnName] = value; } }); }); // 1-0. receiveFromParent ํ•„๋“œ ๊ฐ’๋„ mainData์— ์ถ”๊ฐ€ (์„œ๋ธŒ ํ…Œ์ด๋ธ” ์ €์žฅ์šฉ) // ์ด ํ•„๋“œ๋“ค์€ ๋ฉ”์ธ ํ…Œ์ด๋ธ”์—๋Š” ์ €์žฅ๋˜์ง€ ์•Š์ง€๋งŒ, ์„œ๋ธŒ ํ…Œ์ด๋ธ” ์ €์žฅ ์‹œ ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Œ config.sections.forEach((section) => { if (section.repeatable || section.type === "table") return; (section.fields || []).forEach((field) => { if (field.receiveFromParent && !mainData[field.columnName]) { const value = formData[field.columnName]; if (value !== undefined && value !== null && value !== "") { mainData[field.columnName] = value; } } }); }); // 1-1. ์ฑ„๋ฒˆ๊ทœ์น™ ์ฒ˜๋ฆฌ (์ €์žฅ ์‹œ์ ์— ์‹ค์ œ ์ˆœ๋ฒˆ ํ• ๋‹น) for (const section of config.sections) { if (section.repeatable || section.type === "table") continue; for (const field of (section.fields || [])) { // ์ฑ„๋ฒˆ๊ทœ์น™์ด ํ™œ์„ฑํ™”๋œ ํ•„๋“œ ์ฒ˜๋ฆฌ if (field.numberingRule?.enabled && field.numberingRule?.ruleId) { // ์‹ ๊ทœ ์ƒ์„ฑ์ด๊ฑฐ๋‚˜ ๊ฐ’์ด ์—†๋Š” ๊ฒฝ์šฐ์—๋งŒ ์ฑ„๋ฒˆ const isNewRecord = !initialData?.[multiTable.mainTable.primaryKeyColumn]; const hasNoValue = !mainData[field.columnName]; if (isNewRecord || hasNoValue) { try { // allocateNumberingCode๋กœ ์‹ค์ œ ์ˆœ๋ฒˆ ์ฆ๊ฐ€ const response = await allocateNumberingCode(field.numberingRule.ruleId); if (response.success && response.data?.generatedCode) { mainData[field.columnName] = response.data.generatedCode; } } catch (error) { console.error(`์ฑ„๋ฒˆ๊ทœ์น™ ํ• ๋‹น ์‹คํŒจ (${field.columnName}):`, error); } } } } } // 2. ์„œ๋ธŒ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ const subTablesData: Array<{ tableName: string; linkColumn: { mainField: string; subColumn: string }; items: Record[]; options?: { saveMainAsFirst?: boolean; mainFieldMappings?: Array<{ formField: string; targetColumn: string }>; mainMarkerColumn?: string; mainMarkerValue?: any; subMarkerValue?: any; deleteExistingBefore?: boolean; }; }> = []; for (const subTableConfig of multiTable.subTables || []) { // ์„œ๋ธŒ ํ…Œ์ด๋ธ”์ด ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๊ณ  ํ…Œ์ด๋ธ”๋ช…์ด ์žˆ์–ด์•ผ ํ•จ // repeatSectionId๋Š” ์„ ํƒ์‚ฌํ•ญ (saveMainAsFirst๋งŒ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ์—†์„ ์ˆ˜ ์žˆ์Œ) if (!subTableConfig.enabled || !subTableConfig.tableName) { continue; } const subItems: Record[] = []; // ๋ฐ˜๋ณต ์„น์…˜์ด ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ๋ฐ˜๋ณต ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ if (subTableConfig.repeatSectionId) { const repeatData = repeatSections[subTableConfig.repeatSectionId] || []; // ๋ฐ˜๋ณต ์„น์…˜ ๋ฐ์ดํ„ฐ๋ฅผ ํ•„๋“œ ๋งคํ•‘์— ๋”ฐ๋ผ ๋ณ€ํ™˜ for (const item of repeatData) { const mappedItem: Record = {}; // ์—ฐ๊ฒฐ ์ปฌ๋Ÿผ ๊ฐ’ ์„ค์ • if (subTableConfig.linkColumn?.mainField && subTableConfig.linkColumn?.subColumn) { mappedItem[subTableConfig.linkColumn.subColumn] = mainData[subTableConfig.linkColumn.mainField]; } // ํ•„๋“œ ๋งคํ•‘์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜ for (const mapping of subTableConfig.fieldMappings || []) { if (mapping.formField && mapping.targetColumn) { mappedItem[mapping.targetColumn] = item[mapping.formField]; } } // ๋ฉ”์ธ/์„œ๋ธŒ ๊ตฌ๋ถ„ ์ปฌ๋Ÿผ ์„ค์ • (์„œ๋ธŒ ๋ฐ์ดํ„ฐ๋Š” ์„œ๋ธŒ ๋งˆ์ปค ๊ฐ’) if (subTableConfig.options?.mainMarkerColumn) { mappedItem[subTableConfig.options.mainMarkerColumn] = subTableConfig.options?.subMarkerValue ?? false; } if (Object.keys(mappedItem).length > 0) { subItems.push(mappedItem); } } } // saveMainAsFirst๊ฐ€ ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ, ๋ฉ”์ธ ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ธŒ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ ๋งคํ•‘ ์ƒ์„ฑ let mainFieldMappings: Array<{ formField: string; targetColumn: string }> | undefined; if (subTableConfig.options?.saveMainAsFirst) { mainFieldMappings = []; // fieldMappings์— ์ •์˜๋œ targetColumn๋งŒ ๋งคํ•‘ (์„œ๋ธŒ ํ…Œ์ด๋ธ”์— ์กด์žฌํ•˜๋Š” ์ปฌ๋Ÿผ๋งŒ) for (const mapping of subTableConfig.fieldMappings || []) { if (mapping.targetColumn) { // formData์—์„œ ๋™์ผํ•œ ์ปฌ๋Ÿผ๋ช…์ด ์žˆ์œผ๋ฉด ๋งคํ•‘ (receiveFromParent ํ•„๋“œ ํฌํ•จ) const formValue = formData[mapping.targetColumn]; if (formValue !== undefined && formValue !== null && formValue !== "") { mainFieldMappings.push({ formField: mapping.targetColumn, targetColumn: mapping.targetColumn, }); } // ๋˜๋Š” ๋ฉ”์ธ ์„น์…˜์˜ ํ•„๋“œ ์ค‘ ๊ฐ™์€ ์ด๋ฆ„์ด ์žˆ์œผ๋ฉด ๋งคํ•‘ else { config.sections.forEach((section) => { if (section.repeatable || section.type === "table") return; const matchingField = (section.fields || []).find((f) => f.columnName === mapping.targetColumn); if (matchingField) { const fieldValue = formData[matchingField.columnName]; if (fieldValue !== undefined && fieldValue !== null && fieldValue !== "") { mainFieldMappings!.push({ formField: matchingField.columnName, targetColumn: mapping.targetColumn, }); } } }); } } } // ์ค‘๋ณต ์ œ๊ฑฐ mainFieldMappings = mainFieldMappings.filter( (m, idx, arr) => arr.findIndex((x) => x.targetColumn === m.targetColumn) === idx, ); } // ์„œ๋ธŒ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ (๋ฐ˜๋ณต ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์–ด๋„ saveMainAsFirst๊ฐ€ ์žˆ์œผ๋ฉด ์ถ”๊ฐ€) if (subItems.length > 0 || subTableConfig.options?.saveMainAsFirst) { subTablesData.push({ tableName: subTableConfig.tableName, linkColumn: subTableConfig.linkColumn, items: subItems, options: { ...subTableConfig.options, mainFieldMappings, // ๋ฉ”์ธ ๋ฐ์ดํ„ฐ ๋งคํ•‘ ์ถ”๊ฐ€ }, }); } } // 3. ๋ฒ”์šฉ ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ €์žฅ API ํ˜ธ์ถœ const response = await apiClient.post("/table-management/multi-table-save", { mainTable: multiTable.mainTable, mainData, subTables: subTablesData, isUpdate: !!initialData?.[multiTable.mainTable.primaryKeyColumn], }); if (!response.data?.success) { throw new Error(response.data?.message || "๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์ €์žฅ ์‹คํŒจ"); } }, [config.sections, config.saveConfig, formData, repeatSections, initialData]); // ์ปค์Šคํ…€ API ์ €์žฅ const saveWithCustomApi = useCallback(async () => { const { customApiSave } = config.saveConfig; if (!customApiSave) return; 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 "multi-table": await saveWithMultiTable(); break; case "custom": await saveWithGenericCustomApi(); break; default: throw new Error(`์ง€์›ํ•˜์ง€ ์•Š๋Š” API ํƒ€์ž…: ${customApiSave.apiType}`); } }, [config.saveConfig, formData, repeatSections, saveWithMultiTable]); // ์ €์žฅ ์ฒ˜๋ฆฌ 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 }); } // ์ €์žฅ ์™„๋ฃŒ ํ›„ ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ ์ด๋ฒคํŠธ ๋ฐœ์ƒ if (config.saveConfig.afterSave?.closeModal !== false) { window.dispatchEvent(new CustomEvent("closeEditModal")); } } 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]); // ํ•„๋“œ ์š”์†Œ ๋ Œ๋”๋ง (์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ๋งŒ) // repeatContext: ๋ฐ˜๋ณต ์„น์…˜์ธ ๊ฒฝ์šฐ { sectionId, itemId }๋ฅผ ์ „๋‹ฌ const renderFieldElement = ( field: FormFieldConfig, value: any, onChangeHandler: (value: any) => void, fieldKey: string, isDisabled: boolean, repeatContext?: { sectionId: string; itemId: string }, ) => { return (() => { switch (field.fieldType) { case "textarea": return (