"use client"; import React, { useState, useEffect, useMemo } 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Plus, Trash2, Check, ChevronsUpDown, ArrowRight } from "lucide-react"; import { cn } from "@/lib/utils"; // 타입 import import { TableColumnConfig, ValueMappingConfig, ColumnModeConfig, TableJoinCondition, LookupConfig, LookupOption, LookupCondition, VALUE_MAPPING_TYPE_OPTIONS, JOIN_SOURCE_TYPE_OPTIONS, TABLE_COLUMN_TYPE_OPTIONS, LOOKUP_TYPE_OPTIONS, LOOKUP_CONDITION_SOURCE_OPTIONS, } from "../types"; import { defaultValueMappingConfig, defaultColumnModeConfig, generateColumnModeId, } from "../config"; // 도움말 텍스트 컴포넌트 const HelpText = ({ children }: { children: React.ReactNode }) => (

{children}

); interface TableColumnSettingsModalProps { open: boolean; onOpenChange: (open: boolean) => void; column: TableColumnConfig; sourceTableName: string; // 소스 테이블명 sourceTableColumns: { column_name: string; data_type: string; comment?: string; input_type?: string }[]; formFields: { columnName: string; label: string; sectionId?: string; sectionTitle?: string }[]; // formData 필드 목록 (섹션 정보 포함) sections: { id: string; title: string }[]; // 섹션 목록 onSave: (updatedColumn: TableColumnConfig) => void; tables: { table_name: string; comment?: string }[]; tableColumns: Record; onLoadTableColumns: (tableName: string) => void; } export function TableColumnSettingsModal({ open, onOpenChange, column, sourceTableName, sourceTableColumns, formFields, sections, onSave, tables, tableColumns, onLoadTableColumns, }: TableColumnSettingsModalProps) { // 로컬 상태 const [localColumn, setLocalColumn] = useState({ ...column }); // 외부 테이블 검색 상태 const [externalTableOpen, setExternalTableOpen] = useState(false); // 조회 테이블 검색 상태 (옵션별) const [lookupTableOpenMap, setLookupTableOpenMap] = useState>({}); // 활성 탭 const [activeTab, setActiveTab] = useState("basic"); // open이 변경될 때마다 데이터 동기화 useEffect(() => { if (open) { setLocalColumn({ ...column }); } }, [open, column]); // 외부 테이블 컬럼 로드 const externalTableName = localColumn.valueMapping?.externalRef?.tableName; useEffect(() => { if (externalTableName) { onLoadTableColumns(externalTableName); } }, [externalTableName, onLoadTableColumns]); // 외부 테이블의 컬럼 목록 const externalTableColumns = useMemo(() => { if (!externalTableName) return []; return tableColumns[externalTableName] || []; }, [tableColumns, externalTableName]); // 소스 필드 기준으로 카테고리 타입인지 확인 const actualSourceField = localColumn.sourceField || localColumn.field; const sourceColumnInfo = sourceTableColumns.find((c) => c.column_name === actualSourceField); const isCategoryColumn = sourceColumnInfo?.input_type === "category"; // 카테고리 컬럼인 경우 타입을 자동으로 category로 설정 useEffect(() => { if (isCategoryColumn && localColumn.type !== "category") { updateColumn({ type: "category" }); } }, [isCategoryColumn, localColumn.type]); // 컬럼 업데이트 함수 const updateColumn = (updates: Partial) => { setLocalColumn((prev) => ({ ...prev, ...updates })); }; // 값 매핑 업데이트 const updateValueMapping = (updates: Partial) => { const current = localColumn.valueMapping || { ...defaultValueMappingConfig }; updateColumn({ valueMapping: { ...current, ...updates }, }); }; // 외부 참조 업데이트 const updateExternalRef = (updates: Partial>) => { const current = localColumn.valueMapping?.externalRef || { tableName: "", valueColumn: "", joinConditions: [], }; updateValueMapping({ externalRef: { ...current, ...updates }, }); }; // 조인 조건 추가 const addJoinCondition = () => { const current = localColumn.valueMapping?.externalRef?.joinConditions || []; const newCondition: TableJoinCondition = { sourceType: "row", sourceField: "", targetColumn: "", operator: "=", }; updateExternalRef({ joinConditions: [...current, newCondition], }); }; // 조인 조건 삭제 const removeJoinCondition = (index: number) => { const current = localColumn.valueMapping?.externalRef?.joinConditions || []; updateExternalRef({ joinConditions: current.filter((_, i) => i !== index), }); }; // 조인 조건 업데이트 const updateJoinCondition = (index: number, updates: Partial) => { const current = localColumn.valueMapping?.externalRef?.joinConditions || []; updateExternalRef({ joinConditions: current.map((c, i) => (i === index ? { ...c, ...updates } : c)), }); }; // 컬럼 모드 추가 const addColumnMode = () => { const newMode: ColumnModeConfig = { ...defaultColumnModeConfig, id: generateColumnModeId(), label: `모드 ${(localColumn.columnModes || []).length + 1}`, }; updateColumn({ columnModes: [...(localColumn.columnModes || []), newMode], }); }; // 컬럼 모드 삭제 const removeColumnMode = (index: number) => { updateColumn({ columnModes: (localColumn.columnModes || []).filter((_, i) => i !== index), }); }; // 컬럼 모드 업데이트 const updateColumnMode = (index: number, updates: Partial) => { updateColumn({ columnModes: (localColumn.columnModes || []).map((m, i) => i === index ? { ...m, ...updates } : m ), }); }; // ============================================ // 조회(Lookup) 관련 함수들 // ============================================ // 조회 설정 업데이트 const updateLookup = (updates: Partial) => { const current = localColumn.lookup || { enabled: false, options: [] }; updateColumn({ lookup: { ...current, ...updates }, }); }; // 조회 옵션 추가 const addLookupOption = () => { const newOption: LookupOption = { id: `lookup_${Date.now()}`, label: `조회 옵션 ${(localColumn.lookup?.options || []).length + 1}`, type: "sameTable", tableName: sourceTableName, // 기본값: 소스 테이블 valueColumn: "", conditions: [], isDefault: (localColumn.lookup?.options || []).length === 0, // 첫 번째 옵션은 기본값 }; updateLookup({ options: [...(localColumn.lookup?.options || []), newOption], }); }; // 조회 옵션 삭제 const removeLookupOption = (index: number) => { const newOptions = (localColumn.lookup?.options || []).filter((_, i) => i !== index); // 삭제 후 기본 옵션이 없으면 첫 번째를 기본으로 if (newOptions.length > 0 && !newOptions.some(opt => opt.isDefault)) { newOptions[0].isDefault = true; } updateLookup({ options: newOptions }); }; // 조회 옵션 업데이트 const updateLookupOption = (index: number, updates: Partial) => { updateLookup({ options: (localColumn.lookup?.options || []).map((opt, i) => i === index ? { ...opt, ...updates } : opt ), }); }; // 조회 조건 추가 const addLookupCondition = (optionIndex: number) => { const option = localColumn.lookup?.options?.[optionIndex]; if (!option) return; const newCondition: LookupCondition = { sourceType: "currentRow", sourceField: "", targetColumn: "", }; updateLookupOption(optionIndex, { conditions: [...(option.conditions || []), newCondition], }); }; // 조회 조건 삭제 const removeLookupCondition = (optionIndex: number, conditionIndex: number) => { const option = localColumn.lookup?.options?.[optionIndex]; if (!option) return; updateLookupOption(optionIndex, { conditions: option.conditions.filter((_, i) => i !== conditionIndex), }); }; // 조회 조건 업데이트 const updateLookupCondition = (optionIndex: number, conditionIndex: number, updates: Partial) => { const option = localColumn.lookup?.options?.[optionIndex]; if (!option) return; updateLookupOption(optionIndex, { conditions: option.conditions.map((c, i) => i === conditionIndex ? { ...c, ...updates } : c ), }); }; // 조회 옵션의 테이블 컬럼 로드 useEffect(() => { if (localColumn.lookup?.enabled) { localColumn.lookup.options?.forEach(option => { if (option.tableName) { onLoadTableColumns(option.tableName); } }); } }, [localColumn.lookup?.enabled, localColumn.lookup?.options, onLoadTableColumns]); // 저장 함수 const handleSave = () => { onSave(localColumn); onOpenChange(false); }; // 값 매핑 타입에 따른 설정 UI 렌더링 const renderValueMappingConfig = () => { const mappingType = localColumn.valueMapping?.type || "source"; switch (mappingType) { case "source": return (
소스 테이블에서 복사할 컬럼을 선택하세요.
); case "manual": return (
사용자가 직접 입력하는 필드입니다.
기본값을 설정하려면 "기본 설정" 탭에서 설정하세요.
); case "internal": return (
같은 모달의 다른 필드 값을 참조합니다.
); case "external": return (
{/* 외부 테이블 선택 */}
테이블을 찾을 수 없습니다. {tables.map((table) => ( { updateExternalRef({ tableName: table.table_name }); setExternalTableOpen(false); }} className="text-xs" > {table.table_name} ))}
{/* 가져올 컬럼 선택 */} {externalTableName && (
)} {/* 조인 조건 */} {externalTableName && (
{(localColumn.valueMapping?.externalRef?.joinConditions || []).map((condition, index) => (
{/* 소스 타입 */} {/* 소스 필드 */} {/* 타겟 컬럼 */}
))} {(localColumn.valueMapping?.externalRef?.joinConditions || []).length === 0 && (

조인 조건을 추가하세요.

)}
)}
); default: return null; } }; return ( 컬럼 상세 설정 "{localColumn.label}" 컬럼의 상세 설정을 구성합니다.
기본 설정 조회 설정 값 매핑 컬럼 모드 {/* 기본 설정 탭 */}
updateColumn({ field: e.target.value })} placeholder="field_name" className="h-8 text-xs mt-1" /> 데이터베이스에 저장될 컬럼명입니다.
updateColumn({ label: e.target.value })} placeholder="표시 라벨" className="h-8 text-xs mt-1" />
{isCategoryColumn && (

테이블 타입 관리에서 카테고리로 설정됨

)}
updateColumn({ width: e.target.value })} placeholder="150px" className="h-8 text-xs mt-1" />
updateColumn({ defaultValue: e.target.value })} placeholder="기본값" className="h-8 text-xs mt-1" />

옵션

{/* Select 옵션 (타입이 select일 때) */} {localColumn.type === "select" && ( <>

Select 옵션

{(localColumn.selectOptions || []).map((opt, index) => (
{ const newOptions = [...(localColumn.selectOptions || [])]; newOptions[index] = { ...newOptions[index], value: e.target.value }; updateColumn({ selectOptions: newOptions }); }} placeholder="값" className="h-8 text-xs flex-1" /> { const newOptions = [...(localColumn.selectOptions || [])]; newOptions[index] = { ...newOptions[index], label: e.target.value }; updateColumn({ selectOptions: newOptions }); }} placeholder="라벨" className="h-8 text-xs flex-1" />
))}
{/* 동적 Select 옵션 (소스 테이블에서 로드) */}

동적 옵션 (소스 테이블에서 로드)

소스 테이블에서 옵션을 동적으로 가져옵니다. 조건부 테이블 필터가 자동 적용됩니다.

{ updateColumn({ dynamicSelectOptions: checked ? { enabled: true, sourceField: "", distinct: true, } : undefined, }); }} />
{localColumn.dynamicSelectOptions?.enabled && (
{/* 소스 필드 */}

소스 테이블에서 옵션 값을 가져올 컬럼

{sourceTableColumns.length > 0 ? ( ) : ( { updateColumn({ dynamicSelectOptions: { ...localColumn.dynamicSelectOptions!, sourceField: e.target.value, }, }); }} placeholder="inspection_item" className="h-8 text-xs" /> )}
{/* 라벨 필드 */}

표시할 라벨 컬럼 (없으면 소스 컬럼 값 사용)

{sourceTableColumns.length > 0 ? ( ) : ( { updateColumn({ dynamicSelectOptions: { ...localColumn.dynamicSelectOptions!, labelField: e.target.value || undefined, }, }); }} placeholder="(비워두면 소스 컬럼 사용)" className="h-8 text-xs" /> )}
{/* 행 선택 모드 */}
{ updateColumn({ dynamicSelectOptions: { ...localColumn.dynamicSelectOptions!, rowSelectionMode: checked ? { enabled: true, autoFillMappings: [], } : undefined, }, }); }} className="scale-75" />

이 컬럼 선택 시 같은 소스 행의 다른 컬럼들도 자동 채움

{localColumn.dynamicSelectOptions.rowSelectionMode?.enabled && (
{/* 소스 ID 저장 설정 */}
{sourceTableColumns.length > 0 ? ( ) : ( { updateColumn({ dynamicSelectOptions: { ...localColumn.dynamicSelectOptions!, rowSelectionMode: { ...localColumn.dynamicSelectOptions!.rowSelectionMode!, sourceIdColumn: e.target.value || undefined, }, }, }); }} placeholder="id" className="h-7 text-xs mt-1" /> )}
{ updateColumn({ dynamicSelectOptions: { ...localColumn.dynamicSelectOptions!, rowSelectionMode: { ...localColumn.dynamicSelectOptions!.rowSelectionMode!, targetIdField: e.target.value || undefined, }, }, }); }} placeholder="inspection_standard_id" className="h-7 text-xs mt-1" />
{/* 자동 채움 매핑 */}
{(localColumn.dynamicSelectOptions.rowSelectionMode.autoFillMappings || []).map((mapping, idx) => (
{sourceTableColumns.length > 0 ? ( ) : ( { const newMappings = [...(localColumn.dynamicSelectOptions?.rowSelectionMode?.autoFillMappings || [])]; newMappings[idx] = { ...newMappings[idx], sourceColumn: e.target.value }; updateColumn({ dynamicSelectOptions: { ...localColumn.dynamicSelectOptions!, rowSelectionMode: { ...localColumn.dynamicSelectOptions!.rowSelectionMode!, autoFillMappings: newMappings, }, }, }); }} placeholder="소스 컬럼" className="h-7 text-xs flex-1" /> )} { const newMappings = [...(localColumn.dynamicSelectOptions?.rowSelectionMode?.autoFillMappings || [])]; newMappings[idx] = { ...newMappings[idx], targetField: e.target.value }; updateColumn({ dynamicSelectOptions: { ...localColumn.dynamicSelectOptions!, rowSelectionMode: { ...localColumn.dynamicSelectOptions!.rowSelectionMode!, autoFillMappings: newMappings, }, }, }); }} placeholder="타겟 필드" className="h-7 text-xs flex-1" />
))} {(localColumn.dynamicSelectOptions.rowSelectionMode.autoFillMappings || []).length === 0 && (

매핑을 추가하세요 (예: inspection_criteria → inspection_standard)

)}
)}
)}
)}
{/* 조회 설정 탭 */} {/* 조회 여부 토글 */}

다른 테이블에서 값을 조회하여 가져옵니다.

{ if (checked) { updateLookup({ enabled: true, options: [] }); } else { updateColumn({ lookup: undefined }); } }} />
{/* 조회 설정 (활성화 시) */} {localColumn.lookup?.enabled && (

헤더에서 선택 가능한 조회 방식을 정의합니다.

{(localColumn.lookup?.options || []).length === 0 ? (

조회 옵션이 없습니다

"옵션 추가" 버튼을 클릭하여 조회 방식을 추가하세요.

) : (
{(localColumn.lookup?.options || []).map((option, optIndex) => (
{/* 옵션 헤더 */}
{option.label || `옵션 ${optIndex + 1}`} {option.isDefault && ( 기본 )}
{/* 기본 설정 */}
updateLookupOption(optIndex, { label: e.target.value })} placeholder="예: 기준단가" className="h-8 text-xs mt-1" />
{/* 조회 테이블 선택 */}
{option.type === "sameTable" ? ( ) : ( setLookupTableOpenMap(prev => ({ ...prev, [option.id]: open }))} > 테이블을 찾을 수 없습니다. {tables.map((table) => ( { updateLookupOption(optIndex, { tableName: table.table_name }); onLoadTableColumns(table.table_name); setLookupTableOpenMap(prev => ({ ...prev, [option.id]: false })); }} className="text-xs" > {table.table_name} ))} )}
{/* 기본 옵션 체크박스 */}
{ if (checked) { // 기본 옵션은 하나만 updateLookup({ options: (localColumn.lookup?.options || []).map((opt, i) => ({ ...opt, isDefault: i === optIndex, })), }); } else { updateLookupOption(optIndex, { isDefault: false }); } }} className="scale-75" /> 기본 옵션으로 설정
{/* 조회 조건 */}
{(option.conditions || []).length === 0 ? (

조회 조건을 추가하세요.

) : (
{option.conditions.map((condition, condIndex) => (
{/* 소스 타입 */} {/* 섹션 선택 (sectionField일 때) */} {condition.sourceType === "sectionField" && ( )} {/* 소스 필드 */} = {/* 타겟 컬럼 */}
))}
)} {/* 조회 유형별 설명 */}
{option.type === "sameTable" && ( <> 동일 테이블 조회: 검색 모달에서 선택한 행의 다른 컬럼 값을 가져옵니다.
예: 품목 선택 시 → 품목 테이블의 기준단가 )} {option.type === "relatedTable" && ( <> 연관 테이블 조회: 현재 행 데이터를 기준으로 다른 테이블에서 값을 조회합니다.
예: 품목코드로 → 품목별단가 테이블에서 단가 조회 )} {option.type === "combinedLookup" && ( <> 복합 조건 조회: 다른 섹션 필드와 현재 행을 조합하여 조회합니다.
예: 거래처(섹션1) + 품목(현재행) → 거래처별단가 테이블 )}
))}
)}
)}
{/* 값 매핑 탭 */}
이 컬럼의 값을 어디서 가져올지 설정합니다.
{renderValueMappingConfig()}
{/* 컬럼 모드 탭 */}

하나의 컬럼에서 여러 데이터 소스를 전환하여 사용할 수 있습니다.

{(localColumn.columnModes || []).length === 0 ? (

컬럼 모드가 없습니다

예: 기준 단가 / 거래처별 단가를 전환하여 표시

) : (
{(localColumn.columnModes || []).map((mode, index) => (
{mode.label || `모드 ${index + 1}`} {mode.isDefault && ( 기본 )}
updateColumnMode(index, { label: e.target.value })} placeholder="예: 기준 단가" className="h-8 text-xs mt-1" />
))}
)}
); }