From b15b6e21ea1017d72283b1cec341770d41cfd31a Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Mon, 8 Dec 2025 18:23:28 +0900 Subject: [PATCH] =?UTF-8?q?fix(UniversalFormModal):=20=EB=B0=98=EB=B3=B5?= =?UTF-8?q?=20=EC=84=B9=EC=85=98=20linkedFieldGroup=20=EB=A7=A4=ED=95=91?= =?UTF-8?q?=20=EB=B0=8F=20=EC=84=9C=EB=B8=8C=20=ED=85=8C=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - renderFieldWithColumns()에 repeatContext 파라미터 추가 - linkedFieldGroup 선택 시 repeatContext 유무에 따라 formData/repeatSections 분기 저장 - multiTableSave: UPSERT 대신 SELECT-UPDATE/INSERT 명시적 분기로 변경 - ON CONFLICT 조건 불일치 에러 방지 - 서브 테이블 저장 상세 로그 추가 --- .../controllers/tableManagementController.ts | 94 ++++++++++++++----- .../UniversalFormModalComponent.tsx | 75 +++++++++++++-- 2 files changed, 138 insertions(+), 31 deletions(-) diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index ce6a73b9..e4a67d3b 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -2010,37 +2010,83 @@ export async function multiTableSave( mainSubItem.company_code = companyCode; } - const mainSubColumns = Object.keys(mainSubItem).map(col => `"${col}"`).join(", "); - const mainSubPlaceholders = Object.keys(mainSubItem).map((_, idx) => `$${idx + 1}`).join(", "); - const mainSubValues = Object.values(mainSubItem); + logger.info(`서브 테이블 ${tableName} 메인 데이터 저장 준비:`, JSON.stringify(mainSubItem)); - // UPSERT 쿼리 (PK가 있다면) - const mainSubInsertQuery = ` - INSERT INTO "${tableName}" (${mainSubColumns}) - VALUES (${mainSubPlaceholders}) - ON CONFLICT ("${linkColumn.subColumn}"${options.mainMarkerColumn ? `, "${options.mainMarkerColumn}"` : ""}) - DO UPDATE SET - ${Object.keys(mainSubItem) - .filter(col => col !== linkColumn.subColumn && col !== options.mainMarkerColumn) - .map(col => `"${col}" = EXCLUDED."${col}"`) - .join(", ") || "updated_at = NOW()"} - RETURNING * + // 먼저 기존 데이터 존재 여부 확인 (user_id + is_primary 조합) + const checkQuery = ` + SELECT * FROM "${tableName}" + WHERE "${linkColumn.subColumn}" = $1 + ${options.mainMarkerColumn ? `AND "${options.mainMarkerColumn}" = $2` : ""} + ${companyCode !== "*" ? `AND company_code = $${options.mainMarkerColumn ? 3 : 2}` : ""} + LIMIT 1 `; + const checkParams: any[] = [savedPkValue]; + if (options.mainMarkerColumn) { + checkParams.push(options.mainMarkerValue ?? true); + } + if (companyCode !== "*") { + checkParams.push(companyCode); + } - try { - logger.info(`서브 테이블 ${tableName} 메인 데이터 저장:`, { mainSubInsertQuery, mainSubValues }); - const mainSubResult = await client.query(mainSubInsertQuery, mainSubValues); - subTableResults.push({ tableName, type: "main", data: mainSubResult.rows[0] }); - } catch (err: any) { - // ON CONFLICT 실패 시 일반 INSERT 시도 - logger.warn(`서브 테이블 ${tableName} UPSERT 실패, 일반 INSERT 시도:`, err.message); - const simpleInsertQuery = ` + logger.info(`서브 테이블 ${tableName} 기존 데이터 확인 - 쿼리: ${checkQuery}`); + logger.info(`서브 테이블 ${tableName} 기존 데이터 확인 - 파라미터: ${JSON.stringify(checkParams)}`); + + const existingResult = await client.query(checkQuery, checkParams); + + if (existingResult.rows.length > 0) { + // UPDATE + const updateColumns = Object.keys(mainSubItem) + .filter(col => col !== linkColumn.subColumn && col !== options.mainMarkerColumn && col !== "company_code") + .map((col, idx) => `"${col}" = $${idx + 1}`) + .join(", "); + + const updateValues = Object.keys(mainSubItem) + .filter(col => col !== linkColumn.subColumn && col !== options.mainMarkerColumn && col !== "company_code") + .map(col => mainSubItem[col]); + + if (updateColumns) { + const updateQuery = ` + UPDATE "${tableName}" + SET ${updateColumns} + WHERE "${linkColumn.subColumn}" = $${updateValues.length + 1} + ${options.mainMarkerColumn ? `AND "${options.mainMarkerColumn}" = $${updateValues.length + 2}` : ""} + ${companyCode !== "*" ? `AND company_code = $${updateValues.length + (options.mainMarkerColumn ? 3 : 2)}` : ""} + RETURNING * + `; + const updateParams = [...updateValues, savedPkValue]; + if (options.mainMarkerColumn) { + updateParams.push(options.mainMarkerValue ?? true); + } + if (companyCode !== "*") { + updateParams.push(companyCode); + } + + logger.info(`서브 테이블 ${tableName} 메인 데이터 UPDATE - 쿼리: ${updateQuery}`); + logger.info(`서브 테이블 ${tableName} 메인 데이터 UPDATE - 값: ${JSON.stringify(updateParams)}`); + + const updateResult = await client.query(updateQuery, updateParams); + subTableResults.push({ tableName, type: "main", data: updateResult.rows[0] }); + } else { + logger.info(`서브 테이블 ${tableName} 메인 데이터 - 업데이트할 컬럼 없음, 기존 데이터 유지`); + subTableResults.push({ tableName, type: "main", data: existingResult.rows[0] }); + } + } else { + // INSERT + const mainSubColumns = Object.keys(mainSubItem).map(col => `"${col}"`).join(", "); + const mainSubPlaceholders = Object.keys(mainSubItem).map((_, idx) => `$${idx + 1}`).join(", "); + const mainSubValues = Object.values(mainSubItem); + + const insertQuery = ` INSERT INTO "${tableName}" (${mainSubColumns}) VALUES (${mainSubPlaceholders}) RETURNING * `; - const simpleResult = await client.query(simpleInsertQuery, mainSubValues); - subTableResults.push({ tableName, type: "main", data: simpleResult.rows[0] }); + + logger.info(`서브 테이블 ${tableName} 메인 데이터 INSERT - 쿼리: ${insertQuery}`); + logger.info(`서브 테이블 ${tableName} 메인 데이터 INSERT - 값: ${JSON.stringify(mainSubValues)}`); + + const insertResult = await client.query(insertQuery, mainSubValues); + subTableResults.push({ tableName, type: "main", data: insertResult.rows[0] }); } } diff --git a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx index 4eab9f72..eda2f94d 100644 --- a/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx +++ b/frontend/lib/registry/components/universal-form-modal/UniversalFormModalComponent.tsx @@ -734,11 +734,54 @@ export function UniversalFormModalComponent({ } } + // saveMainAsFirst가 활성화된 경우, 메인 데이터를 서브 테이블에 저장하기 위한 매핑 생성 + let mainFieldMappings: Array<{ formField: string; targetColumn: string }> | undefined; + if (subTableConfig.options?.saveMainAsFirst) { + mainFieldMappings = []; + + // 메인 섹션(비반복)의 필드들을 서브 테이블에 매핑 + // 서브 테이블의 fieldMappings에서 targetColumn을 찾아서 매핑 + for (const mapping of subTableConfig.fieldMappings || []) { + if (mapping.targetColumn) { + // 메인 데이터에서 동일한 컬럼명이 있으면 매핑 + if (mainData[mapping.targetColumn] !== undefined) { + mainFieldMappings.push({ + formField: mapping.targetColumn, + targetColumn: mapping.targetColumn, + }); + } + // 또는 메인 섹션의 필드 중 같은 이름이 있으면 매핑 + else { + config.sections.forEach((section) => { + if (section.repeatable) return; + const matchingField = section.fields.find(f => f.columnName === mapping.targetColumn); + if (matchingField && mainData[matchingField.columnName] !== undefined) { + mainFieldMappings!.push({ + formField: matchingField.columnName, + targetColumn: mapping.targetColumn, + }); + } + }); + } + } + } + + // 중복 제거 + mainFieldMappings = mainFieldMappings.filter((m, idx, arr) => + arr.findIndex(x => x.targetColumn === m.targetColumn) === idx + ); + + console.log("[UniversalFormModal] 메인 필드 매핑 생성:", mainFieldMappings); + } + subTablesData.push({ tableName: subTableConfig.tableName, linkColumn: subTableConfig.linkColumn, items: subItems, - options: subTableConfig.options, + options: { + ...subTableConfig.options, + mainFieldMappings, // 메인 데이터 매핑 추가 + }, }); } @@ -885,12 +928,14 @@ export function UniversalFormModalComponent({ }, [initializeForm]); // 필드 요소 렌더링 (입력 컴포넌트만) + // repeatContext: 반복 섹션인 경우 { sectionId, itemId }를 전달 const renderFieldElement = ( field: FormFieldConfig, value: any, onChangeHandler: (value: any) => void, fieldKey: string, isDisabled: boolean, + repeatContext?: { sectionId: string; itemId: string }, ) => { return (() => { switch (field.fieldType) { @@ -969,11 +1014,24 @@ export function UniversalFormModalComponent({ lfg.mappings.forEach((mapping) => { if (mapping.sourceColumn && mapping.targetColumn) { const mappedValue = selectedRow[mapping.sourceColumn]; - // formData에 직접 저장 - setFormData((prev) => ({ - ...prev, - [mapping.targetColumn]: mappedValue, - })); + + // 반복 섹션인 경우 repeatSections에 저장, 아니면 formData에 저장 + if (repeatContext) { + setRepeatSections((prev) => { + const items = prev[repeatContext.sectionId] || []; + const newItems = items.map((item) => + item._id === repeatContext.itemId + ? { ...item, [mapping.targetColumn]: mappedValue } + : item + ); + return { ...prev, [repeatContext.sectionId]: newItems }; + }); + } else { + setFormData((prev) => ({ + ...prev, + [mapping.targetColumn]: mappedValue, + })); + } } }); } @@ -1116,12 +1174,14 @@ export function UniversalFormModalComponent({ }; // 필드 렌더링 (섹션 열 수 적용) + // repeatContext: 반복 섹션인 경우 { sectionId, itemId }를 전달 const renderFieldWithColumns = ( field: FormFieldConfig, value: any, onChangeHandler: (value: any) => void, fieldKey: string, sectionColumns: number = 2, + repeatContext?: { sectionId: string; itemId: string }, ) => { // 섹션 열 수에 따른 기본 gridSpan 계산 (섹션 열 수가 우선) const defaultSpan = getDefaultGridSpan(sectionColumns); @@ -1135,7 +1195,7 @@ export function UniversalFormModalComponent({ return null; } - const fieldElement = renderFieldElement(field, value, onChangeHandler, fieldKey, isDisabled); + const fieldElement = renderFieldElement(field, value, onChangeHandler, fieldKey, isDisabled, repeatContext); if (field.fieldType === "checkbox") { return ( @@ -1275,6 +1335,7 @@ export function UniversalFormModalComponent({ (value) => handleRepeatFieldChange(section.id, item._id, field.columnName, value), `${section.id}-${item._id}-${field.id}`, sectionColumns, + { sectionId: section.id, itemId: item._id }, // 반복 섹션 컨텍스트 전달 ), )}