"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 { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import {
Plus,
Trash2,
GripVertical,
ChevronUp,
ChevronDown,
Settings,
Database,
Layout,
Hash,
Check,
ChevronsUpDown,
} from "lucide-react";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
import { apiClient } from "@/lib/api/client";
import { getNumberingRules } from "@/lib/api/numberingRule";
import {
UniversalFormModalConfig,
UniversalFormModalConfigPanelProps,
FormSectionConfig,
FormFieldConfig,
FIELD_TYPE_OPTIONS,
MODAL_SIZE_OPTIONS,
SELECT_OPTION_TYPE_OPTIONS,
} from "./types";
import {
defaultFieldConfig,
defaultSectionConfig,
defaultNumberingRuleConfig,
defaultSelectOptionsConfig,
generateSectionId,
generateFieldId,
} from "./config";
// 도움말 텍스트 컴포넌트
const HelpText = ({ children }: { children: React.ReactNode }) => (
{children}
);
export function UniversalFormModalConfigPanel({ config, onChange }: UniversalFormModalConfigPanelProps) {
// 테이블 목록
const [tables, setTables] = useState<{ name: string; label: string }[]>([]);
const [tableColumns, setTableColumns] = useState<{
[tableName: string]: { name: string; type: string; label: string }[];
}>({});
// 채번규칙 목록
const [numberingRules, setNumberingRules] = useState<{ id: string; name: string }[]>([]);
// 선택된 섹션/필드
const [selectedSectionId, setSelectedSectionId] = useState(null);
const [selectedFieldId, setSelectedFieldId] = useState(null);
// 테이블 선택 Combobox 상태
const [tableSelectOpen, setTableSelectOpen] = useState(false);
// 테이블 목록 로드
useEffect(() => {
console.log("[UniversalFormModal ConfigPanel] 초기화 - 테이블 및 채번규칙 로드");
loadTables();
loadNumberingRules();
}, []);
// 저장 테이블 변경 시 컬럼 로드
useEffect(() => {
if (config.saveConfig.tableName) {
loadTableColumns(config.saveConfig.tableName);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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; tableLabel?: string; table_label?: string }) => ({
name: t.tableName || t.table_name || "",
label: t.tableLabel || t.table_label || t.tableName || t.table_name || "",
})),
);
}
} catch (error) {
console.error("테이블 목록 로드 실패:", error);
}
};
const loadTableColumns = async (tableName: string) => {
console.log(`[UniversalFormModal] 테이블 컬럼 로드 시도: ${tableName}`);
if (!tableName || (tableColumns[tableName] && tableColumns[tableName].length > 0)) return;
try {
const response = await apiClient.get(`/table-management/tables/${tableName}/columns`);
console.log("[UniversalFormModal] 테이블 컬럼 응답:", response.data);
// API 응답 구조: { success: true, data: { columns: [...] } }
const data = response.data?.data?.columns || response.data?.data;
if (response.data?.success && Array.isArray(data)) {
console.log(`[UniversalFormModal] 파싱된 컬럼 ${data.length}개:`, data);
setTableColumns((prev) => ({
...prev,
[tableName]: data.map(
(c: {
columnName?: string;
column_name?: string;
dataType?: string;
data_type?: string;
columnLabel?: string;
column_label?: string;
name?: string;
}) => ({
name: c.columnName || c.column_name || c.name || "",
type: c.dataType || c.data_type || "",
label: c.columnLabel || c.column_label || c.columnName || c.column_name || c.name || "",
}),
),
}));
} else {
console.warn("[UniversalFormModal] 컬럼 데이터 없음 또는 형식 오류:", response.data);
setTableColumns((prev) => ({ ...prev, [tableName]: [] }));
}
} catch (error) {
console.error(`테이블 컬럼 로드 실패 (${tableName}):`, error);
setTableColumns((prev) => ({ ...prev, [tableName]: [] }));
}
};
const loadNumberingRules = async () => {
try {
console.log("[UniversalFormModal] 채번규칙 로드 시도");
const response = await getNumberingRules();
console.log("[UniversalFormModal] 채번규칙 응답:", response);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: any = response.data;
if (response.success && Array.isArray(data) && data.length > 0) {
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 || "",
}),
);
console.log("[UniversalFormModal] 파싱된 채번규칙:", rules);
setNumberingRules(rules);
} else {
console.warn("[UniversalFormModal] 채번규칙 데이터 없음:", data);
}
} catch (error) {
console.error("[UniversalFormModal] 채번규칙 목록 로드 실패:", error);
}
};
// 설정 업데이트 헬퍼
const updateModalConfig = useCallback(
(updates: Partial) => {
onChange({
...config,
modal: { ...config.modal, ...updates },
});
},
[config, onChange],
);
const updateSaveConfig = useCallback(
(updates: Partial) => {
onChange({
...config,
saveConfig: { ...config.saveConfig, ...updates },
});
},
[config, onChange],
);
// 섹션 관리
const addSection = useCallback(() => {
const newSection: FormSectionConfig = {
...defaultSectionConfig,
id: generateSectionId(),
title: `섹션 ${config.sections.length + 1}`,
};
onChange({
...config,
sections: [...config.sections, newSection],
});
setSelectedSectionId(newSection.id);
}, [config, onChange]);
const updateSection = useCallback(
(sectionId: string, updates: Partial) => {
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),
});
if (selectedSectionId === sectionId) {
setSelectedSectionId(null);
setSelectedFieldId(null);
}
},
[config, onChange, selectedSectionId],
);
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 addField = useCallback(
(sectionId: string) => {
const newField: FormFieldConfig = {
...defaultFieldConfig,
id: generateFieldId(),
label: "새 필드",
numberingRule: { ...defaultNumberingRuleConfig },
selectOptions: { ...defaultSelectOptionsConfig },
};
onChange({
...config,
sections: config.sections.map((s) =>
s.id === sectionId ? { ...s, fields: [...s.fields, newField] } : s,
),
});
setSelectedSectionId(sectionId);
setSelectedFieldId(newField.id);
},
[config, onChange],
);
const updateField = useCallback(
(sectionId: string, fieldId: string, updates: Partial) => {
onChange({
...config,
sections: config.sections.map((s) =>
s.id === sectionId
? {
...s,
fields: s.fields.map((f) => (f.id === fieldId ? { ...f, ...updates } : f)),
}
: s,
),
});
},
[config, onChange],
);
const removeField = useCallback(
(sectionId: string, fieldId: string) => {
onChange({
...config,
sections: config.sections.map((s) =>
s.id === sectionId ? { ...s, fields: s.fields.filter((f) => f.id !== fieldId) } : s,
),
});
if (selectedFieldId === fieldId) {
setSelectedFieldId(null);
}
},
[config, onChange, selectedFieldId],
);
// 선택된 섹션/필드 가져오기
const selectedSection = config.sections.find((s) => s.id === selectedSectionId);
const selectedField = selectedSection?.fields.find((f) => f.id === selectedFieldId);
// 현재 테이블의 컬럼 목록
const currentColumns = tableColumns[config.saveConfig.tableName] || [];
return (
{/* 모달 기본 설정 */}
모달 기본 설정
updateModalConfig({ title: e.target.value })}
placeholder="모달 제목 입력"
className="h-7 text-xs mt-1"
/>
updateModalConfig({ saveButtonText: e.target.value })}
className="h-7 text-xs mt-1"
/>
updateModalConfig({ cancelButtonText: e.target.value })}
className="h-7 text-xs mt-1"
/>
{/* 저장 설정 */}
저장 설정
{/* 저장 테이블 - Combobox */}
테이블을 찾을 수 없습니다
{tables.map((t) => (
{
updateSaveConfig({ tableName: t.name });
setTableSelectOpen(false);
}}
className="text-xs"
>
{t.name}
{t.label !== t.name && (
({t.label})
)}
))}
{config.saveConfig.tableName && (
컬럼 {currentColumns.length}개 로드됨
)}
{/* 다중 행 저장 설정 */}
다중 행 저장
updateSaveConfig({
multiRowSave: { ...config.saveConfig.multiRowSave, enabled: checked },
})
}
/>
겸직처럼 하나의 폼에서 여러 행을 저장할 때 사용합니다.
{config.saveConfig.multiRowSave?.enabled && (
)}
{/* 저장 후 동작 */}
모달 닫기
updateSaveConfig({
afterSave: { ...config.saveConfig.afterSave, closeModal: checked },
})
}
/>
부모 화면 새로고침
updateSaveConfig({
afterSave: { ...config.saveConfig.afterSave, refreshParent: checked },
})
}
/>
토스트 메시지 표시
updateSaveConfig({
afterSave: { ...config.saveConfig.afterSave, showToast: checked },
})
}
/>
{/* 섹션 관리 */}
섹션 관리 ({config.sections.length})
{config.sections.map((section, sectionIndex) => (
{
setSelectedSectionId(section.id);
setSelectedFieldId(null);
}}
>
{section.title}
{section.repeatable && (
반복
)}
필드 {section.fields.length}개
))}
{/* 선택된 섹션 설정 */}
{selectedSection && (
섹션: {selectedSection.title}
updateSection(selectedSection.id, { title: e.target.value })}
className="h-7 text-xs mt-1"
/>
접을 수 있음
updateSection(selectedSection.id, { collapsible: checked })}
/>
필드들을 몇 열로 배치할지 설정합니다.
반복 섹션
updateSection(selectedSection.id, { repeatable: checked })}
/>
겸직처럼 동일한 필드 그룹을 여러 개 추가할 수 있습니다.
{selectedSection.repeatable && (
updateSection(selectedSection.id, {
repeatConfig: {
...selectedSection.repeatConfig,
minItems: parseInt(e.target.value) || 0,
},
})
}
className="h-6 text-[10px] mt-1"
/>
updateSection(selectedSection.id, {
repeatConfig: {
...selectedSection.repeatConfig,
maxItems: parseInt(e.target.value) || 10,
},
})
}
className="h-6 text-[10px] mt-1"
/>
updateSection(selectedSection.id, {
repeatConfig: {
...selectedSection.repeatConfig,
addButtonText: e.target.value,
},
})
}
className="h-6 text-[10px] mt-1"
/>
)}
{/* 필드 목록 */}
{selectedSection.fields.length === 0 ? (
필드가 없습니다
) : (
{selectedSection.fields.map((field) => (
setSelectedFieldId(field.id)}
>
{field.label}
{field.columnName && (
({field.columnName})
)}
{field.required && *}
))}
)}
)}
{/* 선택된 필드 설정 */}
{selectedSection && selectedField && (
필드: {selectedField.label}
{/* 기본 정보 */}
updateField(selectedSection.id, selectedField.id, { label: e.target.value })}
className="h-7 text-xs mt-1"
/>
{currentColumns.length > 0 ? (
) : (
updateField(selectedSection.id, selectedField.id, { columnName: e.target.value })
}
placeholder="테이블을 먼저 선택하세요"
className="h-7 text-xs mt-1"
/>
)}
12칸 그리드 기준입니다.
updateField(selectedSection.id, selectedField.id, { placeholder: e.target.value })
}
placeholder="입력 힌트"
className="h-7 text-xs mt-1"
/>
{/* 옵션 토글 */}
필수 입력
updateField(selectedSection.id, selectedField.id, { required: checked })
}
/>
비활성화 (읽기전용)
updateField(selectedSection.id, selectedField.id, { disabled: checked })
}
/>
숨김 (자동 저장만)
updateField(selectedSection.id, selectedField.id, { hidden: checked })
}
/>
숨김 필드는 화면에 표시되지 않지만 값이 저장됩니다.
부모에서 값 받기
updateField(selectedSection.id, selectedField.id, { receiveFromParent: checked })
}
/>
부모 화면에서 전달받은 값으로 자동 채워집니다.
{/* 채번규칙 설정 */}
채번규칙 사용
updateField(selectedSection.id, selectedField.id, {
numberingRule: {
...selectedField.numberingRule,
enabled: checked,
},
})
}
/>
자동으로 코드/번호를 생성합니다. (예: EMP-001)
{selectedField.numberingRule?.enabled && (
사용자 수정 가능
updateField(selectedSection.id, selectedField.id, {
numberingRule: {
...selectedField.numberingRule,
editable: checked,
},
})
}
/>
저장 시점에 생성
updateField(selectedSection.id, selectedField.id, {
numberingRule: {
...selectedField.numberingRule,
generateOnSave: checked,
generateOnOpen: !checked,
},
})
}
/>
OFF: 모달 열릴 때 생성 / ON: 저장 버튼 클릭 시 생성
)}
{/* Select 옵션 설정 */}
{selectedField.fieldType === "select" && (
{selectedField.selectOptions?.type === "table" && (
updateField(selectedSection.id, selectedField.id, {
selectOptions: {
...selectedField.selectOptions,
valueColumn: e.target.value,
},
})
}
placeholder="code"
className="h-6 text-[10px] mt-1"
/>
updateField(selectedSection.id, selectedField.id, {
selectOptions: {
...selectedField.selectOptions,
labelColumn: e.target.value,
},
})
}
placeholder="name"
className="h-6 text-[10px] mt-1"
/>
)}
{selectedField.selectOptions?.type === "code" && (
updateField(selectedSection.id, selectedField.id, {
selectOptions: {
...selectedField.selectOptions,
codeCategory: e.target.value,
},
})
}
placeholder="POSITION_CODE"
className="h-6 text-[10px] mt-1"
/>
)}
)}
)}
);
}