ERP-node/frontend/lib/registry/components/universal-form-modal/UniversalFormModalConfigPan...

901 lines
37 KiB
TypeScript

"use client";
import React, { useState, useEffect, useCallback } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { Plus, Trash2, GripVertical, ChevronUp, ChevronDown, Settings, Database, Layout, Table } from "lucide-react";
import { cn } from "@/lib/utils";
import { apiClient } from "@/lib/api/client";
import { getNumberingRules } from "@/lib/api/numberingRule";
import {
UniversalFormModalConfig,
UniversalFormModalConfigPanelProps,
FormSectionConfig,
FormFieldConfig,
MODAL_SIZE_OPTIONS,
SECTION_TYPE_OPTIONS,
} from "./types";
import { defaultSectionConfig, defaultTableSectionConfig, generateSectionId } from "./config";
// 모달 import
import { FieldDetailSettingsModal } from "./modals/FieldDetailSettingsModal";
import { SaveSettingsModal } from "./modals/SaveSettingsModal";
import { SectionLayoutModal } from "./modals/SectionLayoutModal";
import { TableSectionSettingsModal } from "./modals/TableSectionSettingsModal";
// 도움말 텍스트 컴포넌트
const HelpText = ({ children }: { children: React.ReactNode }) => (
<p className="text-muted-foreground mt-0.5 text-[10px]">{children}</p>
);
// 부모 화면에서 전달 가능한 필드 타입
interface AvailableParentField {
name: string; // 필드명 (columnName)
label: string; // 표시 라벨
sourceComponent?: string; // 출처 컴포넌트 (예: "TableList", "SplitPanelLayout2")
sourceTable?: string; // 출처 테이블명
}
export function UniversalFormModalConfigPanel({
config,
onChange,
allComponents = [],
}: UniversalFormModalConfigPanelProps) {
// 테이블 목록
const [tables, setTables] = useState<{ name: string; label: string }[]>([]);
const [tableColumns, setTableColumns] = useState<{
[tableName: string]: { name: string; type: string; label: string; inputType?: string }[];
}>({});
// 부모 화면에서 전달 가능한 필드 목록
const [availableParentFields, setAvailableParentFields] = useState<AvailableParentField[]>([]);
// 채번규칙 목록
const [numberingRules, setNumberingRules] = useState<{ id: string; name: string }[]>([]);
// 모달 상태
const [saveSettingsModalOpen, setSaveSettingsModalOpen] = useState(false);
const [sectionLayoutModalOpen, setSectionLayoutModalOpen] = useState(false);
const [fieldDetailModalOpen, setFieldDetailModalOpen] = useState(false);
const [tableSectionSettingsModalOpen, setTableSectionSettingsModalOpen] = useState(false);
const [selectedSection, setSelectedSection] = useState<FormSectionConfig | null>(null);
const [selectedField, setSelectedField] = useState<FormFieldConfig | null>(null);
// 테이블 목록 로드
useEffect(() => {
loadTables();
loadNumberingRules();
}, []);
// allComponents에서 부모 화면에서 전달 가능한 필드 추출
useEffect(() => {
const extractParentFields = async () => {
if (!allComponents || allComponents.length === 0) {
setAvailableParentFields([]);
return;
}
const fields: AvailableParentField[] = [];
for (const comp of allComponents) {
// 컴포넌트 타입 추출 (여러 위치에서 확인)
const compType = comp.componentId || comp.componentConfig?.type || comp.componentConfig?.id || comp.type;
const compConfig = comp.componentConfig || {};
// 1. TableList / InteractiveDataTable - 테이블 컬럼 추출
if (compType === "table-list" || compType === "interactive-data-table") {
const tableName = compConfig.selectedTable || compConfig.tableName;
if (tableName) {
// 테이블 컬럼 로드
try {
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
const columns = response.data?.data?.columns;
if (response.data?.success && Array.isArray(columns)) {
columns.forEach((col: any) => {
const colName = col.columnName || col.column_name;
const colLabel = col.displayName || col.columnComment || col.column_comment || colName;
fields.push({
name: colName,
label: colLabel,
sourceComponent: "TableList",
sourceTable: tableName,
});
});
}
} catch (error) {
console.error(`테이블 컬럼 로드 실패 (${tableName}):`, error);
}
}
}
// 2. SplitPanelLayout2 - 데이터 전달 필드 및 소스 테이블 컬럼 추출
if (compType === "split-panel-layout2") {
// dataTransferFields 추출
const transferFields = compConfig.dataTransferFields;
if (transferFields && Array.isArray(transferFields)) {
transferFields.forEach((field: any) => {
if (field.targetColumn) {
fields.push({
name: field.targetColumn,
label: field.targetColumn,
sourceComponent: "SplitPanelLayout2",
sourceTable: compConfig.leftPanel?.tableName,
});
}
});
}
// 좌측 패널 테이블 컬럼도 추출
const leftTableName = compConfig.leftPanel?.tableName;
if (leftTableName) {
try {
const response = await apiClient.get(`/table-management/tables/${leftTableName}/columns`);
const columns = response.data?.data?.columns;
if (response.data?.success && Array.isArray(columns)) {
columns.forEach((col: any) => {
const colName = col.columnName || col.column_name;
const colLabel = col.displayName || col.columnComment || col.column_comment || colName;
// 중복 방지
if (!fields.some((f) => f.name === colName && f.sourceTable === leftTableName)) {
fields.push({
name: colName,
label: colLabel,
sourceComponent: "SplitPanelLayout2 (좌측)",
sourceTable: leftTableName,
});
}
});
}
} catch (error) {
console.error(`테이블 컬럼 로드 실패 (${leftTableName}):`, error);
}
}
}
// 3. 기타 테이블 관련 컴포넌트
if (compType === "card-display" || compType === "simple-repeater-table") {
const tableName = compConfig.tableName || compConfig.initialDataConfig?.sourceTable;
if (tableName) {
try {
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
const columns = response.data?.data?.columns;
if (response.data?.success && Array.isArray(columns)) {
columns.forEach((col: any) => {
const colName = col.columnName || col.column_name;
const colLabel = col.displayName || col.columnComment || col.column_comment || colName;
if (!fields.some((f) => f.name === colName && f.sourceTable === tableName)) {
fields.push({
name: colName,
label: colLabel,
sourceComponent: compType,
sourceTable: tableName,
});
}
});
}
} catch (error) {
console.error(`테이블 컬럼 로드 실패 (${tableName}):`, error);
}
}
}
// 4. 버튼 컴포넌트 - openModalWithData의 fieldMappings/dataMapping에서 소스 컬럼 추출
if (compType === "button-primary" || compType === "button" || compType === "button-secondary") {
const action = compConfig.action || {};
// fieldMappings에서 소스 컬럼 추출
const fieldMappings = action.fieldMappings || [];
fieldMappings.forEach((mapping: any) => {
if (mapping.sourceColumn && !fields.some((f) => f.name === mapping.sourceColumn)) {
fields.push({
name: mapping.sourceColumn,
label: mapping.sourceColumn,
sourceComponent: "Button (fieldMappings)",
sourceTable: action.sourceTableName,
});
}
});
// dataMapping에서 소스 컬럼 추출
const dataMapping = action.dataMapping || [];
dataMapping.forEach((mapping: any) => {
if (mapping.sourceColumn && !fields.some((f) => f.name === mapping.sourceColumn)) {
fields.push({
name: mapping.sourceColumn,
label: mapping.sourceColumn,
sourceComponent: "Button (dataMapping)",
sourceTable: action.sourceTableName,
});
}
});
}
}
// 5. 현재 모달의 저장 테이블 컬럼도 추가 (부모에서 전달받을 수 있는 값들)
const currentTableName = config.saveConfig?.tableName;
if (currentTableName) {
try {
const response = await apiClient.get(`/table-management/tables/${currentTableName}/columns`);
const columns = response.data?.data?.columns;
if (response.data?.success && Array.isArray(columns)) {
columns.forEach((col: any) => {
const colName = col.columnName || col.column_name;
const colLabel = col.displayName || col.columnComment || col.column_comment || colName;
if (!fields.some((f) => f.name === colName)) {
fields.push({
name: colName,
label: colLabel,
sourceComponent: "현재 폼 테이블",
sourceTable: currentTableName,
});
}
});
}
} catch (error) {
console.error(`현재 테이블 컬럼 로드 실패 (${currentTableName}):`, error);
}
}
// 중복 제거 (같은 name이면 첫 번째만 유지)
const uniqueFields = fields.filter(
(field, index, self) => index === self.findIndex((f) => f.name === field.name),
);
setAvailableParentFields(uniqueFields);
};
extractParentFields();
}, [allComponents, config.saveConfig?.tableName]);
// 저장 테이블 변경 시 컬럼 로드
useEffect(() => {
if (config.saveConfig.tableName) {
loadTableColumns(config.saveConfig.tableName);
}
}, [config.saveConfig.tableName]);
const loadTables = async () => {
try {
const response = await apiClient.get("/table-management/tables");
const data = response.data?.data;
if (response.data?.success && Array.isArray(data)) {
setTables(
data.map(
(t: {
tableName?: string;
table_name?: string;
displayName?: string;
tableLabel?: string;
table_label?: string;
}) => ({
name: t.tableName || t.table_name || "",
// displayName 우선, 없으면 tableLabel, 그것도 없으면 테이블명
label: t.displayName || t.tableLabel || t.table_label || "",
}),
),
);
}
} catch (error) {
console.error("테이블 목록 로드 실패:", error);
}
};
const loadTableColumns = async (tableName: string) => {
if (!tableName || (tableColumns[tableName] && tableColumns[tableName].length > 0)) return;
try {
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
// API 응답 구조: { success, data: { columns: [...], total, page, ... } }
const columns = response.data?.data?.columns;
if (response.data?.success && Array.isArray(columns)) {
setTableColumns((prev) => ({
...prev,
[tableName]: columns.map(
(c: {
columnName?: string;
column_name?: string;
dataType?: string;
data_type?: string;
displayName?: string;
columnComment?: string;
column_comment?: string;
inputType?: string;
input_type?: string;
}) => ({
name: c.columnName || c.column_name || "",
type: c.dataType || c.data_type || "text",
label: c.displayName || c.columnComment || c.column_comment || c.columnName || c.column_name || "",
inputType: c.inputType || c.input_type || "text",
}),
),
}));
}
} catch (error) {
console.error(`테이블 컬럼 로드 실패 (${tableName}):`, error);
}
};
const loadNumberingRules = async () => {
try {
const response = await getNumberingRules();
const data = response?.data;
if (response?.success && Array.isArray(data)) {
const rules = data.map(
(r: {
id?: string | number;
ruleId?: string;
rule_id?: string;
name?: string;
ruleName?: string;
rule_name?: string;
}) => ({
id: String(r.id || r.ruleId || r.rule_id || ""),
name: r.name || r.ruleName || r.rule_name || "",
}),
);
setNumberingRules(rules);
}
} catch (error) {
console.error("채번규칙 목록 로드 실패:", error);
}
};
// 설정 업데이트 헬퍼
const updateModalConfig = useCallback(
(updates: Partial<UniversalFormModalConfig["modal"]>) => {
onChange({
...config,
modal: { ...config.modal, ...updates },
});
},
[config, onChange],
);
// 섹션 관리
const addSection = useCallback(
(type: "fields" | "table" = "fields") => {
const newSection: FormSectionConfig = {
...defaultSectionConfig,
id: generateSectionId(),
title: type === "table" ? `테이블 섹션 ${config.sections.length + 1}` : `섹션 ${config.sections.length + 1}`,
type,
fields: type === "fields" ? [] : undefined,
tableConfig: type === "table" ? { ...defaultTableSectionConfig } : undefined,
};
onChange({
...config,
sections: [...config.sections, newSection],
});
},
[config, onChange],
);
// 섹션 타입 변경
const changeSectionType = useCallback(
(sectionId: string, newType: "fields" | "table") => {
onChange({
...config,
sections: config.sections.map((s) => {
if (s.id !== sectionId) return s;
if (newType === "table") {
return {
...s,
type: "table",
fields: undefined,
tableConfig: { ...defaultTableSectionConfig },
};
} else {
return {
...s,
type: "fields",
fields: [],
tableConfig: undefined,
};
}
}),
});
},
[config, onChange],
);
// 테이블 섹션 설정 모달 열기
const handleOpenTableSectionSettings = (section: FormSectionConfig) => {
setSelectedSection(section);
setTableSectionSettingsModalOpen(true);
};
const updateSection = useCallback(
(sectionId: string, updates: Partial<FormSectionConfig>) => {
onChange({
...config,
sections: config.sections.map((s) => (s.id === sectionId ? { ...s, ...updates } : s)),
});
},
[config, onChange],
);
const removeSection = useCallback(
(sectionId: string) => {
onChange({
...config,
sections: config.sections.filter((s) => s.id !== sectionId),
});
},
[config, onChange],
);
const moveSectionUp = useCallback(
(index: number) => {
if (index <= 0) return;
const newSections = [...config.sections];
[newSections[index - 1], newSections[index]] = [newSections[index], newSections[index - 1]];
onChange({ ...config, sections: newSections });
},
[config, onChange],
);
const moveSectionDown = useCallback(
(index: number) => {
if (index >= config.sections.length - 1) return;
const newSections = [...config.sections];
[newSections[index], newSections[index + 1]] = [newSections[index + 1], newSections[index]];
onChange({ ...config, sections: newSections });
},
[config, onChange],
);
// 필드 타입별 색상
const getFieldTypeColor = (fieldType: FormFieldConfig["fieldType"]): string => {
switch (fieldType) {
case "text":
case "email":
case "password":
case "tel":
return "text-blue-600 bg-blue-50 border-blue-200";
case "number":
return "text-cyan-600 bg-cyan-50 border-cyan-200";
case "date":
case "datetime":
return "text-purple-600 bg-purple-50 border-purple-200";
case "select":
return "text-green-600 bg-green-50 border-green-200";
case "checkbox":
return "text-pink-600 bg-pink-50 border-pink-200";
case "textarea":
return "text-orange-600 bg-orange-50 border-orange-200";
default:
return "text-gray-600 bg-gray-50 border-gray-200";
}
};
// 섹션 레이아웃 모달 열기
const handleOpenSectionLayout = (section: FormSectionConfig) => {
setSelectedSection(section);
setSectionLayoutModalOpen(true);
};
// 필드 상세 설정 모달 열기
const handleOpenFieldDetail = (section: FormSectionConfig, field: FormFieldConfig) => {
setSelectedSection(section);
setSelectedField(field);
setFieldDetailModalOpen(true);
};
return (
<div className="flex h-full w-full min-w-0 flex-col overflow-hidden">
<div className="w-full min-w-0 flex-1 overflow-x-hidden overflow-y-auto">
<div className="w-full max-w-full min-w-0 space-y-4 p-4">
{/* 모달 기본 설정 */}
<Accordion type="single" collapsible defaultValue="modal-settings" className="w-full min-w-0">
<AccordionItem value="modal-settings" className="w-full min-w-0 rounded-lg border">
<AccordionTrigger className="w-full min-w-0 px-4 py-3 text-sm font-medium hover:no-underline">
<div className="flex w-full min-w-0 items-center gap-2">
<Settings className="h-4 w-4 shrink-0" />
<span className="truncate"> </span>
</div>
</AccordionTrigger>
<AccordionContent className="w-full min-w-0 space-y-4 px-4 pb-4">
<div className="w-full min-w-0">
<Label className="mb-1.5 block text-xs font-medium"> </Label>
<Input
value={config.modal.title}
onChange={(e) => updateModalConfig({ title: e.target.value })}
className="h-9 w-full max-w-full text-sm"
/>
<HelpText> </HelpText>
</div>
<div className="w-full min-w-0">
<Label className="mb-1.5 block text-xs font-medium"> </Label>
<Select value={config.modal.size} onValueChange={(value: any) => updateModalConfig({ size: value })}>
<SelectTrigger className="h-9 w-full max-w-full text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{MODAL_SIZE_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
<HelpText> </HelpText>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
{/* 저장 설정 */}
<Accordion type="single" collapsible defaultValue="save-settings" className="w-full min-w-0">
<AccordionItem value="save-settings" className="w-full min-w-0 rounded-lg border">
<AccordionTrigger className="w-full min-w-0 px-4 py-3 text-sm font-medium hover:no-underline">
<div className="flex w-full min-w-0 items-center gap-2">
<Database className="h-4 w-4 shrink-0" />
<span className="truncate"> </span>
</div>
</AccordionTrigger>
<AccordionContent className="w-full min-w-0 space-y-4 px-4 pb-4">
<div className="w-full min-w-0 space-y-3">
<div className="min-w-0 flex-1">
<Label className="mb-1.5 block text-xs font-medium"> </Label>
<p className="text-muted-foreground text-sm">{config.saveConfig.tableName || "(미설정)"}</p>
{config.saveConfig.customApiSave?.enabled &&
config.saveConfig.customApiSave?.multiTable?.enabled && (
<Badge variant="secondary" className="mt-2 px-2 py-0.5 text-xs">
</Badge>
)}
</div>
<Button
size="sm"
variant="outline"
onClick={() => setSaveSettingsModalOpen(true)}
className="h-9 w-full text-xs"
>
<Settings className="mr-2 h-4 w-4" />
</Button>
</div>
<HelpText>
.
<br />
"저장 설정 열기" .
</HelpText>
</AccordionContent>
</AccordionItem>
</Accordion>
{/* 섹션 구성 */}
<Accordion type="single" collapsible defaultValue="sections" className="w-full min-w-0">
<AccordionItem value="sections" className="w-full min-w-0 rounded-lg border">
<AccordionTrigger className="w-full min-w-0 px-4 py-3 text-sm font-medium hover:no-underline">
<div className="flex w-full min-w-0 items-center gap-2">
<Layout className="h-4 w-4 shrink-0" />
<span className="truncate"> </span>
<Badge variant="secondary" className="shrink-0 px-2 py-0.5 text-xs">
{config.sections.length}
</Badge>
</div>
</AccordionTrigger>
<AccordionContent className="w-full min-w-0 space-y-4 px-4 pb-4">
{/* 섹션 추가 버튼들 */}
<div className="flex w-full min-w-0 gap-2">
<Button
size="sm"
variant="outline"
onClick={() => addSection("fields")}
className="h-9 min-w-0 flex-1 text-xs"
>
<Plus className="mr-1 h-4 w-4 shrink-0" />
<span className="truncate"> </span>
</Button>
<Button
size="sm"
variant="outline"
onClick={() => addSection("table")}
className="h-9 min-w-0 flex-1 text-xs"
>
<Table className="mr-1 h-4 w-4 shrink-0" />
<span className="truncate"> </span>
</Button>
</div>
<HelpText>
섹션: 일반 .
<br />
섹션: 품목 .
</HelpText>
{config.sections.length === 0 ? (
<div className="bg-muted/20 w-full rounded-lg border border-dashed py-12 text-center">
<p className="text-muted-foreground mb-2 text-sm font-medium"> </p>
<p className="text-muted-foreground text-xs"> </p>
</div>
) : (
<div className="w-full min-w-0 space-y-3">
{config.sections.map((section, index) => (
<div
key={section.id}
className="bg-card w-full min-w-0 space-y-3 overflow-hidden rounded-lg border p-3"
>
{/* 헤더: 제목 + 타입 배지 + 삭제 */}
<div className="flex w-full min-w-0 items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="mb-1.5 flex items-center gap-2">
<span className="truncate text-sm font-medium">{section.title}</span>
{section.type === "table" ? (
<Badge
variant="outline"
className="border-purple-200 bg-purple-50 px-1.5 py-0.5 text-xs text-purple-600"
>
</Badge>
) : section.repeatable ? (
<Badge variant="outline" className="px-1.5 py-0.5 text-xs">
</Badge>
) : null}
</div>
{section.type === "table" ? (
<Badge variant="secondary" className="px-2 py-0.5 text-xs">
{section.tableConfig?.source?.tableName || "(소스 미설정)"}
</Badge>
) : (
<Badge variant="secondary" className="px-2 py-0.5 text-xs">
{(section.fields || []).length}
</Badge>
)}
</div>
<Button
size="sm"
variant="ghost"
onClick={() => removeSection(section.id)}
className="text-destructive hover:text-destructive h-7 w-7 shrink-0 p-0"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
{/* 순서 조정 버튼 */}
<div className="flex items-center gap-2">
<GripVertical className="text-muted-foreground h-4 w-4 shrink-0" />
<div className="flex gap-1">
<Button
size="sm"
variant="outline"
onClick={() => moveSectionUp(index)}
disabled={index === 0}
className="h-7 px-2 text-xs"
>
<ChevronUp className="h-3.5 w-3.5" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => moveSectionDown(index)}
disabled={index === config.sections.length - 1}
className="h-7 px-2 text-xs"
>
<ChevronDown className="h-3.5 w-3.5" />
</Button>
</div>
</div>
{/* 필드 목록 (필드 타입만) */}
{section.type !== "table" && (section.fields || []).length > 0 && (
<div className="flex max-w-full flex-wrap gap-1.5 overflow-hidden pt-1">
{(section.fields || []).slice(0, 4).map((field) => (
<Badge
key={field.id}
variant="outline"
className={cn("shrink-0 px-2 py-0.5 text-xs", getFieldTypeColor(field.fieldType))}
>
{field.label}
</Badge>
))}
{(section.fields || []).length > 4 && (
<Badge variant="outline" className="shrink-0 px-2 py-0.5 text-xs">
+{(section.fields || []).length - 4}
</Badge>
)}
</div>
)}
{/* 테이블 컬럼 목록 (테이블 타입만) */}
{section.type === "table" &&
section.tableConfig?.columns &&
section.tableConfig.columns.length > 0 && (
<div className="flex max-w-full flex-wrap gap-1.5 overflow-hidden pt-1">
{section.tableConfig.columns.slice(0, 4).map((col, idx) => (
<Badge
key={col.field || `col_${idx}`}
variant="outline"
className="shrink-0 border-purple-200 bg-purple-50 px-2 py-0.5 text-xs text-purple-600"
>
{col.label || col.field || `컬럼 ${idx + 1}`}
</Badge>
))}
{section.tableConfig.columns.length > 4 && (
<Badge variant="outline" className="shrink-0 px-2 py-0.5 text-xs">
+{section.tableConfig.columns.length - 4}
</Badge>
)}
</div>
)}
{/* 설정 버튼 (타입에 따라 다름) */}
{section.type === "table" ? (
<Button
size="sm"
variant="outline"
onClick={() => handleOpenTableSectionSettings(section)}
className="h-9 w-full text-xs"
>
<Table className="mr-2 h-4 w-4" />
</Button>
) : (
<Button
size="sm"
variant="outline"
onClick={() => handleOpenSectionLayout(section)}
className="h-9 w-full text-xs"
>
<Layout className="mr-2 h-4 w-4" />
</Button>
)}
</div>
))}
</div>
)}
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</div>
{/* 저장 설정 모달 */}
<SaveSettingsModal
open={saveSettingsModalOpen}
onOpenChange={setSaveSettingsModalOpen}
saveConfig={config.saveConfig}
sections={config.sections}
onSave={(updates) => {
onChange({
...config,
saveConfig: updates,
});
}}
tables={tables}
tableColumns={tableColumns}
onLoadTableColumns={loadTableColumns}
/>
{/* 섹션 레이아웃 모달 */}
{selectedSection && (
<SectionLayoutModal
open={sectionLayoutModalOpen}
onOpenChange={setSectionLayoutModalOpen}
section={selectedSection}
onSave={(updates) => {
// config 업데이트
updateSection(selectedSection.id, updates);
// selectedSection 상태도 업데이트 (최신 상태 유지)
setSelectedSection({ ...selectedSection, ...updates });
setSectionLayoutModalOpen(false);
}}
onOpenFieldDetail={(field) => {
setSectionLayoutModalOpen(false);
setSelectedField(field);
setFieldDetailModalOpen(true);
}}
tableName={config.saveConfig.tableName}
tableColumns={
tableColumns[config.saveConfig.tableName || ""]?.map((col) => ({
name: col.name,
type: col.type,
label: col.label || col.name,
})) || []
}
/>
)}
{/* 필드 상세 설정 모달 */}
{selectedSection && selectedField && (
<FieldDetailSettingsModal
open={fieldDetailModalOpen}
onOpenChange={(open) => {
setFieldDetailModalOpen(open);
if (!open) {
// 필드 상세 모달을 닫으면 섹션 레이아웃 모달을 다시 엽니다
setSectionLayoutModalOpen(true);
}
}}
field={selectedField}
onSave={(updatedField) => {
// updatedField는 FieldDetailSettingsModal에서 전달된 전체 필드 객체
const updatedSection = {
...selectedSection,
// 기본 필드 목록에서 업데이트
fields: (selectedSection.fields || []).map((f) => (f.id === updatedField.id ? updatedField : f)),
// 옵셔널 필드 그룹 내 필드도 업데이트
optionalFieldGroups: selectedSection.optionalFieldGroups?.map((group) => ({
...group,
fields: group.fields.map((f) => (f.id === updatedField.id ? updatedField : f)),
})),
};
// config 업데이트
onChange({
...config,
sections: config.sections.map((s) => (s.id === selectedSection.id ? updatedSection : s)),
});
// selectedSection과 selectedField 상태도 업데이트 (다음에 다시 열었을 때 최신 값 반영)
setSelectedSection(updatedSection);
setSelectedField(updatedField as FormFieldConfig);
setFieldDetailModalOpen(false);
setSectionLayoutModalOpen(true);
}}
tables={tables}
tableColumns={tableColumns}
numberingRules={numberingRules}
onLoadTableColumns={loadTableColumns}
targetTableName={config.saveConfig?.tableName}
targetTableColumns={config.saveConfig?.tableName ? tableColumns[config.saveConfig.tableName] || [] : []}
/>
)}
{/* 테이블 섹션 설정 모달 */}
{selectedSection && selectedSection.type === "table" && (
<TableSectionSettingsModal
open={tableSectionSettingsModalOpen}
onOpenChange={setTableSectionSettingsModalOpen}
section={selectedSection}
onSave={(updates) => {
const updatedSection = {
...selectedSection,
...updates,
};
// config 업데이트
onChange({
...config,
sections: config.sections.map((s) => (s.id === selectedSection.id ? updatedSection : s)),
});
setSelectedSection(updatedSection);
setTableSectionSettingsModalOpen(false);
}}
tables={tables.map((t) => ({ table_name: t.name, comment: t.label }))}
tableColumns={Object.fromEntries(
Object.entries(tableColumns).map(([tableName, cols]) => [
tableName,
cols.map((c) => ({
column_name: c.name,
data_type: c.type,
is_nullable: "YES",
comment: c.label,
input_type: c.inputType || "text",
})),
]),
)}
onLoadTableColumns={loadTableColumns}
allSections={config.sections as FormSectionConfig[]}
availableParentFields={availableParentFields}
/>
)}
</div>
);
}