fix: 카테고리 컬럼 중복 데이터 문제 해결
- table_column_category_values JOIN 시 회사 데이터만 사용하도록 수정 - 공통 데이터(company_code='*') 사용 금지 - is_active=true 필터 추가로 활성화된 카테고리만 조회 - entityJoinService.ts의 buildJoinQuery 및 buildCountQuery 수정 - 품목 정보 테이블 등에서 발생하던 2배 중복 문제 해결
This commit is contained in:
parent
36bff64145
commit
f73f788b0a
|
|
@ -223,12 +223,14 @@ export class EntityJoinService {
|
|||
const aliasMap = new Map<string, string>();
|
||||
const usedAliasesForColumns = new Set<string>();
|
||||
|
||||
// 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<string, string>();
|
||||
const usedAliases = new Set<string>();
|
||||
|
||||
// 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");
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -575,6 +575,8 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
|
||||
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<InteractiveDataTableProps> = ({
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -802,6 +802,12 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
// ========================================
|
||||
|
||||
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<TableListComponentProps> = ({
|
|||
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<TableListComponentProps> = ({
|
|||
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<TableListComponentProps> = ({
|
|||
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<TableListComponentProps> = ({
|
|||
}, [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<TableListComponentProps> = ({
|
|||
refreshKey,
|
||||
refreshTrigger, // 강제 새로고침 트리거
|
||||
isDesignMode,
|
||||
fetchTableDataDebounced,
|
||||
// fetchTableDataDebounced 제거: useCallback 재생성으로 인한 무한 루프 방지
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -2157,9 +2185,18 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : 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 (
|
||||
<React.Fragment key={group.groupKey}>
|
||||
|
|
@ -2252,7 +2289,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
})
|
||||
) : (
|
||||
// 일반 렌더링 (그룹 없음)
|
||||
data.map((row, index) => (
|
||||
(() => {
|
||||
console.log("📋 [TableList] 일반 렌더링 시작:", data.length, "개 행");
|
||||
return data;
|
||||
})().map((row, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className={cn(
|
||||
|
|
|
|||
Loading…
Reference in New Issue