From 6d0acdd1ec6edd8bb7b212bae003845d70aa8557 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Thu, 20 Nov 2025 10:16:49 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20ModalRepeaterTable=20reference=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=20=EC=B2=98=EB=A6=AC=20=EC=88=9C=EC=84=9C=20?= =?UTF-8?q?=EB=B0=8F=20API=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문제: - reference 매핑 시 조인 조건의 소스 필드 값이 undefined - API 호출 시 filters 파라미터를 백엔드가 인식 못함 해결: - 컬럼 처리를 2단계로 분리 (source/manual → reference) - API 파라미터 변경 (filters→search, limit/offset→size/page) - 응답 경로 수정 (data.data → data.data.data) 결과: - 외부 테이블 참조 매핑 정상 작동 - 품목 선택 시 customer_item_mapping에서 단가 자동 조회 성공 --- .../ModalRepeaterTableComponent.tsx | 179 +++++++++- .../ModalRepeaterTableConfigPanel.tsx | 312 +++++++++++++++++- .../components/modal-repeater-table/types.ts | 2 + 3 files changed, 472 insertions(+), 21 deletions(-) diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx index e387d50e..2003c5ef 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx @@ -5,14 +5,122 @@ import { Button } from "@/components/ui/button"; import { Plus } from "lucide-react"; import { ItemSelectionModal } from "./ItemSelectionModal"; import { RepeaterTable } from "./RepeaterTable"; -import { ModalRepeaterTableProps, RepeaterColumnConfig } from "./types"; +import { ModalRepeaterTableProps, RepeaterColumnConfig, JoinCondition } from "./types"; import { useCalculation } from "./useCalculation"; import { cn } from "@/lib/utils"; +import { apiClient } from "@/lib/api/client"; interface ModalRepeaterTableComponentProps extends Partial { config?: ModalRepeaterTableProps; } +/** + * 외부 테이블에서 참조 값을 조회하는 함수 + * @param referenceTable 참조 테이블명 (예: "customer_item_mapping") + * @param referenceField 참조할 컬럼명 (예: "basic_price") + * @param joinConditions 조인 조건 배열 + * @param sourceItem 소스 데이터 (모달에서 선택한 항목) + * @param currentItem 현재 빌드 중인 항목 (이미 설정된 필드들) + * @returns 참조된 값 또는 undefined + */ +async function fetchReferenceValue( + referenceTable: string, + referenceField: string, + joinConditions: JoinCondition[], + sourceItem: any, + currentItem: any +): Promise { + if (joinConditions.length === 0) { + console.warn("⚠️ 조인 조건이 없습니다. 참조 조회를 건너뜁니다."); + return undefined; + } + + try { + // 조인 조건을 WHERE 절로 변환 + const whereConditions: Record = {}; + + for (const condition of joinConditions) { + const { sourceTable = "target", sourceField, targetField, operator = "=" } = condition; + + // 소스 테이블에 따라 값을 가져오기 + let value: any; + if (sourceTable === "source") { + // 소스 테이블 (item_info 등): 모달에서 선택한 원본 데이터 + value = sourceItem[sourceField]; + console.log(` 📘 소스 테이블에서 값 가져오기: ${sourceField} =`, value); + } else { + // 저장 테이블 (sales_order_mng 등): 반복 테이블에 이미 복사된 값 + value = currentItem[sourceField]; + console.log(` 📗 저장 테이블(반복테이블)에서 값 가져오기: ${sourceField} =`, value); + } + + if (value === undefined || value === null) { + console.warn(`⚠️ 조인 조건의 소스 필드 "${sourceField}" 값이 없습니다. (sourceTable: ${sourceTable})`); + return undefined; + } + + // 연산자가 "=" 인 경우만 지원 (확장 가능) + if (operator === "=") { + whereConditions[targetField] = value; + } else { + console.warn(`⚠️ 연산자 "${operator}"는 아직 지원되지 않습니다.`); + } + } + + console.log(`🔍 참조 조회 API 호출:`, { + table: referenceTable, + field: referenceField, + where: whereConditions, + }); + + // API 호출: 테이블 데이터 조회 (POST 방식) + const requestBody = { + search: whereConditions, // ✅ filters → search 변경 (백엔드 파라미터명) + size: 1, // 첫 번째 결과만 가져오기 + page: 1, + }; + + console.log("📤 API 요청 Body:", JSON.stringify(requestBody, null, 2)); + + const response = await apiClient.post( + `/table-management/tables/${referenceTable}/data`, + requestBody + ); + + console.log("📥 API 전체 응답:", { + success: response.data.success, + dataLength: response.data.data?.data?.length, // ✅ data.data.data 구조 + total: response.data.data?.total, // ✅ data.data.total + firstRow: response.data.data?.data?.[0], // ✅ data.data.data[0] + }); + + if (response.data.success && response.data.data?.data?.length > 0) { + const firstRow = response.data.data.data[0]; // ✅ data.data.data[0] + const value = firstRow[referenceField]; + + console.log(`✅ 참조 조회 성공:`, { + table: referenceTable, + field: referenceField, + value, + fullRow: firstRow, + }); + + return value; + } else { + console.warn(`⚠️ 참조 조회 결과 없음:`, { + table: referenceTable, + where: whereConditions, + responseData: response.data.data, + total: response.data.total, + }); + return undefined; + } + } catch (error) { + console.error(`❌ 참조 조회 API 오류:`, error); + return undefined; + } +} + export function ModalRepeaterTableComponent({ config, sourceTable: propSourceTable, @@ -126,15 +234,31 @@ export function ModalRepeaterTableComponent({ } }, []); - const handleAddItems = (items: any[]) => { + const handleAddItems = async (items: any[]) => { console.log("➕ handleAddItems 호출:", items.length, "개 항목"); console.log("📋 소스 데이터:", items); - // 매핑 규칙에 따라 데이터 변환 - const mappedItems = items.map((sourceItem) => { + // 매핑 규칙에 따라 데이터 변환 (비동기 처리) + const mappedItems = await Promise.all(items.map(async (sourceItem) => { const newItem: any = {}; - columns.forEach((col) => { + // ⚠️ 중요: reference 매핑은 다른 컬럼에 의존할 수 있으므로 + // 1단계: source/manual 매핑을 먼저 처리 + // 2단계: reference 매핑을 나중에 처리 + + const referenceColumns: typeof columns = []; + const otherColumns: typeof columns = []; + + for (const col of columns) { + if (col.mapping?.type === "reference") { + referenceColumns.push(col); + } else { + otherColumns.push(col); + } + } + + // 1단계: source/manual 컬럼 먼저 처리 + for (const col of otherColumns) { console.log(`🔄 컬럼 "${col.field}" 매핑 처리:`, col.mapping); // 1. 매핑 규칙이 있는 경우 @@ -148,11 +272,6 @@ export function ModalRepeaterTableComponent({ } else { console.warn(` ⚠️ 소스 필드 "${sourceField}" 값이 없음`); } - } else if (col.mapping.type === "reference") { - // 외부 테이블 참조 (TODO: API 호출 필요) - console.log(` ⏳ 참조 조회 필요: ${col.mapping.referenceTable}.${col.mapping.referenceField}`); - // 현재는 빈 값으로 설정 (나중에 API 호출로 구현) - newItem[col.field] = undefined; } else if (col.mapping.type === "manual") { // 사용자 입력 (빈 값) newItem[col.field] = undefined; @@ -170,11 +289,47 @@ export function ModalRepeaterTableComponent({ newItem[col.field] = col.defaultValue; console.log(` 🎯 기본값 적용: ${col.field}:`, col.defaultValue); } - }); + } + + // 2단계: reference 컬럼 처리 (다른 컬럼들이 모두 설정된 후) + console.log("🔗 2단계: reference 컬럼 처리 시작"); + for (const col of referenceColumns) { + console.log(`🔄 컬럼 "${col.field}" 참조 매핑 처리:`, col.mapping); + + // 외부 테이블 참조 (API 호출) + console.log(` ⏳ 참조 조회 시작: ${col.mapping?.referenceTable}.${col.mapping?.referenceField}`); + + try { + const referenceValue = await fetchReferenceValue( + col.mapping!.referenceTable!, + col.mapping!.referenceField!, + col.mapping!.joinCondition || [], + sourceItem, + newItem + ); + + if (referenceValue !== null && referenceValue !== undefined) { + newItem[col.field] = referenceValue; + console.log(` ✅ 참조 조회 성공: ${col.field}:`, referenceValue); + } else { + newItem[col.field] = undefined; + console.warn(` ⚠️ 참조 조회 결과 없음`); + } + } catch (error) { + console.error(` ❌ 참조 조회 오류:`, error); + newItem[col.field] = undefined; + } + + // 기본값 적용 + if (col.defaultValue !== undefined && newItem[col.field] === undefined) { + newItem[col.field] = col.defaultValue; + console.log(` 🎯 기본값 적용: ${col.field}:`, col.defaultValue); + } + } console.log("📦 변환된 항목:", newItem); return newItem; - }); + })); // 계산 필드 업데이트 const calculatedItems = calculateAll(mappedItems); diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx index a952d845..a8068c92 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx @@ -131,7 +131,8 @@ export function ModalRepeaterTableConfigPanel({ // 계산 규칙이 없으면 모든 컬럼의 calculated 속성 제거 if (!initialConfig.calculationRules || initialConfig.calculationRules.length === 0) { const cleanedColumns = (initialConfig.columns || []).map((col) => { - const { calculated: _calc, ...rest } = col; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { calculated, ...rest } = col; return { ...rest, editable: true }; }); return { ...initialConfig, columns: cleanedColumns }; @@ -145,7 +146,8 @@ export function ModalRepeaterTableConfigPanel({ return { ...col, calculated: true, editable: false }; } else { // 나머지 필드는 calculated 제거, editable=true - const { calculated: _calc, ...rest } = col; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { calculated, ...rest } = col; return { ...rest, editable: true }; } }); @@ -281,12 +283,12 @@ export function ModalRepeaterTableConfigPanel({ const columns = localConfig.columns || []; // 이미 존재하는 컬럼인지 확인 - if (columns.some(col => col.field === columnName)) { + if (columns.some((col) => col.field === columnName)) { alert("이미 추가된 컬럼입니다."); return; } - const targetCol = targetTableColumns.find(c => c.columnName === columnName); + const targetCol = targetTableColumns.find((c) => c.columnName === columnName); const newColumn: RepeaterColumnConfig = { field: columnName, @@ -340,16 +342,17 @@ export function ModalRepeaterTableConfigPanel({ // 이전 결과 필드의 calculated 속성 제거 if (oldRule.result) { - const oldResultIndex = columns.findIndex(c => c.field === oldRule.result); + const oldResultIndex = columns.findIndex((c) => c.field === oldRule.result); if (oldResultIndex !== -1) { - const { calculated: _calc, ...rest } = columns[oldResultIndex]; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { calculated, ...rest } = columns[oldResultIndex]; columns[oldResultIndex] = { ...rest, editable: true }; } } // 새 결과 필드를 calculated=true, editable=false로 설정 if (updates.result) { - const newResultIndex = columns.findIndex(c => c.field === updates.result); + const newResultIndex = columns.findIndex((c) => c.field === updates.result); if (newResultIndex !== -1) { columns[newResultIndex] = { ...columns[newResultIndex], @@ -375,9 +378,10 @@ export function ModalRepeaterTableConfigPanel({ // 결과 필드의 calculated 속성 제거 if (removedRule.result) { const columns = [...(localConfig.columns || [])]; - const resultIndex = columns.findIndex(c => c.field === removedRule.result); + const resultIndex = columns.findIndex((c) => c.field === removedRule.result); if (resultIndex !== -1) { - const { calculated: _calc, ...rest } = columns[resultIndex]; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { calculated, ...rest } = columns[resultIndex]; columns[resultIndex] = { ...rest, editable: true }; } rules.splice(index, 1); @@ -978,6 +982,296 @@ export function ModalRepeaterTableConfigPanel({ 가져올 컬럼명

+ + {/* 조인 조건 설정 */} +
+
+ + +
+

+ 소스 테이블과 참조 테이블을 어떻게 매칭할지 설정 +

+ + {/* 조인 조건 목록 */} +
+ {(col.mapping?.joinCondition || []).map((condition, condIndex) => ( +
+
+ + 조인 조건 {condIndex + 1} + + +
+ + {/* 소스 테이블 선택 */} +
+ + +

+ 반복 테이블 = 저장 테이블 컬럼 사용 +

+
+ + {/* 소스 필드 */} +
+ + +

+ {(!condition.sourceTable || condition.sourceTable === "target") + ? "반복 테이블에 이미 추가된 컬럼" + : "모달에서 선택한 원본 데이터의 컬럼"} +

+
+ + {/* 연산자 */} +
+ +
+ + {/* 대상 필드 */} +
+ + { + const currentConditions = [...(col.mapping?.joinCondition || [])]; + currentConditions[condIndex] = { + ...currentConditions[condIndex], + targetField: value + }; + updateRepeaterColumn(index, { + mapping: { + ...col.mapping, + type: "reference", + joinCondition: currentConditions + } as ColumnMapping + }); + }} + /> +
+ + {/* 조인 조건 미리보기 */} + {condition.sourceField && condition.targetField && ( +
+ + {condition.sourceTable === "source" + ? localConfig.sourceTable + : localConfig.targetTable || "저장테이블"} + + .{condition.sourceField} + {condition.operator || "="} + {col.mapping?.referenceTable} + .{condition.targetField} +
+ )} +
+ ))} + + {/* 조인 조건 없을 때 안내 */} + {(!col.mapping?.joinCondition || col.mapping.joinCondition.length === 0) && ( +
+

+ 조인 조건이 없습니다 +

+

+ "조인 조건 추가" 버튼을 클릭하여 매칭 조건을 설정하세요 +

+
+ )} +
+ + {/* 조인 조건 예시 */} + {col.mapping?.referenceTable && ( +
+

조인 조건 예시

+
+

예) 거래처별 품목 단가 조회:

+

• item_code = item_code

+

• customer_code = customer_code

+
+
+ )} +
)} diff --git a/frontend/lib/registry/components/modal-repeater-table/types.ts b/frontend/lib/registry/components/modal-repeater-table/types.ts index 047dddc8..180830ee 100644 --- a/frontend/lib/registry/components/modal-repeater-table/types.ts +++ b/frontend/lib/registry/components/modal-repeater-table/types.ts @@ -77,6 +77,8 @@ export interface ColumnMapping { * 조인 조건 정의 */ export interface JoinCondition { + /** 소스 테이블 (어느 테이블의 컬럼인지) */ + sourceTable?: string; // "source" (item_info) 또는 "target" (sales_order_mng) /** 현재 테이블의 컬럼 (소스 테이블 또는 반복 테이블) */ sourceField: string; /** 참조 테이블의 컬럼 */