feature/screen-management #56
|
|
@ -42,9 +42,23 @@ export class EntityJoinService {
|
|||
},
|
||||
});
|
||||
|
||||
logger.info(`🔍 Entity 컬럼 조회 결과: ${entityColumns.length}개 발견`);
|
||||
entityColumns.forEach((col, index) => {
|
||||
logger.info(
|
||||
` ${index + 1}. ${col.column_name} -> ${col.reference_table}.${col.reference_column} (display: ${col.display_column})`
|
||||
);
|
||||
});
|
||||
|
||||
const joinConfigs: EntityJoinConfig[] = [];
|
||||
|
||||
for (const column of entityColumns) {
|
||||
logger.info(`🔍 Entity 컬럼 상세 정보:`, {
|
||||
column_name: column.column_name,
|
||||
reference_table: column.reference_table,
|
||||
reference_column: column.reference_column,
|
||||
display_column: column.display_column,
|
||||
});
|
||||
|
||||
if (
|
||||
!column.column_name ||
|
||||
!column.reference_table ||
|
||||
|
|
@ -58,6 +72,12 @@ export class EntityJoinService {
|
|||
let displayColumns: string[] = [];
|
||||
let separator = " - ";
|
||||
|
||||
logger.info(`🔍 조건 확인 - 컬럼: ${column.column_name}`, {
|
||||
hasScreenConfig: !!screenConfig,
|
||||
hasDisplayColumns: screenConfig?.displayColumns,
|
||||
displayColumn: column.display_column,
|
||||
});
|
||||
|
||||
if (screenConfig && screenConfig.displayColumns) {
|
||||
// 화면에서 설정된 표시 컬럼들 사용 (기본 테이블 + 조인 테이블 조합 지원)
|
||||
displayColumns = screenConfig.displayColumns;
|
||||
|
|
@ -70,9 +90,12 @@ export class EntityJoinService {
|
|||
} else if (column.display_column && column.display_column !== "none") {
|
||||
// 기존 설정된 단일 표시 컬럼 사용 (none이 아닌 경우만)
|
||||
displayColumns = [column.display_column];
|
||||
logger.info(
|
||||
`🔧 기존 display_column 사용: ${column.column_name} → ${column.display_column}`
|
||||
);
|
||||
} else {
|
||||
// 조인 탭에서 보여줄 기본 표시 컬럼 설정
|
||||
// dept_info 테이블의 경우 dept_name을 기본으로 사용
|
||||
// display_column이 "none"이거나 없는 경우 기본 표시 컬럼 설정
|
||||
// 🚨 display_column이 항상 "none"이므로 이 로직을 기본으로 사용
|
||||
let defaultDisplayColumn = column.reference_column;
|
||||
if (column.reference_table === "dept_info") {
|
||||
defaultDisplayColumn = "dept_name";
|
||||
|
|
@ -83,9 +106,10 @@ export class EntityJoinService {
|
|||
}
|
||||
|
||||
displayColumns = [defaultDisplayColumn];
|
||||
console.log(
|
||||
`🔧 조인 탭용 기본 표시 컬럼 설정: ${column.column_name} → ${defaultDisplayColumn} (${column.reference_table})`
|
||||
logger.info(
|
||||
`🔧 Entity 조인 기본 표시 컬럼 설정: ${column.column_name} → ${defaultDisplayColumn} (${column.reference_table})`
|
||||
);
|
||||
logger.info(`🔍 생성된 displayColumns 배열:`, displayColumns);
|
||||
}
|
||||
|
||||
// 별칭 컬럼명 생성 (writer -> writer_name)
|
||||
|
|
@ -102,13 +126,32 @@ export class EntityJoinService {
|
|||
separator: separator,
|
||||
};
|
||||
|
||||
logger.info(`🔧 기본 조인 설정 생성:`, {
|
||||
sourceTable: joinConfig.sourceTable,
|
||||
sourceColumn: joinConfig.sourceColumn,
|
||||
referenceTable: joinConfig.referenceTable,
|
||||
aliasColumn: joinConfig.aliasColumn,
|
||||
displayColumns: joinConfig.displayColumns,
|
||||
});
|
||||
|
||||
// 조인 설정 유효성 검증
|
||||
logger.info(
|
||||
`🔍 조인 설정 검증 중: ${joinConfig.sourceColumn} -> ${joinConfig.referenceTable}`
|
||||
);
|
||||
if (await this.validateJoinConfig(joinConfig)) {
|
||||
joinConfigs.push(joinConfig);
|
||||
logger.info(`✅ 조인 설정 추가됨: ${joinConfig.aliasColumn}`);
|
||||
} else {
|
||||
logger.warn(`❌ 조인 설정 검증 실패: ${joinConfig.sourceColumn}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Entity 조인 설정 생성 완료: ${joinConfigs.length}개`);
|
||||
logger.info(`🎯 Entity 조인 설정 생성 완료: ${joinConfigs.length}개`);
|
||||
joinConfigs.forEach((config, index) => {
|
||||
logger.info(
|
||||
` ${index + 1}. ${config.sourceColumn} -> ${config.referenceTable}.${config.referenceColumn} AS ${config.aliasColumn}`
|
||||
);
|
||||
});
|
||||
return joinConfigs;
|
||||
} catch (error) {
|
||||
logger.error(`Entity 조인 감지 실패: ${tableName}`, error);
|
||||
|
|
@ -190,6 +233,9 @@ export class EntityJoinService {
|
|||
"master_sabun",
|
||||
"location",
|
||||
"data_type",
|
||||
"company_name",
|
||||
"sales_yn",
|
||||
"status",
|
||||
].includes(col);
|
||||
|
||||
if (isJoinTableColumn) {
|
||||
|
|
@ -213,6 +259,9 @@ export class EntityJoinService {
|
|||
"master_sabun",
|
||||
"location",
|
||||
"data_type",
|
||||
"company_name",
|
||||
"sales_yn",
|
||||
"status",
|
||||
].includes(col);
|
||||
|
||||
if (isJoinTableColumn) {
|
||||
|
|
@ -273,7 +322,7 @@ export class EntityJoinService {
|
|||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
logger.debug(`생성된 Entity 조인 쿼리:`, query);
|
||||
logger.info(`🔍 생성된 Entity 조인 쿼리:`, query);
|
||||
return {
|
||||
query: query,
|
||||
aliasMap: aliasMap,
|
||||
|
|
@ -303,10 +352,18 @@ export class EntityJoinService {
|
|||
}
|
||||
|
||||
// 참조 테이블의 캐시 가능성 확인
|
||||
const displayCol =
|
||||
config.displayColumn ||
|
||||
config.displayColumns?.[0] ||
|
||||
config.referenceColumn;
|
||||
logger.info(
|
||||
`🔍 캐시 확인용 표시 컬럼: ${config.referenceTable} - ${displayCol}`
|
||||
);
|
||||
|
||||
const cachedData = await referenceCacheService.getCachedReference(
|
||||
config.referenceTable,
|
||||
config.referenceColumn,
|
||||
config.displayColumn || config.displayColumns[0]
|
||||
displayCol
|
||||
);
|
||||
|
||||
return cachedData ? "cache" : "join";
|
||||
|
|
@ -336,6 +393,14 @@ export class EntityJoinService {
|
|||
*/
|
||||
private async validateJoinConfig(config: EntityJoinConfig): Promise<boolean> {
|
||||
try {
|
||||
logger.info("🔍 조인 설정 검증 상세:", {
|
||||
sourceColumn: config.sourceColumn,
|
||||
referenceTable: config.referenceTable,
|
||||
displayColumns: config.displayColumns,
|
||||
displayColumn: config.displayColumn,
|
||||
aliasColumn: config.aliasColumn,
|
||||
});
|
||||
|
||||
// 참조 테이블 존재 확인
|
||||
const tableExists = await prisma.$queryRaw`
|
||||
SELECT 1 FROM information_schema.tables
|
||||
|
|
@ -350,23 +415,32 @@ export class EntityJoinService {
|
|||
|
||||
// 참조 컬럼 존재 확인 (displayColumns[0] 사용)
|
||||
const displayColumn = config.displayColumns?.[0] || config.displayColumn;
|
||||
if (!displayColumn) {
|
||||
logger.warn(`표시 컬럼이 설정되지 않음: ${config.sourceColumn}`);
|
||||
return false;
|
||||
}
|
||||
logger.info(
|
||||
`🔍 표시 컬럼 확인: ${displayColumn} (from displayColumns: ${config.displayColumns}, displayColumn: ${config.displayColumn})`
|
||||
);
|
||||
|
||||
const columnExists = await prisma.$queryRaw`
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = ${config.referenceTable}
|
||||
AND column_name = ${displayColumn}
|
||||
LIMIT 1
|
||||
`;
|
||||
// 🚨 display_column이 항상 "none"이므로, 표시 컬럼이 없어도 조인 허용
|
||||
if (displayColumn && displayColumn !== "none") {
|
||||
const columnExists = await prisma.$queryRaw`
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = ${config.referenceTable}
|
||||
AND column_name = ${displayColumn}
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
if (!Array.isArray(columnExists) || columnExists.length === 0) {
|
||||
logger.warn(
|
||||
`표시 컬럼이 존재하지 않음: ${config.referenceTable}.${displayColumn}`
|
||||
if (!Array.isArray(columnExists) || columnExists.length === 0) {
|
||||
logger.warn(
|
||||
`표시 컬럼이 존재하지 않음: ${config.referenceTable}.${displayColumn}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
logger.info(
|
||||
`✅ 표시 컬럼 확인 완료: ${config.referenceTable}.${displayColumn}`
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
`🔧 표시 컬럼 검증 생략: display_column이 none이거나 설정되지 않음`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -2049,6 +2049,17 @@ export class TableManagementService {
|
|||
options.screenEntityConfigs
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`🔍 detectEntityJoins 결과: ${joinConfigs.length}개 조인 설정`
|
||||
);
|
||||
if (joinConfigs.length > 0) {
|
||||
joinConfigs.forEach((config, index) => {
|
||||
logger.info(
|
||||
` 조인 ${index + 1}: ${config.sourceColumn} -> ${config.referenceTable} AS ${config.aliasColumn}`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 추가 조인 컬럼 정보가 있으면 조인 설정에 추가
|
||||
if (
|
||||
options.additionalJoinColumns &&
|
||||
|
|
@ -2057,40 +2068,84 @@ export class TableManagementService {
|
|||
logger.info(
|
||||
`추가 조인 컬럼 처리: ${options.additionalJoinColumns.length}개`
|
||||
);
|
||||
logger.info(
|
||||
"📋 전달받은 additionalJoinColumns:",
|
||||
options.additionalJoinColumns
|
||||
);
|
||||
|
||||
for (const additionalColumn of options.additionalJoinColumns) {
|
||||
// 기존 조인 설정에서 같은 참조 테이블을 사용하는 설정 찾기
|
||||
// 🔍 sourceColumn을 기준으로 기존 조인 설정 찾기 (dept_code로 찾기)
|
||||
const baseJoinConfig = joinConfigs.find(
|
||||
(config) => config.referenceTable === additionalColumn.sourceTable
|
||||
(config) => config.sourceColumn === additionalColumn.sourceColumn
|
||||
);
|
||||
|
||||
if (baseJoinConfig) {
|
||||
// joinAlias에서 실제 컬럼명 추출 (예: dept_code_location_name -> location_name)
|
||||
// sourceColumn을 제거한 나머지 부분이 실제 컬럼명
|
||||
const sourceColumn = baseJoinConfig.sourceColumn; // dept_code
|
||||
const joinAlias = additionalColumn.joinAlias; // dept_code_location_name
|
||||
const actualColumnName = joinAlias.replace(`${sourceColumn}_`, ""); // location_name
|
||||
const joinAlias = additionalColumn.joinAlias; // dept_code_company_name
|
||||
const actualColumnName = joinAlias.replace(`${sourceColumn}_`, ""); // company_name
|
||||
|
||||
logger.info(`🔍 조인 컬럼 상세 분석:`, {
|
||||
sourceColumn,
|
||||
joinAlias,
|
||||
actualColumnName,
|
||||
referenceTable: additionalColumn.sourceTable,
|
||||
});
|
||||
|
||||
// 🚨 기본 Entity 조인과 중복되지 않도록 체크
|
||||
const isBasicEntityJoin =
|
||||
additionalColumn.joinAlias ===
|
||||
`${baseJoinConfig.sourceColumn}_name`;
|
||||
|
||||
if (isBasicEntityJoin) {
|
||||
logger.info(
|
||||
`⚠️ 기본 Entity 조인과 중복: ${additionalColumn.joinAlias} - 건너뜀`
|
||||
);
|
||||
continue; // 기본 Entity 조인과 중복되면 추가하지 않음
|
||||
}
|
||||
|
||||
// 추가 조인 컬럼 설정 생성
|
||||
const additionalJoinConfig: EntityJoinConfig = {
|
||||
sourceTable: tableName,
|
||||
sourceColumn: baseJoinConfig.sourceColumn, // 원본 컬럼 (dept_code)
|
||||
referenceTable: additionalColumn.sourceTable, // 참조 테이블 (dept_info)
|
||||
referenceTable:
|
||||
(additionalColumn as any).referenceTable ||
|
||||
baseJoinConfig.referenceTable, // 참조 테이블 (dept_info)
|
||||
referenceColumn: baseJoinConfig.referenceColumn, // 참조 키 (dept_code)
|
||||
displayColumns: [actualColumnName], // 표시할 컬럼들 (location_name)
|
||||
displayColumns: [actualColumnName], // 표시할 컬럼들 (company_name)
|
||||
displayColumn: actualColumnName, // 하위 호환성
|
||||
aliasColumn: additionalColumn.joinAlias, // 별칭 (dept_code_location_name)
|
||||
aliasColumn: additionalColumn.joinAlias, // 별칭 (dept_code_company_name)
|
||||
separator: " - ", // 기본 구분자
|
||||
};
|
||||
|
||||
joinConfigs.push(additionalJoinConfig);
|
||||
logger.info(
|
||||
`추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn} -> ${actualColumnName}`
|
||||
`✅ 추가 조인 컬럼 설정 추가: ${additionalJoinConfig.aliasColumn} -> ${actualColumnName}`
|
||||
);
|
||||
logger.info(`🔍 추가된 조인 설정 상세:`, {
|
||||
sourceTable: additionalJoinConfig.sourceTable,
|
||||
sourceColumn: additionalJoinConfig.sourceColumn,
|
||||
referenceTable: additionalJoinConfig.referenceTable,
|
||||
displayColumns: additionalJoinConfig.displayColumns,
|
||||
aliasColumn: additionalJoinConfig.aliasColumn,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 최종 조인 설정 배열 로깅
|
||||
logger.info(`🎯 최종 joinConfigs 배열 (${joinConfigs.length}개):`);
|
||||
joinConfigs.forEach((config, index) => {
|
||||
logger.info(
|
||||
` ${index + 1}. ${config.sourceColumn} -> ${config.referenceTable} AS ${config.aliasColumn}`,
|
||||
{
|
||||
displayColumns: config.displayColumns,
|
||||
displayColumn: config.displayColumn,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (joinConfigs.length === 0) {
|
||||
logger.info(`Entity 조인 설정이 없음: ${tableName}`);
|
||||
const basicResult = await this.getTableData(tableName, options);
|
||||
|
|
@ -2104,8 +2159,21 @@ export class TableManagementService {
|
|||
}
|
||||
|
||||
// 조인 전략 결정 (테이블 크기 기반)
|
||||
const strategy =
|
||||
await entityJoinService.determineJoinStrategy(joinConfigs);
|
||||
// 🚨 additionalJoinColumns가 있는 경우 강제로 full_join 사용 (캐시 안정성 보장)
|
||||
let strategy: "full_join" | "cache_lookup" | "hybrid";
|
||||
|
||||
if (
|
||||
options.additionalJoinColumns &&
|
||||
options.additionalJoinColumns.length > 0
|
||||
) {
|
||||
strategy = "full_join";
|
||||
console.log(
|
||||
`🔧 additionalJoinColumns 감지: 강제로 full_join 전략 사용 (${options.additionalJoinColumns.length}개 추가 조인)`
|
||||
);
|
||||
} else {
|
||||
strategy = await entityJoinService.determineJoinStrategy(joinConfigs);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`🎯 선택된 조인 전략: ${strategy} (${joinConfigs.length}개 Entity 조인)`
|
||||
);
|
||||
|
|
@ -2251,10 +2319,18 @@ export class TableManagementService {
|
|||
try {
|
||||
// 캐시 데이터 미리 로드
|
||||
for (const config of joinConfigs) {
|
||||
const displayCol =
|
||||
config.displayColumn ||
|
||||
config.displayColumns?.[0] ||
|
||||
config.referenceColumn;
|
||||
logger.info(
|
||||
`🔍 캐시 로드 - ${config.referenceTable}: keyCol=${config.referenceColumn}, displayCol=${displayCol}`
|
||||
);
|
||||
|
||||
await referenceCacheService.getCachedReference(
|
||||
config.referenceTable,
|
||||
config.referenceColumn,
|
||||
config.displayColumn || config.displayColumns[0]
|
||||
displayCol
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -866,7 +866,6 @@ export default function TableManagementPage() {
|
|||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* 설정 완료 표시 - 간소화 */}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ interface SingleTableWithStickyProps {
|
|||
renderCheckboxCell: (row: any, index: number) => React.ReactNode;
|
||||
formatCellValue: (value: any, format?: string, columnName?: string) => string;
|
||||
getColumnWidth: (column: ColumnConfig) => number;
|
||||
joinColumnMapping: Record<string, string>; // 조인 컬럼 매핑 추가
|
||||
}
|
||||
|
||||
export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
||||
|
|
@ -39,6 +40,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
renderCheckboxCell,
|
||||
formatCellValue,
|
||||
getColumnWidth,
|
||||
joinColumnMapping,
|
||||
}) => {
|
||||
const checkboxConfig = tableConfig.checkbox || {};
|
||||
|
||||
|
|
@ -64,7 +66,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
|
||||
return (
|
||||
<TableHead
|
||||
key={column.columnName}
|
||||
key={`sticky-header-${colIndex}-${column.columnName}`}
|
||||
className={cn(
|
||||
column.columnName === "__checkbox__"
|
||||
? "h-10 border-b px-4 py-2 text-center align-middle"
|
||||
|
|
@ -129,7 +131,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
) : (
|
||||
data.map((row, index) => (
|
||||
<TableRow
|
||||
key={`row-${index}`}
|
||||
key={`sticky-row-${index}`}
|
||||
className={cn(
|
||||
"h-10 cursor-pointer border-b leading-none",
|
||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
|
||||
|
|
@ -155,7 +157,7 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
|
||||
return (
|
||||
<TableCell
|
||||
key={`cell-${column.columnName}`}
|
||||
key={`sticky-cell-${index}-${colIndex}-${column.columnName}`}
|
||||
className={cn(
|
||||
"h-10 px-4 py-2 align-middle text-sm whitespace-nowrap",
|
||||
`text-${column.align}`,
|
||||
|
|
@ -174,7 +176,25 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
|
|||
>
|
||||
{column.columnName === "__checkbox__"
|
||||
? renderCheckboxCell(row, index)
|
||||
: formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"}
|
||||
: (() => {
|
||||
// 🎯 매핑된 컬럼명으로 데이터 찾기 (기본 테이블과 동일한 로직)
|
||||
const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName;
|
||||
|
||||
// 조인 컬럼 매핑 정보 로깅
|
||||
if (column.columnName !== mappedColumnName && index === 0) {
|
||||
console.log(`🔗 Sticky 조인 컬럼 매핑: ${column.columnName} → ${mappedColumnName}`);
|
||||
}
|
||||
|
||||
const cellValue = row[mappedColumnName];
|
||||
if (index === 0) {
|
||||
// 첫 번째 행만 로그 출력 (디버깅용)
|
||||
console.log(
|
||||
`🔍 Sticky 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`,
|
||||
cellValue,
|
||||
);
|
||||
}
|
||||
return formatCellValue(cellValue, column.format, column.columnName) || "\u00A0";
|
||||
})()}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [totalItems, setTotalItems] = useState(0);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [searchTerm] = useState("");
|
||||
const [sortColumn, setSortColumn] = useState<string | null>(null);
|
||||
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
|
||||
const [columnLabels, setColumnLabels] = useState<Record<string, string>>({});
|
||||
|
|
@ -290,30 +290,42 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
// 🎯 조인 탭에서 추가한 컬럼들도 추가 (실제로 존재하는 컬럼만)
|
||||
...joinTabColumns
|
||||
.filter((col) => {
|
||||
// 실제 API 응답에 존재하는 컬럼만 필터링
|
||||
const validJoinColumns = ["dept_code_name", "dept_name"];
|
||||
const isValid = validJoinColumns.includes(col.columnName);
|
||||
if (!isValid) {
|
||||
console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (유효하지 않음)`);
|
||||
// 조인 컬럼인지 확인 (언더스코어가 포함된 컬럼)
|
||||
const isJoinColumn = col.columnName.includes("_") && col.columnName !== "__checkbox__";
|
||||
if (!isJoinColumn) {
|
||||
console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (조인 컬럼이 아님)`);
|
||||
}
|
||||
return isValid;
|
||||
return isJoinColumn;
|
||||
})
|
||||
.map((col) => {
|
||||
// 실제 존재하는 조인 컬럼만 처리
|
||||
let sourceTable = tableConfig.selectedTable;
|
||||
let sourceColumn = col.columnName;
|
||||
// 동적으로 조인 컬럼 정보 추출
|
||||
console.log(`🔍 조인 컬럼 분석: ${col.columnName}`);
|
||||
|
||||
if (col.columnName === "dept_code_name" || col.columnName === "dept_name") {
|
||||
sourceTable = "dept_info";
|
||||
// 컬럼명에서 기본 컬럼과 참조 테이블 추출
|
||||
// 예: dept_code_company_name -> dept_code (기본), company_name (참조)
|
||||
const parts = col.columnName.split("_");
|
||||
let sourceColumn = "";
|
||||
let referenceTable = "";
|
||||
|
||||
// dept_code로 시작하는 경우
|
||||
if (col.columnName.startsWith("dept_code_")) {
|
||||
sourceColumn = "dept_code";
|
||||
referenceTable = "dept_info";
|
||||
}
|
||||
// 다른 패턴들도 추가 가능
|
||||
else {
|
||||
// 기본적으로 첫 번째 부분을 소스 컬럼으로 사용
|
||||
sourceColumn = parts[0];
|
||||
referenceTable = tableConfig.selectedTable || "unknown";
|
||||
}
|
||||
|
||||
console.log(`🔍 조인 탭 컬럼 처리: ${col.columnName} -> ${sourceTable}.${sourceColumn}`);
|
||||
console.log(`🔗 조인 설정: ${col.columnName} -> ${sourceColumn} (${referenceTable})`);
|
||||
|
||||
return {
|
||||
sourceTable: sourceTable,
|
||||
sourceTable: tableConfig.selectedTable || "unknown", // 기본 테이블 (user_info)
|
||||
sourceColumn: sourceColumn,
|
||||
joinAlias: col.columnName,
|
||||
referenceTable: referenceTable, // 참조 테이블 정보도 추가
|
||||
};
|
||||
}),
|
||||
];
|
||||
|
|
@ -410,6 +422,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
console.log("🎯 데이터 개수:", result.data?.length || 0);
|
||||
console.log("🎯 전체 페이지:", result.totalPages);
|
||||
console.log("🎯 총 아이템:", result.total);
|
||||
|
||||
// 🚨 데이터 샘플 확인 (첫 번째 행의 모든 컬럼과 값)
|
||||
if (result.data && result.data.length > 0) {
|
||||
console.log("🔍 첫 번째 행 데이터 샘플:", result.data[0]);
|
||||
Object.entries(result.data[0]).forEach(([key, value]) => {
|
||||
console.log(` 📊 ${key}: "${value}" (타입: ${typeof value})`);
|
||||
});
|
||||
}
|
||||
setData(result.data || []);
|
||||
setTotalPages(result.totalPages || 1);
|
||||
setTotalItems(result.total || 0);
|
||||
|
|
@ -427,7 +447,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
|
||||
// 🎯 코드 컬럼들의 캐시 미리 로드 (전역 캐시 사용)
|
||||
const codeColumns = Object.entries(columnMeta).filter(
|
||||
([_, meta]) => meta.webType === "code" && meta.codeCategory,
|
||||
([, meta]) => meta.webType === "code" && meta.codeCategory,
|
||||
);
|
||||
|
||||
if (codeColumns.length > 0) {
|
||||
|
|
@ -462,16 +482,119 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
const actualApiColumns = Object.keys(result.data[0]);
|
||||
console.log("🔍 API 응답의 실제 컬럼들:", actualApiColumns);
|
||||
|
||||
// 🎯 조인 컬럼 매핑 테이블 (사용자 설정 → API 응답)
|
||||
// 실제 API 응답에 존재하는 컬럼만 매핑
|
||||
const newJoinColumnMapping: Record<string, string> = {
|
||||
dept_code_dept_code: "dept_code", // user_info.dept_code
|
||||
dept_code_status: "status", // user_info.status (dept_info.status가 조인되지 않음)
|
||||
dept_code_company_name: "dept_name", // dept_info.dept_name (company_name이 조인되지 않음)
|
||||
dept_code_name: "dept_code_name", // dept_info.dept_name
|
||||
dept_name: "dept_name", // dept_info.dept_name
|
||||
status: "status", // user_info.status
|
||||
};
|
||||
// 🎯 조인 컬럼 매핑 테이블 동적 생성 (사용자 설정 → API 응답)
|
||||
const newJoinColumnMapping: Record<string, string> = {};
|
||||
|
||||
// 사용자가 설정한 컬럼들과 실제 API 응답 컬럼들을 동적으로 매핑
|
||||
processedColumns.forEach((userColumn) => {
|
||||
// 체크박스는 제외
|
||||
if (userColumn.columnName === "__checkbox__") return;
|
||||
|
||||
console.log(`🔍 컬럼 매핑 분석: "${userColumn.columnName}"`, {
|
||||
displayName: userColumn.displayName,
|
||||
isEntityJoin: userColumn.isEntityJoin,
|
||||
entityJoinInfo: userColumn.entityJoinInfo,
|
||||
available: actualApiColumns,
|
||||
});
|
||||
|
||||
// 사용자 설정 컬럼명이 API 응답에 정확히 있는지 확인
|
||||
if (actualApiColumns.includes(userColumn.columnName)) {
|
||||
// 직접 매칭되는 경우
|
||||
newJoinColumnMapping[userColumn.columnName] = userColumn.columnName;
|
||||
console.log(`✅ 정확 매핑: ${userColumn.columnName} → ${userColumn.columnName}`);
|
||||
} else {
|
||||
// Entity 조인된 컬럼이거나 조인 탭에서 추가한 컬럼인 경우
|
||||
let foundMatch = false;
|
||||
|
||||
// 1. Entity 조인 정보가 있는 경우 aliasColumn 우선 확인
|
||||
if (userColumn.entityJoinInfo?.joinAlias) {
|
||||
const aliasColumn = userColumn.entityJoinInfo.joinAlias;
|
||||
if (actualApiColumns.includes(aliasColumn)) {
|
||||
newJoinColumnMapping[userColumn.columnName] = aliasColumn;
|
||||
console.log(`🔗 Entity 별칭 매핑: ${userColumn.columnName} → ${aliasColumn}`);
|
||||
foundMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 정확한 이름 매칭 (예: dept_code_company_name)
|
||||
if (!foundMatch) {
|
||||
const exactMatches = actualApiColumns.filter((apiCol) => apiCol === userColumn.columnName);
|
||||
|
||||
if (exactMatches.length > 0) {
|
||||
newJoinColumnMapping[userColumn.columnName] = exactMatches[0];
|
||||
console.log(`🎯 정확 이름 매핑: ${userColumn.columnName} → ${exactMatches[0]}`);
|
||||
foundMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 조인 컬럼 검증 및 처리
|
||||
if (!foundMatch) {
|
||||
// 🚨 조인 컬럼인지 확인 (더 정확한 감지 로직)
|
||||
const hasUnderscore = userColumn.columnName.includes("_");
|
||||
let isJoinColumn = false;
|
||||
let baseColumnName = "";
|
||||
|
||||
if (hasUnderscore) {
|
||||
// 가능한 모든 기본 컬럼명을 확인 (dept_code_company_name -> dept_code, dept 순으로)
|
||||
const parts = userColumn.columnName.split("_");
|
||||
for (let i = parts.length - 1; i >= 1; i--) {
|
||||
const possibleBase = parts.slice(0, i).join("_");
|
||||
if (actualApiColumns.includes(possibleBase)) {
|
||||
baseColumnName = possibleBase;
|
||||
isJoinColumn = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🔍 조인 컬럼 검사: "${userColumn.columnName}"`, {
|
||||
hasUnderscore,
|
||||
baseColumnName,
|
||||
isJoinColumn,
|
||||
});
|
||||
|
||||
if (isJoinColumn) {
|
||||
console.log(`🔍 조인 컬럼 기본 컬럼 확인: "${baseColumnName}"`, {
|
||||
existsInApi: actualApiColumns.includes(baseColumnName),
|
||||
actualApiColumns: actualApiColumns.slice(0, 10), // 처음 10개만 표시
|
||||
});
|
||||
|
||||
console.warn(
|
||||
`⚠️ 조인 실패: "${userColumn.columnName}" - 백엔드에서 Entity 조인이 실행되지 않음. 기본 컬럼값 표시합니다.`,
|
||||
);
|
||||
// 조인 실패 시 기본 컬럼값을 표시하도록 매핑
|
||||
newJoinColumnMapping[userColumn.columnName] = baseColumnName;
|
||||
foundMatch = true;
|
||||
} else {
|
||||
// 일반 컬럼인 경우 부분 매칭 시도
|
||||
const partialMatches = actualApiColumns.filter(
|
||||
(apiCol) => apiCol.includes(userColumn.columnName) || userColumn.columnName.includes(apiCol),
|
||||
);
|
||||
|
||||
if (partialMatches.length > 0) {
|
||||
const bestMatch = partialMatches.reduce((best, current) =>
|
||||
Math.abs(current.length - userColumn.columnName.length) <
|
||||
Math.abs(best.length - userColumn.columnName.length)
|
||||
? current
|
||||
: best,
|
||||
);
|
||||
|
||||
newJoinColumnMapping[userColumn.columnName] = bestMatch;
|
||||
console.log(`🔍 부분 매핑: ${userColumn.columnName} → ${bestMatch}`);
|
||||
foundMatch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 매칭 실패한 경우 원본 유지 (하지만 경고 표시)
|
||||
if (!foundMatch) {
|
||||
newJoinColumnMapping[userColumn.columnName] = userColumn.columnName;
|
||||
console.warn(
|
||||
`⚠️ 매핑 실패: "${userColumn.columnName}" - 사용 가능한 컬럼: [${actualApiColumns.join(", ")}]`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 🎯 조인 컬럼 매핑 상태 업데이트
|
||||
setJoinColumnMapping(newJoinColumnMapping);
|
||||
|
|
@ -533,11 +656,23 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
if (result.entityJoinInfo?.joinConfigs) {
|
||||
result.entityJoinInfo.joinConfigs.forEach((joinConfig) => {
|
||||
// 원본 컬럼을 조인된 컬럼으로 교체
|
||||
const originalColumnIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.sourceColumn);
|
||||
let originalColumnIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.sourceColumn);
|
||||
|
||||
if (originalColumnIndex !== -1) {
|
||||
console.log(`🔄 컬럼 교체: ${joinConfig.sourceColumn} → ${joinConfig.aliasColumn}`);
|
||||
const originalColumn = processedColumns[originalColumnIndex];
|
||||
|
||||
// 🚨 중복 방지: 이미 같은 aliasColumn이 있는지 확인
|
||||
const existingAliasIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.aliasColumn);
|
||||
if (existingAliasIndex !== -1 && existingAliasIndex !== originalColumnIndex) {
|
||||
console.warn(`🚨 중복 컬럼 발견: ${joinConfig.aliasColumn}이 이미 존재합니다. 중복 제거합니다.`);
|
||||
processedColumns.splice(existingAliasIndex, 1);
|
||||
// 인덱스 재조정
|
||||
if (existingAliasIndex < originalColumnIndex) {
|
||||
originalColumnIndex--;
|
||||
}
|
||||
}
|
||||
|
||||
processedColumns[originalColumnIndex] = {
|
||||
...originalColumn,
|
||||
columnName: joinConfig.aliasColumn, // dept_code → dept_code_name
|
||||
|
|
@ -583,9 +718,26 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
processedColumns = autoColumns;
|
||||
}
|
||||
|
||||
// 🚨 processedColumns에서 중복 제거
|
||||
const uniqueProcessedColumns = processedColumns.filter(
|
||||
(column, index, self) => self.findIndex((c) => c.columnName === column.columnName) === index,
|
||||
);
|
||||
|
||||
if (uniqueProcessedColumns.length !== processedColumns.length) {
|
||||
console.error("🚨 processedColumns에서 중복 발견:");
|
||||
console.error(
|
||||
"원본:",
|
||||
processedColumns.map((c) => c.columnName),
|
||||
);
|
||||
console.error(
|
||||
"중복 제거 후:",
|
||||
uniqueProcessedColumns.map((c) => c.columnName),
|
||||
);
|
||||
}
|
||||
|
||||
// 🎯 표시할 컬럼 상태 업데이트
|
||||
setDisplayColumns(processedColumns);
|
||||
console.log("🎯 displayColumns 업데이트됨:", processedColumns);
|
||||
setDisplayColumns(uniqueProcessedColumns);
|
||||
console.log("🎯 displayColumns 업데이트됨:", uniqueProcessedColumns);
|
||||
console.log("🎯 데이터 개수:", result.data?.length || 0);
|
||||
console.log("🎯 전체 데이터:", result.data);
|
||||
}
|
||||
|
|
@ -836,6 +988,25 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
"🎯 visibleColumns 컬럼명들:",
|
||||
columns.map((c) => c.columnName),
|
||||
);
|
||||
|
||||
// 🚨 중복 키 검사
|
||||
const columnNames = columns.map((c) => c.columnName);
|
||||
const duplicates = columnNames.filter((name, index) => columnNames.indexOf(name) !== index);
|
||||
if (duplicates.length > 0) {
|
||||
console.error("🚨 중복된 컬럼명 발견:", duplicates);
|
||||
console.error("🚨 전체 컬럼명 목록:", columnNames);
|
||||
|
||||
// 중복 제거
|
||||
const uniqueColumns = columns.filter(
|
||||
(column, index, self) => self.findIndex((c) => c.columnName === column.columnName) === index,
|
||||
);
|
||||
console.log(
|
||||
"🔧 중복 제거 후 컬럼들:",
|
||||
uniqueColumns.map((c) => c.columnName),
|
||||
);
|
||||
return uniqueColumns;
|
||||
}
|
||||
|
||||
return columns;
|
||||
}, [displayColumns, tableConfig.columns, tableConfig.checkbox, isDesignMode]);
|
||||
|
||||
|
|
@ -1084,7 +1255,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
onClearFilters={handleClearAdvancedFilters}
|
||||
tableColumns={visibleColumns.map((col) => ({
|
||||
columnName: col.columnName,
|
||||
webType: columnMeta[col.columnName]?.webType || "text",
|
||||
webType: (columnMeta[col.columnName]?.webType as any) || "text",
|
||||
displayName: columnLabels[col.columnName] || col.displayName || col.columnName,
|
||||
codeCategory: columnMeta[col.columnName]?.codeCategory,
|
||||
isVisible: col.visible,
|
||||
|
|
@ -1132,15 +1303,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
renderCheckboxCell={renderCheckboxCell}
|
||||
formatCellValue={formatCellValue}
|
||||
getColumnWidth={getColumnWidth}
|
||||
joinColumnMapping={joinColumnMapping}
|
||||
/>
|
||||
) : (
|
||||
// 기존 테이블 (가로 스크롤이 필요 없는 경우)
|
||||
<Table>
|
||||
<TableHeader className={tableConfig.stickyHeader ? "sticky top-0 z-10 bg-white" : ""}>
|
||||
<TableRow style={{ minHeight: "40px !important", height: "40px !important", lineHeight: "1" }}>
|
||||
{visibleColumns.map((column) => (
|
||||
{visibleColumns.map((column, colIndex) => (
|
||||
<TableHead
|
||||
key={column.columnName}
|
||||
key={`header-${colIndex}-${column.columnName}`}
|
||||
style={{
|
||||
width: column.width ? `${column.width}px` : undefined,
|
||||
minHeight: "40px !important",
|
||||
|
|
@ -1192,7 +1364,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
) : (
|
||||
data.map((row, index) => (
|
||||
<TableRow
|
||||
key={index}
|
||||
key={`row-${index}-${getRowKey(row, index)}`}
|
||||
className={cn(
|
||||
"h-10 cursor-pointer leading-none",
|
||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
|
||||
|
|
@ -1201,9 +1373,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
style={{ minHeight: "40px", height: "40px", lineHeight: "1" }}
|
||||
onClick={() => handleRowClick(row)}
|
||||
>
|
||||
{visibleColumns.map((column) => (
|
||||
{visibleColumns.map((column, colIndex) => (
|
||||
<TableCell
|
||||
key={column.columnName}
|
||||
key={`cell-${index}-${colIndex}-${column.columnName}`}
|
||||
className={cn("h-10 align-middle whitespace-nowrap", `text-${column.align}`)}
|
||||
style={{ minHeight: "40px", height: "40px", verticalAlign: "middle" }}
|
||||
>
|
||||
|
|
@ -1212,15 +1384,28 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
: (() => {
|
||||
// 🎯 매핑된 컬럼명으로 데이터 찾기
|
||||
const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName;
|
||||
|
||||
// 조인 컬럼 매핑 정보 로깅
|
||||
if (column.columnName !== mappedColumnName && index === 0) {
|
||||
console.log(`🔗 조인 컬럼 매핑: ${column.columnName} → ${mappedColumnName}`);
|
||||
}
|
||||
|
||||
const cellValue = row[mappedColumnName];
|
||||
if (index === 0) {
|
||||
// 첫 번째 행만 로그 출력
|
||||
console.log(
|
||||
`🔍 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`,
|
||||
cellValue,
|
||||
"전체 row:",
|
||||
row,
|
||||
);
|
||||
console.log(`🔍 셀 데이터 [${column.columnName} → ${mappedColumnName}]:`, cellValue);
|
||||
|
||||
// 🚨 조인된 컬럼인 경우 추가 디버깅
|
||||
if (column.columnName !== mappedColumnName) {
|
||||
console.log(" 🔗 조인 컬럼 분석:");
|
||||
console.log(` 👤 사용자 설정 컬럼: "${column.columnName}"`);
|
||||
console.log(` 📡 매핑된 API 컬럼: "${mappedColumnName}"`);
|
||||
console.log(` 📋 컬럼 라벨: "${column.displayName}"`);
|
||||
console.log(` 💾 실제 데이터: "${cellValue}"`);
|
||||
console.log(
|
||||
` 🔄 원본 컬럼 데이터 (${column.columnName}): "${row[column.columnName]}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return formatCellValue(cellValue, column.format, column.columnName) || "\u00A0";
|
||||
})()}
|
||||
|
|
|
|||
Loading…
Reference in New Issue