Merge branch 'feature/v2-renewal' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node

This commit is contained in:
kjs 2026-02-10 12:17:26 +09:00
commit 8253be0048
3 changed files with 149 additions and 59 deletions

View File

@ -457,17 +457,18 @@ export class EntityJoinService {
// table_column_category_values는 특별한 조인 조건 필요 (회사별 필터링) // table_column_category_values는 특별한 조인 조건 필요 (회사별 필터링)
if (config.referenceTable === "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}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT AND ${alias}.table_name = '${tableName}' AND ${alias}.column_name = '${config.sourceColumn}' AND ${alias}.company_code = main.company_code AND ${alias}.is_active = true`;
} }
// user_info는 전역 테이블이므로 company_code 조건 없이 조인 // user_info는 전역 테이블이므로 company_code 조건 없이 조인
if (config.referenceTable === "user_info") { if (config.referenceTable === "user_info") {
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn}`; return `LEFT JOIN ${config.referenceTable} ${alias} ON main."${config.sourceColumn}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT`;
} }
// 일반 테이블: company_code가 있으면 같은 회사 데이터만 조인 (멀티테넌시) // 일반 테이블: company_code가 있으면 같은 회사 데이터만 조인 (멀티테넌시)
// supplier_mng, customer_mng, item_info 등 회사별 데이터 테이블 // supplier_mng, customer_mng, item_info 등 회사별 데이터 테이블
return `LEFT JOIN ${config.referenceTable} ${alias} ON main.${config.sourceColumn} = ${alias}.${config.referenceColumn} AND ${alias}.company_code = main.company_code`; // ::TEXT 캐스팅으로 varchar/integer 등 타입 불일치 방지
return `LEFT JOIN ${config.referenceTable} ${alias} ON main."${config.sourceColumn}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT AND ${alias}.company_code = main.company_code`;
}) })
.join("\n"); .join("\n");
@ -580,6 +581,7 @@ export class EntityJoinService {
logger.info("🔍 조인 설정 검증 상세:", { logger.info("🔍 조인 설정 검증 상세:", {
sourceColumn: config.sourceColumn, sourceColumn: config.sourceColumn,
referenceTable: config.referenceTable, referenceTable: config.referenceTable,
referenceColumn: config.referenceColumn,
displayColumns: config.displayColumns, displayColumns: config.displayColumns,
displayColumn: config.displayColumn, displayColumn: config.displayColumn,
aliasColumn: config.aliasColumn, aliasColumn: config.aliasColumn,
@ -598,7 +600,45 @@ export class EntityJoinService {
return false; return false;
} }
// 참조 컬럼 존재 확인 (displayColumns[0] 사용) // 참조 컬럼(JOIN 키) 존재 확인 - 참조 테이블에 reference_column이 실제로 있는지 검증
if (config.referenceColumn) {
const refColExists = await query<{ exists: number }>(
`SELECT 1 as exists FROM information_schema.columns
WHERE table_name = $1
AND column_name = $2
LIMIT 1`,
[config.referenceTable, config.referenceColumn]
);
if (refColExists.length === 0) {
// reference_column이 없으면 'id' 컬럼으로 자동 대체 시도
const idColExists = await query<{ exists: number }>(
`SELECT 1 as exists FROM information_schema.columns
WHERE table_name = $1
AND column_name = 'id'
LIMIT 1`,
[config.referenceTable]
);
if (idColExists.length > 0) {
logger.warn(
`⚠️ 참조 컬럼 ${config.referenceTable}.${config.referenceColumn}이 존재하지 않음 → 'id'로 자동 대체`
);
config.referenceColumn = "id";
} else {
logger.warn(
`❌ 참조 컬럼 ${config.referenceTable}.${config.referenceColumn}이 존재하지 않고 'id' 컬럼도 없음 → 스킵`
);
return false;
}
} else {
logger.info(
`✅ 참조 컬럼 확인 완료: ${config.referenceTable}.${config.referenceColumn}`
);
}
}
// 표시 컬럼 존재 확인 (displayColumns[0] 사용)
const displayColumn = config.displayColumns?.[0] || config.displayColumn; const displayColumn = config.displayColumns?.[0] || config.displayColumn;
logger.info( logger.info(
`🔍 표시 컬럼 확인: ${displayColumn} (from displayColumns: ${config.displayColumns}, displayColumn: ${config.displayColumn})` `🔍 표시 컬럼 확인: ${displayColumn} (from displayColumns: ${config.displayColumns}, displayColumn: ${config.displayColumn})`
@ -686,10 +726,10 @@ export class EntityJoinService {
// table_column_category_values는 특별한 조인 조건 필요 (회사별 필터링만) // table_column_category_values는 특별한 조인 조건 필요 (회사별 필터링만)
if (config.referenceTable === "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}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT 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}`; return `LEFT JOIN ${config.referenceTable} ${alias} ON main."${config.sourceColumn}"::TEXT = ${alias}."${config.referenceColumn}"::TEXT`;
}) })
.join("\n"); .join("\n");

View File

@ -2979,31 +2979,49 @@ export class TableManagementService {
continue; // 기본 Entity 조인과 중복되면 추가하지 않음 continue; // 기본 Entity 조인과 중복되면 추가하지 않음
} }
// 추가 조인 컬럼 설정 생성 // 🆕 같은 sourceColumn + referenceTable 조합의 기존 config가 있으면 displayColumns에 병합
const additionalJoinConfig: EntityJoinConfig = { const existingConfig = joinConfigs.find(
sourceTable: tableName, (config) =>
sourceColumn: sourceColumn, // 실제 소스 컬럼 (partner_id) config.sourceColumn === sourceColumn &&
referenceTable: config.referenceTable === ((additionalColumn as any).referenceTable || baseJoinConfig.referenceTable)
(additionalColumn as any).referenceTable ||
baseJoinConfig.referenceTable, // 참조 테이블 (customer_mng)
referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (customer_code)
displayColumns: [actualColumnName], // 표시할 컬럼들 (customer_name)
displayColumn: actualColumnName, // 하위 호환성
aliasColumn: correctedJoinAlias, // 수정된 별칭 (partner_id_customer_name)
separator: " - ", // 기본 구분자
};
joinConfigs.push(additionalJoinConfig);
logger.info(
`✅ 추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn} -> ${actualColumnName}`
); );
logger.info(`🔍 추가된 조인 설정 상세:`, {
sourceTable: additionalJoinConfig.sourceTable, if (existingConfig) {
sourceColumn: additionalJoinConfig.sourceColumn, // 기존 config에 display column 추가 (중복 방지)
referenceTable: additionalJoinConfig.referenceTable, if (!existingConfig.displayColumns?.includes(actualColumnName)) {
displayColumns: additionalJoinConfig.displayColumns, existingConfig.displayColumns = existingConfig.displayColumns || [];
aliasColumn: additionalJoinConfig.aliasColumn, existingConfig.displayColumns.push(actualColumnName);
}); logger.info(
`🔄 기존 조인 설정에 컬럼 병합: ${existingConfig.aliasColumn}${actualColumnName} (총 ${existingConfig.displayColumns.length}개)`
);
}
} else {
// 새 조인 설정 생성
const additionalJoinConfig: EntityJoinConfig = {
sourceTable: tableName,
sourceColumn: sourceColumn, // 실제 소스 컬럼 (partner_id)
referenceTable:
(additionalColumn as any).referenceTable ||
baseJoinConfig.referenceTable, // 참조 테이블 (customer_mng)
referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (customer_code)
displayColumns: [actualColumnName], // 표시할 컬럼들 (customer_name)
displayColumn: actualColumnName, // 하위 호환성
aliasColumn: correctedJoinAlias, // 수정된 별칭 (partner_id_customer_name)
separator: " - ", // 기본 구분자
};
joinConfigs.push(additionalJoinConfig);
logger.info(
`✅ 추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn} -> ${actualColumnName}`
);
logger.info(`🔍 추가된 조인 설정 상세:`, {
sourceTable: additionalJoinConfig.sourceTable,
sourceColumn: additionalJoinConfig.sourceColumn,
referenceTable: additionalJoinConfig.referenceTable,
displayColumns: additionalJoinConfig.displayColumns,
aliasColumn: additionalJoinConfig.aliasColumn,
});
}
} }
} }
} }

View File

@ -1250,24 +1250,59 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
setRightData(filteredData); setRightData(filteredData);
} else { } else {
// 단일키 (하위 호환성) // 단일키 (하위 호환성) → entityJoinApi 사용으로 전환 (entity 조인 컬럼 지원)
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn; const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
const rightColumn = componentConfig.rightPanel?.relation?.foreignKey; const rightColumn = componentConfig.rightPanel?.relation?.foreignKey;
if (leftColumn && rightColumn && leftTable) { if (leftColumn && rightColumn && leftTable) {
const leftValue = leftItem[leftColumn]; const leftValue = leftItem[leftColumn];
const joinedData = await dataApi.getJoinedData( const { entityJoinApi } = await import("@/lib/api/entityJoin");
leftTable,
// 단일키를 복합키 형식으로 변환
const searchConditions: Record<string, any> = {};
searchConditions[rightColumn] = leftValue;
// Entity 조인 컬럼 추출
const rightJoinColumnsLegacy = extractAdditionalJoinColumns(
componentConfig.rightPanel?.columns,
rightTableName, rightTableName,
leftColumn,
rightColumn,
leftValue,
componentConfig.rightPanel?.dataFilter, // 🆕 데이터 필터 전달
true, // 🆕 Entity 조인 활성화
componentConfig.rightPanel?.columns, // 🆕 표시 컬럼 전달 (item_info.item_name 등)
componentConfig.rightPanel?.deduplication, // 🆕 중복 제거 설정 전달
); );
setRightData(joinedData || []); // 모든 관련 레코드 (배열) if (rightJoinColumnsLegacy) {
console.log("🔗 [분할패널] 단일키 모드 additionalJoinColumns:", rightJoinColumnsLegacy);
}
const result = await entityJoinApi.getTableDataWithJoins(rightTableName, {
search: searchConditions,
enableEntityJoin: true,
size: 1000,
companyCodeOverride: companyCode,
additionalJoinColumns: rightJoinColumnsLegacy,
});
let filteredDataLegacy = result.data || [];
// 데이터 필터 적용
const dataFilterLegacy = componentConfig.rightPanel?.dataFilter;
if (dataFilterLegacy?.enabled && dataFilterLegacy.conditions?.length > 0) {
filteredDataLegacy = filteredDataLegacy.filter((item: any) => {
return dataFilterLegacy.conditions.every((cond: any) => {
const value = item[cond.column];
const condValue = cond.value;
switch (cond.operator) {
case "equals":
return value === condValue;
case "notEquals":
return value !== condValue;
case "contains":
return String(value).includes(String(condValue));
default:
return true;
}
});
});
}
setRightData(filteredDataLegacy || []);
} }
} }
} }
@ -3802,23 +3837,20 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
if (rightColumns && rightColumns.length > 0) { if (rightColumns && rightColumns.length > 0) {
// 설정된 컬럼만 표시 (엔티티 조인 컬럼 처리) // 설정된 컬럼만 표시 (엔티티 조인 컬럼 처리)
// 설정된 컬럼은 null/empty여도 항상 표시 (사용자가 명시적으로 설정한 컬럼이므로)
const summaryCount = componentConfig.rightPanel?.summaryColumnCount ?? 3; const summaryCount = componentConfig.rightPanel?.summaryColumnCount ?? 3;
firstValues = rightColumns firstValues = rightColumns
.slice(0, summaryCount) .slice(0, summaryCount)
.map((col) => { .map((col) => {
// 🆕 엔티티 조인 컬럼 처리 (getEntityJoinValue 사용)
const value = getEntityJoinValue(item, col.name); const value = getEntityJoinValue(item, col.name);
return [col.name, value, col.label] as [string, any, string]; return [col.name, value, col.label] as [string, any, string];
}) });
.filter(([_, value]) => value !== null && value !== undefined && value !== "");
allValues = rightColumns allValues = rightColumns
.map((col) => { .map((col) => {
// 🆕 엔티티 조인 컬럼 처리 (getEntityJoinValue 사용)
const value = getEntityJoinValue(item, col.name); const value = getEntityJoinValue(item, col.name);
return [col.name, value, col.label] as [string, any, string]; return [col.name, value, col.label] as [string, any, string];
}) });
.filter(([_, value]) => value !== null && value !== undefined && value !== "");
} else { } else {
// 설정 없으면 모든 컬럼 표시 (기존 로직) // 설정 없으면 모든 컬럼 표시 (기존 로직)
const summaryCount = componentConfig.rightPanel?.summaryColumnCount ?? 3; const summaryCount = componentConfig.rightPanel?.summaryColumnCount ?? 3;
@ -3851,8 +3883,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const format = colConfig?.format; const format = colConfig?.format;
const boldValue = colConfig?.bold ?? false; const boldValue = colConfig?.bold ?? false;
// 🆕 포맷 적용 (날짜/숫자/카테고리) // 🆕 포맷 적용 (날짜/숫자/카테고리) - null/empty는 "-"로 표시
const displayValue = formatCellValue(key, value, rightCategoryMappings, format); const displayValue = (value === null || value === undefined || value === "")
? "-"
: formatCellValue(key, value, rightCategoryMappings, format);
const showLabel = componentConfig.rightPanel?.summaryShowLabel ?? true; const showLabel = componentConfig.rightPanel?.summaryShowLabel ?? true;
@ -3929,8 +3963,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
const colConfig = rightColumns?.find((c) => c.name === key); const colConfig = rightColumns?.find((c) => c.name === key);
const format = colConfig?.format; const format = colConfig?.format;
// 🆕 포맷 적용 (날짜/숫자/카테고리) // 🆕 포맷 적용 (날짜/숫자/카테고리) - null/empty는 "-"로 표시
const displayValue = formatCellValue(key, value, rightCategoryMappings, format); const displayValue = (value === null || value === undefined || value === "")
? "-"
: formatCellValue(key, value, rightCategoryMappings, format);
return ( return (
<tr key={key} className="hover:bg-muted"> <tr key={key} className="hover:bg-muted">
@ -3993,13 +4029,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
return [col.name, value, col.label] as [string, any, string]; return [col.name, value, col.label] as [string, any, string];
}) })
.filter(([key, value]) => { ; // 설정된 컬럼은 null/empty여도 항상 표시
const filtered = value === null || value === undefined || value === "";
if (filtered) {
console.log(` ❌ 필터링됨: "${key}" (값: ${value})`);
}
return !filtered;
});
console.log(" ✅ 최종 표시할 항목:", displayEntries.length, "개"); console.log(" ✅ 최종 표시할 항목:", displayEntries.length, "개");
} else { } else {
@ -4017,7 +4047,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
<div className="text-muted-foreground mb-1 text-xs font-semibold tracking-wide uppercase"> <div className="text-muted-foreground mb-1 text-xs font-semibold tracking-wide uppercase">
{label || getColumnLabel(key)} {label || getColumnLabel(key)}
</div> </div>
<div className="text-sm">{String(value)}</div> <div className="text-sm">
{(value === null || value === undefined || value === "") ? <span className="text-muted-foreground">-</span> : String(value)}
</div>
</div> </div>
))} ))}
</div> </div>