"use client"; import React, { useState, useEffect, useCallback, useMemo, useRef } from "react"; import { Truck, DollarSign, FileText, MapPin, Car, Plus, Trash2, Download, Pencil, RefreshCw, Inbox, Loader2, Settings2, } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { Checkbox } from "@/components/ui/checkbox"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { apiClient } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { exportToExcel } from "@/lib/utils/excelExport"; import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; // ========== 타입 & 상수 ========== type TabKey = "carrier" | "cost" | "contract" | "route" | "vehicle"; interface TabColumnDef { key: string; label: string; width?: string; align?: "left" | "center" | "right"; formatNumber?: boolean; } interface FormFieldDef { key: string; label: string; type: "text" | "number" | "select" | "smartselect" | "date"; required?: boolean; referenceKey?: "carrier" | "route"; categoryKey?: string; options?: { value: string; label: string }[]; placeholder?: string; } interface TabConfig { key: TabKey; label: string; icon: React.ReactNode; tableName: string; columns: TabColumnDef[]; formFields: FormFieldDef[]; defaultSortColumn: string; } const TAB_CONFIGS: TabConfig[] = [ { key: "carrier", label: "운송업체", icon: , tableName: "carrier_mng", defaultSortColumn: "carrier_code", columns: [ { key: "carrier_code", label: "업체코드", width: "120px" }, { key: "carrier_name", label: "업체명", width: "160px" }, { key: "carrier_type", label: "업체유형", width: "100px" }, { key: "contact_person", label: "담당자", width: "100px" }, { key: "contact_phone", label: "연락처", width: "130px" }, { key: "email", label: "이메일", width: "180px" }, { key: "address", label: "주소", width: "220px" }, { key: "rating", label: "등급", width: "70px", align: "center" }, { key: "status", label: "상태", width: "80px", align: "center" }, ], formFields: [ { key: "carrier_code", label: "업체코드", type: "text", required: true, placeholder: "업체코드를 입력해주세요" }, { key: "carrier_name", label: "업체명", type: "text", required: true, placeholder: "업체명을 입력해주세요" }, { key: "carrier_type", label: "업체유형", type: "select", required: true, categoryKey: "carrier_mng:carrier_type", placeholder: "업체유형을 선택해주세요" }, { key: "contact_person", label: "담당자", type: "text", placeholder: "담당자명" }, { key: "contact_phone", label: "연락처", type: "text", placeholder: "예: 02-1234-5678" }, { key: "email", label: "이메일", type: "text", placeholder: "email@example.com" }, { key: "address", label: "주소", type: "text", placeholder: "주소를 입력해주세요" }, { key: "rating", label: "등급", type: "select", categoryKey: "carrier_mng:rating", placeholder: "등급을 선택해주세요" }, { key: "status", label: "상태", type: "select", categoryKey: "carrier_mng:status", placeholder: "상태를 선택해주세요" }, ], }, { key: "cost", label: "물류비", icon: , tableName: "logistics_cost_mng", defaultSortColumn: "carrier_code", columns: [ { key: "carrier_code", label: "운송업체", width: "120px" }, { key: "route_code", label: "구간코드", width: "120px" }, { key: "base_fee", label: "기본요금", width: "110px", align: "right", formatNumber: true }, { key: "unit", label: "단위", width: "70px", align: "center" }, { key: "unit_fee", label: "단가", width: "110px", align: "right", formatNumber: true }, { key: "min_weight", label: "최소중량", width: "100px", align: "right", formatNumber: true }, { key: "max_weight", label: "최대중량", width: "100px", align: "right", formatNumber: true }, { key: "delivery_days", label: "배송일수", width: "80px", align: "center" }, ], formFields: [ { key: "carrier_code", label: "운송업체", type: "smartselect", required: true, referenceKey: "carrier" }, { key: "route_code", label: "배송구간", type: "smartselect", required: true, referenceKey: "route" }, { key: "base_fee", label: "기본요금", type: "number", placeholder: "0" }, { key: "unit", label: "단위", type: "text", placeholder: "kg, 건 등" }, { key: "unit_fee", label: "단가", type: "number", placeholder: "0" }, { key: "min_weight", label: "최소중량", type: "number", placeholder: "0" }, { key: "max_weight", label: "최대중량", type: "number", placeholder: "0" }, { key: "delivery_days", label: "배송일수", type: "number", placeholder: "0" }, ], }, { key: "contract", label: "계약서", icon: , tableName: "carrier_contract_mng", defaultSortColumn: "contract_no", columns: [ { key: "contract_no", label: "계약번호", width: "130px" }, { key: "carrier_code", label: "운송업체", width: "120px" }, { key: "contract_start_date", label: "시작일", width: "110px" }, { key: "contract_end_date", label: "종료일", width: "110px" }, { key: "contract_amount", label: "계약금액", width: "130px", align: "right", formatNumber: true }, { key: "contact_person", label: "담당자", width: "100px" }, { key: "status", label: "상태", width: "80px", align: "center" }, ], formFields: [ { key: "contract_no", label: "계약번호", type: "text", required: true, placeholder: "계약번호를 입력해주세요" }, { key: "carrier_code", label: "운송업체", type: "smartselect", required: true, referenceKey: "carrier" }, { key: "contract_start_date", label: "시작일", type: "date", required: true }, { key: "contract_end_date", label: "종료일", type: "date", required: true }, { key: "contract_amount", label: "계약금액", type: "number", placeholder: "0" }, { key: "contact_person", label: "담당자", type: "text", placeholder: "담당자명을 입력해주세요" }, { key: "status", label: "상태", type: "select", categoryKey: "carrier_contract_mng:status", placeholder: "상태를 선택해주세요" }, ], }, { key: "route", label: "배송구간", icon: , tableName: "delivery_route_mng", defaultSortColumn: "route_code", columns: [ { key: "route_code", label: "구간코드", width: "120px" }, { key: "route_name", label: "구간명", width: "160px" }, { key: "departure", label: "출발지", width: "120px" }, { key: "destination", label: "도착지", width: "120px" }, { key: "distance_km", label: "거리(km)", width: "100px", align: "right", formatNumber: true }, { key: "avg_time_hours", label: "평균시간(h)", width: "100px", align: "right" }, { key: "route_type", label: "구간유형", width: "100px" }, { key: "status", label: "상태", width: "80px", align: "center" }, ], formFields: [ { key: "route_code", label: "구간코드", type: "text", required: true, placeholder: "구간코드를 입력해주세요" }, { key: "departure", label: "출발지", type: "text", required: true, placeholder: "출발지" }, { key: "destination", label: "도착지", type: "text", required: true, placeholder: "도착지" }, { key: "distance_km", label: "거리(km)", type: "number", placeholder: "0" }, { key: "avg_time_hours", label: "평균시간(h)", type: "number", placeholder: "0" }, { key: "route_type", label: "구간유형", type: "select", categoryKey: "delivery_route_mng:route_type", placeholder: "유형을 선택해주세요" }, { key: "status", label: "상태", type: "select", categoryKey: "delivery_route_mng:status", placeholder: "상태를 선택해주세요" }, ], }, { key: "vehicle", label: "차량", icon: , tableName: "carrier_vehicle_mng", defaultSortColumn: "vehicle_code", columns: [ { key: "vehicle_code", label: "차량코드", width: "120px" }, { key: "vehicle_number", label: "차량번호", width: "120px" }, { key: "vehicle_type", label: "차량유형", width: "100px" }, { key: "carrier_code", label: "운송업체", width: "120px" }, { key: "load_capacity_kg", label: "적재용량(kg)", width: "120px", align: "right", formatNumber: true }, { key: "driver_name", label: "운전자", width: "100px" }, { key: "last_maintenance_date", label: "최종정비일", width: "110px" }, { key: "status", label: "상태", width: "80px", align: "center" }, ], formFields: [ { key: "vehicle_code", label: "차량코드", type: "text", required: true, placeholder: "차량코드를 입력해주세요" }, { key: "vehicle_number", label: "차량번호", type: "text", required: true, placeholder: "12가 3456" }, { key: "vehicle_type", label: "차량유형", type: "select", required: true, categoryKey: "carrier_vehicle_mng:vehicle_type", placeholder: "차량유형을 선택해주세요" }, { key: "carrier_code", label: "운송업체", type: "smartselect", required: true, referenceKey: "carrier" }, { key: "load_capacity_kg", label: "적재용량(kg)", type: "number", placeholder: "0" }, { key: "driver_name", label: "운전자", type: "text", placeholder: "운전자명" }, { key: "last_maintenance_date", label: "최종정비일", type: "date" }, { key: "status", label: "상태", type: "select", categoryKey: "carrier_vehicle_mng:status", placeholder: "상태를 선택해주세요" }, ], }, ]; // 카테고리 계층 평탄화 function flattenCategories(items: any[]): { value: string; label: string }[] { const result: { value: string; label: string }[] = []; function walk(arr: any[]) { for (const item of arr) { const val = item.valueCode || item.value || item.name; const lbl = item.valueLabel || item.label || item.name || val; if (val) { result.push({ value: val, label: lbl }); } if (item.children?.length) walk(item.children); } } walk(items); return result; } // 채번 대상 필드 매핑: tableName → 코드 필드 key const NUMBERING_FIELD_MAP: Record = { carrier_mng: "carrier_code", delivery_route_mng: "route_code", carrier_vehicle_mng: "vehicle_code", }; // ========== 메인 컴포넌트 ========== export default function LogisticsInfoPage() { const { user } = useAuth(); const { confirm, ConfirmDialogComponent } = useConfirmDialog(); // 탭 상태 const [activeTab, setActiveTab] = useState("carrier"); // 검색 필터 const [searchFilters, setSearchFilters] = useState([]); // 탭별 독립 상태 const [tabData, setTabData] = useState>({ carrier: [], cost: [], contract: [], route: [], vehicle: [], }); const [tabLoading, setTabLoading] = useState>({ carrier: false, cost: false, contract: false, route: false, vehicle: false, }); const [tabChecked, setTabChecked] = useState>({ carrier: [], cost: [], contract: [], route: [], vehicle: [], }); // FK 참조 데이터 (캐싱) const [carrierOptions, setCarrierOptions] = useState<{ code: string; label: string }[]>([]); const [routeOptions, setRouteOptions] = useState<{ code: string; label: string }[]>([]); // 카테고리 옵션 캐시 const [categoryOptions, setCategoryOptions] = useState>({}); const loadedCategories = useRef(new Set()); // 모달 상태 const [formOpen, setFormOpen] = useState(false); const [editMode, setEditMode] = useState(false); const [editId, setEditId] = useState(null); const [formData, setFormData] = useState>({}); // 채번 시스템 const [numberingRuleId, setNumberingRuleId] = useState(null); const [previewCode, setPreviewCode] = useState(null); // 테이블 설정 (탭별) const tsCarrier = useTableSettings("c16-logistics-carrier", TAB_CONFIGS[0].tableName, TAB_CONFIGS[0].columns); const tsCost = useTableSettings("c16-logistics-cost", TAB_CONFIGS[1].tableName, TAB_CONFIGS[1].columns); const tsContract = useTableSettings("c16-logistics-contract", TAB_CONFIGS[2].tableName, TAB_CONFIGS[2].columns); const tsRoute = useTableSettings("c16-logistics-route", TAB_CONFIGS[3].tableName, TAB_CONFIGS[3].columns); const tsVehicle = useTableSettings("c16-logistics-vehicle", TAB_CONFIGS[4].tableName, TAB_CONFIGS[4].columns); const tsMap: Record = { carrier: tsCarrier, cost: tsCost, contract: tsContract, route: tsRoute, vehicle: tsVehicle }; const activeTs = tsMap[activeTab]; const activeConfig = useMemo( () => TAB_CONFIGS.find((c) => c.key === activeTab)!, [activeTab] ); // 컬럼 가시성 헬퍼 const getVisibleColumns = (tabKey: TabKey) => tsMap[tabKey].visibleColumns; // 클라이언트 사이드 필터링 const filteredData = useMemo(() => { const data = tabData[activeTab]; if (searchFilters.length === 0) return data; return data.filter((row) => searchFilters.every((f) => { if (!f.value) return true; const kw = f.value.toLowerCase(); if (f.columnName) { return String(row[f.columnName] ?? "").toLowerCase().includes(kw); } return Object.values(row).some((v) => String(v ?? "").toLowerCase().includes(kw)); }) ); }, [tabData, activeTab, searchFilters]); // FK 참조 데이터 로드 const loadReferences = useCallback(async () => { try { const [carrierRes, routeRes] = await Promise.all([ apiClient.post("/table-management/tables/carrier_mng/data", { page: 1, size: 500, autoFilter: true, sort: { columnName: "carrier_code", order: "asc" }, }), apiClient.post("/table-management/tables/delivery_route_mng/data", { page: 1, size: 500, autoFilter: true, sort: { columnName: "route_code", order: "asc" }, }), ]); const carriers = carrierRes.data?.data?.data || carrierRes.data?.data?.rows || []; setCarrierOptions( carriers.map((r: any) => ({ code: r.carrier_code || "", label: `${r.carrier_code} - ${r.carrier_name || ""}`, })) ); const routes = routeRes.data?.data?.data || routeRes.data?.data?.rows || []; setRouteOptions( routes.map((r: any) => ({ code: r.route_code || "", label: `${r.route_code} - ${r.route_name || ""}`, })) ); } catch { // FK 참조 로드 실패 시 무시 } }, []); useEffect(() => { loadReferences(); }, [loadReferences]); // 카테고리 옵션 로드 const loadCategoryOptions = useCallback(async (tableColumn: string) => { if (loadedCategories.current.has(tableColumn)) return; loadedCategories.current.add(tableColumn); const [tableName, columnName] = tableColumn.split(":"); try { const res = await apiClient.get(`/table-categories/${tableName}/${columnName}/values`); const data = res.data?.data || []; setCategoryOptions((prev) => ({ ...prev, [tableColumn]: data.length > 0 ? flattenCategories(data) : [], })); } catch { setCategoryOptions((prev) => ({ ...prev, [tableColumn]: [] })); } }, []); // 활성 탭의 카테고리 로드 useEffect(() => { const config = TAB_CONFIGS.find((c) => c.key === activeTab); if (!config) return; config.formFields.forEach((f) => { if (f.categoryKey) loadCategoryOptions(f.categoryKey); }); }, [activeTab, loadCategoryOptions]); // 데이터 조회 const fetchTabData = useCallback(async (tab: TabKey) => { const config = TAB_CONFIGS.find((c) => c.key === tab); if (!config) return; setTabLoading((prev) => ({ ...prev, [tab]: true })); try { const res = await apiClient.post( `/table-management/tables/${config.tableName}/data`, { page: 1, size: 500, autoFilter: true, sort: { columnName: config.defaultSortColumn, order: "asc" }, } ); const rows = res.data?.data?.data || res.data?.data?.rows || []; setTabData((prev) => ({ ...prev, [tab]: rows })); } catch { toast.error("데이터를 불러오는 데 실패했어요."); setTabData((prev) => ({ ...prev, [tab]: [] })); } finally { setTabLoading((prev) => ({ ...prev, [tab]: false })); } }, []); // 초기 데이터 로드 (탭 전환 시) useEffect(() => { fetchTabData(activeTab); }, [activeTab, fetchTabData]); // 탭 변경 const handleTabChange = useCallback((tab: string) => { setActiveTab(tab as TabKey); setSearchFilters([]); }, []); // 등록 모달 열기 const handleOpenAdd = useCallback(async () => { setEditMode(false); setEditId(null); setFormData({}); setPreviewCode(null); setNumberingRuleId(null); setFormOpen(true); // 현재 탭의 채번 규칙 조회 const config = TAB_CONFIGS.find((c) => c.key === activeTab); if (!config) return; const codeField = NUMBERING_FIELD_MAP[config.tableName]; if (!codeField) return; // 채번 대상이 아닌 탭 try { const ruleRes = await apiClient.get( `/numbering-rules/by-column/${config.tableName}/${codeField}` ); const ruleData = ruleRes.data; if (ruleData?.success && ruleData?.data?.ruleId) { const ruleId = ruleData.data.ruleId; setNumberingRuleId(ruleId); const previewRes = await previewNumberingCode(ruleId); if (previewRes.success && previewRes.data?.generatedCode) { setPreviewCode(previewRes.data.generatedCode); } } } catch { // 채번 규칙 없으면 무시 — 사용자가 직접 입력 } }, [activeTab]); // 수정 모달 열기 const handleOpenEdit = useCallback((row: any) => { setEditMode(true); setEditId(row.id ? String(row.id) : null); setFormData({ ...row }); setFormOpen(true); }, []); // 저장 const handleSave = useCallback(async () => { const config = TAB_CONFIGS.find((c) => c.key === activeTab); if (!config) return; // 필수값 검증 (등록 모드에서 채번 대상 코드 필드는 자동 할당이므로 스킵) const numberingCodeField = NUMBERING_FIELD_MAP[config.tableName]; for (const field of config.formFields) { if (!editMode && numberingRuleId && field.key === numberingCodeField) continue; if (field.required && !formData[field.key]?.toString().trim()) { toast.error(`${field.label}은(는) 필수 입력이에요.`); return; } } try { // 배송구간: 출발지→도착지 로 구간명 자동 생성 const saveData = { ...formData }; if (activeTab === "route" && saveData.departure && saveData.destination) { saveData.route_name = `${saveData.departure}→${saveData.destination}`; } if (editMode && editId) { await apiClient.put(`/table-management/tables/${config.tableName}/edit`, { originalData: { id: editId }, updatedData: saveData, }); toast.success("수정이 완료되었어요."); } else { // 채번 규칙이 있으면 allocate로 실제 코드 할당 const codeField = NUMBERING_FIELD_MAP[config.tableName]; if (codeField && numberingRuleId) { const allocRes = await allocateNumberingCode(numberingRuleId); if (allocRes.success && allocRes.data?.generatedCode) { saveData[codeField] = allocRes.data.generatedCode; } else { toast.error("채번 코드 할당에 실패했습니다."); return; } } await apiClient.post( `/table-management/tables/${config.tableName}/add`, { id: crypto.randomUUID(), ...saveData } ); toast.success("등록이 완료되었어요."); } setFormOpen(false); fetchTabData(activeTab); // FK 참조 테이블 변경 시 캐시 갱신 if (activeTab === "carrier" || activeTab === "route") { loadReferences(); } } catch (err: any) { toast.error(err?.response?.data?.message || "저장에 실패했어요."); } }, [activeTab, editMode, editId, formData, fetchTabData, loadReferences, numberingRuleId]); // 삭제 const handleDelete = useCallback(async () => { const config = TAB_CONFIGS.find((c) => c.key === activeTab); if (!config) return; const ids = tabChecked[activeTab]; if (ids.length === 0) { toast.error("삭제할 항목을 선택해주세요."); return; } const ok = await confirm(`선택한 ${ids.length}건을 삭제할까요?`, { description: "삭제된 데이터는 복구할 수 없어요.", variant: "destructive", }); if (!ok) return; try { await apiClient.delete( `/table-management/tables/${config.tableName}/delete`, { data: ids.map((id) => ({ id })) } ); toast.success(`${ids.length}건이 삭제되었어요.`); setTabChecked((prev) => ({ ...prev, [activeTab]: [] })); fetchTabData(activeTab); if (activeTab === "carrier" || activeTab === "route") { loadReferences(); } } catch { toast.error("삭제에 실패했어요."); } }, [activeTab, tabChecked, confirm, fetchTabData, loadReferences]); // 엑셀 다운로드 (필터된 데이터 기준) const handleExcelDownload = useCallback(async () => { const config = TAB_CONFIGS.find((c) => c.key === activeTab); if (!config) return; if (filteredData.length === 0) { toast.error("다운로드할 데이터가 없어요."); return; } const exportData = filteredData.map((row) => { const obj: Record = {}; config.columns.forEach((col) => { obj[col.label] = row[col.key] ?? ""; }); return obj; }); await exportToExcel(exportData, `${config.label}.xlsx`, config.label); toast.success("엑셀 다운로드가 완료되었어요."); }, [activeTab, filteredData]); // 폼 필드 변경 const updateFormField = useCallback((key: string, value: any) => { setFormData((prev) => ({ ...prev, [key]: value })); }, []); // 행 체크 토글 const toggleRowCheck = useCallback((tabKey: TabKey, rowId: string) => { setTabChecked((prev) => { const ids = prev[tabKey]; return { ...prev, [tabKey]: ids.includes(rowId) ? ids.filter((x) => x !== rowId) : [...ids, rowId], }; }); }, []); // 전체 체크 토글 const toggleAllCheck = useCallback((tabKey: TabKey, checked: boolean) => { setTabChecked((prev) => ({ ...prev, [tabKey]: checked ? tabData[tabKey].map((r: any) => String(r.id)) : [], })); }, [tabData]); // 폼 필드 렌더 const renderFormField = useCallback( (field: FormFieldDef) => { const value = formData[field.key] ?? ""; // 현재 탭의 채번 대상 코드 필드인지 확인 const numberingCodeField = NUMBERING_FIELD_MAP[activeConfig.tableName]; const isNumberingTarget = !editMode && numberingRuleId && field.key === numberingCodeField; // 수정 모드에서 코드/번호 필드는 읽기전용 const isCodeField = editMode && field.type === "text" && (field.key.endsWith("_code") || field.key.endsWith("_no")); switch (field.type) { case "text": // 등록 모드 + 채번 대상 필드: readOnly로 미리보기 코드 표시 if (isNumberingTarget) { return ( ); } return ( updateFormField(field.key, e.target.value)} placeholder={field.placeholder} readOnly={isCodeField} className={cn( "h-9 text-sm", isCodeField && "bg-muted text-muted-foreground" )} /> ); case "number": return ( updateFormField(field.key, e.target.value)} placeholder={field.placeholder} className="h-9 text-sm" /> ); case "select": { const opts = field.options || (field.categoryKey ? categoryOptions[field.categoryKey] : []) || []; return ( ); } case "smartselect": { // SmartSelect 대신 Select로 직접 구현 const opts = field.referenceKey === "carrier" ? carrierOptions : routeOptions; return ( ); } case "date": return ( updateFormField(field.key, e.target.value)} className="h-9 text-sm" /> ); default: return null; } }, [formData, editMode, carrierOptions, routeOptions, categoryOptions, updateFormField, activeConfig, numberingRuleId, previewCode] ); // ========== 렌더링 ========== return (
{/* 검색 필터 바 */} {/* 탭 + 콘텐츠 영역 */} {TAB_CONFIGS.map((tab) => ( {tab.icon} {tab.label} {tabData[tab.key]?.length || 0} ))} {TAB_CONFIGS.map((tab) => { const displayData = tab.key === activeTab ? filteredData : tabData[tab.key]; const isAllChecked = tabData[tab.key].length > 0 && tabData[tab.key].every((r: any) => tabChecked[tab.key].includes(String(r.id))); return ( {/* 액션 바 */}

{tab.label} 목록

{tab.key === activeTab ? displayData.length : tabData[tab.key]?.length || 0}건
{/* 테이블 영역 */}
({ key: col.key, label: col.label, align: col.align, formatNumber: col.formatNumber, truncate: true, }))} data={tsMap[tab.key].groupData(displayData)} rowKey={(row: any) => String(row.id)} loading={tabLoading[tab.key]} emptyMessage={`등록된 ${tab.label} 정보가 없어요`} showCheckbox checkedIds={tabChecked[tab.key]} onCheckedChange={(ids) => setTabChecked((prev) => ({ ...prev, [tab.key]: ids }))} onRowDoubleClick={(row) => handleOpenEdit(row)} showPagination={false} draggableColumns={false} />
); })} {/* 등록/수정 모달 */} {activeConfig.label} {editMode ? "수정" : "등록"} {editMode ? `${activeConfig.label} 정보를 수정해주세요.` : `새 ${activeConfig.label} 정보를 입력해주세요.`}
{activeConfig.formFields.map((field) => (
{renderFormField(field)}
))}
{/* 테이블 설정 모달 */} {ConfirmDialogComponent}
); }