feat(universal-form-modal): 범용 다중 테이블 저장 기능 추가
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -115,10 +115,37 @@ export function UniversalFormModalComponent({
|
||||
itemId: string;
|
||||
}>({ open: false, sectionId: "", itemId: "" });
|
||||
|
||||
// 초기화
|
||||
// 초기 데이터를 한 번만 캡처 (컴포넌트 마운트 시)
|
||||
const capturedInitialData = useRef<Record<string, any> | undefined>(undefined);
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
// 초기화 - 최초 마운트 시에만 실행
|
||||
useEffect(() => {
|
||||
// 이미 초기화되었으면 스킵
|
||||
if (hasInitialized.current) {
|
||||
console.log("[UniversalFormModal] 이미 초기화됨, 스킵");
|
||||
return;
|
||||
}
|
||||
|
||||
// 최초 initialData 캡처 (이후 변경되어도 이 값 사용)
|
||||
if (initialData && Object.keys(initialData).length > 0) {
|
||||
capturedInitialData.current = JSON.parse(JSON.stringify(initialData)); // 깊은 복사
|
||||
console.log("[UniversalFormModal] initialData 캡처:", capturedInitialData.current);
|
||||
}
|
||||
|
||||
hasInitialized.current = true;
|
||||
initializeForm();
|
||||
}, [config, initialData]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); // 빈 의존성 배열 - 마운트 시 한 번만 실행
|
||||
|
||||
// config 변경 시에만 재초기화 (initialData 변경은 무시)
|
||||
useEffect(() => {
|
||||
if (!hasInitialized.current) return; // 최초 초기화 전이면 스킵
|
||||
|
||||
console.log("[UniversalFormModal] config 변경 감지, 재초기화");
|
||||
initializeForm();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config]);
|
||||
|
||||
// 필드 레벨 linkedFieldGroup 데이터 로드
|
||||
useEffect(() => {
|
||||
@@ -149,6 +176,10 @@ export function UniversalFormModalComponent({
|
||||
|
||||
// 폼 초기화
|
||||
const initializeForm = useCallback(async () => {
|
||||
// 캡처된 initialData 사용 (props로 전달된 initialData가 아닌)
|
||||
const effectiveInitialData = capturedInitialData.current || initialData;
|
||||
console.log("[UniversalFormModal] 폼 초기화 시작, effectiveInitialData:", effectiveInitialData);
|
||||
|
||||
const newFormData: FormDataState = {};
|
||||
const newRepeatSections: { [sectionId: string]: RepeatSectionItem[] } = {};
|
||||
const newCollapsed = new Set<string>();
|
||||
@@ -174,11 +205,15 @@ export function UniversalFormModalComponent({
|
||||
// 기본값 설정
|
||||
let value = field.defaultValue ?? "";
|
||||
|
||||
// 부모에서 전달받은 값 적용
|
||||
if (field.receiveFromParent && initialData) {
|
||||
// 부모에서 전달받은 값 적용 (receiveFromParent 또는 effectiveInitialData에 해당 값이 있으면)
|
||||
if (effectiveInitialData) {
|
||||
const parentField = field.parentFieldName || field.columnName;
|
||||
if (initialData[parentField] !== undefined) {
|
||||
value = initialData[parentField];
|
||||
if (effectiveInitialData[parentField] !== undefined) {
|
||||
// receiveFromParent가 true이거나, effectiveInitialData에 값이 있으면 적용
|
||||
if (field.receiveFromParent || value === "" || value === undefined) {
|
||||
value = effectiveInitialData[parentField];
|
||||
console.log(`[UniversalFormModal] 필드 ${field.columnName}: initialData에서 값 적용 = ${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,11 +225,12 @@ export function UniversalFormModalComponent({
|
||||
setFormData(newFormData);
|
||||
setRepeatSections(newRepeatSections);
|
||||
setCollapsedSections(newCollapsed);
|
||||
setOriginalData(initialData || {});
|
||||
setOriginalData(effectiveInitialData || {});
|
||||
|
||||
// 채번규칙 자동 생성
|
||||
await generateNumberingValues(newFormData);
|
||||
}, [config, initialData]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config]); // initialData는 의존성에서 제거 (capturedInitialData.current 사용)
|
||||
|
||||
// 반복 섹션 아이템 생성
|
||||
const createRepeatItem = (section: FormSectionConfig, index: number): RepeatSectionItem => {
|
||||
@@ -344,15 +380,30 @@ export function UniversalFormModalComponent({
|
||||
if (optionConfig.type === "static") {
|
||||
options = optionConfig.staticOptions || [];
|
||||
} else if (optionConfig.type === "table" && optionConfig.tableName) {
|
||||
const response = await apiClient.get(`/table-management/tables/${optionConfig.tableName}/data`, {
|
||||
params: { limit: 1000 },
|
||||
// POST 방식으로 테이블 데이터 조회 (autoFilter 포함)
|
||||
const response = await apiClient.post(`/table-management/tables/${optionConfig.tableName}/data`, {
|
||||
page: 1,
|
||||
size: 1000,
|
||||
autoFilter: { enabled: true, filterColumn: "company_code" },
|
||||
});
|
||||
if (response.data?.success && response.data?.data) {
|
||||
options = response.data.data.map((row: any) => ({
|
||||
value: String(row[optionConfig.valueColumn || "id"]),
|
||||
label: String(row[optionConfig.labelColumn || "name"]),
|
||||
}));
|
||||
|
||||
// 응답 데이터 파싱
|
||||
let dataArray: any[] = [];
|
||||
if (response.data?.success) {
|
||||
const responseData = response.data?.data;
|
||||
if (responseData?.data && Array.isArray(responseData.data)) {
|
||||
dataArray = responseData.data;
|
||||
} else if (Array.isArray(responseData)) {
|
||||
dataArray = responseData;
|
||||
} else if (responseData?.rows && Array.isArray(responseData.rows)) {
|
||||
dataArray = responseData.rows;
|
||||
}
|
||||
}
|
||||
|
||||
options = dataArray.map((row: any) => ({
|
||||
value: String(row[optionConfig.valueColumn || "id"]),
|
||||
label: String(row[optionConfig.labelColumn || "name"]),
|
||||
}));
|
||||
} else if (optionConfig.type === "code" && optionConfig.codeCategory) {
|
||||
const response = await apiClient.get(`/common-code/${optionConfig.codeCategory}`);
|
||||
if (response.data?.success && response.data?.data) {
|
||||
@@ -444,7 +495,7 @@ export function UniversalFormModalComponent({
|
||||
return { valid: missingFields.length === 0, missingFields };
|
||||
}, [config.sections, formData]);
|
||||
|
||||
// 단일 행 저장
|
||||
// 단일 행 저장
|
||||
const saveSingleRow = useCallback(async () => {
|
||||
const dataToSave = { ...formData };
|
||||
|
||||
@@ -532,9 +583,9 @@ export function UniversalFormModalComponent({
|
||||
// 메인 섹션 필드 데이터 (메인 행에만 적용되는 부서/직급 등)
|
||||
const mainSectionData: any = {};
|
||||
mainSectionFields.forEach((fieldName) => {
|
||||
if (formData[fieldName] !== undefined) {
|
||||
mainSectionData[fieldName] = formData[fieldName];
|
||||
}
|
||||
if (formData[fieldName] !== undefined) {
|
||||
mainSectionData[fieldName] = formData[fieldName];
|
||||
}
|
||||
});
|
||||
|
||||
console.log("[UniversalFormModal] 공통 데이터:", commonData);
|
||||
@@ -612,84 +663,113 @@ export function UniversalFormModalComponent({
|
||||
console.log(`[UniversalFormModal] ${rowsToSave.length}개 행 저장 완료`);
|
||||
}, [config.sections, config.saveConfig, formData, repeatSections]);
|
||||
|
||||
// 커스텀 API 저장 (사원+부서 통합 저장 등)
|
||||
// 다중 테이블 저장 (범용)
|
||||
const saveWithMultiTable = useCallback(async () => {
|
||||
const { customApiSave } = config.saveConfig;
|
||||
if (!customApiSave?.multiTable) return;
|
||||
|
||||
const { multiTable } = customApiSave;
|
||||
console.log("[UniversalFormModal] 다중 테이블 저장 시작:", multiTable);
|
||||
console.log("[UniversalFormModal] 현재 formData:", formData);
|
||||
console.log("[UniversalFormModal] 현재 repeatSections:", repeatSections);
|
||||
|
||||
// 1. 메인 테이블 데이터 구성
|
||||
const mainData: Record<string, any> = {};
|
||||
config.sections.forEach((section) => {
|
||||
if (section.repeatable) return; // 반복 섹션은 제외
|
||||
section.fields.forEach((field) => {
|
||||
const value = formData[field.columnName];
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
mainData[field.columnName] = value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 2. 서브 테이블 데이터 구성
|
||||
const subTablesData: Array<{
|
||||
tableName: string;
|
||||
linkColumn: { mainField: string; subColumn: string };
|
||||
items: Record<string, any>[];
|
||||
options?: {
|
||||
saveMainAsFirst?: boolean;
|
||||
mainFieldMappings?: Array<{ formField: string; targetColumn: string }>;
|
||||
mainMarkerColumn?: string;
|
||||
mainMarkerValue?: any;
|
||||
subMarkerValue?: any;
|
||||
deleteExistingBefore?: boolean;
|
||||
};
|
||||
}> = [];
|
||||
|
||||
for (const subTableConfig of multiTable.subTables || []) {
|
||||
if (!subTableConfig.enabled || !subTableConfig.tableName || !subTableConfig.repeatSectionId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const subItems: Record<string, any>[] = [];
|
||||
const repeatData = repeatSections[subTableConfig.repeatSectionId] || [];
|
||||
|
||||
// 반복 섹션 데이터를 필드 매핑에 따라 변환
|
||||
for (const item of repeatData) {
|
||||
const mappedItem: Record<string, any> = {};
|
||||
|
||||
// 연결 컬럼 값 설정
|
||||
if (subTableConfig.linkColumn?.mainField && subTableConfig.linkColumn?.subColumn) {
|
||||
mappedItem[subTableConfig.linkColumn.subColumn] = mainData[subTableConfig.linkColumn.mainField];
|
||||
}
|
||||
|
||||
// 필드 매핑에 따라 데이터 변환
|
||||
for (const mapping of subTableConfig.fieldMappings || []) {
|
||||
if (mapping.formField && mapping.targetColumn) {
|
||||
mappedItem[mapping.targetColumn] = item[mapping.formField];
|
||||
}
|
||||
}
|
||||
|
||||
// 메인/서브 구분 컬럼 설정 (서브 데이터는 서브 마커 값)
|
||||
if (subTableConfig.options?.mainMarkerColumn) {
|
||||
mappedItem[subTableConfig.options.mainMarkerColumn] = subTableConfig.options?.subMarkerValue ?? false;
|
||||
}
|
||||
|
||||
if (Object.keys(mappedItem).length > 0) {
|
||||
subItems.push(mappedItem);
|
||||
}
|
||||
}
|
||||
|
||||
subTablesData.push({
|
||||
tableName: subTableConfig.tableName,
|
||||
linkColumn: subTableConfig.linkColumn,
|
||||
items: subItems,
|
||||
options: subTableConfig.options,
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 범용 다중 테이블 저장 API 호출
|
||||
console.log("[UniversalFormModal] 다중 테이블 저장 데이터:", {
|
||||
mainTable: multiTable.mainTable,
|
||||
mainData,
|
||||
subTablesData,
|
||||
});
|
||||
|
||||
const response = await apiClient.post("/table-management/multi-table-save", {
|
||||
mainTable: multiTable.mainTable,
|
||||
mainData,
|
||||
subTables: subTablesData,
|
||||
isUpdate: !!initialData?.[multiTable.mainTable.primaryKeyColumn],
|
||||
});
|
||||
|
||||
if (!response.data?.success) {
|
||||
throw new Error(response.data?.message || "다중 테이블 저장 실패");
|
||||
}
|
||||
|
||||
console.log("[UniversalFormModal] 다중 테이블 저장 완료:", response.data);
|
||||
}, [config.sections, config.saveConfig, formData, repeatSections, initialData]);
|
||||
|
||||
// 커스텀 API 저장
|
||||
const saveWithCustomApi = useCallback(async () => {
|
||||
const { customApiSave } = config.saveConfig;
|
||||
if (!customApiSave) return;
|
||||
|
||||
console.log("[UniversalFormModal] 커스텀 API 저장 시작:", customApiSave.apiType);
|
||||
|
||||
const saveUserWithDeptApi = async () => {
|
||||
const { mainDeptFields, subDeptSectionId, subDeptFields } = customApiSave;
|
||||
|
||||
// 1. userInfo 데이터 구성
|
||||
const userInfo: Record<string, any> = {};
|
||||
|
||||
// 모든 필드에서 user_info에 해당하는 데이터 추출
|
||||
config.sections.forEach((section) => {
|
||||
if (section.repeatable) return; // 반복 섹션은 제외
|
||||
|
||||
section.fields.forEach((field) => {
|
||||
const value = formData[field.columnName];
|
||||
if (value !== undefined && value !== null && value !== "") {
|
||||
userInfo[field.columnName] = value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 2. mainDept 데이터 구성
|
||||
let mainDept: { dept_code: string; dept_name?: string; position_name?: string } | undefined;
|
||||
|
||||
if (mainDeptFields) {
|
||||
const deptCode = formData[mainDeptFields.deptCodeField || "dept_code"];
|
||||
if (deptCode) {
|
||||
mainDept = {
|
||||
dept_code: deptCode,
|
||||
dept_name: formData[mainDeptFields.deptNameField || "dept_name"],
|
||||
position_name: formData[mainDeptFields.positionNameField || "position_name"],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 3. subDepts 데이터 구성 (반복 섹션에서)
|
||||
const subDepts: Array<{ dept_code: string; dept_name?: string; position_name?: string }> = [];
|
||||
|
||||
if (subDeptSectionId && repeatSections[subDeptSectionId]) {
|
||||
const subDeptItems = repeatSections[subDeptSectionId];
|
||||
const deptCodeField = subDeptFields?.deptCodeField || "dept_code";
|
||||
const deptNameField = subDeptFields?.deptNameField || "dept_name";
|
||||
const positionNameField = subDeptFields?.positionNameField || "position_name";
|
||||
|
||||
subDeptItems.forEach((item) => {
|
||||
const deptCode = item[deptCodeField];
|
||||
if (deptCode) {
|
||||
subDepts.push({
|
||||
dept_code: deptCode,
|
||||
dept_name: item[deptNameField],
|
||||
position_name: item[positionNameField],
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 4. API 호출
|
||||
console.log("[UniversalFormModal] 사원+부서 저장 데이터:", { userInfo, mainDept, subDepts });
|
||||
|
||||
const { saveUserWithDept } = await import("@/lib/api/user");
|
||||
const response = await saveUserWithDept({
|
||||
userInfo: userInfo as any,
|
||||
mainDept,
|
||||
subDepts,
|
||||
isUpdate: !!initialData?.user_id, // 초기 데이터가 있으면 수정 모드
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.message || "사원 저장 실패");
|
||||
}
|
||||
|
||||
console.log("[UniversalFormModal] 사원+부서 저장 완료:", response.data);
|
||||
};
|
||||
|
||||
const saveWithGenericCustomApi = async () => {
|
||||
if (!customApiSave.customEndpoint) {
|
||||
throw new Error("커스텀 API 엔드포인트가 설정되지 않았습니다.");
|
||||
@@ -720,8 +800,8 @@ export function UniversalFormModalComponent({
|
||||
};
|
||||
|
||||
switch (customApiSave.apiType) {
|
||||
case "user-with-dept":
|
||||
await saveUserWithDeptApi();
|
||||
case "multi-table":
|
||||
await saveWithMultiTable();
|
||||
break;
|
||||
case "custom":
|
||||
await saveWithGenericCustomApi();
|
||||
@@ -729,10 +809,16 @@ export function UniversalFormModalComponent({
|
||||
default:
|
||||
throw new Error(`지원하지 않는 API 타입: ${customApiSave.apiType}`);
|
||||
}
|
||||
}, [config.sections, config.saveConfig, formData, repeatSections, initialData]);
|
||||
}, [config.saveConfig, formData, repeatSections, saveWithMultiTable]);
|
||||
|
||||
// 저장 처리
|
||||
const handleSave = useCallback(async () => {
|
||||
console.log("[UniversalFormModal] 저장 시작, saveConfig:", {
|
||||
tableName: config.saveConfig.tableName,
|
||||
customApiSave: config.saveConfig.customApiSave,
|
||||
multiRowSave: config.saveConfig.multiRowSave,
|
||||
});
|
||||
|
||||
// 커스텀 API 저장 모드가 아닌 경우에만 테이블명 체크
|
||||
if (!config.saveConfig.customApiSave?.enabled && !config.saveConfig.tableName) {
|
||||
toast.error("저장할 테이블이 설정되지 않았습니다.");
|
||||
|
||||
Reference in New Issue
Block a user