diff --git a/backend-node/src/services/entityJoinService.ts b/backend-node/src/services/entityJoinService.ts index f1795ba0..fef50914 100644 --- a/backend-node/src/services/entityJoinService.ts +++ b/backend-node/src/services/entityJoinService.ts @@ -223,12 +223,14 @@ export class EntityJoinService { const aliasMap = new Map(); const usedAliasesForColumns = new Set(); - // joinConfigs를 참조 테이블별로 중복 제거하여 별칭 생성 + // joinConfigs를 참조 테이블 + 소스 컬럼별로 중복 제거하여 별칭 생성 + // (table_column_category_values는 같은 테이블이라도 sourceColumn마다 별도 JOIN 필요) const uniqueReferenceTableConfigs = joinConfigs.reduce((acc, config) => { if ( !acc.some( (existingConfig) => - existingConfig.referenceTable === config.referenceTable + existingConfig.referenceTable === config.referenceTable && + existingConfig.sourceColumn === config.sourceColumn ) ) { acc.push(config); @@ -237,7 +239,7 @@ export class EntityJoinService { }, [] as EntityJoinConfig[]); logger.info( - `🔧 별칭 생성 시작: ${uniqueReferenceTableConfigs.length}개 고유 테이블` + `🔧 별칭 생성 시작: ${uniqueReferenceTableConfigs.length}개 고유 테이블+컬럼 조합` ); uniqueReferenceTableConfigs.forEach((config) => { @@ -250,13 +252,16 @@ export class EntityJoinService { counter++; } usedAliasesForColumns.add(alias); - aliasMap.set(config.referenceTable, alias); - logger.info(`🔧 별칭 생성: ${config.referenceTable} → ${alias}`); + // 같은 테이블이라도 sourceColumn이 다르면 별도 별칭 생성 (table_column_category_values 대응) + const aliasKey = `${config.referenceTable}:${config.sourceColumn}`; + aliasMap.set(aliasKey, alias); + logger.info(`🔧 별칭 생성: ${config.referenceTable}.${config.sourceColumn} → ${alias}`); }); const joinColumns = joinConfigs .map((config) => { - const alias = aliasMap.get(config.referenceTable); + const aliasKey = `${config.referenceTable}:${config.sourceColumn}`; + const alias = aliasMap.get(aliasKey); const displayColumns = config.displayColumns || [ config.displayColumn, ]; @@ -346,14 +351,16 @@ export class EntityJoinService { // FROM 절 (메인 테이블) const fromClause = `FROM ${tableName} main`; - // LEFT JOIN 절들 (위에서 생성한 별칭 매핑 사용, 중복 테이블 제거) + // LEFT JOIN 절들 (위에서 생성한 별칭 매핑 사용, 각 sourceColumn마다 별도 JOIN) const joinClauses = uniqueReferenceTableConfigs .map((config) => { - const alias = aliasMap.get(config.referenceTable); + const aliasKey = `${config.referenceTable}:${config.sourceColumn}`; + const alias = aliasMap.get(aliasKey); - // table_column_category_values는 특별한 조인 조건 필요 + // table_column_category_values는 특별한 조인 조건 필요 (회사별 필터링만) if (config.referenceTable === 'table_column_category_values') { - return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn} AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}'`; + // 멀티테넌시: 회사 데이터만 사용 (공통 데이터 제외) + return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn} AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}' AND ${alias}.company_code = main.company_code AND ${alias}.is_active = true`; } return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn}`; @@ -538,12 +545,13 @@ export class EntityJoinService { const aliasMap = new Map(); const usedAliases = new Set(); - // joinConfigs를 참조 테이블별로 중복 제거하여 별칭 생성 + // joinConfigs를 참조 테이블 + 소스 컬럼별로 중복 제거하여 별칭 생성 const uniqueReferenceTableConfigs = joinConfigs.reduce((acc, config) => { if ( !acc.some( (existingConfig) => - existingConfig.referenceTable === config.referenceTable + existingConfig.referenceTable === config.referenceTable && + existingConfig.sourceColumn === config.sourceColumn ) ) { acc.push(config); @@ -561,13 +569,22 @@ export class EntityJoinService { counter++; } usedAliases.add(alias); - aliasMap.set(config.referenceTable, alias); + const aliasKey = `${config.referenceTable}:${config.sourceColumn}`; + aliasMap.set(aliasKey, alias); }); // JOIN 절들 (COUNT에서는 SELECT 컬럼 불필요) const joinClauses = uniqueReferenceTableConfigs .map((config) => { - const alias = aliasMap.get(config.referenceTable); + const aliasKey = `${config.referenceTable}:${config.sourceColumn}`; + const alias = aliasMap.get(aliasKey); + + // table_column_category_values는 특별한 조인 조건 필요 (회사별 필터링만) + if (config.referenceTable === 'table_column_category_values') { + // 멀티테넌시: 회사 데이터만 사용 (공통 데이터 제외) + return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn} AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}' AND ${alias}.company_code = main.company_code AND ${alias}.is_active = true`; + } + return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn}`; }) .join("\n"); diff --git a/backend-node/src/services/tableManagementService.ts b/backend-node/src/services/tableManagementService.ts index 06bf6abd..112106bd 100644 --- a/backend-node/src/services/tableManagementService.ts +++ b/backend-node/src/services/tableManagementService.ts @@ -2404,11 +2404,16 @@ export class TableManagementService { whereClause ); + // ⚠️ SQL 쿼리 로깅 (디버깅용) + logger.info(`🔍 [executeJoinQuery] 실행할 SQL:\n${dataQuery}`); + // 병렬 실행 const [dataResult, countResult] = await Promise.all([ query(dataQuery), query(countQuery), ]); + + logger.info(`✅ [executeJoinQuery] 조회 완료: ${dataResult?.length}개 행`); const data = Array.isArray(dataResult) ? dataResult : []; const total = diff --git a/frontend/components/screen/InteractiveDataTable.tsx b/frontend/components/screen/InteractiveDataTable.tsx index c1ada0b9..384f41f2 100644 --- a/frontend/components/screen/InteractiveDataTable.tsx +++ b/frontend/components/screen/InteractiveDataTable.tsx @@ -575,6 +575,8 @@ export const InteractiveDataTable: React.FC = ({ setLoading(true); try { + console.log("🔍 데이터 조회 시작:", { tableName: component.tableName, page, pageSize }); + const result = await tableTypeApi.getTableData(component.tableName, { page, size: pageSize, @@ -582,6 +584,13 @@ export const InteractiveDataTable: React.FC = ({ autoFilter: component.autoFilter, // 🆕 자동 필터 설정 전달 }); + console.log("✅ 데이터 조회 완료:", { + tableName: component.tableName, + dataLength: result.data.length, + total: result.total, + page: result.page + }); + setData(result.data); setTotal(result.total); setTotalPages(result.totalPages); diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index f5fecd34..8caf01a0 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -802,6 +802,12 @@ export const TableListComponent: React.FC = ({ // ======================================== const fetchTableDataInternal = useCallback(async () => { + console.log("📡 [TableList] fetchTableDataInternal 호출됨", { + tableName: tableConfig.selectedTable, + isDesignMode, + currentPage, + }); + if (!tableConfig.selectedTable || isDesignMode) { setData([]); setTotalPages(0); @@ -809,11 +815,6 @@ export const TableListComponent: React.FC = ({ return; } - // 테이블명 확인 로그 (개발 시에만) - // console.log("🔍 fetchTableDataInternal - selectedTable:", tableConfig.selectedTable); - // console.log("🔍 selectedTable 타입:", typeof tableConfig.selectedTable); - // console.log("🔍 전체 tableConfig:", tableConfig); - setLoading(true); setError(null); @@ -834,6 +835,14 @@ export const TableListComponent: React.FC = ({ referenceTable: col.additionalJoinInfo!.referenceTable, })); + console.log("🔍 [TableList] API 호출 시작", { + tableName: tableConfig.selectedTable, + page, + pageSize, + sortBy, + sortOrder, + }); + // 🎯 항상 entityJoinApi 사용 (writer 컬럼 자동 조인 지원) const response = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, { page, @@ -845,6 +854,17 @@ export const TableListComponent: React.FC = ({ additionalJoinColumns: entityJoinColumns.length > 0 ? entityJoinColumns : undefined, }); + // 실제 데이터의 item_number만 추출하여 중복 확인 + const itemNumbers = (response.data || []).map((item: any) => item.item_number); + const uniqueItemNumbers = [...new Set(itemNumbers)]; + + console.log("✅ [TableList] API 응답 받음"); + console.log(` - dataLength: ${response.data?.length || 0}`); + console.log(` - total: ${response.total}`); + console.log(` - itemNumbers: ${JSON.stringify(itemNumbers)}`); + console.log(` - uniqueItemNumbers: ${JSON.stringify(uniqueItemNumbers)}`); + console.log(` - isDuplicated: ${itemNumbers.length !== uniqueItemNumbers.length}`); + setData(response.data || []); setTotalPages(response.totalPages || 0); setTotalItems(response.total || 0); @@ -1716,6 +1736,14 @@ export const TableListComponent: React.FC = ({ }, [tableConfig.selectedTable, fetchColumnLabels, fetchTableLabel]); useEffect(() => { + console.log("🔍 [TableList] useEffect 실행 - 데이터 조회 트리거", { + isDesignMode, + tableName: tableConfig.selectedTable, + currentPage, + sortColumn, + sortDirection, + }); + if (!isDesignMode && tableConfig.selectedTable) { fetchTableDataDebounced(); } @@ -1730,7 +1758,7 @@ export const TableListComponent: React.FC = ({ refreshKey, refreshTrigger, // 강제 새로고침 트리거 isDesignMode, - fetchTableDataDebounced, + // fetchTableDataDebounced 제거: useCallback 재생성으로 인한 무한 루프 방지 ]); useEffect(() => { @@ -2157,9 +2185,18 @@ export const TableListComponent: React.FC = ({ - ) : groupByColumns.length > 0 && groupedData.length > 0 ? ( + ) : (() => { + console.log("🔍 [TableList] 렌더링 조건 체크", { + groupByColumns: groupByColumns.length, + groupedDataLength: groupedData.length, + willRenderGrouped: groupByColumns.length > 0 && groupedData.length > 0, + dataLength: data.length, + }); + return groupByColumns.length > 0 && groupedData.length > 0; + })() ? ( // 그룹화된 렌더링 groupedData.map((group) => { + console.log("📊 [TableList] 그룹 렌더링:", group.groupKey, group.count); const isCollapsed = collapsedGroups.has(group.groupKey); return ( @@ -2252,7 +2289,10 @@ export const TableListComponent: React.FC = ({ }) ) : ( // 일반 렌더링 (그룹 없음) - data.map((row, index) => ( + (() => { + console.log("📋 [TableList] 일반 렌더링 시작:", data.length, "개 행"); + return data; + })().map((row, index) => (