import { PrismaClient } from "@prisma/client"; import prisma from "../config/database"; interface GetTableDataParams { tableName: string; limit?: number; offset?: number; orderBy?: string; filters?: Record; userCompany?: string; } interface ServiceResponse { success: boolean; data?: T; message?: string; error?: string; } /** * 안전한 테이블명 목록 (화이트리스트) * SQL 인젝션 방지를 위해 허용된 테이블만 접근 가능 */ const ALLOWED_TABLES = [ "company_mng", "user_info", "dept_info", "code_info", "code_category", "menu_info", "approval", "approval_kind", "board", "comm_code", "product_mng", "part_mng", "material_mng", "order_mng_master", "inventory_mng", "contract_mgmt", "project_mgmt", "screen_definitions", "screen_layouts", "layout_standards", "component_standards", "web_type_standards", "button_action_standards", "template_standards", "grid_standards", "style_templates", "multi_lang_key_master", "multi_lang_text", "language_master", "table_labels", "column_labels", "dynamic_form_data", ]; /** * 회사별 필터링이 필요한 테이블 목록 */ const COMPANY_FILTERED_TABLES = [ "company_mng", "user_info", "dept_info", "approval", "board", "product_mng", "part_mng", "material_mng", "order_mng_master", "inventory_mng", "contract_mgmt", "project_mgmt", ]; class DataService { /** * 테이블 데이터 조회 */ async getTableData( params: GetTableDataParams ): Promise> { const { tableName, limit = 10, offset = 0, orderBy, filters = {}, userCompany, } = params; try { // 테이블명 화이트리스트 검증 if (!ALLOWED_TABLES.includes(tableName)) { return { success: false, message: `접근이 허용되지 않은 테이블입니다: ${tableName}`, error: "TABLE_NOT_ALLOWED", }; } // 테이블 존재 여부 확인 const tableExists = await this.checkTableExists(tableName); if (!tableExists) { return { success: false, message: `테이블을 찾을 수 없습니다: ${tableName}`, error: "TABLE_NOT_FOUND", }; } // 동적 SQL 쿼리 생성 let query = `SELECT * FROM "${tableName}"`; const queryParams: any[] = []; let paramIndex = 1; // WHERE 조건 생성 const whereConditions: string[] = []; // 회사별 필터링 추가 if (COMPANY_FILTERED_TABLES.includes(tableName) && userCompany) { // 슈퍼관리자(*)가 아닌 경우에만 회사 필터 적용 if (userCompany !== "*") { whereConditions.push(`company_code = $${paramIndex}`); queryParams.push(userCompany); paramIndex++; } } // 사용자 정의 필터 추가 for (const [key, value] of Object.entries(filters)) { if ( value && key !== "limit" && key !== "offset" && key !== "orderBy" && key !== "userLang" ) { // 컬럼명 검증 (SQL 인젝션 방지) if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) { continue; // 유효하지 않은 컬럼명은 무시 } whereConditions.push(`"${key}" ILIKE $${paramIndex}`); queryParams.push(`%${value}%`); paramIndex++; } } // WHERE 절 추가 if (whereConditions.length > 0) { query += ` WHERE ${whereConditions.join(" AND ")}`; } // ORDER BY 절 추가 if (orderBy) { // ORDER BY 검증 (SQL 인젝션 방지) const orderParts = orderBy.split(" "); const columnName = orderParts[0]; const direction = orderParts[1]?.toUpperCase(); if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(columnName)) { const validDirection = direction === "DESC" ? "DESC" : "ASC"; query += ` ORDER BY "${columnName}" ${validDirection}`; } } else { // 기본 정렬: 최신순 (가능한 컬럼 시도) const dateColumns = [ "created_date", "regdate", "reg_date", "updated_date", "upd_date", ]; const tableColumns = await this.getTableColumnsSimple(tableName); const availableDateColumn = dateColumns.find((col) => tableColumns.some((tableCol) => tableCol.column_name === col) ); if (availableDateColumn) { query += ` ORDER BY "${availableDateColumn}" DESC`; } } // LIMIT과 OFFSET 추가 query += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`; queryParams.push(limit, offset); console.log("🔍 실행할 쿼리:", query); console.log("📊 쿼리 파라미터:", queryParams); // 쿼리 실행 const result = await prisma.$queryRawUnsafe(query, ...queryParams); return { success: true, data: result as any[], }; } catch (error) { console.error(`데이터 조회 오류 (${tableName}):`, error); return { success: false, message: "데이터 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }; } } /** * 테이블 컬럼 정보 조회 */ async getTableColumns(tableName: string): Promise> { try { // 테이블명 화이트리스트 검증 if (!ALLOWED_TABLES.includes(tableName)) { return { success: false, message: `접근이 허용되지 않은 테이블입니다: ${tableName}`, error: "TABLE_NOT_ALLOWED", }; } const columns = await this.getTableColumnsSimple(tableName); // 컬럼 라벨 정보 추가 const columnsWithLabels = await Promise.all( columns.map(async (column) => { const label = await this.getColumnLabel( tableName, column.column_name ); return { columnName: column.column_name, columnLabel: label || column.column_name, dataType: column.data_type, isNullable: column.is_nullable === "YES", defaultValue: column.column_default, }; }) ); return { success: true, data: columnsWithLabels, }; } catch (error) { console.error(`컬럼 정보 조회 오류 (${tableName}):`, error); return { success: false, message: "컬럼 정보 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }; } } /** * 테이블 존재 여부 확인 */ private async checkTableExists(tableName: string): Promise { try { const result = await prisma.$queryRawUnsafe( ` SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1 ); `, tableName ); return (result as any)[0]?.exists || false; } catch (error) { console.error("테이블 존재 확인 오류:", error); return false; } } /** * 테이블 컬럼 정보 조회 (간단 버전) */ private async getTableColumnsSimple(tableName: string): Promise { const result = await prisma.$queryRawUnsafe( ` SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = $1 AND table_schema = 'public' ORDER BY ordinal_position; `, tableName ); return result as any[]; } /** * 컬럼 라벨 조회 */ private async getColumnLabel( tableName: string, columnName: string ): Promise { try { // column_labels 테이블에서 라벨 조회 const result = await prisma.$queryRawUnsafe( ` SELECT label_ko FROM column_labels WHERE table_name = $1 AND column_name = $2 LIMIT 1; `, tableName, columnName ); const labelResult = result as any[]; return labelResult[0]?.label_ko || null; } catch (error) { // column_labels 테이블이 없거나 오류가 발생하면 null 반환 return null; } } } export const dataService = new DataService();