라벨명 표시기능
This commit is contained in:
@@ -6,8 +6,22 @@ import { AuthenticatedRequest } from "../types/auth";
|
||||
export const getScreens = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const { companyCode } = req.user as any;
|
||||
const screens = await screenManagementService.getScreens(companyCode);
|
||||
res.json({ success: true, data: screens });
|
||||
const { page = 1, size = 20, searchTerm } = req.query;
|
||||
|
||||
const result = await screenManagementService.getScreensByCompany(
|
||||
companyCode,
|
||||
parseInt(page as string),
|
||||
parseInt(size as string)
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.data,
|
||||
total: result.pagination.total,
|
||||
page: result.pagination.page,
|
||||
size: result.pagination.size,
|
||||
totalPages: result.pagination.totalPages,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("화면 목록 조회 실패:", error);
|
||||
res
|
||||
|
||||
@@ -60,7 +60,11 @@ export async function getColumnList(
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
logger.info(`=== 컬럼 정보 조회 시작: ${tableName} ===`);
|
||||
const { page = 1, size = 50 } = req.query;
|
||||
|
||||
logger.info(
|
||||
`=== 컬럼 정보 조회 시작: ${tableName} (page: ${page}, size: ${size}) ===`
|
||||
);
|
||||
|
||||
if (!tableName) {
|
||||
const response: ApiResponse<null> = {
|
||||
@@ -76,14 +80,20 @@ export async function getColumnList(
|
||||
}
|
||||
|
||||
const tableManagementService = new TableManagementService();
|
||||
const columnList = await tableManagementService.getColumnList(tableName);
|
||||
const result = await tableManagementService.getColumnList(
|
||||
tableName,
|
||||
parseInt(page as string),
|
||||
parseInt(size as string)
|
||||
);
|
||||
|
||||
logger.info(`컬럼 정보 조회 결과: ${tableName}, ${columnList.length}개`);
|
||||
logger.info(
|
||||
`컬럼 정보 조회 결과: ${tableName}, ${result.columns.length}/${result.total}개 (${result.page}/${result.totalPages} 페이지)`
|
||||
);
|
||||
|
||||
const response: ApiResponse<ColumnTypeInfo[]> = {
|
||||
const response: ApiResponse<typeof result> = {
|
||||
success: true,
|
||||
message: "컬럼 목록을 성공적으로 조회했습니다.",
|
||||
data: columnList,
|
||||
data: result,
|
||||
};
|
||||
|
||||
res.status(200).json(response);
|
||||
@@ -377,6 +387,65 @@ export async function getColumnLabels(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 라벨 설정
|
||||
*/
|
||||
export async function updateTableLabel(
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
const { displayName, description } = req.body;
|
||||
|
||||
logger.info(`=== 테이블 라벨 설정 시작: ${tableName} ===`);
|
||||
logger.info(`표시명: ${displayName}, 설명: ${description}`);
|
||||
|
||||
if (!tableName) {
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "테이블명이 필요합니다.",
|
||||
error: {
|
||||
code: "MISSING_TABLE_NAME",
|
||||
details: "테이블명 파라미터가 누락되었습니다.",
|
||||
},
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const tableManagementService = new TableManagementService();
|
||||
await tableManagementService.updateTableLabel(
|
||||
tableName,
|
||||
displayName,
|
||||
description
|
||||
);
|
||||
|
||||
logger.info(`테이블 라벨 설정 완료: ${tableName}`);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: true,
|
||||
message: "테이블 라벨이 성공적으로 설정되었습니다.",
|
||||
data: null,
|
||||
};
|
||||
|
||||
res.status(200).json(response);
|
||||
} catch (error) {
|
||||
logger.error("테이블 라벨 설정 중 오류 발생:", error);
|
||||
|
||||
const response: ApiResponse<null> = {
|
||||
success: false,
|
||||
message: "테이블 라벨 설정 중 오류가 발생했습니다.",
|
||||
error: {
|
||||
code: "TABLE_LABEL_UPDATE_ERROR",
|
||||
details: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
|
||||
res.status(500).json(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 컬럼 웹 타입 설정
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getTableLabels,
|
||||
getColumnLabels,
|
||||
updateColumnWebType,
|
||||
updateTableLabel,
|
||||
getTableData,
|
||||
addTableData,
|
||||
editTableData,
|
||||
@@ -31,6 +32,12 @@ router.get("/tables", getTableList);
|
||||
*/
|
||||
router.get("/tables/:tableName/columns", getColumnList);
|
||||
|
||||
/**
|
||||
* 테이블 라벨 설정
|
||||
* PUT /api/table-management/tables/:tableName/label
|
||||
*/
|
||||
router.put("/tables/:tableName/label", updateTableLabel);
|
||||
|
||||
/**
|
||||
* 개별 컬럼 설정 업데이트
|
||||
* POST /api/table-management/tables/:tableName/columns/:columnName/settings
|
||||
|
||||
@@ -105,8 +105,45 @@ export class ScreenManagementService {
|
||||
prisma.screen_definitions.count({ where: whereClause }),
|
||||
]);
|
||||
|
||||
// 테이블 라벨 정보를 한 번에 조회
|
||||
const tableNames = [
|
||||
...new Set(screens.map((s) => s.table_name).filter(Boolean)),
|
||||
];
|
||||
|
||||
let tableLabelMap = new Map<string, string>();
|
||||
|
||||
if (tableNames.length > 0) {
|
||||
try {
|
||||
const tableLabels = await prisma.table_labels.findMany({
|
||||
where: { table_name: { in: tableNames } },
|
||||
select: { table_name: true, table_label: true },
|
||||
});
|
||||
|
||||
tableLabelMap = new Map(
|
||||
tableLabels.map((tl) => [
|
||||
tl.table_name,
|
||||
tl.table_label || tl.table_name,
|
||||
])
|
||||
);
|
||||
|
||||
// 테스트: company_mng 라벨 직접 확인
|
||||
if (tableLabelMap.has("company_mng")) {
|
||||
console.log(
|
||||
"✅ company_mng 라벨 찾음:",
|
||||
tableLabelMap.get("company_mng")
|
||||
);
|
||||
} else {
|
||||
console.log("❌ company_mng 라벨 없음");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("테이블 라벨 조회 오류:", error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: screens.map((screen) => this.mapToScreenDefinition(screen)),
|
||||
data: screens.map((screen) =>
|
||||
this.mapToScreenDefinition(screen, tableLabelMap)
|
||||
),
|
||||
pagination: {
|
||||
page,
|
||||
size,
|
||||
@@ -404,6 +441,8 @@ export class ScreenManagementService {
|
||||
}
|
||||
|
||||
// 메뉴 할당 확인
|
||||
// 메뉴에 할당된 화면인지 확인 (임시 주석 처리)
|
||||
/*
|
||||
const menuAssignments = await prisma.screen_menu_assignments.findMany({
|
||||
where: {
|
||||
screen_id: screenId,
|
||||
@@ -425,6 +464,7 @@ export class ScreenManagementService {
|
||||
referenceType: "menu_assignment",
|
||||
});
|
||||
}
|
||||
*/
|
||||
|
||||
return {
|
||||
hasDependencies: dependencies.length > 0,
|
||||
@@ -666,9 +706,22 @@ export class ScreenManagementService {
|
||||
prisma.screen_definitions.count({ where: whereClause }),
|
||||
]);
|
||||
|
||||
// 테이블 라벨 정보를 한 번에 조회
|
||||
const tableNames = [
|
||||
...new Set(screens.map((s) => s.table_name).filter(Boolean)),
|
||||
];
|
||||
const tableLabels = await prisma.table_labels.findMany({
|
||||
where: { table_name: { in: tableNames } },
|
||||
select: { table_name: true, table_label: true },
|
||||
});
|
||||
|
||||
const tableLabelMap = new Map(
|
||||
tableLabels.map((tl) => [tl.table_name, tl.table_label || tl.table_name])
|
||||
);
|
||||
|
||||
return {
|
||||
data: screens.map((screen) => ({
|
||||
...this.mapToScreenDefinition(screen),
|
||||
...this.mapToScreenDefinition(screen, tableLabelMap),
|
||||
deletedDate: screen.deleted_date || undefined,
|
||||
deletedBy: screen.deleted_by || undefined,
|
||||
deleteReason: screen.delete_reason || undefined,
|
||||
@@ -1528,12 +1581,18 @@ export class ScreenManagementService {
|
||||
// 유틸리티 메서드
|
||||
// ========================================
|
||||
|
||||
private mapToScreenDefinition(data: any): ScreenDefinition {
|
||||
private mapToScreenDefinition(
|
||||
data: any,
|
||||
tableLabelMap?: Map<string, string>
|
||||
): ScreenDefinition {
|
||||
const tableLabel = tableLabelMap?.get(data.table_name) || data.table_name;
|
||||
|
||||
return {
|
||||
screenId: data.screen_id,
|
||||
screenName: data.screen_name,
|
||||
screenCode: data.screen_code,
|
||||
tableName: data.table_name,
|
||||
tableLabel: tableLabel, // 라벨이 있으면 라벨, 없으면 테이블명
|
||||
companyCode: data.company_code,
|
||||
description: data.description,
|
||||
isActive: data.is_active,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { logger } from "../utils/logger";
|
||||
import { cache, CacheKeys } from "../utils/cache";
|
||||
import {
|
||||
TableInfo,
|
||||
ColumnTypeInfo,
|
||||
@@ -21,6 +22,13 @@ export class TableManagementService {
|
||||
try {
|
||||
logger.info("테이블 목록 조회 시작");
|
||||
|
||||
// 캐시에서 먼저 확인
|
||||
const cachedTables = cache.get<TableInfo[]>(CacheKeys.TABLE_LIST);
|
||||
if (cachedTables) {
|
||||
logger.info(`테이블 목록 캐시에서 조회: ${cachedTables.length}개`);
|
||||
return cachedTables;
|
||||
}
|
||||
|
||||
// information_schema는 여전히 $queryRaw 사용
|
||||
const rawTables = await prisma.$queryRaw<any[]>`
|
||||
SELECT
|
||||
@@ -44,6 +52,9 @@ export class TableManagementService {
|
||||
columnCount: Number(table.columnCount), // BigInt → Number 변환
|
||||
}));
|
||||
|
||||
// 캐시에 저장 (10분 TTL)
|
||||
cache.set(CacheKeys.TABLE_LIST, tables, 10 * 60 * 1000);
|
||||
|
||||
logger.info(`테이블 목록 조회 완료: ${tables.length}개`);
|
||||
return tables;
|
||||
} catch (error) {
|
||||
@@ -55,14 +66,59 @@ export class TableManagementService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 컬럼 정보 조회
|
||||
* 테이블 컬럼 정보 조회 (페이지네이션 지원)
|
||||
* 메타데이터 조회는 Prisma로 변경 불가
|
||||
*/
|
||||
async getColumnList(tableName: string): Promise<ColumnTypeInfo[]> {
|
||||
async getColumnList(
|
||||
tableName: string,
|
||||
page: number = 1,
|
||||
size: number = 50
|
||||
): Promise<{
|
||||
columns: ColumnTypeInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
totalPages: number;
|
||||
}> {
|
||||
try {
|
||||
logger.info(`컬럼 정보 조회 시작: ${tableName}`);
|
||||
logger.info(
|
||||
`컬럼 정보 조회 시작: ${tableName} (page: ${page}, size: ${size})`
|
||||
);
|
||||
|
||||
// information_schema는 여전히 $queryRaw 사용
|
||||
// 캐시 키 생성
|
||||
const cacheKey = CacheKeys.TABLE_COLUMNS(tableName, page, size);
|
||||
const countCacheKey = CacheKeys.TABLE_COLUMN_COUNT(tableName);
|
||||
|
||||
// 캐시에서 먼저 확인
|
||||
const cachedResult = cache.get<{
|
||||
columns: ColumnTypeInfo[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
totalPages: number;
|
||||
}>(cacheKey);
|
||||
if (cachedResult) {
|
||||
logger.info(
|
||||
`컬럼 정보 캐시에서 조회: ${tableName}, ${cachedResult.columns.length}/${cachedResult.total}개`
|
||||
);
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
// 전체 컬럼 수 조회 (캐시 확인)
|
||||
let total = cache.get<number>(countCacheKey);
|
||||
if (!total) {
|
||||
const totalResult = await prisma.$queryRaw<[{ count: bigint }]>`
|
||||
SELECT COUNT(*) as count
|
||||
FROM information_schema.columns c
|
||||
WHERE c.table_name = ${tableName}
|
||||
`;
|
||||
total = Number(totalResult[0].count);
|
||||
// 컬럼 수는 자주 변하지 않으므로 30분 캐시
|
||||
cache.set(countCacheKey, total, 30 * 60 * 1000);
|
||||
}
|
||||
|
||||
// 페이지네이션 적용한 컬럼 조회
|
||||
const offset = (page - 1) * size;
|
||||
const rawColumns = await prisma.$queryRaw<any[]>`
|
||||
SELECT
|
||||
c.column_name as "columnName",
|
||||
@@ -87,6 +143,7 @@ export class TableManagementService {
|
||||
LEFT JOIN column_labels cl ON c.table_name = cl.table_name AND c.column_name = cl.column_name
|
||||
WHERE c.table_name = ${tableName}
|
||||
ORDER BY c.ordinal_position
|
||||
LIMIT ${size} OFFSET ${offset}
|
||||
`;
|
||||
|
||||
// BigInt를 Number로 변환하여 JSON 직렬화 문제 해결
|
||||
@@ -100,8 +157,23 @@ export class TableManagementService {
|
||||
displayOrder: column.displayOrder ? Number(column.displayOrder) : null,
|
||||
}));
|
||||
|
||||
logger.info(`컬럼 정보 조회 완료: ${tableName}, ${columns.length}개`);
|
||||
return columns;
|
||||
const totalPages = Math.ceil(total / size);
|
||||
|
||||
const result = {
|
||||
columns,
|
||||
total,
|
||||
page,
|
||||
size,
|
||||
totalPages,
|
||||
};
|
||||
|
||||
// 캐시에 저장 (5분 TTL)
|
||||
cache.set(cacheKey, result, 5 * 60 * 1000);
|
||||
|
||||
logger.info(
|
||||
`컬럼 정보 조회 완료: ${tableName}, ${columns.length}/${total}개 (${page}/${totalPages} 페이지)`
|
||||
);
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error(`컬럼 정보 조회 중 오류 발생: ${tableName}`, error);
|
||||
throw new Error(
|
||||
@@ -137,6 +209,40 @@ export class TableManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블 라벨 업데이트
|
||||
*/
|
||||
async updateTableLabel(
|
||||
tableName: string,
|
||||
displayName: string,
|
||||
description?: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
logger.info(`테이블 라벨 업데이트 시작: ${tableName}`);
|
||||
|
||||
// table_labels 테이블에 UPSERT
|
||||
await prisma.$executeRaw`
|
||||
INSERT INTO table_labels (table_name, table_label, description, created_date, updated_date)
|
||||
VALUES (${tableName}, ${displayName}, ${description || ""}, NOW(), NOW())
|
||||
ON CONFLICT (table_name)
|
||||
DO UPDATE SET
|
||||
table_label = EXCLUDED.table_label,
|
||||
description = EXCLUDED.description,
|
||||
updated_date = NOW()
|
||||
`;
|
||||
|
||||
// 캐시 무효화
|
||||
cache.delete(CacheKeys.TABLE_LIST);
|
||||
|
||||
logger.info(`테이블 라벨 업데이트 완료: ${tableName}`);
|
||||
} catch (error) {
|
||||
logger.error("테이블 라벨 업데이트 중 오류 발생:", error);
|
||||
throw new Error(
|
||||
`테이블 라벨 업데이트 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 컬럼 설정 업데이트 (UPSERT 방식)
|
||||
* Prisma ORM으로 변경
|
||||
|
||||
@@ -151,6 +151,7 @@ export interface ScreenDefinition {
|
||||
screenName: string;
|
||||
screenCode: string;
|
||||
tableName: string;
|
||||
tableLabel?: string; // 테이블 라벨 (한글명)
|
||||
companyCode: string;
|
||||
description?: string;
|
||||
isActive: string;
|
||||
|
||||
143
backend-node/src/utils/cache.ts
Normal file
143
backend-node/src/utils/cache.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 간단한 메모리 캐시 구현
|
||||
* 테이블 타입관리 성능 최적화용
|
||||
*/
|
||||
|
||||
interface CacheItem<T> {
|
||||
data: T;
|
||||
timestamp: number;
|
||||
ttl: number; // Time to live in milliseconds
|
||||
}
|
||||
|
||||
class MemoryCache {
|
||||
private cache = new Map<string, CacheItem<any>>();
|
||||
private readonly DEFAULT_TTL = 5 * 60 * 1000; // 5분
|
||||
|
||||
/**
|
||||
* 캐시에 데이터 저장
|
||||
*/
|
||||
set<T>(key: string, data: T, ttl: number = this.DEFAULT_TTL): void {
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
ttl,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시에서 데이터 조회
|
||||
*/
|
||||
get<T>(key: string): T | null {
|
||||
const item = this.cache.get(key);
|
||||
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TTL 체크
|
||||
if (Date.now() - item.timestamp > item.ttl) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
return item.data as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시에서 데이터 삭제
|
||||
*/
|
||||
delete(key: string): boolean {
|
||||
return this.cache.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 패턴으로 캐시 삭제 (테이블 관련 캐시 일괄 삭제용)
|
||||
*/
|
||||
deleteByPattern(pattern: string): number {
|
||||
let deletedCount = 0;
|
||||
const regex = new RegExp(pattern);
|
||||
|
||||
for (const key of this.cache.keys()) {
|
||||
if (regex.test(key)) {
|
||||
this.cache.delete(key);
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 만료된 캐시 정리
|
||||
*/
|
||||
cleanup(): number {
|
||||
let cleanedCount = 0;
|
||||
const now = Date.now();
|
||||
|
||||
for (const [key, item] of this.cache.entries()) {
|
||||
if (now - item.timestamp > item.ttl) {
|
||||
this.cache.delete(key);
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return cleanedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐시 통계
|
||||
*/
|
||||
getStats(): {
|
||||
totalKeys: number;
|
||||
expiredKeys: number;
|
||||
memoryUsage: string;
|
||||
} {
|
||||
const now = Date.now();
|
||||
let expiredKeys = 0;
|
||||
|
||||
for (const item of this.cache.values()) {
|
||||
if (now - item.timestamp > item.ttl) {
|
||||
expiredKeys++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalKeys: this.cache.size,
|
||||
expiredKeys,
|
||||
memoryUsage: `${Math.round(JSON.stringify([...this.cache.entries()]).length / 1024)} KB`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 캐시 초기화
|
||||
*/
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 싱글톤 인스턴스
|
||||
export const cache = new MemoryCache();
|
||||
|
||||
// 캐시 키 생성 헬퍼
|
||||
export const CacheKeys = {
|
||||
TABLE_LIST: "table_list",
|
||||
TABLE_COLUMNS: (tableName: string, page: number, size: number) =>
|
||||
`table_columns:${tableName}:${page}:${size}`,
|
||||
TABLE_COLUMN_COUNT: (tableName: string) => `table_column_count:${tableName}`,
|
||||
WEB_TYPE_OPTIONS: "web_type_options",
|
||||
COMMON_CODES: (category: string) => `common_codes:${category}`,
|
||||
} as const;
|
||||
|
||||
// 자동 정리 스케줄러 (10분마다)
|
||||
setInterval(
|
||||
() => {
|
||||
const cleaned = cache.cleanup();
|
||||
if (cleaned > 0) {
|
||||
console.log(`[Cache] 만료된 캐시 ${cleaned}개 정리됨`);
|
||||
}
|
||||
},
|
||||
10 * 60 * 1000
|
||||
);
|
||||
|
||||
export default cache;
|
||||
Reference in New Issue
Block a user