ERP-node/backend-node/src/services/tableManagementService.ts

1061 lines
32 KiB
TypeScript
Raw Normal View History

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.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",
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>,
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
// 기존 컬럼 라벨 업데이트
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,
},
data: updateData,
2025-09-01 15:22:47 +09:00
});
2025-09-01 11:48:12 +09:00
logger.info(
`컬럼 웹 타입 업데이트 완료: ${tableName}.${columnName} = ${webType}`
);
} else {
// 새로운 컬럼 라벨 생성
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({
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
/**
* ( + )
*/
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);
// 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-03 15:23:12 +09:00
const 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
);
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
}