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";
|
|
|
|
|
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-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-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-01 11:00:38 +09:00
|
|
|
* 메타데이터 조회는 Prisma로 변경 불가
|
2025-08-25 14:08:08 +09:00
|
|
|
*/
|
|
|
|
|
async getColumnList(tableName: string): Promise<ColumnTypeInfo[]> {
|
|
|
|
|
try {
|
|
|
|
|
logger.info(`컬럼 정보 조회 시작: ${tableName}`);
|
|
|
|
|
|
2025-09-01 11:00:38 +09:00
|
|
|
// information_schema는 여전히 $queryRaw 사용
|
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",
|
|
|
|
|
c.data_type as "dbType",
|
|
|
|
|
COALESCE(cl.web_type, 'text') as "webType",
|
|
|
|
|
COALESCE(cl.detail_settings, '') as "detailSettings",
|
|
|
|
|
COALESCE(cl.description, '') as "description",
|
|
|
|
|
c.is_nullable as "isNullable",
|
|
|
|
|
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-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-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-01 11:00:38 +09:00
|
|
|
logger.info(`컬럼 정보 조회 완료: ${tableName}, ${columns.length}개`);
|
|
|
|
|
return columns;
|
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"}`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 컬럼 설정 업데이트 (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
|
|
|
|
|
|
|
|
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-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,
|
|
|
|
|
detailSettings?: Record<string, any>
|
|
|
|
|
): 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-01 15:22:47 +09:00
|
|
|
await prisma.column_labels.update({
|
|
|
|
|
where: {
|
|
|
|
|
id: existingColumn.id,
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
web_type: webType,
|
|
|
|
|
detail_settings: JSON.stringify(finalDetailSettings),
|
|
|
|
|
updated_date: new Date(),
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-09-01 11:48:12 +09:00
|
|
|
logger.info(
|
|
|
|
|
`컬럼 웹 타입 업데이트 완료: ${tableName}.${columnName} = ${webType}`
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// 새로운 컬럼 라벨 생성
|
2025-09-01 15:22:47 +09:00
|
|
|
await prisma.column_labels.create({
|
|
|
|
|
data: {
|
|
|
|
|
table_name: tableName,
|
|
|
|
|
column_name: columnName,
|
|
|
|
|
web_type: webType,
|
|
|
|
|
detail_settings: JSON.stringify(finalDetailSettings),
|
|
|
|
|
created_date: new Date(),
|
|
|
|
|
updated_date: new Date(),
|
|
|
|
|
},
|
|
|
|
|
});
|
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
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테이블 데이터 조회 (페이징 + 검색)
|
|
|
|
|
*/
|
|
|
|
|
async getTableData(
|
|
|
|
|
tableName: string,
|
|
|
|
|
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 {
|
|
|
|
|
const { page, size, search = {}, sortBy, sortOrder = 'asc' } = options;
|
|
|
|
|
const offset = (page - 1) * size;
|
|
|
|
|
|
|
|
|
|
logger.info(`테이블 데이터 조회: ${tableName}`, options);
|
|
|
|
|
|
|
|
|
|
// 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)) {
|
|
|
|
|
if (value !== null && value !== undefined && value !== '') {
|
|
|
|
|
// 안전한 컬럼명 검증 (SQL 인젝션 방지)
|
|
|
|
|
const safeColumn = column.replace(/[^a-zA-Z0-9_]/g, '');
|
|
|
|
|
|
|
|
|
|
if (typeof value === 'string') {
|
|
|
|
|
whereConditions.push(`${safeColumn}::text ILIKE $${paramIndex}`);
|
|
|
|
|
searchValues.push(`%${value}%`);
|
|
|
|
|
} else {
|
|
|
|
|
whereConditions.push(`${safeColumn} = $${paramIndex}`);
|
|
|
|
|
searchValues.push(value);
|
|
|
|
|
}
|
|
|
|
|
paramIndex++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const whereClause = whereConditions.length > 0
|
|
|
|
|
? `WHERE ${whereConditions.join(' AND ')}`
|
|
|
|
|
: '';
|
|
|
|
|
|
|
|
|
|
// ORDER BY 조건 구성
|
|
|
|
|
let orderClause = '';
|
|
|
|
|
if (sortBy) {
|
|
|
|
|
const safeSortBy = sortBy.replace(/[^a-zA-Z0-9_]/g, '');
|
|
|
|
|
const safeSortOrder = sortOrder.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
|
|
|
orderClause = `ORDER BY ${safeSortBy} ${safeSortOrder}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 안전한 테이블명 검증
|
|
|
|
|
const safeTableName = tableName.replace(/[^a-zA-Z0-9_]/g, '');
|
|
|
|
|
|
|
|
|
|
// 전체 개수 조회
|
|
|
|
|
const countQuery = `SELECT COUNT(*) as count FROM ${safeTableName} ${whereClause}`;
|
|
|
|
|
const countResult = await prisma.$queryRawUnsafe<any[]>(countQuery, ...searchValues);
|
|
|
|
|
const total = parseInt(countResult[0].count);
|
|
|
|
|
|
|
|
|
|
// 데이터 조회
|
|
|
|
|
const dataQuery = `
|
|
|
|
|
SELECT * FROM ${safeTableName}
|
|
|
|
|
${whereClause}
|
|
|
|
|
${orderClause}
|
|
|
|
|
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const data = await prisma.$queryRawUnsafe<any[]>(
|
|
|
|
|
dataQuery,
|
|
|
|
|
...searchValues,
|
|
|
|
|
size,
|
|
|
|
|
offset
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const totalPages = Math.ceil(total / size);
|
|
|
|
|
|
|
|
|
|
logger.info(`테이블 데이터 조회 완료: ${tableName}, 총 ${total}건, ${data.length}개 반환`);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
data,
|
|
|
|
|
total,
|
|
|
|
|
page,
|
|
|
|
|
size,
|
|
|
|
|
totalPages
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.error(`테이블 데이터 조회 오류: ${tableName}`, error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-25 14:08:08 +09:00
|
|
|
}
|