import prisma from "../config/database"; import { Prisma } from "@prisma/client"; export interface FormDataResult { id: number; screenId: number; tableName: string; data: Record; createdAt: Date | null; updatedAt: Date | null; createdBy: string; updatedBy: string; } export interface PaginatedFormData { content: FormDataResult[]; totalElements: number; totalPages: number; currentPage: number; size: number; } export interface ValidationError { field: string; message: string; code: string; } export interface ValidationResult { valid: boolean; errors: ValidationError[]; } export interface TableColumn { columnName: string; dataType: string; nullable: boolean; primaryKey: boolean; maxLength?: number; defaultValue?: any; } export class DynamicFormService { /** * 테이블의 컬럼명 목록 조회 (간단 버전) */ private async getTableColumnNames(tableName: string): Promise { try { const result = (await prisma.$queryRawUnsafe(` SELECT column_name FROM information_schema.columns WHERE table_name = '${tableName}' AND table_schema = 'public' `)) as any[]; return result.map((row) => row.column_name); } catch (error) { console.error(`❌ 테이블 ${tableName} 컬럼 정보 조회 실패:`, error); return []; } } /** * 테이블의 Primary Key 컬럼 조회 */ private async getTablePrimaryKeys(tableName: string): Promise { try { const result = (await prisma.$queryRawUnsafe(` SELECT kcu.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 = '${tableName}' AND tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public' `)) as any[]; return result.map((row) => row.column_name); } catch (error) { console.error(`❌ 테이블 ${tableName} Primary Key 조회 실패:`, error); return []; } } /** * 폼 데이터 저장 (실제 테이블에 직접 저장) */ async saveFormData( screenId: number, tableName: string, data: Record ): Promise { try { console.log("💾 서비스: 실제 테이블에 폼 데이터 저장 시작:", { screenId, tableName, data, }); // 테이블의 실제 컬럼 정보와 Primary Key 조회 const tableColumns = await this.getTableColumnNames(tableName); const primaryKeys = await this.getTablePrimaryKeys(tableName); console.log(`📋 테이블 ${tableName}의 컬럼:`, tableColumns); console.log(`🔑 테이블 ${tableName}의 Primary Key:`, primaryKeys); // 메타데이터 제거 (실제 테이블 컬럼이 아님) const { created_by, updated_by, company_code, screen_id, ...actualData } = data; // 기본 데이터 준비 const dataToInsert: any = { ...actualData }; // 테이블에 존재하는 공통 필드들만 추가 if (tableColumns.includes("created_at")) { dataToInsert.created_at = new Date(); } if (tableColumns.includes("updated_at")) { dataToInsert.updated_at = new Date(); } if (tableColumns.includes("regdate") && !dataToInsert.regdate) { dataToInsert.regdate = new Date(); } // 생성자/수정자 정보가 있고 해당 컬럼이 존재한다면 추가 if (created_by && tableColumns.includes("created_by")) { dataToInsert.created_by = created_by; } if (updated_by && tableColumns.includes("updated_by")) { dataToInsert.updated_by = updated_by; } if (company_code && tableColumns.includes("company_code")) { dataToInsert.company_code = company_code; } // 존재하지 않는 컬럼 제거 Object.keys(dataToInsert).forEach((key) => { if (!tableColumns.includes(key)) { console.log( `⚠️ 컬럼 ${key}는 테이블 ${tableName}에 존재하지 않아 제거됨` ); delete dataToInsert[key]; } }); console.log("🎯 실제 테이블에 삽입할 데이터:", { tableName, dataToInsert, }); // 동적 SQL을 사용하여 실제 테이블에 UPSERT const columns = Object.keys(dataToInsert); const values: any[] = Object.values(dataToInsert); const placeholders = values.map((_, index) => `$${index + 1}`).join(", "); let upsertQuery: string; if (primaryKeys.length > 0) { // Primary Key가 있는 경우 UPSERT 사용 const conflictColumns = primaryKeys.join(", "); const updateSet = columns .filter((col) => !primaryKeys.includes(col)) // Primary Key는 UPDATE에서 제외 .map((col) => `${col} = EXCLUDED.${col}`) .join(", "); if (updateSet) { upsertQuery = ` INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders}) ON CONFLICT (${conflictColumns}) DO UPDATE SET ${updateSet} RETURNING * `; } else { // 업데이트할 컬럼이 없는 경우 (Primary Key만 있는 테이블) upsertQuery = ` INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders}) ON CONFLICT (${conflictColumns}) DO NOTHING RETURNING * `; } } else { // Primary Key가 없는 경우 일반 INSERT upsertQuery = ` INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders}) RETURNING * `; } console.log("📝 실행할 UPSERT SQL:", upsertQuery); console.log("📊 SQL 파라미터:", values); const result = await prisma.$queryRawUnsafe(upsertQuery, ...values); console.log("✅ 서비스: 실제 테이블 저장 성공:", result); // 결과를 표준 형식으로 변환 const insertedRecord = Array.isArray(result) ? result[0] : result; return { id: insertedRecord.id || insertedRecord.objid || 0, screenId: screenId, tableName: tableName, data: insertedRecord as Record, createdAt: insertedRecord.created_at || new Date(), updatedAt: insertedRecord.updated_at || new Date(), createdBy: insertedRecord.created_by || created_by || "system", updatedBy: insertedRecord.updated_by || updated_by || "system", }; } catch (error) { console.error("❌ 서비스: 실제 테이블 저장 실패:", error); throw new Error(`실제 테이블 저장 실패: ${error}`); } } /** * 폼 데이터 업데이트 (실제 테이블에서 직접 업데이트) */ async updateFormData( id: number, tableName: string, data: Record ): Promise { try { console.log("🔄 서비스: 실제 테이블에서 폼 데이터 업데이트 시작:", { id, tableName, data, }); // 테이블의 실제 컬럼 정보 조회 const tableColumns = await this.getTableColumnNames(tableName); console.log(`📋 테이블 ${tableName}의 컬럼:`, tableColumns); // 메타데이터 제거 const { created_by, updated_by, company_code, screen_id, ...actualData } = data; // 기본 데이터 준비 const dataToUpdate: any = { ...actualData }; // 테이블에 존재하는 업데이트 관련 필드들만 추가 if (tableColumns.includes("updated_at")) { dataToUpdate.updated_at = new Date(); } if (tableColumns.includes("regdate") && !dataToUpdate.regdate) { dataToUpdate.regdate = new Date(); } // 수정자 정보가 있고 해당 컬럼이 존재한다면 추가 if (updated_by && tableColumns.includes("updated_by")) { dataToUpdate.updated_by = updated_by; } // 존재하지 않는 컬럼 제거 Object.keys(dataToUpdate).forEach((key) => { if (!tableColumns.includes(key)) { console.log( `⚠️ 컬럼 ${key}는 테이블 ${tableName}에 존재하지 않아 제거됨` ); delete dataToUpdate[key]; } }); console.log("🎯 실제 테이블에서 업데이트할 데이터:", { tableName, id, dataToUpdate, }); // 동적 UPDATE SQL 생성 const setClause = Object.keys(dataToUpdate) .map((key, index) => `${key} = $${index + 1}`) .join(", "); const values: any[] = Object.values(dataToUpdate); values.push(id); // WHERE 조건용 ID 추가 // ID 또는 objid로 찾기 시도 const updateQuery = ` UPDATE ${tableName} SET ${setClause} WHERE (id = $${values.length} OR objid = $${values.length}) RETURNING * `; console.log("📝 실행할 UPDATE SQL:", updateQuery); console.log("📊 SQL 파라미터:", values); const result = await prisma.$queryRawUnsafe(updateQuery, ...values); console.log("✅ 서비스: 실제 테이블 업데이트 성공:", result); const updatedRecord = Array.isArray(result) ? result[0] : result; return { id: updatedRecord.id || updatedRecord.objid || id, screenId: 0, // 실제 테이블에는 screenId가 없으므로 0으로 설정 tableName: tableName, data: updatedRecord as Record, createdAt: updatedRecord.created_at || new Date(), updatedAt: updatedRecord.updated_at || new Date(), createdBy: updatedRecord.created_by || "system", updatedBy: updatedRecord.updated_by || updated_by || "system", }; } catch (error) { console.error("❌ 서비스: 실제 테이블 업데이트 실패:", error); throw new Error(`실제 테이블 업데이트 실패: ${error}`); } } /** * 폼 데이터 삭제 (실제 테이블에서 직접 삭제) */ async deleteFormData(id: number, tableName: string): Promise { try { console.log("🗑️ 서비스: 실제 테이블에서 폼 데이터 삭제 시작:", { id, tableName, }); // 동적 DELETE SQL 생성 const deleteQuery = ` DELETE FROM ${tableName} WHERE (id = $1 OR objid = $1) RETURNING * `; console.log("📝 실행할 DELETE SQL:", deleteQuery); console.log("📊 SQL 파라미터:", [id]); const result = await prisma.$queryRawUnsafe(deleteQuery, id); console.log("✅ 서비스: 실제 테이블 삭제 성공:", result); } catch (error) { console.error("❌ 서비스: 실제 테이블 삭제 실패:", error); throw new Error(`실제 테이블 삭제 실패: ${error}`); } } /** * 단일 폼 데이터 조회 */ async getFormData(id: number): Promise { try { console.log("📄 서비스: 폼 데이터 단건 조회 시작:", { id }); const result = await prisma.dynamic_form_data.findUnique({ where: { id }, }); if (!result) { console.log("❌ 서비스: 폼 데이터를 찾을 수 없음"); return null; } console.log("✅ 서비스: 폼 데이터 단건 조회 성공"); return { id: result.id, screenId: result.screen_id, tableName: result.table_name, data: result.form_data as Record, createdAt: result.created_at, updatedAt: result.updated_at, createdBy: result.created_by, updatedBy: result.updated_by, }; } catch (error) { console.error("❌ 서비스: 폼 데이터 단건 조회 실패:", error); throw new Error(`폼 데이터 조회 실패: ${error}`); } } /** * 화면별 폼 데이터 목록 조회 (페이징) */ async getFormDataList( screenId: number, params: { page: number; size: number; search?: string; sortBy?: string; sortOrder?: "asc" | "desc"; } ): Promise { try { console.log("📋 서비스: 폼 데이터 목록 조회 시작:", { screenId, params }); const { page, size, search, sortBy = "created_at", sortOrder = "desc", } = params; const skip = (page - 1) * size; // 검색 조건 구성 const where: Prisma.dynamic_form_dataWhereInput = { screen_id: screenId, }; // 검색어가 있는 경우 form_data 필드에서 검색 if (search) { where.OR = [ { form_data: { path: [], string_contains: search, }, }, { table_name: { contains: search, mode: "insensitive", }, }, ]; } // 정렬 조건 구성 const orderBy: Prisma.dynamic_form_dataOrderByWithRelationInput = {}; if (sortBy === "created_at" || sortBy === "updated_at") { orderBy[sortBy] = sortOrder; } else { orderBy.created_at = "desc"; // 기본값 } // 데이터 조회 const [results, totalCount] = await Promise.all([ prisma.dynamic_form_data.findMany({ where, orderBy, skip, take: size, }), prisma.dynamic_form_data.count({ where }), ]); const formDataResults: FormDataResult[] = results.map((result) => ({ id: result.id, screenId: result.screen_id, tableName: result.table_name, data: result.form_data as Record, createdAt: result.created_at, updatedAt: result.updated_at, createdBy: result.created_by, updatedBy: result.updated_by, })); const totalPages = Math.ceil(totalCount / size); console.log("✅ 서비스: 폼 데이터 목록 조회 성공:", { totalCount, totalPages, currentPage: page, }); return { content: formDataResults, totalElements: totalCount, totalPages, currentPage: page, size, }; } catch (error) { console.error("❌ 서비스: 폼 데이터 목록 조회 실패:", error); throw new Error(`폼 데이터 목록 조회 실패: ${error}`); } } /** * 폼 데이터 검증 */ async validateFormData( tableName: string, data: Record ): Promise { try { console.log("✅ 서비스: 폼 데이터 검증 시작:", { tableName, data }); const errors: ValidationError[] = []; // 기본 검증 로직 (실제로는 테이블 스키마를 확인해야 함) Object.entries(data).forEach(([key, value]) => { // 예시: 빈 값 검증 if (value === null || value === undefined || value === "") { // 특정 필드가 required인지 확인하는 로직이 필요 // 지금은 간단히 모든 필드를 선택사항으로 처리 } // 예시: 데이터 타입 검증 // 실제로는 테이블 스키마의 컬럼 타입과 비교해야 함 }); const result: ValidationResult = { valid: errors.length === 0, errors, }; console.log("✅ 서비스: 폼 데이터 검증 완료:", result); return result; } catch (error) { console.error("❌ 서비스: 폼 데이터 검증 실패:", error); throw new Error(`폼 데이터 검증 실패: ${error}`); } } /** * 테이블 컬럼 정보 조회 (PostgreSQL 시스템 테이블 활용) */ async getTableColumns(tableName: string): Promise { try { console.log("📊 서비스: 테이블 컬럼 정보 조회 시작:", { tableName }); // PostgreSQL의 information_schema를 사용하여 컬럼 정보 조회 const columns = await prisma.$queryRaw` SELECT column_name, data_type, is_nullable, column_default, character_maximum_length FROM information_schema.columns WHERE table_name = ${tableName} AND table_schema = 'public' ORDER BY ordinal_position `; // Primary key 정보 조회 const primaryKeys = await prisma.$queryRaw` SELECT kcu.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_name = ${tableName} AND tc.table_schema = 'public' `; const primaryKeyColumns = new Set( primaryKeys.map((pk) => pk.column_name) ); const result: TableColumn[] = columns.map((col) => ({ columnName: col.column_name, dataType: col.data_type, nullable: col.is_nullable === "YES", primaryKey: primaryKeyColumns.has(col.column_name), maxLength: col.character_maximum_length, defaultValue: col.column_default, })); console.log("✅ 서비스: 테이블 컬럼 정보 조회 성공:", result); return result; } catch (error) { console.error("❌ 서비스: 테이블 컬럼 정보 조회 실패:", error); throw new Error(`테이블 컬럼 정보 조회 실패: ${error}`); } } } // 싱글톤 인스턴스 생성 및 export export const dynamicFormService = new DynamicFormService();