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;
/** 참조 테이블의 컬럼 */