From 228fd33a2ada4a82b597d20845231729aeff8a60 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Fri, 19 Dec 2025 13:43:26 +0900 Subject: [PATCH] =?UTF-8?q?fix(RepeaterTable):=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=BC=20=ED=97=A4=EB=8D=94=20=EB=9D=BC=EB=B2=A8=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=BD=94=EB=93=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=20=ED=97=A4=EB=8D=94=EC=97=90=20"=EC=BB=AC=EB=9F=BC?= =?UTF-8?q?=EB=AA=85=20-=20=EC=98=B5=EC=85=98=EB=9D=BC=EB=B2=A8"=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=ED=91=9C=EC=8B=9C=20=EC=98=B5=EC=85=98=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EC=8B=9C=20=EC=BB=AC=EB=9F=BC=20=EB=84=88?= =?UTF-8?q?=EB=B9=84=20=EC=9E=90=EB=8F=99=20=EC=9E=AC=EA=B3=84=EC=82=B0=20?= =?UTF-8?q?API=20=EA=B2=80=EC=83=89=20=EC=8B=9C=20=EC=A0=95=ED=99=95?= =?UTF-8?q?=ED=95=9C=20=EC=9D=BC=EC=B9=98=20=EA=B2=80=EC=83=89(equals)=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EB=94=94=EB=B2=84=EA=B7=B8=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=A0=9C=EA=B1=B0=20=EC=84=A4=EC=A0=95=20UI=20?= =?UTF-8?q?=EB=9D=BC=EB=B2=A8=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=B9=9C?= =?UTF-8?q?=ED=99=94=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modal-repeater-table/RepeaterTable.tsx | 27 ++++++- .../components/modal-repeater-table/types.ts | 1 + .../TableSectionRenderer.tsx | 80 +++++++------------ .../modals/TableSectionSettingsModal.tsx | 14 ++-- 4 files changed, 59 insertions(+), 63 deletions(-) diff --git a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx index 47849849..88da4aef 100644 --- a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx @@ -238,9 +238,17 @@ export function RepeaterTable({ return equalWidth; } - // 헤더 텍스트 너비 - const headerText = column.label || field; - const headerWidth = measureTextWidth(headerText) + 24; // padding + // 헤더 텍스트 너비 (동적 데이터 소스가 있으면 headerLabel 사용) + let headerText = column.label || field; + if (column.dynamicDataSource?.enabled && column.dynamicDataSource.options.length > 0) { + const activeOptionId = activeDataSources[field] || column.dynamicDataSource.defaultOptionId; + const activeOption = column.dynamicDataSource.options.find((opt) => opt.id === activeOptionId) + || column.dynamicDataSource.options[0]; + if (activeOption?.headerLabel) { + headerText = activeOption.headerLabel; + } + } + const headerWidth = measureTextWidth(headerText) + 32; // padding + 드롭다운 아이콘 // 헤더와 데이터 중 큰 값 사용 return Math.max(headerWidth, maxDataWidth); @@ -554,7 +562,10 @@ export function RepeaterTable({ "-mx-1 rounded px-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500", )} > - {col.label} + {/* 컬럼명 - 선택된 옵션라벨 형식으로 표시 */} + + {activeOption?.headerLabel || `${col.label} - ${activeOption?.label || ''}`} + @@ -569,6 +580,14 @@ export function RepeaterTable({ onClick={() => { onDataSourceChange?.(col.field, option.id); setOpenPopover(null); + // 옵션 변경 시 해당 컬럼 너비 재계산 + if (option.headerLabel) { + const newHeaderWidth = measureTextWidth(option.headerLabel) + 32; + setColumnWidths((prev) => ({ + ...prev, + [col.field]: Math.max(prev[col.field] || 60, newHeaderWidth), + })); + } }} className={cn( "flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-xs", diff --git a/frontend/lib/registry/components/modal-repeater-table/types.ts b/frontend/lib/registry/components/modal-repeater-table/types.ts index 3f5ae63b..092c27c6 100644 --- a/frontend/lib/registry/components/modal-repeater-table/types.ts +++ b/frontend/lib/registry/components/modal-repeater-table/types.ts @@ -76,6 +76,7 @@ export interface DynamicDataSourceConfig { export interface DynamicDataSourceOption { id: string; label: string; // 표시 라벨 (예: "거래처별 단가") + headerLabel?: string; // 헤더에 표시될 전체 라벨 (예: "단가 - 거래처별 단가") // 조회 방식 sourceType: "table" | "multiTable" | "api"; diff --git a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx index 106c1c18..3c64e543 100644 --- a/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx +++ b/frontend/lib/registry/components/universal-form-modal/TableSectionRenderer.tsx @@ -53,8 +53,10 @@ function convertToRepeaterColumn(col: TableColumnConfig): RepeaterColumnConfig { enabled: true, options: col.lookup.options.map((option) => ({ id: option.id, - // displayLabel이 있으면 그것을 사용, 없으면 원래 label 사용 + // "컬럼명 - 옵션라벨" 형식으로 헤더에 표시 label: option.displayLabel || option.label, + // 헤더에 표시될 전체 라벨 (컬럼명 - 옵션라벨) + headerLabel: `${col.label} - ${option.displayLabel || option.label}`, sourceType: "table" as const, tableConfig: { tableName: option.tableName, @@ -131,9 +133,19 @@ async function transformValue( } try { + // 정확히 일치하는 검색 const response = await apiClient.post( `/table-management/tables/${transform.tableName}/data`, - { search: { [transform.matchColumn]: value }, size: 1, page: 1 } + { + search: { + [transform.matchColumn]: { + value: value, + operator: "equals" + } + }, + size: 1, + page: 1 + } ); if (response.data.success && response.data.data?.data?.length > 0) { @@ -181,11 +193,20 @@ async function fetchExternalLookupValue( return undefined; } - // 2. 외부 테이블에서 값 조회 + // 2. 외부 테이블에서 값 조회 (정확히 일치하는 검색) try { const response = await apiClient.post( `/table-management/tables/${externalLookup.tableName}/data`, - { search: { [externalLookup.matchColumn]: matchValue }, size: 1, page: 1 } + { + search: { + [externalLookup.matchColumn]: { + value: matchValue, + operator: "equals" + } + }, + size: 1, + page: 1 + } ); if (response.data.success && response.data.data?.data?.length > 0) { @@ -218,10 +239,7 @@ async function fetchExternalValue( sourceData: any, formData: FormDataState ): Promise { - console.log("📡 [fetchExternalValue] 시작:", { tableName, valueColumn, joinConditions }); - if (joinConditions.length === 0) { - console.warn("📡 [fetchExternalValue] 조인 조건이 없습니다."); return undefined; } @@ -231,40 +249,29 @@ async function fetchExternalValue( for (const condition of joinConditions) { let value: any; - console.log("📡 [fetchExternalValue] 조건 처리:", { condition, rowData, sourceData, formData }); - // 값 출처에 따라 가져오기 (4가지 소스 타입 지원) if (condition.sourceType === "row") { // 현재 행 데이터 (설정된 컬럼 필드) value = rowData[condition.sourceField]; - console.log("📡 [fetchExternalValue] row에서 값 가져옴:", { field: condition.sourceField, value }); } else if (condition.sourceType === "sourceData") { // 원본 소스 테이블 데이터 (_sourceData) value = sourceData?.[condition.sourceField]; - console.log("📡 [fetchExternalValue] sourceData에서 값 가져옴:", { field: condition.sourceField, value }); } else if (condition.sourceType === "formData") { // formData에서 가져오기 (다른 섹션) value = formData[condition.sourceField]; - console.log("📡 [fetchExternalValue] formData에서 값 가져옴:", { field: condition.sourceField, value }); } else if (condition.sourceType === "externalTable" && condition.externalLookup) { // 외부 테이블에서 조회하여 가져오기 - console.log("📡 [fetchExternalValue] externalTable 조회 시작:", condition.externalLookup); value = await fetchExternalLookupValue(condition.externalLookup, rowData, sourceData, formData); - console.log("📡 [fetchExternalValue] externalTable 조회 결과:", { value }); } if (value === undefined || value === null || value === "") { - console.warn(`📡 [fetchExternalValue] 조인 조건의 필드 "${condition.sourceField}" 값이 없습니다. (sourceType: ${condition.sourceType})`); return undefined; } // 값 변환이 필요한 경우 (예: 이름 → 코드) - 레거시 호환 if (condition.transform) { - console.log("📡 [fetchExternalValue] 값 변환 시작:", { originalValue: value, transform: condition.transform }); value = await transformValue(value, condition.transform); - console.log("📡 [fetchExternalValue] 값 변환 결과:", { transformedValue: value }); if (value === undefined) { - console.warn(`📡 [fetchExternalValue] 값 변환 후 결과가 없습니다. 원본 값: "${formData[condition.sourceField]}"`); return undefined; } } @@ -283,28 +290,21 @@ async function fetchExternalValue( value: convertedValue, operator: "equals" }; - console.log("📡 [fetchExternalValue] WHERE 조건 추가:", { targetColumn: condition.targetColumn, value: convertedValue }); } // API 호출 - console.log("📡 [fetchExternalValue] API 호출:", { tableName, whereConditions }); const response = await apiClient.post( `/table-management/tables/${tableName}/data`, { search: whereConditions, size: 1, page: 1 } ); - console.log("📡 [fetchExternalValue] API 응답:", response.data); - if (response.data.success && response.data.data?.data?.length > 0) { - const result = response.data.data.data[0][valueColumn]; - console.log("📡 [fetchExternalValue] 최종 결과:", { valueColumn, result }); - return result; + return response.data.data.data[0][valueColumn]; } - console.warn("📡 [fetchExternalValue] 조회 결과 없음"); return undefined; } catch (error) { - console.error("📡 [fetchExternalValue] 외부 테이블 조회 오류:", error); + console.error("외부 테이블 조회 오류:", error); return undefined; } } @@ -581,8 +581,6 @@ export function TableSectionRenderer({ // 컬럼 모드/조회 옵션 변경 핸들러 const handleDataSourceChange = useCallback( async (columnField: string, optionId: string) => { - console.log("🔍 [handleDataSourceChange] 시작:", { columnField, optionId }); - setActiveDataSources((prev) => ({ ...prev, [columnField]: optionId, @@ -590,23 +588,19 @@ export function TableSectionRenderer({ // 해당 컬럼의 모든 행 데이터 재조회 const column = tableConfig.columns.find((col) => col.field === columnField); - console.log("🔍 [handleDataSourceChange] 컬럼 찾기:", { column: column?.field, hasLookup: column?.lookup?.enabled }); // lookup 설정이 있는 경우 (새로운 조회 기능) if (column?.lookup?.enabled && column.lookup.options) { const selectedOption = column.lookup.options.find((opt) => opt.id === optionId); - console.log("🔍 [handleDataSourceChange] 선택된 옵션:", { selectedOption, optionId }); if (!selectedOption) return; // sameTable 타입: 현재 행의 소스 데이터에서 값 복사 (외부 조회 필요 없음) if (selectedOption.type === "sameTable") { - console.log("🔍 [handleDataSourceChange] sameTable 타입 - 소스 데이터에서 복사"); const updatedData = tableData.map((row) => { // sourceField에서 값을 가져와 해당 컬럼에 복사 // row에 _sourceData가 있으면 거기서, 없으면 row 자체에서 가져옴 const sourceData = row._sourceData || row; const newValue = sourceData[selectedOption.valueColumn] ?? row[columnField]; - console.log("🔍 [handleDataSourceChange] sameTable 값 복사:", { valueColumn: selectedOption.valueColumn, sourceData, newValue }); return { ...row, [columnField]: newValue }; }); @@ -616,16 +610,8 @@ export function TableSectionRenderer({ } // 모든 행에 대해 새 값 조회 - console.log("🔍 [handleDataSourceChange] 외부 테이블 조회 시작:", { - type: selectedOption.type, - tableName: selectedOption.tableName, - valueColumn: selectedOption.valueColumn, - conditions: selectedOption.conditions, - tableDataLength: tableData.length, - }); - const updatedData = await Promise.all( - tableData.map(async (row, rowIndex) => { + tableData.map(async (row) => { let newValue: any = row[columnField]; // 조인 조건 구성 (4가지 소스 타입 지원) @@ -657,13 +643,6 @@ export function TableSectionRenderer({ }; }); - console.log(`🔍 [handleDataSourceChange] 행 ${rowIndex} 조회:`, { - rowData: row, - sourceData: row._sourceData, - formData, - joinConditions, - }); - // 외부 테이블에서 값 조회 (_sourceData 전달) const sourceData = row._sourceData || row; const value = await fetchExternalValue( @@ -675,8 +654,6 @@ export function TableSectionRenderer({ formData ); - console.log(`🔍 [handleDataSourceChange] 행 ${rowIndex} 조회 결과:`, { value }); - if (value !== undefined) { newValue = value; } @@ -688,7 +665,6 @@ export function TableSectionRenderer({ // 계산 필드 업데이트 const calculatedData = calculateAll(updatedData); handleDataChange(calculatedData); - console.log("🔍 [handleDataSourceChange] 완료:", { calculatedData }); return; } diff --git a/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx b/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx index 542d4467..baf21194 100644 --- a/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx +++ b/frontend/lib/registry/components/universal-form-modal/modals/TableSectionSettingsModal.tsx @@ -807,7 +807,7 @@ function ColumnSettingItem({
-

비교 컬럼 (WHERE)

+

찾을 컬럼

{ @@ -850,7 +850,7 @@ function ColumnSettingItem({ {/* 2행: 비교 값 출처 */}
-

비교 값 출처

+

비교 값 출처 (찾을 때 사용할 값)

updateLookupCondition(optIndex, condIndex, { targetColumn: value })} > - + {option.tableName && ( @@ -958,9 +958,9 @@ function ColumnSettingItem({
{/* 설명 텍스트 */} - {cond.externalLookup.tableName && cond.externalLookup.matchColumn && cond.externalLookup.resultColumn && ( + {cond.externalLookup.tableName && cond.externalLookup.matchColumn && cond.externalLookup.resultColumn && cond.targetColumn && (

- {cond.externalLookup.tableName}에서 {cond.externalLookup.matchColumn} = 입력값 인 행의{" "} + {cond.externalLookup.tableName}에서 {cond.externalLookup.matchColumn} = 입력값(비교 값 출처)인 행의{" "} {cond.externalLookup.resultColumn} 값을 가져와 {option.tableName}.{cond.targetColumn}와 비교

)}