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}와 비교

)}