2025-12-04 17:40:41 +09:00
|
|
|
"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";
|
2025-12-22 14:36:13 +09:00
|
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
2025-12-04 17:40:41 +09:00
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
|
|
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
|
2025-12-11 15:29:37 +09:00
|
|
|
import { Badge } from "@/components/ui/badge";
|
2025-12-04 17:40:41 +09:00
|
|
|
import { Separator } from "@/components/ui/separator";
|
|
|
|
|
import {
|
|
|
|
|
Plus,
|
|
|
|
|
Trash2,
|
|
|
|
|
GripVertical,
|
|
|
|
|
ChevronUp,
|
|
|
|
|
ChevronDown,
|
|
|
|
|
Settings,
|
|
|
|
|
Database,
|
|
|
|
|
Layout,
|
2025-12-18 15:19:59 +09:00
|
|
|
Table,
|
2025-12-04 17:40:41 +09:00
|
|
|
} 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,
|
2025-12-18 15:19:59 +09:00
|
|
|
SECTION_TYPE_OPTIONS,
|
2025-12-04 17:40:41 +09:00
|
|
|
} from "./types";
|
|
|
|
|
import {
|
|
|
|
|
defaultSectionConfig,
|
2025-12-18 15:19:59 +09:00
|
|
|
defaultTableSectionConfig,
|
2025-12-04 17:40:41 +09:00
|
|
|
generateSectionId,
|
|
|
|
|
} from "./config";
|
|
|
|
|
|
2025-12-11 15:29:37 +09:00
|
|
|
// 모달 import
|
|
|
|
|
import { FieldDetailSettingsModal } from "./modals/FieldDetailSettingsModal";
|
|
|
|
|
import { SaveSettingsModal } from "./modals/SaveSettingsModal";
|
|
|
|
|
import { SectionLayoutModal } from "./modals/SectionLayoutModal";
|
2025-12-18 15:19:59 +09:00
|
|
|
import { TableSectionSettingsModal } from "./modals/TableSectionSettingsModal";
|
2025-12-11 15:29:37 +09:00
|
|
|
|
2025-12-04 17:40:41 +09:00
|
|
|
// 도움말 텍스트 컴포넌트
|
|
|
|
|
const HelpText = ({ children }: { children: React.ReactNode }) => (
|
|
|
|
|
<p className="text-[10px] text-muted-foreground mt-0.5">{children}</p>
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-23 09:24:59 +09:00
|
|
|
// 부모 화면에서 전달 가능한 필드 타입
|
|
|
|
|
interface AvailableParentField {
|
|
|
|
|
name: string; // 필드명 (columnName)
|
|
|
|
|
label: string; // 표시 라벨
|
|
|
|
|
sourceComponent?: string; // 출처 컴포넌트 (예: "TableList", "SplitPanelLayout2")
|
|
|
|
|
sourceTable?: string; // 출처 테이블명
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function UniversalFormModalConfigPanel({ config, onChange, allComponents = [] }: UniversalFormModalConfigPanelProps) {
|
2025-12-04 17:40:41 +09:00
|
|
|
// 테이블 목록
|
|
|
|
|
const [tables, setTables] = useState<{ name: string; label: string }[]>([]);
|
|
|
|
|
const [tableColumns, setTableColumns] = useState<{
|
|
|
|
|
[tableName: string]: { name: string; type: string; label: string }[];
|
|
|
|
|
}>({});
|
|
|
|
|
|
2025-12-23 09:24:59 +09:00
|
|
|
// 부모 화면에서 전달 가능한 필드 목록
|
|
|
|
|
const [availableParentFields, setAvailableParentFields] = useState<AvailableParentField[]>([]);
|
|
|
|
|
|
2025-12-04 17:40:41 +09:00
|
|
|
// 채번규칙 목록
|
|
|
|
|
const [numberingRules, setNumberingRules] = useState<{ id: string; name: string }[]>([]);
|
|
|
|
|
|
2025-12-11 15:29:37 +09:00
|
|
|
// 모달 상태
|
|
|
|
|
const [saveSettingsModalOpen, setSaveSettingsModalOpen] = useState(false);
|
|
|
|
|
const [sectionLayoutModalOpen, setSectionLayoutModalOpen] = useState(false);
|
|
|
|
|
const [fieldDetailModalOpen, setFieldDetailModalOpen] = useState(false);
|
2025-12-18 15:19:59 +09:00
|
|
|
const [tableSectionSettingsModalOpen, setTableSectionSettingsModalOpen] = useState(false);
|
2025-12-11 15:29:37 +09:00
|
|
|
const [selectedSection, setSelectedSection] = useState<FormSectionConfig | null>(null);
|
|
|
|
|
const [selectedField, setSelectedField] = useState<FormFieldConfig | null>(null);
|
2025-12-04 17:40:41 +09:00
|
|
|
|
|
|
|
|
// 테이블 목록 로드
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
loadTables();
|
|
|
|
|
loadNumberingRules();
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-12-23 09:24:59 +09:00
|
|
|
// 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]);
|
|
|
|
|
|
2025-12-04 17:40:41 +09:00
|
|
|
// 저장 테이블 변경 시 컬럼 로드
|
|
|
|
|
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(
|
2025-12-23 09:24:59 +09:00
|
|
|
data.map((t: { tableName?: string; table_name?: string; displayName?: string; tableLabel?: string; table_label?: string }) => ({
|
2025-12-04 17:40:41 +09:00
|
|
|
name: t.tableName || t.table_name || "",
|
2025-12-23 09:24:59 +09:00
|
|
|
// displayName 우선, 없으면 tableLabel, 그것도 없으면 테이블명
|
|
|
|
|
label: t.displayName || t.tableLabel || t.table_label || "",
|
2025-12-04 17:40:41 +09:00
|
|
|
})),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} 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`);
|
2025-12-18 15:19:59 +09:00
|
|
|
// API 응답 구조: { success, data: { columns: [...], total, page, ... } }
|
|
|
|
|
const columns = response.data?.data?.columns;
|
2025-12-04 17:40:41 +09:00
|
|
|
|
2025-12-18 15:19:59 +09:00
|
|
|
if (response.data?.success && Array.isArray(columns)) {
|
2025-12-04 17:40:41 +09:00
|
|
|
setTableColumns((prev) => ({
|
|
|
|
|
...prev,
|
2025-12-18 15:19:59 +09:00
|
|
|
[tableName]: columns.map(
|
2025-12-04 17:40:41 +09:00
|
|
|
(c: {
|
|
|
|
|
columnName?: string;
|
|
|
|
|
column_name?: string;
|
|
|
|
|
dataType?: string;
|
|
|
|
|
data_type?: string;
|
2025-12-18 15:19:59 +09:00
|
|
|
displayName?: string;
|
2025-12-11 15:29:37 +09:00
|
|
|
columnComment?: string;
|
|
|
|
|
column_comment?: string;
|
2025-12-04 17:40:41 +09:00
|
|
|
}) => ({
|
2025-12-11 15:29:37 +09:00
|
|
|
name: c.columnName || c.column_name || "",
|
|
|
|
|
type: c.dataType || c.data_type || "text",
|
2025-12-18 15:19:59 +09:00
|
|
|
label: c.displayName || c.columnComment || c.column_comment || c.columnName || c.column_name || "",
|
2025-12-04 17:40:41 +09:00
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`테이블 컬럼 로드 실패 (${tableName}):`, error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loadNumberingRules = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await getNumberingRules();
|
2025-12-11 15:29:37 +09:00
|
|
|
const data = response?.data;
|
2025-12-04 17:40:41 +09:00
|
|
|
|
2025-12-11 15:29:37 +09:00
|
|
|
if (response?.success && Array.isArray(data)) {
|
2025-12-04 17:40:41 +09:00
|
|
|
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) {
|
2025-12-11 15:29:37 +09:00
|
|
|
console.error("채번규칙 목록 로드 실패:", error);
|
2025-12-04 17:40:41 +09:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 설정 업데이트 헬퍼
|
|
|
|
|
const updateModalConfig = useCallback(
|
|
|
|
|
(updates: Partial<UniversalFormModalConfig["modal"]>) => {
|
|
|
|
|
onChange({
|
|
|
|
|
...config,
|
|
|
|
|
modal: { ...config.modal, ...updates },
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
[config, onChange],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 섹션 관리
|
2025-12-18 15:19:59 +09:00
|
|
|
const addSection = useCallback((type: "fields" | "table" = "fields") => {
|
2025-12-04 17:40:41 +09:00
|
|
|
const newSection: FormSectionConfig = {
|
|
|
|
|
...defaultSectionConfig,
|
|
|
|
|
id: generateSectionId(),
|
2025-12-18 15:19:59 +09:00
|
|
|
title: type === "table" ? `테이블 섹션 ${config.sections.length + 1}` : `섹션 ${config.sections.length + 1}`,
|
|
|
|
|
type,
|
|
|
|
|
fields: type === "fields" ? [] : undefined,
|
|
|
|
|
tableConfig: type === "table" ? { ...defaultTableSectionConfig } : undefined,
|
2025-12-04 17:40:41 +09:00
|
|
|
};
|
|
|
|
|
onChange({
|
|
|
|
|
...config,
|
|
|
|
|
sections: [...config.sections, newSection],
|
|
|
|
|
});
|
|
|
|
|
}, [config, onChange]);
|
2025-12-18 15:19:59 +09:00
|
|
|
|
|
|
|
|
// 섹션 타입 변경
|
|
|
|
|
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);
|
|
|
|
|
};
|
2025-12-04 17:40:41 +09:00
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
});
|
|
|
|
|
},
|
2025-12-11 15:29:37 +09:00
|
|
|
[config, onChange],
|
2025-12-04 17:40:41 +09:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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],
|
|
|
|
|
);
|
|
|
|
|
|
2025-12-11 15:29:37 +09:00
|
|
|
// 필드 타입별 색상
|
|
|
|
|
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";
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-12-04 17:40:41 +09:00
|
|
|
|
2025-12-11 15:29:37 +09:00
|
|
|
// 섹션 레이아웃 모달 열기
|
|
|
|
|
const handleOpenSectionLayout = (section: FormSectionConfig) => {
|
|
|
|
|
setSelectedSection(section);
|
|
|
|
|
setSectionLayoutModalOpen(true);
|
|
|
|
|
};
|
2025-12-04 17:40:41 +09:00
|
|
|
|
2025-12-11 15:29:37 +09:00
|
|
|
// 필드 상세 설정 모달 열기
|
|
|
|
|
const handleOpenFieldDetail = (section: FormSectionConfig, field: FormFieldConfig) => {
|
|
|
|
|
setSelectedSection(section);
|
|
|
|
|
setSelectedField(field);
|
|
|
|
|
setFieldDetailModalOpen(true);
|
|
|
|
|
};
|
2025-12-04 17:40:41 +09:00
|
|
|
|
|
|
|
|
return (
|
2025-12-11 15:29:37 +09:00
|
|
|
<div className="h-full flex flex-col overflow-hidden w-full min-w-0">
|
|
|
|
|
<div className="flex-1 overflow-y-auto overflow-x-hidden w-full min-w-0">
|
|
|
|
|
<div className="space-y-4 p-4 w-full min-w-0 max-w-full">
|
2025-12-04 17:40:41 +09:00
|
|
|
{/* 모달 기본 설정 */}
|
2025-12-11 15:29:37 +09:00
|
|
|
<Accordion type="single" collapsible defaultValue="modal-settings" className="w-full min-w-0">
|
|
|
|
|
<AccordionItem value="modal-settings" className="border rounded-lg w-full min-w-0">
|
|
|
|
|
<AccordionTrigger className="px-4 py-3 text-sm font-medium hover:no-underline w-full min-w-0">
|
|
|
|
|
<div className="flex items-center gap-2 w-full min-w-0">
|
|
|
|
|
<Settings className="h-4 w-4 shrink-0" />
|
|
|
|
|
<span className="truncate">모달 기본 설정</span>
|
2025-12-04 17:40:41 +09:00
|
|
|
</div>
|
|
|
|
|
</AccordionTrigger>
|
2025-12-11 15:29:37 +09:00
|
|
|
<AccordionContent className="px-4 pb-4 space-y-4 w-full min-w-0">
|
|
|
|
|
<div className="w-full min-w-0">
|
|
|
|
|
<Label className="text-xs font-medium mb-1.5 block">모달 제목</Label>
|
2025-12-04 17:40:41 +09:00
|
|
|
<Input
|
2025-12-11 15:29:37 +09:00
|
|
|
value={config.modal.title}
|
2025-12-04 17:40:41 +09:00
|
|
|
onChange={(e) => updateModalConfig({ title: e.target.value })}
|
2025-12-11 15:29:37 +09:00
|
|
|
className="h-9 text-sm w-full max-w-full"
|
2025-12-04 17:40:41 +09:00
|
|
|
/>
|
2025-12-11 15:29:37 +09:00
|
|
|
<HelpText>모달 상단에 표시될 제목입니다</HelpText>
|
2025-12-04 17:40:41 +09:00
|
|
|
</div>
|
|
|
|
|
|
2025-12-11 15:29:37 +09:00
|
|
|
<div className="w-full min-w-0">
|
|
|
|
|
<Label className="text-xs font-medium mb-1.5 block">모달 크기</Label>
|
|
|
|
|
<Select value={config.modal.size} onValueChange={(value: any) => updateModalConfig({ size: value })}>
|
|
|
|
|
<SelectTrigger className="h-9 text-sm w-full max-w-full">
|
2025-12-04 17:40:41 +09:00
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{MODAL_SIZE_OPTIONS.map((opt) => (
|
|
|
|
|
<SelectItem key={opt.value} value={opt.value}>
|
|
|
|
|
{opt.label}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-12-11 15:29:37 +09:00
|
|
|
<HelpText>모달 창의 크기를 선택하세요</HelpText>
|
2025-12-04 17:40:41 +09:00
|
|
|
</div>
|
|
|
|
|
|
2025-12-22 14:36:13 +09:00
|
|
|
{/* 저장 버튼 표시 설정 */}
|
|
|
|
|
<div className="w-full min-w-0">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id="show-save-button"
|
|
|
|
|
checked={config.modal.showSaveButton !== false}
|
|
|
|
|
onCheckedChange={(checked) => updateModalConfig({ showSaveButton: checked === true })}
|
|
|
|
|
/>
|
|
|
|
|
<Label htmlFor="show-save-button" className="text-xs font-medium cursor-pointer">
|
|
|
|
|
저장 버튼 표시
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
<HelpText>체크 해제 시 모달 하단의 저장 버튼이 숨겨집니다</HelpText>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-11 15:29:37 +09:00
|
|
|
<div className="space-y-3 w-full min-w-0">
|
|
|
|
|
<div className="w-full min-w-0">
|
|
|
|
|
<Label className="text-xs font-medium mb-1.5 block">저장 버튼 텍스트</Label>
|
|
|
|
|
<Input
|
|
|
|
|
value={config.modal.saveButtonText || "저장"}
|
|
|
|
|
onChange={(e) => updateModalConfig({ saveButtonText: e.target.value })}
|
|
|
|
|
className="h-9 text-sm w-full max-w-full"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="w-full min-w-0">
|
|
|
|
|
<Label className="text-xs font-medium mb-1.5 block">취소 버튼 텍스트</Label>
|
|
|
|
|
<Input
|
|
|
|
|
value={config.modal.cancelButtonText || "취소"}
|
|
|
|
|
onChange={(e) => updateModalConfig({ cancelButtonText: e.target.value })}
|
|
|
|
|
className="h-9 text-sm w-full max-w-full"
|
2025-12-09 14:55:49 +09:00
|
|
|
/>
|
|
|
|
|
</div>
|
2025-12-04 17:40:41 +09:00
|
|
|
</div>
|
|
|
|
|
</AccordionContent>
|
|
|
|
|
</AccordionItem>
|
|
|
|
|
</Accordion>
|
|
|
|
|
|
|
|
|
|
{/* 저장 설정 */}
|
2025-12-11 15:29:37 +09:00
|
|
|
<Accordion type="single" collapsible defaultValue="save-settings" className="w-full min-w-0">
|
|
|
|
|
<AccordionItem value="save-settings" className="border rounded-lg w-full min-w-0">
|
|
|
|
|
<AccordionTrigger className="px-4 py-3 text-sm font-medium hover:no-underline w-full min-w-0">
|
|
|
|
|
<div className="flex items-center gap-2 w-full min-w-0">
|
|
|
|
|
<Database className="h-4 w-4 shrink-0" />
|
|
|
|
|
<span className="truncate">저장 설정</span>
|
2025-12-04 17:40:41 +09:00
|
|
|
</div>
|
|
|
|
|
</AccordionTrigger>
|
2025-12-11 15:29:37 +09:00
|
|
|
<AccordionContent className="px-4 pb-4 space-y-4 w-full min-w-0">
|
|
|
|
|
<div className="space-y-3 w-full min-w-0">
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<Label className="text-xs font-medium mb-1.5 block">저장 테이블</Label>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
{config.saveConfig.tableName || "(미설정)"}
|
2025-12-08 17:54:11 +09:00
|
|
|
</p>
|
2025-12-11 15:29:37 +09:00
|
|
|
{config.saveConfig.customApiSave?.enabled && config.saveConfig.customApiSave?.multiTable?.enabled && (
|
|
|
|
|
<Badge variant="secondary" className="text-xs px-2 py-0.5 mt-2">
|
|
|
|
|
다중 테이블 모드
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
2025-12-04 17:40:41 +09:00
|
|
|
</div>
|
2025-12-11 15:29:37 +09:00
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => setSaveSettingsModalOpen(true)}
|
|
|
|
|
className="h-9 text-xs w-full"
|
|
|
|
|
>
|
|
|
|
|
<Settings className="h-4 w-4 mr-2" />
|
|
|
|
|
저장 설정 열기
|
|
|
|
|
</Button>
|
2025-12-04 17:40:41 +09:00
|
|
|
</div>
|
2025-12-11 15:29:37 +09:00
|
|
|
<HelpText>
|
|
|
|
|
데이터를 저장할 테이블과 방식을 설정합니다.
|
|
|
|
|
<br />
|
|
|
|
|
"저장 설정 열기"를 클릭하여 상세 설정을 변경하세요.
|
|
|
|
|
</HelpText>
|
2025-12-04 17:40:41 +09:00
|
|
|
</AccordionContent>
|
|
|
|
|
</AccordionItem>
|
|
|
|
|
</Accordion>
|
|
|
|
|
|
2025-12-11 15:29:37 +09:00
|
|
|
{/* 섹션 구성 */}
|
|
|
|
|
<Accordion type="single" collapsible defaultValue="sections" className="w-full min-w-0">
|
|
|
|
|
<AccordionItem value="sections" className="border rounded-lg w-full min-w-0">
|
|
|
|
|
<AccordionTrigger className="px-4 py-3 text-sm font-medium hover:no-underline w-full min-w-0">
|
|
|
|
|
<div className="flex items-center gap-2 w-full min-w-0">
|
|
|
|
|
<Layout className="h-4 w-4 shrink-0" />
|
|
|
|
|
<span className="truncate">섹션 구성</span>
|
|
|
|
|
<Badge variant="secondary" className="text-xs px-2 py-0.5 shrink-0">
|
|
|
|
|
{config.sections.length}개
|
|
|
|
|
</Badge>
|
2025-12-04 17:40:41 +09:00
|
|
|
</div>
|
|
|
|
|
</AccordionTrigger>
|
2025-12-11 15:29:37 +09:00
|
|
|
<AccordionContent className="px-4 pb-4 space-y-4 w-full min-w-0">
|
2025-12-18 15:19:59 +09:00
|
|
|
{/* 섹션 추가 버튼들 */}
|
|
|
|
|
<div className="flex gap-2 w-full min-w-0">
|
|
|
|
|
<Button size="sm" variant="outline" onClick={() => addSection("fields")} className="h-9 text-xs flex-1 min-w-0">
|
|
|
|
|
<Plus className="h-4 w-4 mr-1 shrink-0" />
|
|
|
|
|
<span className="truncate">필드 섹션</span>
|
|
|
|
|
</Button>
|
|
|
|
|
<Button size="sm" variant="outline" onClick={() => addSection("table")} className="h-9 text-xs flex-1 min-w-0">
|
|
|
|
|
<Table className="h-4 w-4 mr-1 shrink-0" />
|
|
|
|
|
<span className="truncate">테이블 섹션</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-12-11 15:29:37 +09:00
|
|
|
<HelpText>
|
2025-12-18 15:19:59 +09:00
|
|
|
필드 섹션: 일반 입력 필드들을 배치합니다.
|
2025-12-11 15:29:37 +09:00
|
|
|
<br />
|
2025-12-18 15:19:59 +09:00
|
|
|
테이블 섹션: 품목 목록 등 반복 테이블 형식 데이터를 관리합니다.
|
2025-12-11 15:29:37 +09:00
|
|
|
</HelpText>
|
|
|
|
|
|
|
|
|
|
{config.sections.length === 0 ? (
|
|
|
|
|
<div className="text-center py-12 border border-dashed rounded-lg w-full bg-muted/20">
|
|
|
|
|
<p className="text-sm text-muted-foreground mb-2 font-medium">섹션이 없습니다</p>
|
2025-12-18 15:19:59 +09:00
|
|
|
<p className="text-xs text-muted-foreground">위 버튼으로 섹션을 추가하세요</p>
|
2025-12-11 15:29:37 +09:00
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-3 w-full min-w-0">
|
|
|
|
|
{config.sections.map((section, index) => (
|
|
|
|
|
<div key={section.id} className="border rounded-lg p-3 bg-card w-full min-w-0 overflow-hidden space-y-3">
|
2025-12-18 15:19:59 +09:00
|
|
|
{/* 헤더: 제목 + 타입 배지 + 삭제 */}
|
2025-12-11 15:29:37 +09:00
|
|
|
<div className="flex items-start justify-between gap-3 w-full min-w-0">
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<div className="flex items-center gap-2 mb-1.5">
|
|
|
|
|
<span className="text-sm font-medium truncate">{section.title}</span>
|
2025-12-18 15:19:59 +09:00
|
|
|
{section.type === "table" ? (
|
|
|
|
|
<Badge variant="outline" className="text-xs px-1.5 py-0.5 text-purple-600 bg-purple-50 border-purple-200">
|
|
|
|
|
테이블
|
|
|
|
|
</Badge>
|
|
|
|
|
) : section.repeatable ? (
|
2025-12-11 15:29:37 +09:00
|
|
|
<Badge variant="outline" className="text-xs px-1.5 py-0.5">
|
|
|
|
|
반복
|
|
|
|
|
</Badge>
|
2025-12-18 15:19:59 +09:00
|
|
|
) : null}
|
2025-12-11 15:29:37 +09:00
|
|
|
</div>
|
2025-12-18 15:19:59 +09:00
|
|
|
{section.type === "table" ? (
|
|
|
|
|
<Badge variant="secondary" className="text-xs px-2 py-0.5">
|
|
|
|
|
{section.tableConfig?.source?.tableName || "(소스 미설정)"}
|
|
|
|
|
</Badge>
|
|
|
|
|
) : (
|
|
|
|
|
<Badge variant="secondary" className="text-xs px-2 py-0.5">
|
|
|
|
|
{(section.fields || []).length}개 필드
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
2025-12-11 15:29:37 +09:00
|
|
|
</div>
|
|
|
|
|
|
2025-12-04 17:40:41 +09:00
|
|
|
<Button
|
2025-12-11 15:29:37 +09:00
|
|
|
size="sm"
|
2025-12-04 17:40:41 +09:00
|
|
|
variant="ghost"
|
2025-12-11 15:29:37 +09:00
|
|
|
onClick={() => removeSection(section.id)}
|
|
|
|
|
className="h-7 w-7 p-0 text-destructive hover:text-destructive shrink-0"
|
2025-12-04 17:40:41 +09:00
|
|
|
>
|
2025-12-11 15:29:37 +09:00
|
|
|
<Trash2 className="h-4 w-4" />
|
2025-12-04 17:40:41 +09:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-12-05 12:59:03 +09:00
|
|
|
|
2025-12-11 15:29:37 +09:00
|
|
|
{/* 순서 조정 버튼 */}
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<GripVertical className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
|
|
|
<div className="flex gap-1">
|
2025-12-04 17:40:41 +09:00
|
|
|
<Button
|
2025-12-11 15:29:37 +09:00
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => moveSectionUp(index)}
|
|
|
|
|
disabled={index === 0}
|
|
|
|
|
className="h-7 px-2 text-xs"
|
2025-12-04 17:40:41 +09:00
|
|
|
>
|
2025-12-11 15:29:37 +09:00
|
|
|
<ChevronUp className="h-3.5 w-3.5" />
|
2025-12-04 17:40:41 +09:00
|
|
|
</Button>
|
2025-12-11 15:29:37 +09:00
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => moveSectionDown(index)}
|
|
|
|
|
disabled={index === config.sections.length - 1}
|
|
|
|
|
className="h-7 px-2 text-xs"
|
2025-12-04 17:40:41 +09:00
|
|
|
>
|
2025-12-11 15:29:37 +09:00
|
|
|
<ChevronDown className="h-3.5 w-3.5" />
|
|
|
|
|
</Button>
|
2025-12-04 17:40:41 +09:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-12-18 15:19:59 +09:00
|
|
|
{/* 필드 목록 (필드 타입만) */}
|
|
|
|
|
{section.type !== "table" && (section.fields || []).length > 0 && (
|
2025-12-11 15:29:37 +09:00
|
|
|
<div className="flex flex-wrap gap-1.5 max-w-full overflow-hidden pt-1">
|
2025-12-18 15:19:59 +09:00
|
|
|
{(section.fields || []).slice(0, 4).map((field) => (
|
2025-12-11 15:29:37 +09:00
|
|
|
<Badge
|
|
|
|
|
key={field.id}
|
2025-12-05 17:25:12 +09:00
|
|
|
variant="outline"
|
2025-12-11 15:29:37 +09:00
|
|
|
className={cn("text-xs px-2 py-0.5 shrink-0", getFieldTypeColor(field.fieldType))}
|
2025-12-05 17:25:12 +09:00
|
|
|
>
|
2025-12-11 15:29:37 +09:00
|
|
|
{field.label}
|
|
|
|
|
</Badge>
|
|
|
|
|
))}
|
2025-12-18 15:19:59 +09:00
|
|
|
{(section.fields || []).length > 4 && (
|
2025-12-11 15:29:37 +09:00
|
|
|
<Badge variant="outline" className="text-xs px-2 py-0.5 shrink-0">
|
2025-12-18 15:19:59 +09:00
|
|
|
+{(section.fields || []).length - 4}
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 테이블 컬럼 목록 (테이블 타입만) */}
|
|
|
|
|
{section.type === "table" && section.tableConfig?.columns && section.tableConfig.columns.length > 0 && (
|
|
|
|
|
<div className="flex flex-wrap gap-1.5 max-w-full overflow-hidden pt-1">
|
|
|
|
|
{section.tableConfig.columns.slice(0, 4).map((col) => (
|
|
|
|
|
<Badge
|
|
|
|
|
key={col.field}
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="text-xs px-2 py-0.5 shrink-0 text-purple-600 bg-purple-50 border-purple-200"
|
|
|
|
|
>
|
|
|
|
|
{col.label}
|
|
|
|
|
</Badge>
|
|
|
|
|
))}
|
|
|
|
|
{section.tableConfig.columns.length > 4 && (
|
|
|
|
|
<Badge variant="outline" className="text-xs px-2 py-0.5 shrink-0">
|
|
|
|
|
+{section.tableConfig.columns.length - 4}
|
2025-12-11 15:29:37 +09:00
|
|
|
</Badge>
|
2025-12-05 17:25:12 +09:00
|
|
|
)}
|
|
|
|
|
</div>
|
2025-12-11 15:29:37 +09:00
|
|
|
)}
|
|
|
|
|
|
2025-12-18 15:19:59 +09:00
|
|
|
{/* 설정 버튼 (타입에 따라 다름) */}
|
|
|
|
|
{section.type === "table" ? (
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => handleOpenTableSectionSettings(section)}
|
|
|
|
|
className="h-9 text-xs w-full"
|
|
|
|
|
>
|
|
|
|
|
<Table className="h-4 w-4 mr-2" />
|
|
|
|
|
테이블 설정
|
|
|
|
|
</Button>
|
|
|
|
|
) : (
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
onClick={() => handleOpenSectionLayout(section)}
|
|
|
|
|
className="h-9 text-xs w-full"
|
|
|
|
|
>
|
|
|
|
|
<Layout className="h-4 w-4 mr-2" />
|
|
|
|
|
레이아웃 설정
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2025-12-11 15:29:37 +09:00
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</AccordionContent>
|
|
|
|
|
</AccordionItem>
|
|
|
|
|
</Accordion>
|
|
|
|
|
</div>
|
2025-12-04 17:40:41 +09:00
|
|
|
</div>
|
2025-12-11 15:29:37 +09:00
|
|
|
|
|
|
|
|
{/* 저장 설정 모달 */}
|
|
|
|
|
<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) => {
|
2025-12-17 14:30:29 +09:00
|
|
|
// config 업데이트
|
2025-12-11 15:29:37 +09:00
|
|
|
updateSection(selectedSection.id, updates);
|
2025-12-17 14:30:29 +09:00
|
|
|
// selectedSection 상태도 업데이트 (최신 상태 유지)
|
|
|
|
|
setSelectedSection({ ...selectedSection, ...updates });
|
2025-12-11 15:29:37 +09:00
|
|
|
setSectionLayoutModalOpen(false);
|
|
|
|
|
}}
|
|
|
|
|
onOpenFieldDetail={(field) => {
|
|
|
|
|
setSectionLayoutModalOpen(false);
|
|
|
|
|
setSelectedField(field);
|
|
|
|
|
setFieldDetailModalOpen(true);
|
|
|
|
|
}}
|
2025-12-23 09:24:59 +09:00
|
|
|
tableName={config.saveConfig.tableName}
|
|
|
|
|
tableColumns={tableColumns[config.saveConfig.tableName || ""]?.map(col => ({
|
|
|
|
|
name: col.name,
|
|
|
|
|
type: col.type,
|
|
|
|
|
label: col.label || col.name
|
|
|
|
|
})) || []}
|
2025-12-11 15:29:37 +09:00
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 필드 상세 설정 모달 */}
|
|
|
|
|
{selectedSection && selectedField && (
|
|
|
|
|
<FieldDetailSettingsModal
|
|
|
|
|
open={fieldDetailModalOpen}
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
setFieldDetailModalOpen(open);
|
|
|
|
|
if (!open) {
|
|
|
|
|
// 필드 상세 모달을 닫으면 섹션 레이아웃 모달을 다시 엽니다
|
|
|
|
|
setSectionLayoutModalOpen(true);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
field={selectedField}
|
2025-12-17 14:30:29 +09:00
|
|
|
onSave={(updatedField) => {
|
|
|
|
|
// updatedField는 FieldDetailSettingsModal에서 전달된 전체 필드 객체
|
|
|
|
|
const updatedSection = {
|
|
|
|
|
...selectedSection,
|
|
|
|
|
// 기본 필드 목록에서 업데이트
|
2025-12-18 15:19:59 +09:00
|
|
|
fields: (selectedSection.fields || []).map((f) => (f.id === updatedField.id ? updatedField : f)),
|
2025-12-17 14:30:29 +09:00
|
|
|
// 옵셔널 필드 그룹 내 필드도 업데이트
|
|
|
|
|
optionalFieldGroups: selectedSection.optionalFieldGroups?.map((group) => ({
|
|
|
|
|
...group,
|
|
|
|
|
fields: group.fields.map((f) => (f.id === updatedField.id ? updatedField : f)),
|
|
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// config 업데이트
|
2025-12-11 15:29:37 +09:00
|
|
|
onChange({
|
|
|
|
|
...config,
|
|
|
|
|
sections: config.sections.map((s) =>
|
2025-12-17 14:30:29 +09:00
|
|
|
s.id === selectedSection.id ? updatedSection : s
|
2025-12-11 15:29:37 +09:00
|
|
|
),
|
|
|
|
|
});
|
2025-12-17 14:30:29 +09:00
|
|
|
|
|
|
|
|
// selectedSection과 selectedField 상태도 업데이트 (다음에 다시 열었을 때 최신 값 반영)
|
|
|
|
|
setSelectedSection(updatedSection);
|
|
|
|
|
setSelectedField(updatedField as FormFieldConfig);
|
2025-12-11 15:29:37 +09:00
|
|
|
setFieldDetailModalOpen(false);
|
|
|
|
|
setSectionLayoutModalOpen(true);
|
|
|
|
|
}}
|
|
|
|
|
tables={tables}
|
|
|
|
|
tableColumns={tableColumns}
|
|
|
|
|
numberingRules={numberingRules}
|
|
|
|
|
onLoadTableColumns={loadTableColumns}
|
2025-12-23 09:24:59 +09:00
|
|
|
availableParentFields={availableParentFields}
|
2025-12-11 15:29:37 +09:00
|
|
|
/>
|
|
|
|
|
)}
|
2025-12-18 15:19:59 +09:00
|
|
|
|
|
|
|
|
{/* 테이블 섹션 설정 모달 */}
|
|
|
|
|
{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,
|
|
|
|
|
})),
|
|
|
|
|
])
|
|
|
|
|
)}
|
|
|
|
|
onLoadTableColumns={loadTableColumns}
|
2025-12-19 11:48:46 +09:00
|
|
|
allSections={config.sections as FormSectionConfig[]}
|
2025-12-23 09:24:59 +09:00
|
|
|
availableParentFields={availableParentFields}
|
2025-12-18 15:19:59 +09:00
|
|
|
/>
|
|
|
|
|
)}
|
2025-12-11 15:29:37 +09:00
|
|
|
</div>
|
2025-12-04 17:40:41 +09:00
|
|
|
);
|
|
|
|
|
}
|