feat(UniversalFormModal): 전용 API 저장 기능 및 사원+부서 통합 저장 API 구현

- CustomApiSaveConfig 타입 정의 (apiType, mainDeptFields, subDeptFields)

- saveWithCustomApi() 함수 추가로 테이블 직접 저장 대신 전용 API 호출

- adminController에 saveUserWithDept(), getUserWithDept() API 추가

- user_info + user_dept 트랜잭션 저장, 메인 부서 변경 시 자동 겸직 전환

- ConfigPanel에 전용 API 저장 설정 UI 추가

- SplitPanelLayout2: getColumnValue()로 조인 테이블 컬럼 값 추출 개선

- 검색 컬럼 선택 시 표시 컬럼 기반으로 변경
This commit is contained in:
SeongHyun Kim
2025-12-08 11:33:35 +09:00
parent a5055cae15
commit 892278853c
8 changed files with 1311 additions and 188 deletions

View File

@@ -444,65 +444,8 @@ export function UniversalFormModalComponent({
return { valid: missingFields.length === 0, missingFields };
}, [config.sections, formData]);
// 저장 처리
const handleSave = useCallback(async () => {
if (!config.saveConfig.tableName) {
toast.error("저장할 테이블이 설정되지 않았습니다.");
return;
}
// 필수 필드 검증
const { valid, missingFields } = validateRequiredFields();
if (!valid) {
toast.error(`필수 항목을 입력해주세요: ${missingFields.join(", ")}`);
return;
}
setSaving(true);
try {
const { multiRowSave } = config.saveConfig;
if (multiRowSave?.enabled) {
// 다중 행 저장
await saveMultipleRows();
} else {
// 단일 행 저장
await saveSingleRow();
}
// 저장 후 동작
if (config.saveConfig.afterSave?.showToast) {
toast.success("저장되었습니다.");
}
if (config.saveConfig.afterSave?.refreshParent) {
window.dispatchEvent(new CustomEvent("refreshParentData"));
}
// onSave 콜백은 저장 완료 알림용으로만 사용
// 실제 저장은 이미 위에서 완료됨 (saveSingleRow 또는 saveMultipleRows)
// EditModal 등 부모 컴포넌트의 저장 로직이 다시 실행되지 않도록
// _saveCompleted 플래그를 포함하여 전달
if (onSave) {
onSave({ ...formData, _saveCompleted: true });
}
} catch (error: any) {
console.error("저장 실패:", error);
// axios 에러의 경우 서버 응답 메시지 추출
const errorMessage =
error.response?.data?.message ||
error.response?.data?.error?.details ||
error.message ||
"저장에 실패했습니다.";
toast.error(errorMessage);
} finally {
setSaving(false);
}
}, [config, formData, repeatSections, onSave, validateRequiredFields]);
// 단일 행 저장
const saveSingleRow = async () => {
const saveSingleRow = useCallback(async () => {
const dataToSave = { ...formData };
// 메타데이터 필드 제거
@@ -534,15 +477,15 @@ export function UniversalFormModalComponent({
if (!response.data?.success) {
throw new Error(response.data?.message || "저장 실패");
}
};
}, [config.sections, config.saveConfig.tableName, formData]);
// 다중 행 저장 (겸직 등)
const saveMultipleRows = async () => {
const saveMultipleRows = useCallback(async () => {
const { multiRowSave } = config.saveConfig;
if (!multiRowSave) return;
let { commonFields = [], repeatSectionId = "", typeColumn, mainTypeValue, subTypeValue, mainSectionFields = [] } =
multiRowSave;
let { commonFields = [], repeatSectionId = "" } = multiRowSave;
const { typeColumn, mainTypeValue, subTypeValue, mainSectionFields = [] } = multiRowSave;
// 공통 필드가 설정되지 않은 경우, 기본정보 섹션의 모든 필드를 공통 필드로 사용
if (commonFields.length === 0) {
@@ -563,56 +506,57 @@ export function UniversalFormModalComponent({
// 디버깅: 설정 확인
console.log("[UniversalFormModal] 다중 행 저장 설정:", {
commonFields,
mainSectionFields,
repeatSectionId,
mainSectionFields,
typeColumn,
mainTypeValue,
subTypeValue,
repeatSections,
formData,
});
console.log("[UniversalFormModal] 현재 formData:", formData);
// 공통 필드 데이터 추출
const commonData: Record<string, any> = {};
for (const fieldName of commonFields) {
// 반복 섹션 데이터
const repeatItems = repeatSections[repeatSectionId] || [];
// 저장할 행들 생성
const rowsToSave: any[] = [];
// 공통 데이터 (모든 행에 적용)
const commonData: any = {};
commonFields.forEach((fieldName) => {
if (formData[fieldName] !== undefined) {
commonData[fieldName] = formData[fieldName];
}
}
console.log("[UniversalFormModal] 추출된 공통 데이터:", commonData);
});
// 메인 섹션 필드 데이터 추출
const mainSectionData: Record<string, any> = {};
if (mainSectionFields && mainSectionFields.length > 0) {
for (const fieldName of mainSectionFields) {
if (formData[fieldName] !== undefined) {
mainSectionData[fieldName] = formData[fieldName];
}
// 메인 섹션 필드 데이터 (메인 행에만 적용되는 부서/직급 등)
const mainSectionData: any = {};
mainSectionFields.forEach((fieldName) => {
if (formData[fieldName] !== undefined) {
mainSectionData[fieldName] = formData[fieldName];
}
}
console.log("[UniversalFormModal] 추출된 메인 섹션 데이터:", mainSectionData);
});
// 저장할 행들 준비
const rowsToSave: Record<string, any>[] = [];
console.log("[UniversalFormModal] 공통 데이터:", commonData);
console.log("[UniversalFormModal] 메인 섹션 데이터:", mainSectionData);
console.log("[UniversalFormModal] 반복 항목:", repeatItems);
// 1. 메인 행 생성
const mainRow: Record<string, any> = {
...commonData,
...mainSectionData,
};
// 메인 행 (공통 데이터 + 메인 섹션 필드)
const mainRow: any = { ...commonData, ...mainSectionData };
if (typeColumn) {
mainRow[typeColumn] = mainTypeValue || "main";
}
rowsToSave.push(mainRow);
// 2. 반복 섹션 행들 생성 (겸직 등)
const repeatItems = repeatSections[repeatSectionId] || [];
// 반복 섹션 행들 (공통 데이터 + 반복 섹션 필드)
for (const item of repeatItems) {
const subRow: Record<string, any> = { ...commonData };
const subRow: any = { ...commonData };
// 반복 섹션 필드 복사
Object.keys(item).forEach((key) => {
if (!key.startsWith("_")) {
subRow[key] = item[key];
// 반복 섹션 필드 값 추가
const repeatSection = config.sections.find((s) => s.id === repeatSectionId);
repeatSection?.fields.forEach((field) => {
if (item[field.columnName] !== undefined) {
subRow[field.columnName] = item[field.columnName];
}
});
@@ -666,7 +610,187 @@ export function UniversalFormModalComponent({
}
console.log(`[UniversalFormModal] ${rowsToSave.length}개 행 저장 완료`);
};
}, [config.sections, config.saveConfig, formData, repeatSections]);
// 커스텀 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 엔드포인트가 설정되지 않았습니다.");
}
const dataToSave = { ...formData };
// 메타데이터 필드 제거
Object.keys(dataToSave).forEach((key) => {
if (key.startsWith("_")) {
delete dataToSave[key];
}
});
// 반복 섹션 데이터 포함
if (Object.keys(repeatSections).length > 0) {
dataToSave._repeatSections = repeatSections;
}
const method = customApiSave.customMethod || "POST";
const response = method === "PUT"
? await apiClient.put(customApiSave.customEndpoint, dataToSave)
: await apiClient.post(customApiSave.customEndpoint, dataToSave);
if (!response.data?.success) {
throw new Error(response.data?.message || "저장 실패");
}
};
switch (customApiSave.apiType) {
case "user-with-dept":
await saveUserWithDeptApi();
break;
case "custom":
await saveWithGenericCustomApi();
break;
default:
throw new Error(`지원하지 않는 API 타입: ${customApiSave.apiType}`);
}
}, [config.sections, config.saveConfig, formData, repeatSections, initialData]);
// 저장 처리
const handleSave = useCallback(async () => {
// 커스텀 API 저장 모드가 아닌 경우에만 테이블명 체크
if (!config.saveConfig.customApiSave?.enabled && !config.saveConfig.tableName) {
toast.error("저장할 테이블이 설정되지 않았습니다.");
return;
}
// 필수 필드 검증
const { valid, missingFields } = validateRequiredFields();
if (!valid) {
toast.error(`필수 항목을 입력해주세요: ${missingFields.join(", ")}`);
return;
}
setSaving(true);
try {
const { multiRowSave, customApiSave } = config.saveConfig;
// 커스텀 API 저장 모드
if (customApiSave?.enabled) {
await saveWithCustomApi();
} else if (multiRowSave?.enabled) {
// 다중 행 저장
await saveMultipleRows();
} else {
// 단일 행 저장
await saveSingleRow();
}
// 저장 후 동작
if (config.saveConfig.afterSave?.showToast) {
toast.success("저장되었습니다.");
}
if (config.saveConfig.afterSave?.refreshParent) {
window.dispatchEvent(new CustomEvent("refreshParentData"));
}
// onSave 콜백은 저장 완료 알림용으로만 사용
// 실제 저장은 이미 위에서 완료됨 (saveSingleRow 또는 saveMultipleRows)
// EditModal 등 부모 컴포넌트의 저장 로직이 다시 실행되지 않도록
// _saveCompleted 플래그를 포함하여 전달
if (onSave) {
onSave({ ...formData, _saveCompleted: true });
}
} catch (error: any) {
console.error("저장 실패:", error);
// axios 에러의 경우 서버 응답 메시지 추출
const errorMessage =
error.response?.data?.message ||
error.response?.data?.error?.details ||
error.message ||
"저장에 실패했습니다.";
toast.error(errorMessage);
} finally {
setSaving(false);
}
}, [config, formData, repeatSections, onSave, validateRequiredFields, saveSingleRow, saveMultipleRows, saveWithCustomApi]);
// 폼 초기화
const handleReset = useCallback(() => {