import { PrismaClient } from "@prisma/client"; import { logger } from "../utils/logger"; import { TableInfo, ColumnTypeInfo, ColumnSettings, TableLabels, ColumnLabels, } from "../types/tableManagement"; const prisma = new PrismaClient(); export class TableManagementService { constructor() {} /** * 테이블 목록 조회 (PostgreSQL information_schema 활용) * 메타데이터 조회는 Prisma로 변경 불가 */ async getTableList(): Promise { try { logger.info("테이블 목록 조회 시작"); // information_schema는 여전히 $queryRaw 사용 const rawTables = await prisma.$queryRaw` 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 `; // BigInt를 Number로 변환하여 JSON 직렬화 문제 해결 const tables: TableInfo[] = rawTables.map((table) => ({ ...table, columnCount: Number(table.columnCount), // BigInt → Number 변환 })); logger.info(`테이블 목록 조회 완료: ${tables.length}개`); return tables; } catch (error) { logger.error("테이블 목록 조회 중 오류 발생:", error); throw new Error( `테이블 목록 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}` ); } } /** * 테이블 컬럼 정보 조회 * 메타데이터 조회는 Prisma로 변경 불가 */ async getColumnList(tableName: string): Promise { try { logger.info(`컬럼 정보 조회 시작: ${tableName}`); // information_schema는 여전히 $queryRaw 사용 const rawColumns = await prisma.$queryRaw` 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", 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 WHERE c.table_name = ${tableName} ORDER BY c.ordinal_position `; // 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, })); logger.info(`컬럼 정보 조회 완료: ${tableName}, ${columns.length}개`); return columns; } catch (error) { logger.error(`컬럼 정보 조회 중 오류 발생: ${tableName}`, error); throw new Error( `컬럼 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}` ); } } /** * 테이블이 table_labels에 없으면 자동 추가 * Prisma ORM으로 변경 */ async insertTableIfNotExists(tableName: string): Promise { try { logger.info(`테이블 라벨 자동 추가 시작: ${tableName}`); await prisma.table_labels.upsert({ where: { table_name: tableName }, update: {}, // 이미 존재하면 변경하지 않음 create: { table_name: tableName, table_label: tableName, description: "", }, }); logger.info(`테이블 라벨 자동 추가 완료: ${tableName}`); } catch (error) { logger.error(`테이블 라벨 자동 추가 중 오류 발생: ${tableName}`, error); throw new Error( `테이블 라벨 자동 추가 실패: ${error instanceof Error ? error.message : "Unknown error"}` ); } } /** * 컬럼 설정 업데이트 (UPSERT 방식) * Prisma ORM으로 변경 */ async updateColumnSettings( tableName: string, columnName: string, settings: ColumnSettings ): Promise { try { logger.info(`컬럼 설정 업데이트 시작: ${tableName}.${columnName}`); // 테이블이 table_labels에 없으면 자동 추가 await this.insertTableIfNotExists(tableName); // 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, }, }); logger.info(`컬럼 설정 업데이트 완료: ${tableName}.${columnName}`); } catch (error) { logger.error( `컬럼 설정 업데이트 중 오류 발생: ${tableName}.${columnName}`, error ); throw new Error( `컬럼 설정 업데이트 실패: ${error instanceof Error ? error.message : "Unknown error"}` ); } } /** * 전체 컬럼 설정 일괄 업데이트 * Prisma 트랜잭션으로 변경 */ async updateAllColumnSettings( tableName: string, columnSettings: ColumnSettings[] ): Promise { try { logger.info( `전체 컬럼 설정 일괄 업데이트 시작: ${tableName}, ${columnSettings.length}개` ); // Prisma 트랜잭션 사용 await prisma.$transaction(async (tx) => { // 테이블이 table_labels에 없으면 자동 추가 await this.insertTableIfNotExists(tableName); // 각 컬럼 설정을 순차적으로 업데이트 for (const columnSetting of columnSettings) { // columnName은 실제 DB 컬럼명을 유지해야 함 const columnName = columnSetting.columnName; if (columnName) { await this.updateColumnSettings( tableName, columnName, columnSetting ); } else { logger.warn( `컬럼명이 누락된 설정: ${JSON.stringify(columnSetting)}` ); } } }); logger.info(`전체 컬럼 설정 일괄 업데이트 완료: ${tableName}`); } catch (error) { logger.error( `전체 컬럼 설정 일괄 업데이트 중 오류 발생: ${tableName}`, error ); throw new Error( `전체 컬럼 설정 일괄 업데이트 실패: ${error instanceof Error ? error.message : "Unknown error"}` ); } } /** * 테이블 라벨 정보 조회 * Prisma ORM으로 변경 */ async getTableLabels(tableName: string): Promise { try { logger.info(`테이블 라벨 정보 조회 시작: ${tableName}`); 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) { return null; } 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, }; logger.info(`테이블 라벨 정보 조회 완료: ${tableName}`); return result; } catch (error) { logger.error(`테이블 라벨 정보 조회 중 오류 발생: ${tableName}`, error); throw new Error( `테이블 라벨 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}` ); } } /** * 컬럼 라벨 정보 조회 * Prisma ORM으로 변경 */ async getColumnLabels( tableName: string, columnName: string ): Promise { try { logger.info(`컬럼 라벨 정보 조회 시작: ${tableName}.${columnName}`); 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) { return null; } 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, }; logger.info(`컬럼 라벨 정보 조회 완료: ${tableName}.${columnName}`); return result; } catch (error) { logger.error( `컬럼 라벨 정보 조회 중 오류 발생: ${tableName}.${columnName}`, error ); throw new Error( `컬럼 라벨 정보 조회 실패: ${error instanceof Error ? error.message : "Unknown error"}` ); } } /** * 컬럼 웹 타입 설정 */ async updateColumnWebType( tableName: string, columnName: string, webType: string, detailSettings?: Record, inputType?: string ): Promise { try { logger.info( `컬럼 웹 타입 설정 시작: ${tableName}.${columnName} = ${webType}` ); // 웹 타입별 기본 상세 설정 생성 const defaultDetailSettings = this.generateDefaultDetailSettings(webType); // 사용자 정의 설정과 기본 설정 병합 const finalDetailSettings = { ...defaultDetailSettings, ...detailSettings, }; // column_labels 테이블에 해당 컬럼이 있는지 확인 const existingColumn = await prisma.column_labels.findFirst({ where: { table_name: tableName, column_name: columnName, }, }); if (existingColumn) { // 기존 컬럼 라벨 업데이트 const updateData: any = { web_type: webType, detail_settings: JSON.stringify(finalDetailSettings), updated_date: new Date(), }; if (inputType) { updateData.input_type = inputType; } await prisma.column_labels.update({ where: { id: existingColumn.id, }, data: updateData, }); 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; } await prisma.column_labels.create({ data: createData, }); 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 { 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 {}; } } /** * 테이블 데이터 조회 (페이징 + 검색) */ async getTableData( tableName: string, options: { page: number; size: number; search?: Record; 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( 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( 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; } } /** * 현재 사용자 정보 조회 (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 ): Promise { 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(); 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, updatedData: Record ): Promise { 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(); 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[] ): Promise { 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; } } }