543 lines
17 KiB
TypeScript
543 lines
17 KiB
TypeScript
import { Request, Response } from "express";
|
|
import { logger } from "../utils/logger";
|
|
import { TableManagementService } from "../services/tableManagementService";
|
|
import { entityJoinService } from "../services/entityJoinService";
|
|
import { referenceCacheService } from "../services/referenceCacheService";
|
|
|
|
const tableManagementService = new TableManagementService();
|
|
|
|
/**
|
|
* Entity 조인 기능 컨트롤러
|
|
* ID값을 의미있는 데이터로 자동 변환하는 API 제공
|
|
*/
|
|
export class EntityJoinController {
|
|
/**
|
|
* Entity 조인이 포함된 테이블 데이터 조회
|
|
* GET /api/table-management/tables/:tableName/data-with-joins
|
|
*/
|
|
async getTableDataWithJoins(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const { tableName } = req.params;
|
|
const {
|
|
page = 1,
|
|
size = 20,
|
|
search,
|
|
sortBy,
|
|
sortOrder = "asc",
|
|
enableEntityJoin = true,
|
|
additionalJoinColumns, // 추가 조인 컬럼 정보 (JSON 문자열)
|
|
screenEntityConfigs, // 화면별 엔티티 설정 (JSON 문자열)
|
|
autoFilter, // 🔒 멀티테넌시 자동 필터
|
|
dataFilter, // 🆕 데이터 필터 (JSON 문자열)
|
|
excludeFilter, // 🆕 제외 필터 (JSON 문자열) - 다른 테이블에 이미 존재하는 데이터 제외
|
|
userLang, // userLang은 별도로 분리하여 search에 포함되지 않도록 함
|
|
...otherParams
|
|
} = req.query;
|
|
|
|
logger.info(`Entity 조인 데이터 요청: ${tableName}`, {
|
|
page,
|
|
size,
|
|
enableEntityJoin,
|
|
search,
|
|
autoFilter,
|
|
});
|
|
|
|
// 검색 조건 처리
|
|
let searchConditions: Record<string, any> = {};
|
|
if (search) {
|
|
try {
|
|
// search가 문자열인 경우 JSON 파싱
|
|
searchConditions =
|
|
typeof search === "string" ? JSON.parse(search) : search;
|
|
} catch (error) {
|
|
logger.warn("검색 조건 파싱 오류:", error);
|
|
searchConditions = {};
|
|
}
|
|
}
|
|
|
|
// 🔒 멀티테넌시: 자동 필터 처리
|
|
if (autoFilter) {
|
|
try {
|
|
const parsedAutoFilter =
|
|
typeof autoFilter === "string" ? JSON.parse(autoFilter) : autoFilter;
|
|
|
|
if (parsedAutoFilter.enabled && (req as any).user) {
|
|
const filterColumn = parsedAutoFilter.filterColumn || "company_code";
|
|
const userField = parsedAutoFilter.userField || "companyCode";
|
|
const userValue = ((req as any).user as any)[userField];
|
|
|
|
if (userValue) {
|
|
searchConditions[filterColumn] = userValue;
|
|
logger.info("🔒 Entity 조인에 멀티테넌시 필터 적용:", {
|
|
filterColumn,
|
|
userValue,
|
|
tableName,
|
|
});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.warn("자동 필터 파싱 오류:", error);
|
|
}
|
|
}
|
|
|
|
// 추가 조인 컬럼 정보 처리
|
|
let parsedAdditionalJoinColumns: any[] = [];
|
|
if (additionalJoinColumns) {
|
|
try {
|
|
parsedAdditionalJoinColumns =
|
|
typeof additionalJoinColumns === "string"
|
|
? JSON.parse(additionalJoinColumns)
|
|
: additionalJoinColumns;
|
|
logger.info("추가 조인 컬럼 파싱 완료:", parsedAdditionalJoinColumns);
|
|
} catch (error) {
|
|
logger.warn("추가 조인 컬럼 파싱 오류:", error);
|
|
parsedAdditionalJoinColumns = [];
|
|
}
|
|
}
|
|
|
|
// 화면별 엔티티 설정 처리
|
|
let parsedScreenEntityConfigs: Record<string, any> = {};
|
|
if (screenEntityConfigs) {
|
|
try {
|
|
parsedScreenEntityConfigs =
|
|
typeof screenEntityConfigs === "string"
|
|
? JSON.parse(screenEntityConfigs)
|
|
: screenEntityConfigs;
|
|
logger.info(
|
|
"화면별 엔티티 설정 파싱 완료:",
|
|
parsedScreenEntityConfigs
|
|
);
|
|
} catch (error) {
|
|
logger.warn("화면별 엔티티 설정 파싱 오류:", error);
|
|
parsedScreenEntityConfigs = {};
|
|
}
|
|
}
|
|
|
|
// 🆕 데이터 필터 처리
|
|
let parsedDataFilter: any = undefined;
|
|
if (dataFilter) {
|
|
try {
|
|
parsedDataFilter =
|
|
typeof dataFilter === "string" ? JSON.parse(dataFilter) : dataFilter;
|
|
logger.info("데이터 필터 파싱 완료:", parsedDataFilter);
|
|
} catch (error) {
|
|
logger.warn("데이터 필터 파싱 오류:", error);
|
|
parsedDataFilter = undefined;
|
|
}
|
|
}
|
|
|
|
// 🆕 제외 필터 처리 (다른 테이블에 이미 존재하는 데이터 제외)
|
|
let parsedExcludeFilter: any = undefined;
|
|
if (excludeFilter) {
|
|
try {
|
|
parsedExcludeFilter =
|
|
typeof excludeFilter === "string" ? JSON.parse(excludeFilter) : excludeFilter;
|
|
logger.info("제외 필터 파싱 완료:", parsedExcludeFilter);
|
|
} catch (error) {
|
|
logger.warn("제외 필터 파싱 오류:", error);
|
|
parsedExcludeFilter = undefined;
|
|
}
|
|
}
|
|
|
|
const result = await tableManagementService.getTableDataWithEntityJoins(
|
|
tableName,
|
|
{
|
|
page: Number(page),
|
|
size: Number(size),
|
|
search:
|
|
Object.keys(searchConditions).length > 0
|
|
? searchConditions
|
|
: undefined,
|
|
sortBy: sortBy as string,
|
|
sortOrder: sortOrder as string,
|
|
enableEntityJoin:
|
|
enableEntityJoin === "true" || enableEntityJoin === true,
|
|
additionalJoinColumns: parsedAdditionalJoinColumns,
|
|
screenEntityConfigs: parsedScreenEntityConfigs,
|
|
dataFilter: parsedDataFilter, // 🆕 데이터 필터 전달
|
|
excludeFilter: parsedExcludeFilter, // 🆕 제외 필터 전달
|
|
}
|
|
);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "Entity 조인 데이터 조회 성공",
|
|
data: result,
|
|
});
|
|
} catch (error) {
|
|
logger.error("Entity 조인 데이터 조회 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "Entity 조인 데이터 조회 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 테이블의 Entity 조인 설정 조회
|
|
* GET /api/table-management/tables/:tableName/entity-joins
|
|
*/
|
|
async getEntityJoinConfigs(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const { tableName } = req.params;
|
|
|
|
logger.info(`Entity 조인 설정 조회: ${tableName}`);
|
|
|
|
const joinConfigs = await entityJoinService.detectEntityJoins(tableName);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "Entity 조인 설정 조회 성공",
|
|
data: {
|
|
tableName,
|
|
joinConfigs,
|
|
count: joinConfigs.length,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error("Entity 조인 설정 조회 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "Entity 조인 설정 조회 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 참조 테이블의 표시 가능한 컬럼 목록 조회
|
|
* GET /api/table-management/reference-tables/:tableName/columns
|
|
*/
|
|
async getReferenceTableColumns(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const { tableName } = req.params;
|
|
|
|
logger.info(`참조 테이블 컬럼 조회: ${tableName}`);
|
|
|
|
const columns =
|
|
await tableManagementService.getReferenceTableColumns(tableName);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "참조 테이블 컬럼 조회 성공",
|
|
data: {
|
|
tableName,
|
|
columns,
|
|
count: columns.length,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error("참조 테이블 컬럼 조회 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "참조 테이블 컬럼 조회 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 컬럼 Entity 설정 업데이트 (display_column 포함)
|
|
* PUT /api/table-management/tables/:tableName/columns/:columnName/entity-settings
|
|
*/
|
|
async updateEntitySettings(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const { tableName, columnName } = req.params;
|
|
const {
|
|
webType,
|
|
referenceTable,
|
|
referenceColumn,
|
|
displayColumn,
|
|
columnLabel,
|
|
description,
|
|
} = req.body;
|
|
|
|
logger.info(`Entity 설정 업데이트: ${tableName}.${columnName}`, req.body);
|
|
|
|
// Entity 타입인 경우 필수 필드 검증
|
|
if (webType === "entity") {
|
|
if (!referenceTable || !referenceColumn) {
|
|
res.status(400).json({
|
|
success: false,
|
|
message:
|
|
"Entity 타입의 경우 referenceTable과 referenceColumn이 필수입니다.",
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
await tableManagementService.updateColumnLabel(tableName, columnName, {
|
|
webType,
|
|
referenceTable,
|
|
referenceColumn,
|
|
displayColumn,
|
|
columnLabel,
|
|
description,
|
|
});
|
|
|
|
// Entity 설정 변경 시 관련 캐시 무효화
|
|
if (webType === "entity" && referenceTable) {
|
|
referenceCacheService.invalidateCache(
|
|
referenceTable,
|
|
referenceColumn,
|
|
displayColumn
|
|
);
|
|
}
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "Entity 설정 업데이트 성공",
|
|
data: {
|
|
tableName,
|
|
columnName,
|
|
settings: {
|
|
webType,
|
|
referenceTable,
|
|
referenceColumn,
|
|
displayColumn,
|
|
},
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error("Entity 설정 업데이트 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "Entity 설정 업데이트 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 캐시 상태 조회
|
|
* GET /api/table-management/cache/status
|
|
*/
|
|
async getCacheStatus(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
logger.info("캐시 상태 조회");
|
|
|
|
const cacheInfo = referenceCacheService.getCacheInfo();
|
|
const overallHitRate = referenceCacheService.getOverallCacheHitRate();
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "캐시 상태 조회 성공",
|
|
data: {
|
|
overallHitRate,
|
|
caches: cacheInfo,
|
|
summary: {
|
|
totalCaches: cacheInfo.length,
|
|
totalSize: cacheInfo.reduce(
|
|
(sum, cache) => sum + cache.dataSize,
|
|
0
|
|
),
|
|
averageHitRate:
|
|
cacheInfo.length > 0
|
|
? cacheInfo.reduce((sum, cache) => sum + cache.hitRate, 0) /
|
|
cacheInfo.length
|
|
: 0,
|
|
},
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error("캐시 상태 조회 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "캐시 상태 조회 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 캐시 무효화
|
|
* DELETE /api/table-management/cache
|
|
*/
|
|
async invalidateCache(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const { table, keyColumn, displayColumn } = req.query;
|
|
|
|
logger.info("캐시 무효화 요청", { table, keyColumn, displayColumn });
|
|
|
|
if (table && keyColumn && displayColumn) {
|
|
// 특정 캐시만 무효화
|
|
referenceCacheService.invalidateCache(
|
|
table as string,
|
|
keyColumn as string,
|
|
displayColumn as string
|
|
);
|
|
} else {
|
|
// 전체 캐시 무효화
|
|
referenceCacheService.invalidateCache();
|
|
}
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "캐시 무효화 완료",
|
|
data: {
|
|
target: table ? `${table}.${keyColumn}.${displayColumn}` : "전체",
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error("캐시 무효화 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "캐시 무효화 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Entity 조인된 테이블의 추가 컬럼 목록 조회
|
|
* GET /api/table-management/tables/:tableName/entity-join-columns
|
|
*/
|
|
async getEntityJoinColumns(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
const { tableName } = req.params;
|
|
|
|
logger.info(`Entity 조인 컬럼 조회: ${tableName}`);
|
|
|
|
// 1. 현재 테이블의 Entity 조인 설정 조회
|
|
const joinConfigs = await entityJoinService.detectEntityJoins(tableName);
|
|
|
|
if (joinConfigs.length === 0) {
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "Entity 조인 설정이 없습니다.",
|
|
data: {
|
|
tableName,
|
|
joinTables: [],
|
|
availableColumns: [],
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 2. 각 조인 테이블의 컬럼 정보 조회
|
|
const joinTablesInfo = await Promise.all(
|
|
joinConfigs.map(async (config) => {
|
|
try {
|
|
const columns =
|
|
await tableManagementService.getReferenceTableColumns(
|
|
config.referenceTable
|
|
);
|
|
|
|
// 현재 display_column 정보 (참고용으로만 사용, 필터링하지 않음)
|
|
const currentDisplayColumn =
|
|
config.displayColumn || config.displayColumns[0];
|
|
|
|
// 모든 컬럼 표시 (기본 표시 컬럼도 포함)
|
|
return {
|
|
joinConfig: config,
|
|
tableName: config.referenceTable,
|
|
currentDisplayColumn: currentDisplayColumn,
|
|
availableColumns: columns.map((col) => ({
|
|
columnName: col.columnName,
|
|
columnLabel: col.displayName || col.columnName,
|
|
dataType: col.dataType,
|
|
isNullable: true, // 기본값으로 설정
|
|
maxLength: undefined, // 정보가 없으므로 undefined
|
|
description: col.displayName,
|
|
})),
|
|
};
|
|
} catch (error) {
|
|
logger.warn(
|
|
`참조 테이블 컬럼 조회 실패: ${config.referenceTable}`,
|
|
error
|
|
);
|
|
return {
|
|
joinConfig: config,
|
|
tableName: config.referenceTable,
|
|
currentDisplayColumn:
|
|
config.displayColumn || config.displayColumns[0],
|
|
availableColumns: [],
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
};
|
|
}
|
|
})
|
|
);
|
|
|
|
// 3. 사용 가능한 모든 컬럼 목록 생성 (중복 제거)
|
|
const allAvailableColumns: Array<{
|
|
tableName: string;
|
|
columnName: string;
|
|
columnLabel: string;
|
|
dataType: string;
|
|
joinAlias: string;
|
|
suggestedLabel: string;
|
|
}> = [];
|
|
|
|
joinTablesInfo.forEach((info) => {
|
|
info.availableColumns.forEach((col) => {
|
|
const joinAlias = `${info.joinConfig.sourceColumn}_${col.columnName}`;
|
|
const suggestedLabel = col.columnLabel; // 라벨명만 사용
|
|
|
|
allAvailableColumns.push({
|
|
tableName: info.tableName,
|
|
columnName: col.columnName,
|
|
columnLabel: col.columnLabel,
|
|
dataType: col.dataType,
|
|
joinAlias,
|
|
suggestedLabel,
|
|
});
|
|
});
|
|
});
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "Entity 조인 컬럼 조회 성공",
|
|
data: {
|
|
tableName,
|
|
joinTables: joinTablesInfo,
|
|
availableColumns: allAvailableColumns,
|
|
summary: {
|
|
totalJoinTables: joinConfigs.length,
|
|
totalAvailableColumns: allAvailableColumns.length,
|
|
},
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error("Entity 조인 컬럼 조회 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "Entity 조인 컬럼 조회 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 공통 참조 테이블 자동 캐싱
|
|
* POST /api/table-management/cache/preload
|
|
*/
|
|
async preloadCommonCaches(req: Request, res: Response): Promise<void> {
|
|
try {
|
|
logger.info("공통 참조 테이블 자동 캐싱 시작");
|
|
|
|
await referenceCacheService.autoPreloadCommonTables();
|
|
|
|
const cacheInfo = referenceCacheService.getCacheInfo();
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: "공통 참조 테이블 캐싱 완료",
|
|
data: {
|
|
preloadedCaches: cacheInfo.length,
|
|
caches: cacheInfo,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error("공통 참조 테이블 캐싱 실패", error);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: "공통 참조 테이블 캐싱 중 오류가 발생했습니다.",
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export const entityJoinController = new EntityJoinController();
|