"use client"; import React, { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; import { ScrollArea } from "@/components/ui/scroll-area"; 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 { Plus, Trash2, Database, Layers, Info, Check, ChevronsUpDown } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; import { SaveConfig, SubTableSaveConfig, SubTableFieldMapping, FormSectionConfig, FormFieldConfig, SectionSaveMode } from "../types"; // 도움말 텍스트 컴포넌트 const HelpText = ({ children }: { children: React.ReactNode }) => (

{children}

); interface SaveSettingsModalProps { open: boolean; onOpenChange: (open: boolean) => void; saveConfig: SaveConfig; sections: FormSectionConfig[]; onSave: (updates: SaveConfig) => void; tables: { name: string; label: string }[]; tableColumns: { [tableName: string]: { name: string; type: string; label: string }[] }; onLoadTableColumns: (tableName: string) => void; } export function SaveSettingsModal({ open, onOpenChange, saveConfig, sections, onSave, tables, tableColumns, onLoadTableColumns, }: SaveSettingsModalProps) { // 로컬 상태로 저장 설정 관리 const [localSaveConfig, setLocalSaveConfig] = useState(saveConfig); // 저장 모드 (단일 테이블 vs 다중 테이블) const [saveMode, setSaveMode] = useState<"single" | "multi">( saveConfig.customApiSave?.enabled && saveConfig.customApiSave?.multiTable?.enabled ? "multi" : "single" ); // 테이블 검색 Popover 상태 const [singleTableSearchOpen, setSingleTableSearchOpen] = useState(false); const [mainTableSearchOpen, setMainTableSearchOpen] = useState(false); const [subTableSearchOpen, setSubTableSearchOpen] = useState>({}); // 컬럼 검색 Popover 상태 const [mainKeyColumnSearchOpen, setMainKeyColumnSearchOpen] = useState(false); const [mainFieldSearchOpen, setMainFieldSearchOpen] = useState>({}); const [subColumnSearchOpen, setSubColumnSearchOpen] = useState>({}); const [subTableColumnSearchOpen, setSubTableColumnSearchOpen] = useState>({}); const [markerColumnSearchOpen, setMarkerColumnSearchOpen] = useState>({}); // open이 변경될 때마다 데이터 동기화 useEffect(() => { if (open) { setLocalSaveConfig(saveConfig); setSaveMode(saveConfig.customApiSave?.enabled && saveConfig.customApiSave?.multiTable?.enabled ? "multi" : "single"); // 모달이 열릴 때 기존에 설정된 테이블들의 컬럼 정보 로드 const mainTableName = saveConfig.customApiSave?.multiTable?.mainTable?.tableName; if (mainTableName && !tableColumns[mainTableName]) { onLoadTableColumns(mainTableName); } // 서브 테이블들의 컬럼 정보도 로드 const subTables = saveConfig.customApiSave?.multiTable?.subTables || []; subTables.forEach((subTable) => { if (subTable.tableName && !tableColumns[subTable.tableName]) { onLoadTableColumns(subTable.tableName); } }); } }, [open, saveConfig, tableColumns, onLoadTableColumns]); // 저장 설정 업데이트 함수 const updateSaveConfig = (updates: Partial) => { setLocalSaveConfig((prev) => ({ ...prev, ...updates })); }; // 저장 함수 const handleSave = () => { // 저장 모드에 따라 설정 조정 let finalConfig = { ...localSaveConfig }; if (saveMode === "single") { // 단일 테이블 모드: customApiSave 비활성화 finalConfig = { ...finalConfig, customApiSave: { enabled: false, apiType: "custom", }, }; } else { // 다중 테이블 모드: customApiSave 활성화 finalConfig = { ...finalConfig, customApiSave: { ...finalConfig.customApiSave, enabled: true, apiType: "multi-table", multiTable: { ...finalConfig.customApiSave?.multiTable, enabled: true, }, }, }; } onSave(finalConfig); onOpenChange(false); }; // 서브 테이블 추가 const addSubTable = () => { const newSubTable: SubTableSaveConfig = { enabled: true, tableName: "", repeatSectionId: "", linkColumn: { mainField: "", subColumn: "", }, fieldMappings: [], }; const subTables = [...(localSaveConfig.customApiSave?.multiTable?.subTables || []), newSubTable]; updateSaveConfig({ customApiSave: { ...localSaveConfig.customApiSave, apiType: "multi-table", multiTable: { ...localSaveConfig.customApiSave?.multiTable, enabled: true, subTables, }, }, }); }; // 서브 테이블 삭제 const removeSubTable = (index: number) => { const subTables = [...(localSaveConfig.customApiSave?.multiTable?.subTables || [])]; subTables.splice(index, 1); updateSaveConfig({ customApiSave: { ...localSaveConfig.customApiSave, multiTable: { ...localSaveConfig.customApiSave?.multiTable, subTables, }, }, }); }; // 서브 테이블 업데이트 const updateSubTable = (index: number, updates: Partial) => { const subTables = [...(localSaveConfig.customApiSave?.multiTable?.subTables || [])]; subTables[index] = { ...subTables[index], ...updates }; updateSaveConfig({ customApiSave: { ...localSaveConfig.customApiSave, multiTable: { ...localSaveConfig.customApiSave?.multiTable, subTables, }, }, }); }; // 필드 매핑 추가 const addFieldMapping = (subTableIndex: number) => { const newMapping: SubTableFieldMapping = { formField: "", targetColumn: "", }; const subTables = [...(localSaveConfig.customApiSave?.multiTable?.subTables || [])]; const fieldMappings = [...(subTables[subTableIndex].fieldMappings || []), newMapping]; subTables[subTableIndex] = { ...subTables[subTableIndex], fieldMappings }; updateSaveConfig({ customApiSave: { ...localSaveConfig.customApiSave, multiTable: { ...localSaveConfig.customApiSave?.multiTable, subTables, }, }, }); }; // 필드 매핑 삭제 const removeFieldMapping = (subTableIndex: number, mappingIndex: number) => { const subTables = [...(localSaveConfig.customApiSave?.multiTable?.subTables || [])]; const fieldMappings = [...(subTables[subTableIndex].fieldMappings || [])]; fieldMappings.splice(mappingIndex, 1); subTables[subTableIndex] = { ...subTables[subTableIndex], fieldMappings }; updateSaveConfig({ customApiSave: { ...localSaveConfig.customApiSave, multiTable: { ...localSaveConfig.customApiSave?.multiTable, subTables, }, }, }); }; // 필드 매핑 업데이트 const updateFieldMapping = (subTableIndex: number, mappingIndex: number, updates: Partial) => { const subTables = [...(localSaveConfig.customApiSave?.multiTable?.subTables || [])]; const fieldMappings = [...(subTables[subTableIndex].fieldMappings || [])]; fieldMappings[mappingIndex] = { ...fieldMappings[mappingIndex], ...updates }; subTables[subTableIndex] = { ...subTables[subTableIndex], fieldMappings }; updateSaveConfig({ customApiSave: { ...localSaveConfig.customApiSave, multiTable: { ...localSaveConfig.customApiSave?.multiTable, subTables, }, }, }); }; // 메인 테이블 컬럼 목록 const mainTableColumns = localSaveConfig.customApiSave?.multiTable?.mainTable?.tableName ? tableColumns[localSaveConfig.customApiSave.multiTable.mainTable.tableName] || [] : []; // 반복 섹션 목록 const repeatSections = sections.filter((s) => s.repeatable); // 모든 필드 목록 (반복 섹션 포함) const getAllFields = (): { columnName: string; label: string; sectionTitle: string; sectionId: string }[] => { const fields: { columnName: string; label: string; sectionTitle: string; sectionId: string }[] = []; sections.forEach((section) => { // 필드 타입 섹션만 처리 (테이블 타입은 fields가 undefined) if (section.fields && Array.isArray(section.fields)) { section.fields.forEach((field) => { fields.push({ columnName: field.columnName, label: field.label, sectionTitle: section.title, sectionId: section.id, }); }); } }); return fields; }; const allFields = getAllFields(); // 섹션별 저장 방식 조회 (없으면 기본값 반환) const getSectionSaveMode = (sectionId: string, sectionType: "fields" | "table"): "common" | "individual" => { const sectionMode = localSaveConfig.sectionSaveModes?.find((s) => s.sectionId === sectionId); if (sectionMode) { return sectionMode.saveMode; } // 기본값: fields 타입은 공통 저장, table 타입은 개별 저장 return sectionType === "fields" ? "common" : "individual"; }; // 필드별 저장 방식 조회 (오버라이드 확인) const getFieldSaveMode = (sectionId: string, fieldName: string, sectionType: "fields" | "table"): "common" | "individual" => { const sectionMode = localSaveConfig.sectionSaveModes?.find((s) => s.sectionId === sectionId); if (sectionMode) { // 필드별 오버라이드 확인 const fieldOverride = sectionMode.fieldOverrides?.find((f) => f.fieldName === fieldName); if (fieldOverride) { return fieldOverride.saveMode; } return sectionMode.saveMode; } // 기본값 return sectionType === "fields" ? "common" : "individual"; }; // 섹션별 저장 방식 업데이트 const updateSectionSaveMode = (sectionId: string, mode: "common" | "individual") => { const currentModes = localSaveConfig.sectionSaveModes || []; const existingIndex = currentModes.findIndex((s) => s.sectionId === sectionId); let newModes: SectionSaveMode[]; if (existingIndex >= 0) { newModes = [...currentModes]; newModes[existingIndex] = { ...newModes[existingIndex], saveMode: mode }; } else { newModes = [...currentModes, { sectionId, saveMode: mode }]; } updateSaveConfig({ sectionSaveModes: newModes }); }; // 필드별 오버라이드 토글 const toggleFieldOverride = (sectionId: string, fieldName: string, sectionType: "fields" | "table") => { const currentModes = localSaveConfig.sectionSaveModes || []; const sectionIndex = currentModes.findIndex((s) => s.sectionId === sectionId); // 섹션 설정이 없으면 먼저 생성 let newModes = [...currentModes]; if (sectionIndex < 0) { const defaultMode = sectionType === "fields" ? "common" : "individual"; newModes.push({ sectionId, saveMode: defaultMode, fieldOverrides: [] }); } const targetIndex = newModes.findIndex((s) => s.sectionId === sectionId); const sectionMode = newModes[targetIndex]; const currentFieldOverrides = sectionMode.fieldOverrides || []; const fieldOverrideIndex = currentFieldOverrides.findIndex((f) => f.fieldName === fieldName); let newFieldOverrides; if (fieldOverrideIndex >= 0) { // 이미 오버라이드가 있으면 제거 (섹션 기본값으로 돌아감) newFieldOverrides = currentFieldOverrides.filter((f) => f.fieldName !== fieldName); } else { // 오버라이드 추가 (섹션 기본값의 반대) const oppositeMode = sectionMode.saveMode === "common" ? "individual" : "common"; newFieldOverrides = [...currentFieldOverrides, { fieldName, saveMode: oppositeMode }]; } newModes[targetIndex] = { ...sectionMode, fieldOverrides: newFieldOverrides }; updateSaveConfig({ sectionSaveModes: newModes }); }; // 섹션의 필드 목록 가져오기 const getSectionFields = (section: FormSectionConfig): { fieldName: string; label: string }[] => { if (section.type === "table" && section.tableConfig) { // 테이블 타입: tableConfig.columns에서 필드 목록 가져오기 return (section.tableConfig.columns || []).map((col) => ({ fieldName: col.field, label: col.label, })); } else if (section.fields) { // 필드 타입: fields에서 목록 가져오기 return section.fields.map((field) => ({ fieldName: field.columnName, label: field.label, })); } return []; }; return ( 저장 설정 폼 데이터를 데이터베이스에 저장하는 방식을 설정합니다.
{/* 저장 모드 선택 */}
setSaveMode(value as "single" | "multi")}>
폼 데이터를 하나의 테이블에 1개 행으로 저장합니다.
예: 사원 등록, 부서 등록, 거래처 등록 등 단순 등록 화면
하나의 폼으로 여러 테이블에 동시 저장합니다. (트랜잭션으로 묶임)
메인 테이블: 폼의 모든 필드 중 해당 테이블 컬럼과 일치하는 것 자동 저장
서브 테이블: 필드 매핑에서 지정한 필드만 저장 (메인 테이블의 키 값이 자동 연결됨)
예: 사원+부서배정(user_info+user_dept), 주문+주문상세(orders+order_items)
{/* 단일 테이블 저장 설정 */} {saveMode === "single" && (

단일 테이블 설정

테이블을 찾을 수 없습니다. {tables.map((t) => ( { updateSaveConfig({ tableName: t.name }); onLoadTableColumns(t.name); setSingleTableSearchOpen(false); }} className="text-xs" >
{t.name} {t.label && ( {t.label} )}
))}
폼 데이터를 저장할 테이블을 선택하세요
updateSaveConfig({ primaryKeyColumn: e.target.value })} placeholder="id" className="h-7 text-xs mt-1" /> 수정 모드에서 사용할 기본키 컬럼명
예: id, user_id, order_id
)} {/* 다중 테이블 저장 설정 */} {saveMode === "multi" && (
{/* 메인 테이블 설정 */}

메인 테이블 설정

테이블을 찾을 수 없습니다. {tables.map((t) => ( { updateSaveConfig({ customApiSave: { ...localSaveConfig.customApiSave, apiType: "multi-table", multiTable: { ...localSaveConfig.customApiSave?.multiTable, enabled: true, mainTable: { ...localSaveConfig.customApiSave?.multiTable?.mainTable, tableName: t.name, }, }, }, }); onLoadTableColumns(t.name); setMainTableSearchOpen(false); }} className="text-xs" >
{t.name} {t.label && ( {t.label} )}
))}
주요 데이터를 저장할 메인 테이블 (예: orders, user_info)
{mainTableColumns.length > 0 ? ( 컬럼을 찾을 수 없습니다. {mainTableColumns.map((col) => ( { updateSaveConfig({ customApiSave: { ...localSaveConfig.customApiSave, multiTable: { ...localSaveConfig.customApiSave?.multiTable, mainTable: { ...localSaveConfig.customApiSave?.multiTable?.mainTable, primaryKeyColumn: col.name, }, }, }, }); setMainKeyColumnSearchOpen(false); }} className="text-xs" >
{col.name} {col.label && col.label !== col.name && ( {col.label} )}
))}
) : ( updateSaveConfig({ customApiSave: { ...localSaveConfig.customApiSave, multiTable: { ...localSaveConfig.customApiSave?.multiTable, mainTable: { ...localSaveConfig.customApiSave?.multiTable?.mainTable, primaryKeyColumn: e.target.value, }, }, }, }) } placeholder="id" className="h-7 text-xs mt-1" /> )} 메인 테이블의 기본키 컬럼 (예: order_id, user_id)
{/* 서브 테이블 목록 */}

서브 테이블 설정

({(localSaveConfig.customApiSave?.multiTable?.subTables || []).length}개)
폼에서 입력한 필드를 서브 테이블에 나눠서 저장합니다.
메인 테이블의 키 값(예: user_id)이 서브 테이블에 자동으로 연결됩니다.
필드 매핑에서 지정한 필드만 서브 테이블에 저장됩니다.
{(localSaveConfig.customApiSave?.multiTable?.subTables || []).length === 0 ? (

서브 테이블이 없습니다

위의 "서브 테이블 추가" 버튼을 클릭하세요

) : (
{(localSaveConfig.customApiSave?.multiTable?.subTables || []).map((subTable, subIndex) => { const subTableColumns = subTable.tableName ? tableColumns[subTable.tableName] || [] : []; return (
서브 테이블 {subIndex + 1}: {subTable.tableName || "(미설정)"} ({subTable.fieldMappings?.length || 0}개 매핑)
setSubTableSearchOpen(prev => ({ ...prev, [subIndex]: open }))} > 테이블을 찾을 수 없습니다. {tables.map((t) => ( { updateSubTable(subIndex, { tableName: t.name }); onLoadTableColumns(t.name); setSubTableSearchOpen(prev => ({ ...prev, [subIndex]: false })); }} className="text-xs" >
{t.name} {t.label && ( {t.label} )}
))}
반복 데이터를 저장할 서브 테이블
반복 섹션: 폼 안에서 동적으로 항목을 추가/삭제할 수 있는 섹션 (예: 주문 품목 목록)
반복 섹션이 있으면 해당 섹션의 각 항목이 서브 테이블에 여러 행으로 저장됩니다.
반복 섹션 없이 필드 매핑만 사용하면 1개 행만 저장됩니다.
메인 테이블과 서브 테이블을 연결하는 키 컬럼
{mainTableColumns.length > 0 ? ( setMainFieldSearchOpen(prev => ({ ...prev, [subIndex]: open }))} > 컬럼을 찾을 수 없습니다. {mainTableColumns.map((col) => ( { updateSubTable(subIndex, { linkColumn: { ...subTable.linkColumn, mainField: col.name }, }); setMainFieldSearchOpen(prev => ({ ...prev, [subIndex]: false })); }} className="text-[10px]" >
{col.name} {col.label && col.label !== col.name && ( {col.label} )}
))}
) : ( updateSubTable(subIndex, { linkColumn: { ...subTable.linkColumn, mainField: e.target.value }, }) } placeholder="order_id" className="h-6 text-[9px] mt-0.5" /> )}
{subTableColumns.length > 0 ? ( setSubColumnSearchOpen(prev => ({ ...prev, [subIndex]: open }))} > 컬럼을 찾을 수 없습니다. {subTableColumns.map((col) => ( { updateSubTable(subIndex, { linkColumn: { ...subTable.linkColumn, subColumn: col.name }, }); setSubColumnSearchOpen(prev => ({ ...prev, [subIndex]: false })); }} className="text-[10px]" >
{col.name} {col.label && col.label !== col.name && ( {col.label} )}
))}
) : ( updateSubTable(subIndex, { linkColumn: { ...subTable.linkColumn, subColumn: e.target.value }, }) } placeholder="order_id" className="h-6 text-[9px] mt-0.5" /> )}
폼 필드를 서브 테이블 컬럼에 매핑합니다 {(subTable.fieldMappings || []).length === 0 ? (

매핑이 없습니다

) : (
{(subTable.fieldMappings || []).map((mapping, mapIndex) => (
매핑 {mapIndex + 1}
{subTableColumns.length > 0 ? ( setSubTableColumnSearchOpen(prev => ({ ...prev, [`${subIndex}-${mapIndex}`]: open }))} > 컬럼을 찾을 수 없습니다. {subTableColumns.map((col) => ( { updateFieldMapping(subIndex, mapIndex, { targetColumn: col.name }); setSubTableColumnSearchOpen(prev => ({ ...prev, [`${subIndex}-${mapIndex}`]: false })); }} className="text-[10px]" >
{col.name} {col.label && col.label !== col.name && ( {col.label} )}
))}
) : ( updateFieldMapping(subIndex, mapIndex, { targetColumn: e.target.value, }) } placeholder="item_name" className="h-5 text-[8px] mt-0.5" /> )}
))}
)}
{/* 대표 데이터 구분 저장 옵션 */}
{!subTable.options?.saveMainAsFirst ? ( // 비활성화 상태: 추가 버튼 표시

대표/일반 구분 저장

저장되는 데이터를 대표와 일반으로 구분합니다

) : ( // 활성화 상태: 설정 필드 표시

대표/일반 구분 저장

저장되는 데이터를 대표와 일반으로 구분합니다

{subTableColumns.length > 0 ? ( setMarkerColumnSearchOpen(prev => ({ ...prev, [subIndex]: open }))} > 컬럼을 찾을 수 없습니다. {subTableColumns.map((col) => ( { updateSubTable(subIndex, { options: { ...subTable.options, mainMarkerColumn: col.name, } }); setMarkerColumnSearchOpen(prev => ({ ...prev, [subIndex]: false })); }} className="text-[10px]" >
{col.name} {col.label && col.label !== col.name && ( {col.label} )}
))}
) : ( updateSubTable(subIndex, { options: { ...subTable.options, mainMarkerColumn: e.target.value, } })} placeholder="is_primary" className="h-6 text-[9px] mt-0.5" /> )} 대표/일반을 구분하는 컬럼
{ const val = e.target.value; // true/false 문자열은 boolean으로 변환 let parsedValue: any = val; if (val === "true") parsedValue = true; else if (val === "false") parsedValue = false; else if (!isNaN(Number(val)) && val !== "") parsedValue = Number(val); updateSubTable(subIndex, { options: { ...subTable.options, mainMarkerValue: parsedValue, } }); }} placeholder="true, Y, 1 등" className="h-6 text-[9px] mt-0.5" /> 기본 정보와 함께 저장될 때 값
{ const val = e.target.value; let parsedValue: any = val; if (val === "true") parsedValue = true; else if (val === "false") parsedValue = false; else if (!isNaN(Number(val)) && val !== "") parsedValue = Number(val); updateSubTable(subIndex, { options: { ...subTable.options, subMarkerValue: parsedValue, } }); }} placeholder="false, N, 0 등" className="h-6 text-[9px] mt-0.5" /> 겸직 추가 시 저장될 때 값
)}
{/* 수정 시 데이터 로드 옵션 */}
{!subTable.options?.loadOnEdit ? ( // 비활성화 상태: 추가 버튼 표시

수정 시 데이터 로드

수정 모드에서 서브 테이블 데이터를 불러옵니다

) : ( // 활성화 상태: 설정 필드 표시

수정 시 데이터 로드

수정 모드에서 서브 테이블 데이터를 불러옵니다

updateSubTable(subIndex, { options: { ...subTable.options, loadOnlySubItems: checked, } })} />
활성화하면 겸직 데이터만 불러오고, 비활성화하면 모든 데이터를 불러옵니다
)}
); })}
)}
)} {/* 섹션별 저장 방식 */}

섹션별 저장 방식

{/* 설명 */}

공통 저장: 이 섹션의 필드 값이 모든 품목 행에 동일하게 저장됩니다
예: 수주번호, 거래처, 수주일 - 품목이 3개면 3개 행 모두 같은 값

개별 저장: 이 섹션의 필드 값이 각 품목마다 다르게 저장됩니다
예: 품목코드, 수량, 단가 - 품목마다 다른 값

{/* 섹션 목록 */} {sections.length === 0 ? (

섹션이 없습니다

) : ( {sections.map((section) => { const sectionType = section.type || "fields"; const currentMode = getSectionSaveMode(section.id, sectionType); const sectionFields = getSectionFields(section); return (
{section.title} {sectionType === "table" ? "테이블" : "필드"}
{currentMode === "common" ? "공통 저장" : "개별 저장"}
{/* 저장 방식 선택 */}
updateSectionSaveMode(section.id, value as "common" | "individual")} className="flex gap-4" >
{/* 필드 목록 */} {sectionFields.length > 0 && ( <>
필드를 클릭하면 섹션 기본값과 다르게 설정할 수 있습니다
{sectionFields.map((field) => { const fieldMode = getFieldSaveMode(section.id, field.fieldName, sectionType); const isOverridden = fieldMode !== currentMode; return ( ); })}
)}
); })}
)}
{/* 저장 후 동작 */}

저장 후 동작

토스트 메시지 표시 updateSaveConfig({ afterSave: { ...localSaveConfig.afterSave, showToast: checked, }, }) } />
저장 성공 시 "저장되었습니다" 메시지를 표시합니다
모달 자동 닫기 updateSaveConfig({ afterSave: { ...localSaveConfig.afterSave, closeModal: checked, }, }) } />
저장 성공 시 모달을 자동으로 닫습니다
부모 화면 새로고침 updateSaveConfig({ afterSave: { ...localSaveConfig.afterSave, refreshParent: checked, }, }) } />
저장 후 부모 화면의 데이터를 새로고침합니다
); }