/** * 동적 데이터 서비스 * * 주요 특징: * 1. 화이트리스트 제거 - 모든 테이블에 동적으로 접근 가능 * 2. 블랙리스트 방식 - 시스템 중요 테이블만 접근 금지 * 3. 자동 회사별 필터링 - company_code 컬럼 자동 감지 및 필터 적용 * 4. SQL 인젝션 방지 - 정규식 기반 테이블명/컬럼명 검증 * * 보안: * - 테이블명은 영문, 숫자, 언더스코어만 허용 * - 시스템 테이블(pg_*, information_schema 등) 접근 금지 * - company_code 컬럼이 있는 테이블은 자동으로 회사별 격리 * - 최고 관리자(company_code = "*")만 전체 데이터 조회 가능 */ import { query, queryOne } from "../database/db"; interface GetTableDataParams { tableName: string; limit?: number; offset?: number; orderBy?: string; filters?: Record; userCompany?: string; } interface ServiceResponse { success: boolean; data?: T; message?: string; error?: string; } /** * 접근 금지 테이블 목록 (블랙리스트) * 시스템 중요 테이블 및 보안상 접근 금지할 테이블 */ const BLOCKED_TABLES = [ "pg_catalog", "pg_statistic", "pg_database", "pg_user", "information_schema", "session_tokens", // 세션 토큰 테이블 "password_history", // 패스워드 이력 ]; /** * 테이블 이름 검증 정규식 * SQL 인젝션 방지: 영문, 숫자, 언더스코어만 허용 */ const TABLE_NAME_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/; class DataService { /** * 테이블 접근 검증 (공통 메서드) */ private async validateTableAccess( tableName: string ): Promise<{ valid: boolean; error?: ServiceResponse }> { // 1. 테이블명 형식 검증 (SQL 인젝션 방지) if (!TABLE_NAME_REGEX.test(tableName)) { return { valid: false, error: { success: false, message: `유효하지 않은 테이블명입니다: ${tableName}`, error: "INVALID_TABLE_NAME", }, }; } // 2. 블랙리스트 검증 if (BLOCKED_TABLES.includes(tableName)) { return { valid: false, error: { success: false, message: `접근이 금지된 테이블입니다: ${tableName}`, error: "TABLE_ACCESS_DENIED", }, }; } // 3. 테이블 존재 여부 확인 const tableExists = await this.checkTableExists(tableName); if (!tableExists) { return { valid: false, error: { success: false, message: `테이블을 찾을 수 없습니다: ${tableName}`, error: "TABLE_NOT_FOUND", }, }; } return { valid: true }; } /** * 테이블 데이터 조회 */ async getTableData( params: GetTableDataParams ): Promise> { const { tableName, limit = 10, offset = 0, orderBy, filters = {}, userCompany, } = params; try { // 테이블 접근 검증 const validation = await this.validateTableAccess(tableName); if (!validation.valid) { return validation.error!; } // 동적 SQL 쿼리 생성 let sql = `SELECT * FROM "${tableName}"`; const queryParams: any[] = []; let paramIndex = 1; // WHERE 조건 생성 const whereConditions: string[] = []; // 4. 회사별 필터링 자동 적용 (company_code 컬럼이 있는 경우) if (userCompany && userCompany !== "*") { const hasCompanyCode = await this.checkColumnExists(tableName, "company_code"); if (hasCompanyCode) { whereConditions.push(`company_code = $${paramIndex}`); queryParams.push(userCompany); paramIndex++; console.log(`🏢 회사별 필터링 적용: ${tableName}.company_code = ${userCompany}`); } } // 사용자 정의 필터 추가 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) { sql += ` 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"; sql += ` 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) { sql += ` ORDER BY "${availableDateColumn}" DESC`; } } // LIMIT과 OFFSET 추가 sql += ` LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`; queryParams.push(limit, offset); console.log("🔍 실행할 쿼리:", sql); console.log("📊 쿼리 파라미터:", queryParams); // 쿼리 실행 const result = await query(sql, queryParams); return { success: true, data: result, }; } catch (error) { console.error(`데이터 조회 오류 (${tableName}):`, error); return { success: false, message: "데이터 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }; } } /** * 테이블 컬럼 정보 조회 */ async getTableColumns(tableName: string): Promise> { try { // 테이블 접근 검증 const validation = await this.validateTableAccess(tableName); if (!validation.valid) { return validation.error!; } 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 query<{ exists: boolean }>( `SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1 )`, [tableName] ); return result[0]?.exists || false; } catch (error) { console.error("테이블 존재 확인 오류:", error); return false; } } /** * 특정 컬럼 존재 여부 확인 */ private async checkColumnExists( tableName: string, columnName: string ): Promise { try { const result = await query<{ exists: boolean }>( `SELECT EXISTS ( SELECT FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 AND column_name = $2 )`, [tableName, columnName] ); return result[0]?.exists || false; } catch (error) { console.error("컬럼 존재 확인 오류:", error); return false; } } /** * 테이블 컬럼 정보 조회 (간단 버전) */ private async getTableColumnsSimple(tableName: string): Promise { const result = await query( `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; } /** * 컬럼 라벨 조회 */ private async getColumnLabel( tableName: string, columnName: string ): Promise { try { // column_labels 테이블에서 라벨 조회 const result = await query<{ label_ko: string }>( `SELECT label_ko FROM column_labels WHERE table_name = $1 AND column_name = $2 LIMIT 1`, [tableName, columnName] ); return result[0]?.label_ko || null; } catch (error) { // column_labels 테이블이 없거나 오류가 발생하면 null 반환 return null; } } /** * 레코드 상세 조회 */ async getRecordDetail( tableName: string, id: string | number ): Promise> { try { // 테이블 접근 검증 const validation = await this.validateTableAccess(tableName); if (!validation.valid) { return validation.error!; } // Primary Key 컬럼 찾기 const pkResult = await query<{ attname: string }>( `SELECT a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = $1::regclass AND i.indisprimary`, [tableName] ); let pkColumn = "id"; // 기본값 if (pkResult.length > 0) { pkColumn = pkResult[0].attname; } const queryText = `SELECT * FROM "${tableName}" WHERE "${pkColumn}" = $1`; const result = await query(queryText, [id]); if (result.length === 0) { return { success: false, message: "레코드를 찾을 수 없습니다.", error: "RECORD_NOT_FOUND", }; } return { success: true, data: result[0], }; } catch (error) { console.error(`레코드 상세 조회 오류 (${tableName}/${id}):`, error); return { success: false, message: "레코드 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }; } } /** * 조인된 데이터 조회 */ async getJoinedData( leftTable: string, rightTable: string, leftColumn: string, rightColumn: string, leftValue?: string | number ): Promise> { try { // 왼쪽 테이블 접근 검증 const leftValidation = await this.validateTableAccess(leftTable); if (!leftValidation.valid) { return leftValidation.error!; } // 오른쪽 테이블 접근 검증 const rightValidation = await this.validateTableAccess(rightTable); if (!rightValidation.valid) { return rightValidation.error!; } let queryText = ` SELECT r.* FROM "${rightTable}" r INNER JOIN "${leftTable}" l ON l."${leftColumn}" = r."${rightColumn}" `; const values: any[] = []; if (leftValue !== undefined && leftValue !== null) { queryText += ` WHERE l."${leftColumn}" = $1`; values.push(leftValue); } const result = await query(queryText, values); return { success: true, data: result, }; } catch (error) { console.error( `조인 데이터 조회 오류 (${leftTable} → ${rightTable}):`, error ); return { success: false, message: "조인 데이터 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }; } } /** * 레코드 생성 */ async createRecord( tableName: string, data: Record ): Promise> { try { // 테이블 접근 검증 const validation = await this.validateTableAccess(tableName); if (!validation.valid) { return validation.error!; } const columns = Object.keys(data); const values = Object.values(data); const placeholders = values.map((_, index) => `$${index + 1}`).join(", "); const columnNames = columns.map((col) => `"${col}"`).join(", "); const queryText = ` INSERT INTO "${tableName}" (${columnNames}) VALUES (${placeholders}) RETURNING * `; const result = await query(queryText, values); return { success: true, data: result[0], }; } catch (error) { console.error(`레코드 생성 오류 (${tableName}):`, error); return { success: false, message: "레코드 생성 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }; } } /** * 레코드 수정 */ async updateRecord( tableName: string, id: string | number, data: Record ): Promise> { try { // 테이블 접근 검증 const validation = await this.validateTableAccess(tableName); if (!validation.valid) { return validation.error!; } // Primary Key 컬럼 찾기 const pkResult = await query<{ attname: string }>( `SELECT a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = $1::regclass AND i.indisprimary`, [tableName] ); let pkColumn = "id"; if (pkResult.length > 0) { pkColumn = pkResult[0].attname; } const columns = Object.keys(data); const values = Object.values(data); const setClause = columns .map((col, index) => `"${col}" = $${index + 1}`) .join(", "); const queryText = ` UPDATE "${tableName}" SET ${setClause} WHERE "${pkColumn}" = $${values.length + 1} RETURNING * `; values.push(id); const result = await query(queryText, values); if (result.length === 0) { return { success: false, message: "레코드를 찾을 수 없습니다.", error: "RECORD_NOT_FOUND", }; } return { success: true, data: result[0], }; } catch (error) { console.error(`레코드 수정 오류 (${tableName}/${id}):`, error); return { success: false, message: "레코드 수정 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }; } } /** * 레코드 삭제 */ async deleteRecord( tableName: string, id: string | number ): Promise> { try { // 테이블 접근 검증 const validation = await this.validateTableAccess(tableName); if (!validation.valid) { return validation.error!; } // Primary Key 컬럼 찾기 const pkResult = await query<{ attname: string }>( `SELECT a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) WHERE i.indrelid = $1::regclass AND i.indisprimary`, [tableName] ); let pkColumn = "id"; if (pkResult.length > 0) { pkColumn = pkResult[0].attname; } const queryText = `DELETE FROM "${tableName}" WHERE "${pkColumn}" = $1`; await query(queryText, [id]); return { success: true, }; } catch (error) { console.error(`레코드 삭제 오류 (${tableName}/${id}):`, error); return { success: false, message: "레코드 삭제 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }; } } } export const dataService = new DataService();