import { Request, Response } from "express"; import pool from "../database/db"; import { logger } from "../utils/logger"; interface AuthenticatedRequest extends Request { user?: { userId: string; userName: string; companyCode: string; }; } /** * 코드 병합 - 모든 관련 테이블에 적용 * 데이터(레코드)는 삭제하지 않고, 컬럼 값만 변경 */ export async function mergeCodeAllTables( req: AuthenticatedRequest, res: Response ): Promise { const { columnName, oldValue, newValue } = req.body; const companyCode = req.user?.companyCode; try { // 입력값 검증 if (!columnName || !oldValue || !newValue) { res.status(400).json({ success: false, message: "필수 필드가 누락되었습니다. (columnName, oldValue, newValue)", }); return; } if (!companyCode) { res.status(401).json({ success: false, message: "인증 정보가 없습니다.", }); return; } // 같은 값으로 병합 시도 방지 if (oldValue === newValue) { res.status(400).json({ success: false, message: "기존 값과 새 값이 동일합니다.", }); return; } logger.info("코드 병합 시작", { columnName, oldValue, newValue, companyCode, userId: req.user?.userId, }); // PostgreSQL 함수 호출 const result = await pool.query( "SELECT * FROM merge_code_all_tables($1, $2, $3, $4)", [columnName, oldValue, newValue, companyCode] ); // 결과 처리 (pool.query 반환 타입 처리) const affectedTables = Array.isArray(result) ? result : ((result as any).rows || []); const totalRows = affectedTables.reduce( (sum: number, row: any) => sum + parseInt(row.rows_updated || 0), 0 ); logger.info("코드 병합 완료", { columnName, oldValue, newValue, affectedTablesCount: affectedTables.length, totalRowsUpdated: totalRows, }); res.json({ success: true, message: `코드 병합 완료: ${oldValue} → ${newValue}`, data: { columnName, oldValue, newValue, affectedTables: affectedTables.map((row) => ({ tableName: row.table_name, rowsUpdated: parseInt(row.rows_updated), })), totalRowsUpdated: totalRows, }, }); } catch (error: any) { logger.error("코드 병합 실패:", { error: error.message, stack: error.stack, columnName, oldValue, newValue, }); res.status(500).json({ success: false, message: "코드 병합 중 오류가 발생했습니다.", error: { code: "CODE_MERGE_ERROR", details: error.message, }, }); } } /** * 특정 컬럼을 가진 테이블 목록 조회 */ export async function getTablesWithColumn( req: AuthenticatedRequest, res: Response ): Promise { const { columnName } = req.params; try { if (!columnName) { res.status(400).json({ success: false, message: "컬럼명이 필요합니다.", }); return; } logger.info("컬럼을 가진 테이블 목록 조회", { columnName }); const query = ` SELECT DISTINCT t.table_name FROM information_schema.columns c JOIN information_schema.tables t ON c.table_name = t.table_name WHERE c.column_name = $1 AND t.table_schema = 'public' AND t.table_type = 'BASE TABLE' AND EXISTS ( SELECT 1 FROM information_schema.columns c2 WHERE c2.table_name = t.table_name AND c2.column_name = 'company_code' ) ORDER BY t.table_name `; const result = await pool.query(query, [columnName]); const rows = (result as any).rows || []; logger.info(`컬럼을 가진 테이블 조회 완료: ${rows.length}개`); res.json({ success: true, message: "테이블 목록 조회 성공", data: { columnName, tables: rows.map((row: any) => row.table_name), count: rows.length, }, }); } catch (error: any) { logger.error("테이블 목록 조회 실패:", error); res.status(500).json({ success: false, message: "테이블 목록 조회 중 오류가 발생했습니다.", error: { code: "TABLE_LIST_ERROR", details: error.message, }, }); } } /** * 코드 병합 미리보기 (실제 실행 없이 영향받을 데이터 확인) */ export async function previewCodeMerge( req: AuthenticatedRequest, res: Response ): Promise { const { columnName, oldValue } = req.body; const companyCode = req.user?.companyCode; try { if (!columnName || !oldValue) { res.status(400).json({ success: false, message: "필수 필드가 누락되었습니다. (columnName, oldValue)", }); return; } if (!companyCode) { res.status(401).json({ success: false, message: "인증 정보가 없습니다.", }); return; } logger.info("코드 병합 미리보기", { columnName, oldValue, companyCode }); // 해당 컬럼을 가진 테이블 찾기 const tablesQuery = ` SELECT DISTINCT t.table_name FROM information_schema.columns c JOIN information_schema.tables t ON c.table_name = t.table_name WHERE c.column_name = $1 AND t.table_schema = 'public' AND t.table_type = 'BASE TABLE' AND EXISTS ( SELECT 1 FROM information_schema.columns c2 WHERE c2.table_name = t.table_name AND c2.column_name = 'company_code' ) `; const tablesResult = await pool.query(tablesQuery, [columnName]); // 각 테이블에서 영향받을 행 수 계산 const preview = []; const tableRows = Array.isArray(tablesResult) ? tablesResult : ((tablesResult as any).rows || []); for (const row of tableRows) { const tableName = row.table_name; // 동적 SQL 생성 (테이블명과 컬럼명은 파라미터 바인딩 불가) // SQL 인젝션 방지: 테이블명과 컬럼명은 information_schema에서 검증된 값 const countQuery = `SELECT COUNT(*) as count FROM "${tableName}" WHERE "${columnName}" = $1 AND company_code = $2`; try { const countResult = await pool.query(countQuery, [oldValue, companyCode]); const rows = (countResult as any).rows || []; const count = rows.length > 0 ? parseInt(rows[0].count) : 0; if (count > 0) { preview.push({ tableName, affectedRows: count, }); } } catch (error: any) { logger.warn(`테이블 ${tableName} 조회 실패:`, error.message); // 테이블 접근 실패 시 건너뛰기 continue; } } const totalRows = preview.reduce((sum, item) => sum + item.affectedRows, 0); logger.info("코드 병합 미리보기 완료", { tablesCount: preview.length, totalRows, }); res.json({ success: true, message: "코드 병합 미리보기 완료", data: { columnName, oldValue, preview, totalAffectedRows: totalRows, }, }); } catch (error: any) { logger.error("코드 병합 미리보기 실패:", error); res.status(500).json({ success: false, message: "코드 병합 미리보기 중 오류가 발생했습니다.", error: { code: "PREVIEW_ERROR", details: error.message, }, }); } }