"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 { 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 { Plus, Trash2, Settings as SettingsIcon, Check, ChevronsUpDown } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { cn } from "@/lib/utils"; import { FormFieldConfig, LinkedFieldMapping, FIELD_TYPE_OPTIONS, SELECT_OPTION_TYPE_OPTIONS, LINKED_FIELD_DISPLAY_FORMAT_OPTIONS, } from "../types"; import { apiClient } from "@/lib/api/client"; import { getCascadingRelations, getCascadingRelationByCode, CascadingRelation } from "@/lib/api/cascadingRelation"; // 카테고리 컬럼 타입 (table_column_category_values 용) interface CategoryColumnOption { tableName: string; columnName: string; columnLabel: string; valueCount: number; // 조합키: tableName.columnName key: string; } // 도움말 텍스트 컴포넌트 const HelpText = ({ children }: { children: React.ReactNode }) => (

{children}

); /** * 부모 화면에서 전달 가능한 필드 타입 * 유니버셜 폼 모달에서 "부모에서 값 받기" 설정 시 선택 가능한 필드 목록 */ export interface AvailableParentField { name: string; // 필드명 (columnName) label: string; // 표시 라벨 sourceComponent?: string; // 출처 컴포넌트 (예: "TableList", "SplitPanelLayout2") sourceTable?: string; // 출처 테이블명 } // 섹션별 필드 그룹 interface SectionFieldGroup { sectionId: string; sectionTitle: string; fields: FormFieldConfig[]; } interface FieldDetailSettingsModalProps { open: boolean; onOpenChange: (open: boolean) => void; field: FormFieldConfig; onSave: (updates: Partial) => void; tables: { name: string; label: string }[]; tableColumns: { [tableName: string]: { name: string; type: string; label: string }[] }; numberingRules: { id: string; name: string }[]; onLoadTableColumns: (tableName: string) => void; // 저장 테이블 정보 (타겟 컬럼 선택용) targetTableName?: string; targetTableColumns?: { name: string; type: string; label: string }[]; // 연쇄 드롭다운 부모 필드 선택용 - 모든 섹션의 필드 목록 (섹션별 그룹핑) allFieldsWithSections?: SectionFieldGroup[]; } export function FieldDetailSettingsModal({ open, onOpenChange, field, onSave, tables, tableColumns, numberingRules, onLoadTableColumns, // targetTableName은 타겟 컬럼 선택 시 참고용으로 전달됨 (현재 targetTableColumns만 사용) targetTableName: _targetTableName, targetTableColumns = [], allFieldsWithSections = [], }: FieldDetailSettingsModalProps) { // eslint-disable-next-line @typescript-eslint/no-unused-vars void _targetTableName; // 향후 사용 가능성을 위해 유지 // 로컬 상태로 필드 설정 관리 const [localField, setLocalField] = useState(field); // 전체 카테고리 컬럼 목록 상태 const [categoryColumns, setCategoryColumns] = useState([]); const [loadingCategoryColumns, setLoadingCategoryColumns] = useState(false); // 연쇄 관계 목록 상태 const [cascadingRelations, setCascadingRelations] = useState([]); const [loadingCascadingRelations, setLoadingCascadingRelations] = useState(false); const [cascadingRelationOpen, setCascadingRelationOpen] = useState(false); const [parentFieldOpen, setParentFieldOpen] = useState(false); // Combobox 열림 상태 const [sourceTableOpen, setSourceTableOpen] = useState(false); const [targetColumnOpenMap, setTargetColumnOpenMap] = useState>({}); const [displayColumnOpen, setDisplayColumnOpen] = useState(false); const [subDisplayColumnOpen, setSubDisplayColumnOpen] = useState(false); // 서브 표시 컬럼 Popover 상태 const [sourceColumnOpenMap, setSourceColumnOpenMap] = useState>({}); // open이 변경될 때마다 필드 데이터 동기화 useEffect(() => { if (open) { setLocalField(field); } }, [open, field]); // 모달이 열릴 때 소스 테이블 컬럼 자동 로드 useEffect(() => { if (open && field.linkedFieldGroup?.sourceTable) { // tableColumns에 해당 테이블 컬럼이 없으면 로드 if (!tableColumns[field.linkedFieldGroup.sourceTable] || tableColumns[field.linkedFieldGroup.sourceTable].length === 0) { onLoadTableColumns(field.linkedFieldGroup.sourceTable); } } }, [open, field.linkedFieldGroup?.sourceTable, tableColumns, onLoadTableColumns]); // 모달이 열릴 때 Select 옵션의 참조 테이블 컬럼 자동 로드 useEffect(() => { if (open && field.selectOptions?.tableName) { // tableColumns에 해당 테이블 컬럼이 없으면 로드 if (!tableColumns[field.selectOptions.tableName] || tableColumns[field.selectOptions.tableName].length === 0) { onLoadTableColumns(field.selectOptions.tableName); } } }, [open, field.selectOptions?.tableName, tableColumns, onLoadTableColumns]); // 모든 카테고리 컬럼 목록 로드 (모달 열릴 때) useEffect(() => { const loadAllCategoryColumns = async () => { if (!open) return; setLoadingCategoryColumns(true); try { // /api/table-categories/all-columns API 호출 const response = await apiClient.get("/table-categories/all-columns"); if (response.data?.success && response.data?.data) { // 중복 제거를 위해 Map 사용 const uniqueMap = new Map(); response.data.data.forEach((col: any) => { const tableName = col.tableName || col.table_name; const columnName = col.columnName || col.column_name; const key = `${tableName}.${columnName}`; // 이미 존재하는 경우 valueCount가 더 큰 것을 유지 if (!uniqueMap.has(key)) { uniqueMap.set(key, { tableName, columnName, columnLabel: col.columnLabel || col.column_label || columnName, valueCount: parseInt(col.valueCount || col.value_count || "0"), key, }); } }); setCategoryColumns(Array.from(uniqueMap.values())); } else { setCategoryColumns([]); } } catch (error) { setCategoryColumns([]); } finally { setLoadingCategoryColumns(false); } }; loadAllCategoryColumns(); }, [open]); // 연쇄 관계 목록 로드 (모달 열릴 때) useEffect(() => { const loadCascadingRelations = async () => { if (!open) return; setLoadingCascadingRelations(true); try { const result = await getCascadingRelations("Y"); // 활성화된 것만 if (result?.success && result?.data) { setCascadingRelations(result.data); } else { setCascadingRelations([]); } } catch (error) { setCascadingRelations([]); } finally { setLoadingCascadingRelations(false); } }; loadCascadingRelations(); }, [open]); // 관계 코드 선택 시 상세 설정 자동 채움 const handleRelationCodeSelect = async (relationCode: string) => { if (!relationCode) return; try { const result = await getCascadingRelationByCode(relationCode); if (result?.success && result?.data) { const relation = result.data as CascadingRelation; updateField({ selectOptions: { ...localField.selectOptions, type: "cascading", tableName: relation.child_table, valueColumn: relation.child_value_column, labelColumn: relation.child_label_column, cascading: { ...localField.selectOptions?.cascading, relationCode: relation.relation_code, sourceTable: relation.child_table, parentKeyColumn: relation.child_filter_column, emptyParentMessage: relation.empty_parent_message, noOptionsMessage: relation.no_options_message, clearOnParentChange: relation.clear_on_parent_change === "Y", }, }, }); // 소스 테이블 컬럼 로드 if (relation.child_table) { onLoadTableColumns(relation.child_table); } } } catch (error) { console.error("관계 코드 조회 실패:", error); } }; // 필드 업데이트 함수 const updateField = (updates: Partial) => { setLocalField((prev) => ({ ...prev, ...updates })); }; // 저장 함수 const handleSave = () => { onSave(localField); onOpenChange(false); }; // 연결 필드 매핑 추가 const addLinkedFieldMapping = () => { const newMapping: LinkedFieldMapping = { sourceColumn: "", targetColumn: "", }; const mappings = [...(localField.linkedFieldGroup?.mappings || []), newMapping]; updateField({ linkedFieldGroup: { ...localField.linkedFieldGroup, enabled: true, mappings, }, }); }; // 연결 필드 매핑 삭제 const removeLinkedFieldMapping = (index: number) => { const mappings = [...(localField.linkedFieldGroup?.mappings || [])]; mappings.splice(index, 1); updateField({ linkedFieldGroup: { ...localField.linkedFieldGroup, mappings, }, }); }; // 연결 필드 매핑 업데이트 const updateLinkedFieldMapping = (index: number, updates: Partial) => { const mappings = [...(localField.linkedFieldGroup?.mappings || [])]; mappings[index] = { ...mappings[index], ...updates }; updateField({ linkedFieldGroup: { ...localField.linkedFieldGroup, mappings, }, }); }; // 소스 테이블 컬럼 목록 (연결 필드용) const sourceTableColumns = localField.linkedFieldGroup?.sourceTable ? tableColumns[localField.linkedFieldGroup.sourceTable] || [] : []; // Select 옵션의 참조 테이블 컬럼 목록 const selectTableColumns = localField.selectOptions?.tableName ? tableColumns[localField.selectOptions.tableName] || [] : []; return ( 필드 상세 설정: {localField.label} 필드의 타입, 동작 방식, 고급 옵션을 설정합니다.
{/* 기본 정보 섹션 */}

기본 정보

입력 필드의 유형을 선택하세요 (텍스트, 숫자, 날짜 등)
폼에서 차지할 너비를 설정합니다 (12칸 그리드 기준)
updateField({ placeholder: e.target.value })} placeholder="입력 힌트" className="h-7 text-xs mt-1" /> 입력 필드에 표시될 힌트 텍스트입니다
{/* 옵션 토글 */}

필드 옵션

필수 입력 updateField({ required: checked })} />
이 필드를 필수 입력으로 만듭니다
비활성화 (읽기전용) updateField({ disabled: checked })} />
필드를 비활성화하여 수정할 수 없게 만듭니다
숨김 (자동 저장만) updateField({ hidden: checked })} />
화면에 표시하지 않지만 값은 저장됩니다
{/* Accordion으로 고급 설정 */} {/* Select 옵션 설정 */} {localField.fieldType === "select" && (
Select 옵션 설정 {localField.selectOptions?.type && ( ({localField.selectOptions.type === "code" ? "공통코드" : "직접 입력"}) )}
드롭다운에 표시될 옵션 목록을 어디서 가져올지 설정합니다.
{localField.selectOptions?.type === "cascading" ? "연쇄 드롭다운: 부모 필드 선택에 따라 옵션이 동적으로 변경됩니다" : "테이블 참조: DB 테이블에서 옵션 목록을 가져옵니다."}
{/* 직접 입력 허용 - 모든 Select 타입에 공통 적용 */}
직접 입력 허용 목록 선택 + 직접 타이핑 가능
updateField({ selectOptions: { ...localField.selectOptions, allowCustomInput: checked, }, }) } />
활성화 시 드롭다운 목록에서 선택하거나, 직접 값을 입력할 수 있습니다. 목록에 없는 새로운 값도 입력 가능합니다. {localField.selectOptions?.type === "table" && (
테이블 참조: DB 테이블에서 옵션 목록을 가져옵니다.
드롭다운 목록을 가져올 테이블을 선택하세요
{selectTableColumns.length > 0 ? ( ) : ( updateField({ selectOptions: { ...localField.selectOptions, valueColumn: e.target.value, }, }) } placeholder="customer_code" className="h-7 text-xs mt-1" /> )} 참조 테이블에서 조인할 컬럼 (기본키)
예: customer_code, customer_id
{selectTableColumns.length > 0 ? ( ) : ( updateField({ selectOptions: { ...localField.selectOptions, labelColumn: e.target.value, }, }) } placeholder="customer_name" className="h-7 text-xs mt-1" /> )} 드롭다운에 표시할 컬럼 (이름)
예: customer_name, dept_name
{selectTableColumns.length > 0 ? ( ) : ( updateField({ selectOptions: { ...localField.selectOptions, saveColumn: e.target.value, }, }) } placeholder="비워두면 조인 컬럼 저장" className="h-7 text-xs mt-1" /> )} 실제로 DB에 저장할 컬럼을 선택하세요
예: customer_name 저장 (비워두면 customer_code 저장)
)} {localField.selectOptions?.type === "code" && (
공통코드: 코드설정에서 등록한 카테고리 값을 가져옵니다.
코드설정에서 등록한 카테고리를 선택하세요
)} {localField.selectOptions?.type === "cascading" && (
연쇄 드롭다운: 부모 필드의 값에 따라 옵션이 동적으로 필터링됩니다.
예: 거래처 선택 → 해당 거래처의 납품처만 표시
{/* 부모 필드 선택 - 콤보박스 (섹션별 그룹핑) */}
{allFieldsWithSections.length > 0 ? ( 선택 가능한 필드가 없습니다. {allFieldsWithSections.map((section) => { // 자기 자신 제외한 필드 목록 const availableFields = section.fields.filter( (f) => f.columnName !== field.columnName ); if (availableFields.length === 0) return null; return ( {availableFields.map((f) => ( { updateField({ selectOptions: { ...localField.selectOptions, cascading: { ...localField.selectOptions?.cascading, parentField: f.columnName, }, }, }); setParentFieldOpen(false); }} className="text-xs" >
{f.label} {f.columnName} ({f.fieldType})
))}
); })}
) : ( updateField({ selectOptions: { ...localField.selectOptions, cascading: { ...localField.selectOptions?.cascading, parentField: e.target.value, }, }, }) } placeholder="customer_code" className="h-7 text-xs mt-1" /> )} 이 드롭다운의 옵션을 결정할 부모 필드를 선택하세요
예: 거래처 선택 → 납품처 필터링
{/* 관계 코드 선택 */}
등록된 연쇄 관계가 없습니다. {/* 직접 설정 옵션 */} { updateField({ selectOptions: { ...localField.selectOptions, cascading: { ...localField.selectOptions?.cascading, relationCode: undefined, }, }, }); setCascadingRelationOpen(false); }} className="text-xs" > 직접 설정 {cascadingRelations.map((relation) => ( { handleRelationCodeSelect(relation.relation_code); setCascadingRelationOpen(false); }} className="text-xs" >
{relation.relation_name} {relation.parent_table} → {relation.child_table}
))}
미리 등록된 관계를 선택하면 설정이 자동으로 채워집니다.
직접 설정을 선택하면 아래에서 수동으로 입력할 수 있습니다.
{/* 상세 설정 (수정 가능) */}
상세 설정 (수정 가능)
옵션을 가져올 테이블 (예: delivery_destination)
{selectTableColumns.length > 0 ? ( ) : ( updateField({ selectOptions: { ...localField.selectOptions, cascading: { ...localField.selectOptions?.cascading, parentKeyColumn: e.target.value, }, }, }) } placeholder="customer_code" className="h-7 text-xs mt-1" /> )} 부모 값과 매칭할 컬럼 (예: customer_code)
{selectTableColumns.length > 0 ? ( ) : ( updateField({ selectOptions: { ...localField.selectOptions, valueColumn: e.target.value, }, }) } placeholder="destination_code" className="h-7 text-xs mt-1" /> )} 드롭다운 value로 사용할 컬럼
{selectTableColumns.length > 0 ? ( ) : ( updateField({ selectOptions: { ...localField.selectOptions, labelColumn: e.target.value, }, }) } placeholder="destination_name" className="h-7 text-xs mt-1" /> )} 드롭다운에 표시할 컬럼
updateField({ selectOptions: { ...localField.selectOptions, cascading: { ...localField.selectOptions?.cascading, emptyParentMessage: e.target.value, }, }, }) } placeholder="상위 항목을 먼저 선택하세요" className="h-7 text-xs mt-1" />
updateField({ selectOptions: { ...localField.selectOptions, cascading: { ...localField.selectOptions?.cascading, noOptionsMessage: e.target.value, }, }, }) } placeholder="선택 가능한 항목이 없습니다" className="h-7 text-xs mt-1" />
부모 변경 시 값 초기화 updateField({ selectOptions: { ...localField.selectOptions, cascading: { ...localField.selectOptions?.cascading, clearOnParentChange: checked, }, }, }) } />
부모 필드 값이 변경되면 이 필드의 값을 자동으로 초기화합니다
)}
)} {/* 연결 필드 설정 */}
연결 필드 설정 (다중 컬럼 저장) {localField.linkedFieldGroup?.enabled && ( ({(localField.linkedFieldGroup?.mappings || []).length}개) )}
연결 필드 사용 updateField({ linkedFieldGroup: { ...localField.linkedFieldGroup, enabled: checked, }, }) } />
드롭다운 선택 시 다른 테이블의 값도 함께 저장합니다.
예: 고객 선택 → 고객코드, 고객명, 연락처를 각각 저장
{localField.linkedFieldGroup?.enabled && (
테이블을 찾을 수 없습니다. {tables.map((t) => ( { updateField({ linkedFieldGroup: { ...localField.linkedFieldGroup, sourceTable: t.name, }, }); onLoadTableColumns(t.name); setSourceTableOpen(false); }} className="text-xs" > {t.label || t.name} ({t.name}) ))} 값을 가져올 소스 테이블 (예: customer_mng)
{/* 표시 형식 선택 */}
드롭다운에 표시할 형식을 선택합니다
{/* 메인 표시 컬럼 */}
{sourceTableColumns.length > 0 ? ( 컬럼을 찾을 수 없습니다. {sourceTableColumns.map((col) => ( { updateField({ linkedFieldGroup: { ...localField.linkedFieldGroup, displayColumn: col.name, }, }); setDisplayColumnOpen(false); }} className="text-xs" > {col.name} ({col.label}) ))} ) : ( updateField({ linkedFieldGroup: { ...localField.linkedFieldGroup, displayColumn: e.target.value, }, }) } placeholder="item_name" className="h-7 text-xs mt-1" /> )} 드롭다운에 표시할 메인 컬럼 (예: item_name)
{/* 서브 표시 컬럼 - 표시 형식이 name_only가 아닌 경우에만 표시 */} {localField.linkedFieldGroup?.displayFormat && localField.linkedFieldGroup.displayFormat !== "name_only" && (
{sourceTableColumns.length > 0 ? ( 컬럼을 찾을 수 없습니다. {sourceTableColumns.map((col) => ( { updateField({ linkedFieldGroup: { ...localField.linkedFieldGroup, subDisplayColumn: col.name, }, }); setSubDisplayColumnOpen(false); }} className="text-xs" > {col.name} ({col.label}) ))} ) : ( updateField({ linkedFieldGroup: { ...localField.linkedFieldGroup, subDisplayColumn: e.target.value, }, }) } placeholder="item_code" className="h-7 text-xs mt-1" /> )} {localField.linkedFieldGroup?.displayFormat === "code_name" ? "메인 앞에 표시될 서브 컬럼 (예: 서브 - 메인)" : "메인 뒤에 표시될 서브 컬럼 (예: 메인 (서브))"}
)} {/* 미리보기 - 메인 컬럼이 선택된 경우에만 표시 */} {localField.linkedFieldGroup?.displayColumn && (

미리보기:

{(() => { const mainCol = localField.linkedFieldGroup?.displayColumn || ""; const subCol = localField.linkedFieldGroup?.subDisplayColumn || ""; const mainLabel = sourceTableColumns.find(c => c.name === mainCol)?.label || mainCol; const subLabel = sourceTableColumns.find(c => c.name === subCol)?.label || subCol; const format = localField.linkedFieldGroup?.displayFormat || "name_only"; let preview = ""; if (format === "name_only") { preview = mainLabel; } else if (format === "code_name" && subCol) { preview = `${subLabel} - ${mainLabel}`; } else if (format === "name_code" && subCol) { preview = `${mainLabel} (${subLabel})`; } else if (!subCol) { preview = `${mainLabel} (서브 컬럼을 선택하세요)`; } else { preview = mainLabel; } return (

{preview}

); })()}
)}
소스 테이블의 컬럼을 현재 폼의 어느 컬럼에 저장할지 매핑합니다.
예: customer_code → partner_id, customer_name → partner_name
{(localField.linkedFieldGroup?.mappings || []).length === 0 ? (

매핑이 없습니다

위의 "매핑 추가" 버튼을 클릭하세요

) : (
{(localField.linkedFieldGroup?.mappings || []).map((mapping, index) => (
매핑 {index + 1}
{sourceTableColumns.length > 0 ? ( setSourceColumnOpenMap((prev) => ({ ...prev, [index]: open })) } > 컬럼을 찾을 수 없습니다. {sourceTableColumns.map((col) => ( { updateLinkedFieldMapping(index, { sourceColumn: col.name }); setSourceColumnOpenMap((prev) => ({ ...prev, [index]: false })); }} className="text-[9px]" > {col.name} ({col.label}) ))} ) : ( updateLinkedFieldMapping(index, { sourceColumn: e.target.value }) } placeholder="customer_code" className="h-6 text-[9px] mt-0.5" /> )}
{targetTableColumns.length > 0 ? ( setTargetColumnOpenMap((prev) => ({ ...prev, [index]: open })) } > 컬럼을 찾을 수 없습니다. {targetTableColumns.map((col) => ( { updateLinkedFieldMapping(index, { targetColumn: col.name }); setTargetColumnOpenMap((prev) => ({ ...prev, [index]: false })); }} className="text-[9px]" > {col.name} ({col.label}) ))} ) : ( updateLinkedFieldMapping(index, { targetColumn: e.target.value }) } placeholder="partner_id" className="h-6 text-[9px] mt-0.5" /> )}
))}
)}
)}
{/* 채번규칙 설정 */}
채번규칙 설정 {localField.numberingRule?.enabled && ( (활성화됨) )}
채번규칙 사용 updateField({ numberingRule: { ...localField.numberingRule, enabled: checked, }, }) } />
자동으로 코드/번호를 생성합니다.
예: EMP-001, ORD-20240101-001
{localField.numberingRule?.enabled && (
사용할 채번규칙을 선택하세요
사용자 수정 가능 updateField({ numberingRule: { ...localField.numberingRule, editable: checked, }, }) } />
생성된 번호를 사용자가 수정할 수 있게 합니다
저장 시점에 생성 updateField({ numberingRule: { ...localField.numberingRule, generateOnSave: checked, generateOnOpen: !checked, }, }) } />
OFF: 모달 열릴 때 생성 / ON: 저장 버튼 클릭 시 생성
)}
); }