2025-09-01 11:00:38 +09:00
|
|
|
import { PrismaClient } from "@prisma/client";
|
2025-08-25 14:08:08 +09:00
|
|
|
import { logger } from "../utils/logger";
|
2025-09-08 14:20:01 +09:00
|
|
|
import { cache, CacheKeys } from "../utils/cache";
|
2025-08-25 14:08:08 +09:00
|
|
|
import {
|
|
|
|
|
TableInfo,
|
|
|
|
|
ColumnTypeInfo,
|
|
|
|
|
ColumnSettings,
|
|
|
|
|
TableLabels,
|
|
|
|
|
ColumnLabels,
|
|
|
|
|
} from "../types/tableManagement";
|
|
|
|
|
|
2025-09-01 11:00:38 +09:00
|
|
|
const prisma = new PrismaClient();
|
2025-08-25 14:08:08 +09:00
|
|
|
|
2025-09-01 11:00:38 +09:00
|
|
|
export class TableManagementService {
|
|
|
|
|
constructor() {}
|
2025-08-25 14:08:08 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테이블 목록 조회 (PostgreSQL information_schema 활용)
|
2025-09-01 11:00:38 +09:00
|
|
|
* 메타데이터 조회는 Prisma로 변경 불가
|
2025-08-25 14:08:08 +09:00
|
|
|
*/
|
|
|
|
|
async getTableList(): Promise<TableInfo[]> {
|
|
|
|
|
try {
|
|
|
|
|
logger.info("테이블 목록 조회 시작");
|
|
|
|
|
|
2025-09-08 14:20:01 +09:00
|
|
|
// 캐시에서 먼저 확인
|
|
|
|
|
const cachedTables = cache.get<TableInfo[]>(CacheKeys.TABLE_LIST);
|
|
|
|
|
if (cachedTables) {
|
|
|
|
|
logger.info(`테이블 목록 캐시에서 조회: ${cachedTables.length}개`);
|
|
|
|
|
return cachedTables;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 11:00:38 +09:00
|
|
|
// information_schema는 여전히 $queryRaw 사용
|
2025-09-01 15:37:49 +09:00
|
|
|
const rawTables = await prisma.$queryRaw<any[]>`
|
2025-08-25 14:08:08 +09:00
|
|
|
SELECT
|
|
|
|
|
t.table_name as "tableName",
|
|
|
|
|
COALESCE(tl.table_label, t.table_name) as "displayName",
|
|
|
|
|
COALESCE(tl.description, '') as "description",
|
|
|
|
|
(SELECT COUNT(*) FROM information_schema.columns
|
|
|
|
|
WHERE table_name = t.table_name AND table_schema = 'public') as "columnCount"
|
|
|
|
|
FROM information_schema.tables t
|
|
|
|
|
LEFT JOIN table_labels tl ON t.table_name = tl.table_name
|
|
|
|
|
WHERE t.table_schema = 'public'
|
|
|
|
|
AND t.table_type = 'BASE TABLE'
|
|
|
|
|
AND t.table_name NOT LIKE 'pg_%'
|
|
|
|
|
AND t.table_name NOT LIKE 'sql_%'
|
|
|
|
|
ORDER BY t.table_name
|
|
|
|
|
`;
|
|
|
|
|
|
2025-09-01 15:37:49 +09:00
|
|
|
// BigInt를 Number로 변환하여 JSON 직렬화 문제 해결
|
|
|
|
|
const tables: TableInfo[] = rawTables.map((table) => ({
|
|
|
|
|
...table,
|
|
|
|
|
columnCount: Number(table.columnCount), // BigInt → Number 변환
|
|
|
|
|
}));
|
|
|
|
|
|
2025-09-08 14:20:01 +09:00
|
|
|
// 캐시에 저장 (10분 TTL)
|
|
|
|
|
cache.set(CacheKeys.TABLE_LIST, tables, 10 * 60 * 1000);
|
|
|
|
|
|
2025-09-01 11:00:38 +09:00
|
|
|
logger.info(`테이블 목록 조회 완료: ${tables.length}개`);
|
|
|
|
|
return tables;
|
2025-08-25 14:08:08 +09:00
|
|
|
} catch (error) {
|
|
|
|
|
logger.error("테이블 목록 조회 중 오류 발생:", error);
|
|
|
|
|
throw new Error(
|
|
|
|
|
`테이블 목록 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-08 14:20:01 +09:00
|
|
|
* 테이블 컬럼 정보 조회 (페이지네이션 지원)
|
2025-09-01 11:00:38 +09:00
|
|
|
* 메타데이터 조회는 Prisma로 변경 불가
|
2025-08-25 14:08:08 +09:00
|
|
|
*/
|
2025-09-08 14:20:01 +09:00
|
|
|
async getColumnList(
|
|
|
|
|
tableName: string,
|
|
|
|
|
page: number = 1,
|
|
|
|
|
size: number = 50
|
|
|
|
|
): Promise<{
|
|
|
|
|
columns: ColumnTypeInfo[];
|
|
|
|
|
total: number;
|
|
|
|
|
page: number;
|
|
|
|
|
size: number;
|
|
|
|
|
totalPages: number;
|
|
|
|
|
}> {
|
2025-08-25 14:08:08 +09:00
|
|
|
try {
|
2025-09-08 14:20:01 +09:00
|
|
|
logger.info(
|
|
|
|
|
`컬럼 정보 조회 시작: ${tableName} (page: ${page}, size: ${size})`
|
|
|
|
|
);
|
2025-08-25 14:08:08 +09:00
|
|
|
|
2025-09-08 14:20:01 +09:00
|
|
|
// 캐시 키 생성
|
|
|
|
|
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;
|
2025-09-01 15:37:49 +09:00
|
|
|
const rawColumns = await prisma.$queryRaw<any[]>`
|
2025-08-25 14:08:08 +09:00
|
|
|
SELECT
|
|
|
|
|
c.column_name as "columnName",
|
|
|
|
|
COALESCE(cl.column_label, c.column_name) as "displayName",
|
2025-09-05 18:00:18 +09:00
|
|
|
c.data_type as "dataType",
|
2025-08-25 14:08:08 +09:00
|
|
|
c.data_type as "dbType",
|
|
|
|
|
COALESCE(cl.web_type, 'text') as "webType",
|
2025-09-04 14:23:35 +09:00
|
|
|
COALESCE(cl.input_type, 'direct') as "inputType",
|
2025-08-25 14:08:08 +09:00
|
|
|
COALESCE(cl.detail_settings, '') as "detailSettings",
|
|
|
|
|
COALESCE(cl.description, '') as "description",
|
|
|
|
|
c.is_nullable as "isNullable",
|
2025-09-05 18:00:18 +09:00
|
|
|
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as "isPrimaryKey",
|
2025-08-25 14:08:08 +09:00
|
|
|
c.column_default as "defaultValue",
|
|
|
|
|
c.character_maximum_length as "maxLength",
|
|
|
|
|
c.numeric_precision as "numericPrecision",
|
|
|
|
|
c.numeric_scale as "numericScale",
|
|
|
|
|
cl.code_category as "codeCategory",
|
|
|
|
|
cl.code_value as "codeValue",
|
|
|
|
|
cl.reference_table as "referenceTable",
|
|
|
|
|
cl.reference_column as "referenceColumn",
|
|
|
|
|
cl.display_order as "displayOrder",
|
|
|
|
|
cl.is_visible as "isVisible"
|
|
|
|
|
FROM information_schema.columns c
|
|
|
|
|
LEFT JOIN column_labels cl ON c.table_name = cl.table_name AND c.column_name = cl.column_name
|
2025-09-05 18:00:18 +09:00
|
|
|
LEFT JOIN (
|
|
|
|
|
SELECT kcu.column_name, kcu.table_name
|
|
|
|
|
FROM information_schema.table_constraints tc
|
|
|
|
|
JOIN information_schema.key_column_usage kcu
|
|
|
|
|
ON tc.constraint_name = kcu.constraint_name
|
|
|
|
|
AND tc.table_schema = kcu.table_schema
|
|
|
|
|
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
|
|
|
AND tc.table_name = ${tableName}
|
|
|
|
|
) pk ON c.column_name = pk.column_name AND c.table_name = pk.table_name
|
2025-09-01 11:00:38 +09:00
|
|
|
WHERE c.table_name = ${tableName}
|
2025-08-25 14:08:08 +09:00
|
|
|
ORDER BY c.ordinal_position
|
2025-09-08 14:20:01 +09:00
|
|
|
LIMIT ${size} OFFSET ${offset}
|
2025-08-25 14:08:08 +09:00
|
|
|
`;
|
|
|
|
|
|
2025-09-01 15:37:49 +09:00
|
|
|
// BigInt를 Number로 변환하여 JSON 직렬화 문제 해결
|
|
|
|
|
const columns: ColumnTypeInfo[] = rawColumns.map((column) => ({
|
|
|
|
|
...column,
|
|
|
|
|
maxLength: column.maxLength ? Number(column.maxLength) : null,
|
|
|
|
|
numericPrecision: column.numericPrecision
|
|
|
|
|
? Number(column.numericPrecision)
|
|
|
|
|
: null,
|
|
|
|
|
numericScale: column.numericScale ? Number(column.numericScale) : null,
|
|
|
|
|
displayOrder: column.displayOrder ? Number(column.displayOrder) : null,
|
|
|
|
|
}));
|
|
|
|
|
|
2025-09-08 14:20:01 +09:00
|
|
|
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;
|
2025-08-25 14:08:08 +09:00
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`컬럼 정보 조회 중 오류 발생: ${tableName}`, error);
|
|
|
|
|
throw new Error(
|
|
|
|
|
`컬럼 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테이블이 table_labels에 없으면 자동 추가
|
2025-09-01 11:00:38 +09:00
|
|
|
* Prisma ORM으로 변경
|
2025-08-25 14:08:08 +09:00
|
|
|
*/
|
|
|
|
|
async insertTableIfNotExists(tableName: string): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
logger.info(`테이블 라벨 자동 추가 시작: ${tableName}`);
|
|
|
|
|
|
2025-09-01 11:00:38 +09:00
|
|
|
await prisma.table_labels.upsert({
|
|
|
|
|
where: { table_name: tableName },
|
|
|
|
|
update: {}, // 이미 존재하면 변경하지 않음
|
|
|
|
|
create: {
|
|
|
|
|
table_name: tableName,
|
|
|
|
|
table_label: tableName,
|
|
|
|
|
description: "",
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-08-25 14:08:08 +09:00
|
|
|
|
|
|
|
|
logger.info(`테이블 라벨 자동 추가 완료: ${tableName}`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`테이블 라벨 자동 추가 중 오류 발생: ${tableName}`, error);
|
|
|
|
|
throw new Error(
|
|
|
|
|
`테이블 라벨 자동 추가 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-08 14:20:01 +09:00
|
|
|
/**
|
|
|
|
|
* 테이블 라벨 업데이트
|
|
|
|
|
*/
|
|
|
|
|
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"}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-25 14:08:08 +09:00
|
|
|
/**
|
|
|
|
|
* 컬럼 설정 업데이트 (UPSERT 방식)
|
2025-09-01 11:00:38 +09:00
|
|
|
* Prisma ORM으로 변경
|
2025-08-25 14:08:08 +09:00
|
|
|
*/
|
|
|
|
|
async updateColumnSettings(
|
|
|
|
|
tableName: string,
|
|
|
|
|
columnName: string,
|
|
|
|
|
settings: ColumnSettings
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
logger.info(`컬럼 설정 업데이트 시작: ${tableName}.${columnName}`);
|
|
|
|
|
|
|
|
|
|
// 테이블이 table_labels에 없으면 자동 추가
|
|
|
|
|
await this.insertTableIfNotExists(tableName);
|
|
|
|
|
|
2025-09-01 11:00:38 +09:00
|
|
|
// column_labels 업데이트 또는 생성
|
|
|
|
|
await prisma.column_labels.upsert({
|
|
|
|
|
where: {
|
|
|
|
|
table_name_column_name: {
|
|
|
|
|
table_name: tableName,
|
|
|
|
|
column_name: columnName,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
update: {
|
|
|
|
|
column_label: settings.columnLabel,
|
|
|
|
|
web_type: settings.webType,
|
|
|
|
|
detail_settings: settings.detailSettings,
|
|
|
|
|
code_category: settings.codeCategory,
|
|
|
|
|
code_value: settings.codeValue,
|
|
|
|
|
reference_table: settings.referenceTable,
|
|
|
|
|
reference_column: settings.referenceColumn,
|
|
|
|
|
display_order: settings.displayOrder || 0,
|
|
|
|
|
is_visible:
|
|
|
|
|
settings.isVisible !== undefined ? settings.isVisible : true,
|
|
|
|
|
updated_date: new Date(),
|
|
|
|
|
},
|
|
|
|
|
create: {
|
|
|
|
|
table_name: tableName,
|
|
|
|
|
column_name: columnName,
|
|
|
|
|
column_label: settings.columnLabel,
|
|
|
|
|
web_type: settings.webType,
|
|
|
|
|
detail_settings: settings.detailSettings,
|
|
|
|
|
code_category: settings.codeCategory,
|
|
|
|
|
code_value: settings.codeValue,
|
|
|
|
|
reference_table: settings.referenceTable,
|
|
|
|
|
reference_column: settings.referenceColumn,
|
|
|
|
|
display_order: settings.displayOrder || 0,
|
|
|
|
|
is_visible:
|
|
|
|
|
settings.isVisible !== undefined ? settings.isVisible : true,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-08-25 14:08:08 +09:00
|
|
|
|
2025-09-15 15:38:48 +09:00
|
|
|
// 캐시 무효화 - 해당 테이블의 컬럼 캐시 삭제
|
|
|
|
|
cache.deleteByPattern(`table_columns:${tableName}:`);
|
|
|
|
|
cache.delete(CacheKeys.TABLE_COLUMN_COUNT(tableName));
|
|
|
|
|
|
2025-08-25 14:08:08 +09:00
|
|
|
logger.info(`컬럼 설정 업데이트 완료: ${tableName}.${columnName}`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(
|
|
|
|
|
`컬럼 설정 업데이트 중 오류 발생: ${tableName}.${columnName}`,
|
|
|
|
|
error
|
|
|
|
|
);
|
|
|
|
|
throw new Error(
|
|
|
|
|
`컬럼 설정 업데이트 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 전체 컬럼 설정 일괄 업데이트
|
2025-09-01 11:00:38 +09:00
|
|
|
* Prisma 트랜잭션으로 변경
|
2025-08-25 14:08:08 +09:00
|
|
|
*/
|
|
|
|
|
async updateAllColumnSettings(
|
|
|
|
|
tableName: string,
|
|
|
|
|
columnSettings: ColumnSettings[]
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
logger.info(
|
|
|
|
|
`전체 컬럼 설정 일괄 업데이트 시작: ${tableName}, ${columnSettings.length}개`
|
|
|
|
|
);
|
|
|
|
|
|
2025-09-01 11:00:38 +09:00
|
|
|
// Prisma 트랜잭션 사용
|
|
|
|
|
await prisma.$transaction(async (tx) => {
|
2025-08-25 14:08:08 +09:00
|
|
|
// 테이블이 table_labels에 없으면 자동 추가
|
|
|
|
|
await this.insertTableIfNotExists(tableName);
|
|
|
|
|
|
|
|
|
|
// 각 컬럼 설정을 순차적으로 업데이트
|
|
|
|
|
for (const columnSetting of columnSettings) {
|
2025-09-01 11:48:12 +09:00
|
|
|
// columnName은 실제 DB 컬럼명을 유지해야 함
|
|
|
|
|
const columnName = columnSetting.columnName;
|
2025-08-25 14:08:08 +09:00
|
|
|
if (columnName) {
|
|
|
|
|
await this.updateColumnSettings(
|
|
|
|
|
tableName,
|
|
|
|
|
columnName,
|
|
|
|
|
columnSetting
|
|
|
|
|
);
|
2025-09-01 11:48:12 +09:00
|
|
|
} else {
|
|
|
|
|
logger.warn(
|
|
|
|
|
`컬럼명이 누락된 설정: ${JSON.stringify(columnSetting)}`
|
|
|
|
|
);
|
2025-08-25 14:08:08 +09:00
|
|
|
}
|
|
|
|
|
}
|
2025-09-01 11:00:38 +09:00
|
|
|
});
|
2025-08-25 14:08:08 +09:00
|
|
|
|
2025-09-15 15:38:48 +09:00
|
|
|
// 캐시 무효화 - 해당 테이블의 컬럼 캐시 삭제
|
|
|
|
|
cache.deleteByPattern(`table_columns:${tableName}:`);
|
|
|
|
|
cache.delete(CacheKeys.TABLE_COLUMN_COUNT(tableName));
|
|
|
|
|
|
2025-09-01 11:00:38 +09:00
|
|
|
logger.info(`전체 컬럼 설정 일괄 업데이트 완료: ${tableName}`);
|
2025-08-25 14:08:08 +09:00
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(
|
|
|
|
|
`전체 컬럼 설정 일괄 업데이트 중 오류 발생: ${tableName}`,
|
|
|
|
|
error
|
|
|
|
|
);
|
|
|
|
|
throw new Error(
|
|
|
|
|
`전체 컬럼 설정 일괄 업데이트 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테이블 라벨 정보 조회
|
2025-09-01 11:00:38 +09:00
|
|
|
* Prisma ORM으로 변경
|
2025-08-25 14:08:08 +09:00
|
|
|
*/
|
|
|
|
|
async getTableLabels(tableName: string): Promise<TableLabels | null> {
|
|
|
|
|
try {
|
|
|
|
|
logger.info(`테이블 라벨 정보 조회 시작: ${tableName}`);
|
|
|
|
|
|
2025-09-01 11:00:38 +09:00
|
|
|
const tableLabel = await prisma.table_labels.findUnique({
|
|
|
|
|
where: { table_name: tableName },
|
|
|
|
|
select: {
|
|
|
|
|
table_name: true,
|
|
|
|
|
table_label: true,
|
|
|
|
|
description: true,
|
|
|
|
|
created_date: true,
|
|
|
|
|
updated_date: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!tableLabel) {
|
2025-08-25 14:08:08 +09:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 11:00:38 +09:00
|
|
|
const result: TableLabels = {
|
|
|
|
|
tableName: tableLabel.table_name,
|
|
|
|
|
tableLabel: tableLabel.table_label || undefined,
|
|
|
|
|
description: tableLabel.description || undefined,
|
|
|
|
|
createdDate: tableLabel.created_date || undefined,
|
|
|
|
|
updatedDate: tableLabel.updated_date || undefined,
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-25 14:08:08 +09:00
|
|
|
logger.info(`테이블 라벨 정보 조회 완료: ${tableName}`);
|
2025-09-01 11:00:38 +09:00
|
|
|
return result;
|
2025-08-25 14:08:08 +09:00
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`테이블 라벨 정보 조회 중 오류 발생: ${tableName}`, error);
|
|
|
|
|
throw new Error(
|
|
|
|
|
`테이블 라벨 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 컬럼 라벨 정보 조회
|
2025-09-01 11:00:38 +09:00
|
|
|
* Prisma ORM으로 변경
|
2025-08-25 14:08:08 +09:00
|
|
|
*/
|
|
|
|
|
async getColumnLabels(
|
|
|
|
|
tableName: string,
|
|
|
|
|
columnName: string
|
|
|
|
|
): Promise<ColumnLabels | null> {
|
|
|
|
|
try {
|
|
|
|
|
logger.info(`컬럼 라벨 정보 조회 시작: ${tableName}.${columnName}`);
|
|
|
|
|
|
2025-09-01 11:00:38 +09:00
|
|
|
const columnLabel = await prisma.column_labels.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
table_name_column_name: {
|
|
|
|
|
table_name: tableName,
|
|
|
|
|
column_name: columnName,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
table_name: true,
|
|
|
|
|
column_name: true,
|
|
|
|
|
column_label: true,
|
|
|
|
|
web_type: true,
|
|
|
|
|
detail_settings: true,
|
|
|
|
|
description: true,
|
|
|
|
|
display_order: true,
|
|
|
|
|
is_visible: true,
|
|
|
|
|
code_category: true,
|
|
|
|
|
code_value: true,
|
|
|
|
|
reference_table: true,
|
|
|
|
|
reference_column: true,
|
|
|
|
|
created_date: true,
|
|
|
|
|
updated_date: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!columnLabel) {
|
2025-08-25 14:08:08 +09:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 11:00:38 +09:00
|
|
|
const result: ColumnLabels = {
|
|
|
|
|
id: columnLabel.id,
|
|
|
|
|
tableName: columnLabel.table_name || "",
|
|
|
|
|
columnName: columnLabel.column_name || "",
|
|
|
|
|
columnLabel: columnLabel.column_label || undefined,
|
|
|
|
|
webType: columnLabel.web_type || undefined,
|
|
|
|
|
detailSettings: columnLabel.detail_settings || undefined,
|
|
|
|
|
description: columnLabel.description || undefined,
|
|
|
|
|
displayOrder: columnLabel.display_order || undefined,
|
|
|
|
|
isVisible: columnLabel.is_visible || undefined,
|
|
|
|
|
codeCategory: columnLabel.code_category || undefined,
|
|
|
|
|
codeValue: columnLabel.code_value || undefined,
|
|
|
|
|
referenceTable: columnLabel.reference_table || undefined,
|
|
|
|
|
referenceColumn: columnLabel.reference_column || undefined,
|
|
|
|
|
createdDate: columnLabel.created_date || undefined,
|
|
|
|
|
updatedDate: columnLabel.updated_date || undefined,
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-25 14:08:08 +09:00
|
|
|
logger.info(`컬럼 라벨 정보 조회 완료: ${tableName}.${columnName}`);
|
2025-09-01 11:00:38 +09:00
|
|
|
return result;
|
2025-08-25 14:08:08 +09:00
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(
|
|
|
|
|
`컬럼 라벨 정보 조회 중 오류 발생: ${tableName}.${columnName}`,
|
|
|
|
|
error
|
|
|
|
|
);
|
|
|
|
|
throw new Error(
|
|
|
|
|
`컬럼 라벨 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-01 11:48:12 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 컬럼 웹 타입 설정
|
|
|
|
|
*/
|
|
|
|
|
async updateColumnWebType(
|
|
|
|
|
tableName: string,
|
|
|
|
|
columnName: string,
|
|
|
|
|
webType: string,
|
2025-09-04 14:23:35 +09:00
|
|
|
detailSettings?: Record<string, any>,
|
|
|
|
|
inputType?: string
|
2025-09-01 11:48:12 +09:00
|
|
|
): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
logger.info(
|
|
|
|
|
`컬럼 웹 타입 설정 시작: ${tableName}.${columnName} = ${webType}`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 웹 타입별 기본 상세 설정 생성
|
|
|
|
|
const defaultDetailSettings = this.generateDefaultDetailSettings(webType);
|
|
|
|
|
|
|
|
|
|
// 사용자 정의 설정과 기본 설정 병합
|
|
|
|
|
const finalDetailSettings = {
|
|
|
|
|
...defaultDetailSettings,
|
|
|
|
|
...detailSettings,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// column_labels 테이블에 해당 컬럼이 있는지 확인
|
2025-09-01 15:22:47 +09:00
|
|
|
const existingColumn = await prisma.column_labels.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
table_name: tableName,
|
|
|
|
|
column_name: columnName,
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-09-01 11:48:12 +09:00
|
|
|
|
2025-09-01 15:22:47 +09:00
|
|
|
if (existingColumn) {
|
2025-09-01 11:48:12 +09:00
|
|
|
// 기존 컬럼 라벨 업데이트
|
2025-09-04 14:23:35 +09:00
|
|
|
const updateData: any = {
|
|
|
|
|
web_type: webType,
|
|
|
|
|
detail_settings: JSON.stringify(finalDetailSettings),
|
|
|
|
|
updated_date: new Date(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (inputType) {
|
|
|
|
|
updateData.input_type = inputType;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 15:22:47 +09:00
|
|
|
await prisma.column_labels.update({
|
|
|
|
|
where: {
|
|
|
|
|
id: existingColumn.id,
|
|
|
|
|
},
|
2025-09-04 14:23:35 +09:00
|
|
|
data: updateData,
|
2025-09-01 15:22:47 +09:00
|
|
|
});
|
2025-09-01 11:48:12 +09:00
|
|
|
logger.info(
|
|
|
|
|
`컬럼 웹 타입 업데이트 완료: ${tableName}.${columnName} = ${webType}`
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// 새로운 컬럼 라벨 생성
|
2025-09-04 14:23:35 +09:00
|
|
|
const createData: any = {
|
|
|
|
|
table_name: tableName,
|
|
|
|
|
column_name: columnName,
|
|
|
|
|
web_type: webType,
|
|
|
|
|
detail_settings: JSON.stringify(finalDetailSettings),
|
|
|
|
|
created_date: new Date(),
|
|
|
|
|
updated_date: new Date(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (inputType) {
|
|
|
|
|
createData.input_type = inputType;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-01 15:22:47 +09:00
|
|
|
await prisma.column_labels.create({
|
2025-09-04 14:23:35 +09:00
|
|
|
data: createData,
|
2025-09-01 15:22:47 +09:00
|
|
|
});
|
2025-09-01 11:48:12 +09:00
|
|
|
logger.info(
|
|
|
|
|
`컬럼 라벨 생성 및 웹 타입 설정 완료: ${tableName}.${columnName} = ${webType}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(
|
|
|
|
|
`컬럼 웹 타입 설정 중 오류 발생: ${tableName}.${columnName}`,
|
|
|
|
|
error
|
|
|
|
|
);
|
|
|
|
|
throw new Error(
|
|
|
|
|
`컬럼 웹 타입 설정 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 웹 타입별 기본 상세 설정 생성
|
|
|
|
|
*/
|
|
|
|
|
private generateDefaultDetailSettings(webType: string): Record<string, any> {
|
|
|
|
|
switch (webType) {
|
|
|
|
|
case "text":
|
|
|
|
|
return {
|
|
|
|
|
maxLength: 255,
|
|
|
|
|
pattern: null,
|
|
|
|
|
placeholder: null,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
case "number":
|
|
|
|
|
return {
|
|
|
|
|
min: null,
|
|
|
|
|
max: null,
|
|
|
|
|
step: 1,
|
|
|
|
|
precision: 2,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
case "date":
|
|
|
|
|
return {
|
|
|
|
|
format: "YYYY-MM-DD",
|
|
|
|
|
minDate: null,
|
|
|
|
|
maxDate: null,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
case "code":
|
|
|
|
|
return {
|
|
|
|
|
codeCategory: null,
|
|
|
|
|
displayFormat: "label",
|
|
|
|
|
searchable: true,
|
|
|
|
|
multiple: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
case "entity":
|
|
|
|
|
return {
|
|
|
|
|
referenceTable: null,
|
|
|
|
|
referenceColumn: null,
|
|
|
|
|
searchable: true,
|
|
|
|
|
multiple: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
case "textarea":
|
|
|
|
|
return {
|
|
|
|
|
rows: 3,
|
|
|
|
|
maxLength: 1000,
|
|
|
|
|
placeholder: null,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
case "select":
|
|
|
|
|
return {
|
|
|
|
|
options: [],
|
|
|
|
|
multiple: false,
|
|
|
|
|
searchable: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
case "checkbox":
|
|
|
|
|
return {
|
|
|
|
|
defaultChecked: false,
|
|
|
|
|
label: null,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
case "radio":
|
|
|
|
|
return {
|
|
|
|
|
options: [],
|
|
|
|
|
inline: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
case "file":
|
|
|
|
|
return {
|
|
|
|
|
accept: "*/*",
|
|
|
|
|
maxSize: 10485760, // 10MB
|
|
|
|
|
multiple: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-03 15:23:12 +09:00
|
|
|
|
2025-09-05 21:52:19 +09:00
|
|
|
/**
|
|
|
|
|
* 파일 데이터 보강 (attach_file_info에서 파일 정보 가져오기)
|
|
|
|
|
*/
|
|
|
|
|
private async enrichFileData(
|
|
|
|
|
data: any[],
|
|
|
|
|
fileColumns: string[],
|
|
|
|
|
tableName: string
|
|
|
|
|
): Promise<any[]> {
|
|
|
|
|
try {
|
|
|
|
|
logger.info(
|
|
|
|
|
`파일 데이터 보강 시작: ${tableName}, ${fileColumns.join(", ")}`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 각 행의 파일 정보를 보강
|
|
|
|
|
const enrichedData = await Promise.all(
|
|
|
|
|
data.map(async (row) => {
|
|
|
|
|
const enrichedRow = { ...row };
|
|
|
|
|
|
|
|
|
|
// 각 파일 컬럼에 대해 처리
|
|
|
|
|
for (const fileColumn of fileColumns) {
|
|
|
|
|
const filePath = row[fileColumn];
|
|
|
|
|
if (filePath && typeof filePath === "string") {
|
2025-09-08 10:02:30 +09:00
|
|
|
// 🎯 컴포넌트별 파일 정보 조회
|
|
|
|
|
// 파일 경로에서 컴포넌트 ID 추출하거나 컬럼명 사용
|
|
|
|
|
const componentId =
|
|
|
|
|
this.extractComponentIdFromPath(filePath) || fileColumn;
|
|
|
|
|
const fileInfos = await this.getFileInfoByColumnAndTarget(
|
|
|
|
|
componentId,
|
|
|
|
|
row.id || row.objid || row.seq, // 기본키 값
|
|
|
|
|
tableName
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (fileInfos && fileInfos.length > 0) {
|
2025-09-05 21:52:19 +09:00
|
|
|
// 파일 정보를 JSON 형태로 저장
|
2025-09-08 10:02:30 +09:00
|
|
|
const totalSize = fileInfos.reduce(
|
|
|
|
|
(sum, file) => sum + (file.size || 0),
|
|
|
|
|
0
|
|
|
|
|
);
|
2025-09-05 21:52:19 +09:00
|
|
|
enrichedRow[fileColumn] = JSON.stringify({
|
2025-09-08 10:02:30 +09:00
|
|
|
files: fileInfos,
|
|
|
|
|
totalCount: fileInfos.length,
|
|
|
|
|
totalSize: totalSize,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// 파일이 없으면 빈 상태로 설정
|
2025-09-05 21:52:19 +09:00
|
|
|
enrichedRow[fileColumn] = JSON.stringify({
|
2025-09-08 10:02:30 +09:00
|
|
|
files: [],
|
|
|
|
|
totalCount: 0,
|
|
|
|
|
totalSize: 0,
|
2025-09-05 21:52:19 +09:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return enrichedRow;
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
logger.info(`파일 데이터 보강 완료: ${enrichedData.length}개 행 처리`);
|
|
|
|
|
return enrichedData;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error("파일 데이터 보강 실패:", error);
|
|
|
|
|
return data; // 실패 시 원본 데이터 반환
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-08 10:02:30 +09:00
|
|
|
* 파일 경로에서 컴포넌트 ID 추출 (현재는 사용하지 않음)
|
|
|
|
|
*/
|
|
|
|
|
private extractComponentIdFromPath(filePath: string): string | null {
|
|
|
|
|
// 현재는 파일 경로에서 컴포넌트 ID를 추출할 수 없으므로 null 반환
|
|
|
|
|
// 추후 필요시 구현
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 컬럼별 파일 정보 조회 (컬럼명과 target_objid로 구분)
|
|
|
|
|
*/
|
|
|
|
|
private async getFileInfoByColumnAndTarget(
|
|
|
|
|
columnName: string,
|
|
|
|
|
targetObjid: any,
|
|
|
|
|
tableName: string
|
|
|
|
|
): Promise<any[]> {
|
|
|
|
|
try {
|
|
|
|
|
logger.info(
|
|
|
|
|
`컬럼별 파일 정보 조회: ${tableName}.${columnName}, target: ${targetObjid}`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 🎯 컬럼명을 doc_type으로 사용하여 파일 구분
|
|
|
|
|
const fileInfos = await prisma.attach_file_info.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
target_objid: String(targetObjid),
|
|
|
|
|
doc_type: columnName, // 컬럼명으로 파일 구분
|
|
|
|
|
status: "ACTIVE",
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
objid: true,
|
|
|
|
|
real_file_name: true,
|
|
|
|
|
file_size: true,
|
|
|
|
|
file_ext: true,
|
|
|
|
|
file_path: true,
|
|
|
|
|
doc_type: true,
|
|
|
|
|
doc_type_name: true,
|
|
|
|
|
regdate: true,
|
|
|
|
|
writer: true,
|
|
|
|
|
},
|
|
|
|
|
orderBy: {
|
|
|
|
|
regdate: "desc",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 파일 정보 포맷팅
|
|
|
|
|
return fileInfos.map((fileInfo) => ({
|
|
|
|
|
name: fileInfo.real_file_name,
|
|
|
|
|
size: Number(fileInfo.file_size) || 0,
|
|
|
|
|
path: fileInfo.file_path,
|
|
|
|
|
ext: fileInfo.file_ext,
|
|
|
|
|
objid: String(fileInfo.objid),
|
|
|
|
|
docType: fileInfo.doc_type,
|
|
|
|
|
docTypeName: fileInfo.doc_type_name,
|
|
|
|
|
regdate: fileInfo.regdate?.toISOString(),
|
|
|
|
|
writer: fileInfo.writer,
|
|
|
|
|
}));
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.warn(`컬럼별 파일 정보 조회 실패: ${columnName}`, error);
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 파일 경로로 파일 정보 조회 (기존 메서드 - 호환성 유지)
|
2025-09-05 21:52:19 +09:00
|
|
|
*/
|
|
|
|
|
private async getFileInfoByPath(filePath: string): Promise<any | null> {
|
|
|
|
|
try {
|
|
|
|
|
const fileInfo = await prisma.attach_file_info.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
file_path: filePath,
|
|
|
|
|
status: "ACTIVE",
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
objid: true,
|
|
|
|
|
real_file_name: true,
|
|
|
|
|
file_size: true,
|
|
|
|
|
file_ext: true,
|
|
|
|
|
file_path: true,
|
|
|
|
|
doc_type: true,
|
|
|
|
|
doc_type_name: true,
|
|
|
|
|
regdate: true,
|
|
|
|
|
writer: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!fileInfo) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
name: fileInfo.real_file_name,
|
|
|
|
|
path: fileInfo.file_path,
|
|
|
|
|
size: Number(fileInfo.file_size) || 0,
|
|
|
|
|
type: fileInfo.file_ext,
|
|
|
|
|
objid: fileInfo.objid.toString(),
|
|
|
|
|
docType: fileInfo.doc_type,
|
|
|
|
|
docTypeName: fileInfo.doc_type_name,
|
|
|
|
|
regdate: fileInfo.regdate?.toISOString(),
|
|
|
|
|
writer: fileInfo.writer,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.warn(`파일 정보 조회 실패: ${filePath}`, error);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 파일 타입 컬럼 조회
|
|
|
|
|
*/
|
|
|
|
|
private async getFileTypeColumns(tableName: string): Promise<string[]> {
|
|
|
|
|
try {
|
|
|
|
|
const fileColumns = await prisma.column_labels.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
table_name: tableName,
|
|
|
|
|
web_type: "file",
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
column_name: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const columnNames = fileColumns.map((col: any) => col.column_name);
|
|
|
|
|
logger.info(`파일 타입 컬럼 감지: ${tableName}`, columnNames);
|
|
|
|
|
return columnNames;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.warn(`파일 타입 컬럼 조회 실패: ${tableName}`, error);
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
/**
|
|
|
|
|
* 테이블 데이터 조회 (페이징 + 검색)
|
|
|
|
|
*/
|
|
|
|
|
async getTableData(
|
2025-09-03 16:38:10 +09:00
|
|
|
tableName: string,
|
2025-09-03 15:23:12 +09:00
|
|
|
options: {
|
|
|
|
|
page: number;
|
|
|
|
|
size: number;
|
|
|
|
|
search?: Record<string, any>;
|
|
|
|
|
sortBy?: string;
|
|
|
|
|
sortOrder?: string;
|
|
|
|
|
}
|
|
|
|
|
): Promise<{
|
|
|
|
|
data: any[];
|
|
|
|
|
total: number;
|
|
|
|
|
page: number;
|
|
|
|
|
size: number;
|
|
|
|
|
totalPages: number;
|
|
|
|
|
}> {
|
|
|
|
|
try {
|
2025-09-03 16:38:10 +09:00
|
|
|
const { page, size, search = {}, sortBy, sortOrder = "asc" } = options;
|
2025-09-03 15:23:12 +09:00
|
|
|
const offset = (page - 1) * size;
|
|
|
|
|
|
|
|
|
|
logger.info(`테이블 데이터 조회: ${tableName}`, options);
|
|
|
|
|
|
2025-09-09 17:42:23 +09:00
|
|
|
// 🎯 파일 타입 컬럼 감지 (비활성화됨 - 자동 파일 컬럼 생성 방지)
|
|
|
|
|
// const fileColumns = await this.getFileTypeColumns(tableName);
|
|
|
|
|
const fileColumns: string[] = []; // 자동 파일 컬럼 생성 비활성화
|
2025-09-05 21:52:19 +09:00
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
// WHERE 조건 구성
|
|
|
|
|
let whereConditions: string[] = [];
|
|
|
|
|
let searchValues: any[] = [];
|
|
|
|
|
let paramIndex = 1;
|
|
|
|
|
|
|
|
|
|
if (search && Object.keys(search).length > 0) {
|
|
|
|
|
for (const [column, value] of Object.entries(search)) {
|
2025-09-03 16:38:10 +09:00
|
|
|
if (value !== null && value !== undefined && value !== "") {
|
2025-09-03 15:23:12 +09:00
|
|
|
// 안전한 컬럼명 검증 (SQL 인젝션 방지)
|
2025-09-03 16:38:10 +09:00
|
|
|
const safeColumn = column.replace(/[^a-zA-Z0-9_]/g, "");
|
|
|
|
|
|
|
|
|
|
if (typeof value === "string") {
|
2025-09-03 15:23:12 +09:00
|
|
|
whereConditions.push(`${safeColumn}::text ILIKE $${paramIndex}`);
|
|
|
|
|
searchValues.push(`%${value}%`);
|
|
|
|
|
} else {
|
|
|
|
|
whereConditions.push(`${safeColumn} = $${paramIndex}`);
|
|
|
|
|
searchValues.push(value);
|
|
|
|
|
}
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-03 16:38:10 +09:00
|
|
|
const whereClause =
|
|
|
|
|
whereConditions.length > 0
|
|
|
|
|
? `WHERE ${whereConditions.join(" AND ")}`
|
|
|
|
|
: "";
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
|
|
|
// ORDER BY 조건 구성
|
2025-09-03 16:38:10 +09:00
|
|
|
let orderClause = "";
|
2025-09-03 15:23:12 +09:00
|
|
|
if (sortBy) {
|
2025-09-03 16:38:10 +09:00
|
|
|
const safeSortBy = sortBy.replace(/[^a-zA-Z0-9_]/g, "");
|
|
|
|
|
const safeSortOrder =
|
|
|
|
|
sortOrder.toLowerCase() === "desc" ? "DESC" : "ASC";
|
2025-09-03 15:23:12 +09:00
|
|
|
orderClause = `ORDER BY ${safeSortBy} ${safeSortOrder}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 안전한 테이블명 검증
|
2025-09-03 16:38:10 +09:00
|
|
|
const safeTableName = tableName.replace(/[^a-zA-Z0-9_]/g, "");
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
|
|
|
// 전체 개수 조회
|
|
|
|
|
const countQuery = `SELECT COUNT(*) as count FROM ${safeTableName} ${whereClause}`;
|
2025-09-03 16:38:10 +09:00
|
|
|
const countResult = await prisma.$queryRawUnsafe<any[]>(
|
|
|
|
|
countQuery,
|
|
|
|
|
...searchValues
|
|
|
|
|
);
|
2025-09-03 15:23:12 +09:00
|
|
|
const total = parseInt(countResult[0].count);
|
|
|
|
|
|
|
|
|
|
// 데이터 조회
|
|
|
|
|
const dataQuery = `
|
|
|
|
|
SELECT * FROM ${safeTableName}
|
|
|
|
|
${whereClause}
|
|
|
|
|
${orderClause}
|
|
|
|
|
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
|
|
|
|
`;
|
2025-09-03 16:38:10 +09:00
|
|
|
|
2025-09-05 21:52:19 +09:00
|
|
|
let data = await prisma.$queryRawUnsafe<any[]>(
|
2025-09-03 16:38:10 +09:00
|
|
|
dataQuery,
|
|
|
|
|
...searchValues,
|
|
|
|
|
size,
|
2025-09-03 15:23:12 +09:00
|
|
|
offset
|
|
|
|
|
);
|
|
|
|
|
|
2025-09-05 21:52:19 +09:00
|
|
|
// 🎯 파일 컬럼이 있으면 파일 정보 보강
|
|
|
|
|
if (fileColumns.length > 0) {
|
|
|
|
|
data = await this.enrichFileData(data, fileColumns, safeTableName);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-03 15:23:12 +09:00
|
|
|
const totalPages = Math.ceil(total / size);
|
|
|
|
|
|
2025-09-03 16:38:10 +09:00
|
|
|
logger.info(
|
|
|
|
|
`테이블 데이터 조회 완료: ${tableName}, 총 ${total}건, ${data.length}개 반환`
|
|
|
|
|
);
|
2025-09-03 15:23:12 +09:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
data,
|
|
|
|
|
total,
|
|
|
|
|
page,
|
|
|
|
|
size,
|
2025-09-03 16:38:10 +09:00
|
|
|
totalPages,
|
2025-09-03 15:23:12 +09:00
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`테이블 데이터 조회 오류: ${tableName}`, error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-03 16:38:10 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 현재 사용자 정보 조회 (JWT 토큰에서)
|
|
|
|
|
*/
|
|
|
|
|
private getCurrentUserFromRequest(req?: any): {
|
|
|
|
|
userId: string;
|
|
|
|
|
userName: string;
|
|
|
|
|
} {
|
|
|
|
|
// 실제 프로젝트에서는 req 객체에서 JWT 토큰을 파싱하여 사용자 정보를 가져올 수 있습니다
|
|
|
|
|
// 현재는 기본값을 반환
|
|
|
|
|
return {
|
|
|
|
|
userId: "system",
|
|
|
|
|
userName: "시스템 사용자",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 값을 PostgreSQL 타입에 맞게 변환
|
|
|
|
|
*/
|
|
|
|
|
private convertValueForPostgreSQL(value: any, dataType: string): any {
|
|
|
|
|
if (value === null || value === undefined || value === "") {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const lowerDataType = dataType.toLowerCase();
|
|
|
|
|
|
|
|
|
|
// 날짜/시간 타입 처리
|
|
|
|
|
if (
|
|
|
|
|
lowerDataType.includes("timestamp") ||
|
|
|
|
|
lowerDataType.includes("datetime")
|
|
|
|
|
) {
|
|
|
|
|
// YYYY-MM-DDTHH:mm:ss 형식을 PostgreSQL timestamp로 변환
|
|
|
|
|
if (typeof value === "string") {
|
|
|
|
|
try {
|
|
|
|
|
const date = new Date(value);
|
|
|
|
|
return date.toISOString();
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 날짜 타입 처리
|
|
|
|
|
if (lowerDataType.includes("date")) {
|
|
|
|
|
if (typeof value === "string") {
|
|
|
|
|
try {
|
|
|
|
|
// YYYY-MM-DD 형식 유지
|
|
|
|
|
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
const date = new Date(value);
|
|
|
|
|
return date.toISOString().split("T")[0];
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 시간 타입 처리
|
|
|
|
|
if (lowerDataType.includes("time")) {
|
|
|
|
|
if (typeof value === "string") {
|
|
|
|
|
// HH:mm:ss 형식 유지
|
|
|
|
|
if (/^\d{2}:\d{2}:\d{2}$/.test(value)) {
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 숫자 타입 처리
|
|
|
|
|
if (
|
|
|
|
|
lowerDataType.includes("integer") ||
|
|
|
|
|
lowerDataType.includes("bigint") ||
|
|
|
|
|
lowerDataType.includes("serial")
|
|
|
|
|
) {
|
|
|
|
|
return parseInt(value) || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
lowerDataType.includes("numeric") ||
|
|
|
|
|
lowerDataType.includes("decimal") ||
|
|
|
|
|
lowerDataType.includes("real") ||
|
|
|
|
|
lowerDataType.includes("double")
|
|
|
|
|
) {
|
|
|
|
|
return parseFloat(value) || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 불린 타입 처리
|
|
|
|
|
if (lowerDataType.includes("boolean")) {
|
|
|
|
|
if (typeof value === "string") {
|
|
|
|
|
return value.toLowerCase() === "true" || value === "1";
|
|
|
|
|
}
|
|
|
|
|
return Boolean(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 기본적으로 문자열로 처리
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테이블에 데이터 추가
|
|
|
|
|
*/
|
|
|
|
|
async addTableData(
|
|
|
|
|
tableName: string,
|
|
|
|
|
data: Record<string, any>
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
logger.info(`=== 테이블 데이터 추가 시작: ${tableName} ===`);
|
|
|
|
|
logger.info(`추가할 데이터:`, data);
|
|
|
|
|
|
|
|
|
|
// 테이블의 컬럼 정보 조회
|
|
|
|
|
const columnInfoQuery = `
|
|
|
|
|
SELECT column_name, data_type, is_nullable
|
|
|
|
|
FROM information_schema.columns
|
|
|
|
|
WHERE table_name = $1
|
|
|
|
|
ORDER BY ordinal_position
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const columnInfoResult = (await prisma.$queryRawUnsafe(
|
|
|
|
|
columnInfoQuery,
|
|
|
|
|
tableName
|
|
|
|
|
)) as any[];
|
|
|
|
|
const columnTypeMap = new Map<string, string>();
|
|
|
|
|
|
|
|
|
|
columnInfoResult.forEach((col: any) => {
|
|
|
|
|
columnTypeMap.set(col.column_name, col.data_type);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
logger.info(`컬럼 타입 정보:`, Object.fromEntries(columnTypeMap));
|
|
|
|
|
|
|
|
|
|
// 컬럼명과 값을 분리하고 타입에 맞게 변환
|
|
|
|
|
const columns = Object.keys(data);
|
|
|
|
|
const values = Object.values(data).map((value, index) => {
|
|
|
|
|
const columnName = columns[index];
|
|
|
|
|
const dataType = columnTypeMap.get(columnName) || "text";
|
|
|
|
|
const convertedValue = this.convertValueForPostgreSQL(value, dataType);
|
|
|
|
|
logger.info(
|
|
|
|
|
`컬럼 "${columnName}" (${dataType}): "${value}" → "${convertedValue}"`
|
|
|
|
|
);
|
|
|
|
|
return convertedValue;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 동적 INSERT 쿼리 생성 (타입 캐스팅 포함)
|
|
|
|
|
const placeholders = columns
|
|
|
|
|
.map((col, index) => {
|
|
|
|
|
const dataType = columnTypeMap.get(col) || "text";
|
|
|
|
|
const lowerDataType = dataType.toLowerCase();
|
|
|
|
|
|
|
|
|
|
// PostgreSQL에서 직접 타입 캐스팅
|
|
|
|
|
if (
|
|
|
|
|
lowerDataType.includes("timestamp") ||
|
|
|
|
|
lowerDataType.includes("datetime")
|
|
|
|
|
) {
|
|
|
|
|
return `$${index + 1}::timestamp`;
|
|
|
|
|
} else if (lowerDataType.includes("date")) {
|
|
|
|
|
return `$${index + 1}::date`;
|
|
|
|
|
} else if (lowerDataType.includes("time")) {
|
|
|
|
|
return `$${index + 1}::time`;
|
|
|
|
|
} else if (
|
|
|
|
|
lowerDataType.includes("integer") ||
|
|
|
|
|
lowerDataType.includes("bigint") ||
|
|
|
|
|
lowerDataType.includes("serial")
|
|
|
|
|
) {
|
|
|
|
|
return `$${index + 1}::integer`;
|
|
|
|
|
} else if (
|
|
|
|
|
lowerDataType.includes("numeric") ||
|
|
|
|
|
lowerDataType.includes("decimal")
|
|
|
|
|
) {
|
|
|
|
|
return `$${index + 1}::numeric`;
|
|
|
|
|
} else if (lowerDataType.includes("boolean")) {
|
|
|
|
|
return `$${index + 1}::boolean`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `$${index + 1}`;
|
|
|
|
|
})
|
|
|
|
|
.join(", ");
|
|
|
|
|
const columnNames = columns.map((col) => `"${col}"`).join(", ");
|
|
|
|
|
|
|
|
|
|
const query = `
|
|
|
|
|
INSERT INTO "${tableName}" (${columnNames})
|
|
|
|
|
VALUES (${placeholders})
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
logger.info(`실행할 쿼리: ${query}`);
|
|
|
|
|
logger.info(`쿼리 파라미터:`, values);
|
|
|
|
|
|
|
|
|
|
await prisma.$queryRawUnsafe(query, ...values);
|
|
|
|
|
|
|
|
|
|
logger.info(`테이블 데이터 추가 완료: ${tableName}`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`테이블 데이터 추가 오류: ${tableName}`, error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테이블 데이터 수정
|
|
|
|
|
*/
|
|
|
|
|
async editTableData(
|
|
|
|
|
tableName: string,
|
|
|
|
|
originalData: Record<string, any>,
|
|
|
|
|
updatedData: Record<string, any>
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
try {
|
|
|
|
|
logger.info(`=== 테이블 데이터 수정 시작: ${tableName} ===`);
|
|
|
|
|
logger.info(`원본 데이터:`, originalData);
|
|
|
|
|
logger.info(`수정할 데이터:`, updatedData);
|
|
|
|
|
|
|
|
|
|
// 테이블의 컬럼 정보 조회 (PRIMARY KEY 찾기용)
|
|
|
|
|
const columnInfoQuery = `
|
|
|
|
|
SELECT c.column_name, c.data_type, c.is_nullable,
|
|
|
|
|
CASE WHEN tc.constraint_type = 'PRIMARY KEY' THEN 'YES' ELSE 'NO' END as is_primary_key
|
|
|
|
|
FROM information_schema.columns c
|
|
|
|
|
LEFT JOIN information_schema.key_column_usage kcu ON c.column_name = kcu.column_name AND c.table_name = kcu.table_name
|
|
|
|
|
LEFT JOIN information_schema.table_constraints tc ON kcu.constraint_name = tc.constraint_name AND tc.table_name = c.table_name
|
|
|
|
|
WHERE c.table_name = $1
|
|
|
|
|
ORDER BY c.ordinal_position
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const columnInfoResult = (await prisma.$queryRawUnsafe(
|
|
|
|
|
columnInfoQuery,
|
|
|
|
|
tableName
|
|
|
|
|
)) as any[];
|
|
|
|
|
const columnTypeMap = new Map<string, string>();
|
|
|
|
|
const primaryKeys: string[] = [];
|
|
|
|
|
|
|
|
|
|
columnInfoResult.forEach((col: any) => {
|
|
|
|
|
columnTypeMap.set(col.column_name, col.data_type);
|
|
|
|
|
if (col.is_primary_key === "YES") {
|
|
|
|
|
primaryKeys.push(col.column_name);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
logger.info(`컬럼 타입 정보:`, Object.fromEntries(columnTypeMap));
|
|
|
|
|
logger.info(`PRIMARY KEY 컬럼들:`, primaryKeys);
|
|
|
|
|
|
|
|
|
|
// SET 절 생성 (수정할 데이터) - 먼저 생성
|
|
|
|
|
const setConditions: string[] = [];
|
|
|
|
|
const setValues: any[] = [];
|
|
|
|
|
let paramIndex = 1;
|
|
|
|
|
|
|
|
|
|
Object.keys(updatedData).forEach((column) => {
|
|
|
|
|
const dataType = columnTypeMap.get(column) || "text";
|
|
|
|
|
setConditions.push(
|
|
|
|
|
`"${column}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}`
|
|
|
|
|
);
|
|
|
|
|
setValues.push(
|
|
|
|
|
this.convertValueForPostgreSQL(updatedData[column], dataType)
|
|
|
|
|
);
|
|
|
|
|
paramIndex++;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// WHERE 조건 생성 (PRIMARY KEY 우선, 없으면 모든 원본 데이터 사용)
|
|
|
|
|
let whereConditions: string[] = [];
|
|
|
|
|
let whereValues: any[] = [];
|
|
|
|
|
|
|
|
|
|
if (primaryKeys.length > 0) {
|
|
|
|
|
// PRIMARY KEY로 WHERE 조건 생성
|
|
|
|
|
primaryKeys.forEach((pkColumn) => {
|
|
|
|
|
if (originalData[pkColumn] !== undefined) {
|
|
|
|
|
const dataType = columnTypeMap.get(pkColumn) || "text";
|
|
|
|
|
whereConditions.push(
|
|
|
|
|
`"${pkColumn}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}`
|
|
|
|
|
);
|
|
|
|
|
whereValues.push(
|
|
|
|
|
this.convertValueForPostgreSQL(originalData[pkColumn], dataType)
|
|
|
|
|
);
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// PRIMARY KEY가 없으면 모든 원본 데이터로 WHERE 조건 생성
|
|
|
|
|
Object.keys(originalData).forEach((column) => {
|
|
|
|
|
const dataType = columnTypeMap.get(column) || "text";
|
|
|
|
|
whereConditions.push(
|
|
|
|
|
`"${column}" = $${paramIndex}::${this.getPostgreSQLType(dataType)}`
|
|
|
|
|
);
|
|
|
|
|
whereValues.push(
|
|
|
|
|
this.convertValueForPostgreSQL(originalData[column], dataType)
|
|
|
|
|
);
|
|
|
|
|
paramIndex++;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UPDATE 쿼리 생성
|
|
|
|
|
const query = `
|
|
|
|
|
UPDATE "${tableName}"
|
|
|
|
|
SET ${setConditions.join(", ")}
|
|
|
|
|
WHERE ${whereConditions.join(" AND ")}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const allValues = [...setValues, ...whereValues];
|
|
|
|
|
|
|
|
|
|
logger.info(`실행할 UPDATE 쿼리: ${query}`);
|
|
|
|
|
logger.info(`쿼리 파라미터:`, allValues);
|
|
|
|
|
|
|
|
|
|
const result = await prisma.$queryRawUnsafe(query, ...allValues);
|
|
|
|
|
|
|
|
|
|
logger.info(`테이블 데이터 수정 완료: ${tableName}`, result);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`테이블 데이터 수정 오류: ${tableName}`, error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* PostgreSQL 타입명 반환
|
|
|
|
|
*/
|
|
|
|
|
private getPostgreSQLType(dataType: string): string {
|
|
|
|
|
const lowerDataType = dataType.toLowerCase();
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
lowerDataType.includes("timestamp") ||
|
|
|
|
|
lowerDataType.includes("datetime")
|
|
|
|
|
) {
|
|
|
|
|
return "timestamp";
|
|
|
|
|
} else if (lowerDataType.includes("date")) {
|
|
|
|
|
return "date";
|
|
|
|
|
} else if (lowerDataType.includes("time")) {
|
|
|
|
|
return "time";
|
|
|
|
|
} else if (
|
|
|
|
|
lowerDataType.includes("integer") ||
|
|
|
|
|
lowerDataType.includes("bigint") ||
|
|
|
|
|
lowerDataType.includes("serial")
|
|
|
|
|
) {
|
|
|
|
|
return "integer";
|
|
|
|
|
} else if (
|
|
|
|
|
lowerDataType.includes("numeric") ||
|
|
|
|
|
lowerDataType.includes("decimal")
|
|
|
|
|
) {
|
|
|
|
|
return "numeric";
|
|
|
|
|
} else if (lowerDataType.includes("boolean")) {
|
|
|
|
|
return "boolean";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "text"; // 기본값
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테이블에서 데이터 삭제
|
|
|
|
|
*/
|
|
|
|
|
async deleteTableData(
|
|
|
|
|
tableName: string,
|
|
|
|
|
dataToDelete: Record<string, any>[]
|
|
|
|
|
): Promise<number> {
|
|
|
|
|
try {
|
|
|
|
|
logger.info(`테이블 데이터 삭제: ${tableName}`, dataToDelete);
|
|
|
|
|
|
|
|
|
|
if (!Array.isArray(dataToDelete) || dataToDelete.length === 0) {
|
|
|
|
|
throw new Error("삭제할 데이터가 없습니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let deletedCount = 0;
|
|
|
|
|
|
|
|
|
|
// 테이블의 기본 키 컬럼 찾기 (정확한 식별을 위해)
|
|
|
|
|
const primaryKeyQuery = `
|
|
|
|
|
SELECT column_name
|
|
|
|
|
FROM information_schema.table_constraints tc
|
|
|
|
|
JOIN information_schema.key_column_usage kcu
|
|
|
|
|
ON tc.constraint_name = kcu.constraint_name
|
|
|
|
|
WHERE tc.table_name = $1
|
|
|
|
|
AND tc.constraint_type = 'PRIMARY KEY'
|
|
|
|
|
ORDER BY kcu.ordinal_position
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const primaryKeys = await prisma.$queryRawUnsafe<
|
|
|
|
|
{ column_name: string }[]
|
|
|
|
|
>(primaryKeyQuery, tableName);
|
|
|
|
|
|
|
|
|
|
if (primaryKeys.length === 0) {
|
|
|
|
|
// 기본 키가 없는 경우, 모든 컬럼으로 삭제 조건 생성
|
|
|
|
|
logger.warn(
|
|
|
|
|
`테이블 ${tableName}에 기본 키가 없습니다. 모든 컬럼으로 삭제 조건을 생성합니다.`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for (const rowData of dataToDelete) {
|
|
|
|
|
const conditions = Object.keys(rowData)
|
|
|
|
|
.map((key, index) => `"${key}" = $${index + 1}`)
|
|
|
|
|
.join(" AND ");
|
|
|
|
|
|
|
|
|
|
const values = Object.values(rowData);
|
|
|
|
|
|
|
|
|
|
const deleteQuery = `DELETE FROM "${tableName}" WHERE ${conditions}`;
|
|
|
|
|
|
|
|
|
|
const result = await prisma.$queryRawUnsafe(deleteQuery, ...values);
|
|
|
|
|
deletedCount += Number(result);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 기본 키를 사용한 삭제
|
|
|
|
|
const primaryKeyNames = primaryKeys.map((pk) => pk.column_name);
|
|
|
|
|
|
|
|
|
|
for (const rowData of dataToDelete) {
|
|
|
|
|
const conditions = primaryKeyNames
|
|
|
|
|
.map((key, index) => `"${key}" = $${index + 1}`)
|
|
|
|
|
.join(" AND ");
|
|
|
|
|
|
|
|
|
|
const values = primaryKeyNames.map((key) => rowData[key]);
|
|
|
|
|
|
|
|
|
|
// null 값이 있는 경우 스킵
|
|
|
|
|
if (values.some((val) => val === null || val === undefined)) {
|
|
|
|
|
logger.warn(`기본 키 값이 null인 행을 스킵합니다:`, rowData);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const deleteQuery = `DELETE FROM "${tableName}" WHERE ${conditions}`;
|
|
|
|
|
|
|
|
|
|
const result = await prisma.$queryRawUnsafe(deleteQuery, ...values);
|
|
|
|
|
deletedCount += Number(result);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
`테이블 데이터 삭제 완료: ${tableName}, ${deletedCount}건 삭제`
|
|
|
|
|
);
|
|
|
|
|
return deletedCount;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`테이블 데이터 삭제 오류: ${tableName}`, error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-25 14:08:08 +09:00
|
|
|
}
|