From 3f81c449ad218a0f34372f39d2d238f5a31c4f19 Mon Sep 17 00:00:00 2001 From: kjs Date: Thu, 8 Jan 2026 14:24:07 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BD=94=EB=93=9C=EB=B3=91=ED=95=A9=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/controllers/codeMergeController.ts | 172 ++++++++++++++++++ backend-node/src/routes/codeMergeRoutes.ts | 20 +- frontend/lib/utils/buttonActions.ts | 36 +++- 3 files changed, 216 insertions(+), 12 deletions(-) diff --git a/backend-node/src/controllers/codeMergeController.ts b/backend-node/src/controllers/codeMergeController.ts index 29abfa8e..74d9e893 100644 --- a/backend-node/src/controllers/codeMergeController.ts +++ b/backend-node/src/controllers/codeMergeController.ts @@ -282,3 +282,175 @@ export async function previewCodeMerge( } } +/** + * 값 기반 코드 병합 - 모든 테이블의 모든 컬럼에서 해당 값을 찾아 변경 + * 컬럼명에 상관없이 oldValue를 가진 모든 곳을 newValue로 변경 + */ +export async function mergeCodeByValue( + req: AuthenticatedRequest, + res: Response +): Promise { + const { oldValue, newValue } = req.body; + const companyCode = req.user?.companyCode; + + try { + // 입력값 검증 + if (!oldValue || !newValue) { + res.status(400).json({ + success: false, + message: "필수 필드가 누락되었습니다. (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("값 기반 코드 병합 시작", { + oldValue, + newValue, + companyCode, + userId: req.user?.userId, + }); + + // PostgreSQL 함수 호출 + const result = await pool.query( + "SELECT * FROM merge_code_by_value($1, $2, $3)", + [oldValue, newValue, companyCode] + ); + + // 결과 처리 + const affectedData = Array.isArray(result) ? result : ((result as any).rows || []); + const totalRows = affectedData.reduce( + (sum: number, row: any) => sum + parseInt(row.out_rows_updated || 0), + 0 + ); + + logger.info("값 기반 코드 병합 완료", { + oldValue, + newValue, + affectedTablesCount: affectedData.length, + totalRowsUpdated: totalRows, + }); + + res.json({ + success: true, + message: `코드 병합 완료: ${oldValue} → ${newValue}`, + data: { + oldValue, + newValue, + affectedData: affectedData.map((row: any) => ({ + tableName: row.out_table_name, + columnName: row.out_column_name, + rowsUpdated: parseInt(row.out_rows_updated), + })), + totalRowsUpdated: totalRows, + }, + }); + } catch (error: any) { + logger.error("값 기반 코드 병합 실패:", { + error: error.message, + stack: error.stack, + oldValue, + newValue, + }); + + res.status(500).json({ + success: false, + message: "코드 병합 중 오류가 발생했습니다.", + error: { + code: "CODE_MERGE_BY_VALUE_ERROR", + details: error.message, + }, + }); + } +} + +/** + * 값 기반 코드 병합 미리보기 + * 컬럼명에 상관없이 해당 값을 가진 모든 테이블/컬럼 조회 + */ +export async function previewMergeCodeByValue( + req: AuthenticatedRequest, + res: Response +): Promise { + const { oldValue } = req.body; + const companyCode = req.user?.companyCode; + + try { + if (!oldValue) { + res.status(400).json({ + success: false, + message: "필수 필드가 누락되었습니다. (oldValue)", + }); + return; + } + + if (!companyCode) { + res.status(401).json({ + success: false, + message: "인증 정보가 없습니다.", + }); + return; + } + + logger.info("값 기반 코드 병합 미리보기", { oldValue, companyCode }); + + // PostgreSQL 함수 호출 + const result = await pool.query( + "SELECT * FROM preview_merge_code_by_value($1, $2)", + [oldValue, companyCode] + ); + + const preview = Array.isArray(result) ? result : ((result as any).rows || []); + const totalRows = preview.reduce( + (sum: number, row: any) => sum + parseInt(row.out_affected_rows || 0), + 0 + ); + + logger.info("값 기반 코드 병합 미리보기 완료", { + tablesCount: preview.length, + totalRows, + }); + + res.json({ + success: true, + message: "코드 병합 미리보기 완료", + data: { + oldValue, + preview: preview.map((row: any) => ({ + tableName: row.out_table_name, + columnName: row.out_column_name, + affectedRows: parseInt(row.out_affected_rows), + })), + totalAffectedRows: totalRows, + }, + }); + } catch (error: any) { + logger.error("값 기반 코드 병합 미리보기 실패:", error); + + res.status(500).json({ + success: false, + message: "코드 병합 미리보기 중 오류가 발생했습니다.", + error: { + code: "PREVIEW_BY_VALUE_ERROR", + details: error.message, + }, + }); + } +} + diff --git a/backend-node/src/routes/codeMergeRoutes.ts b/backend-node/src/routes/codeMergeRoutes.ts index 78cbd3e1..2cb41923 100644 --- a/backend-node/src/routes/codeMergeRoutes.ts +++ b/backend-node/src/routes/codeMergeRoutes.ts @@ -3,6 +3,8 @@ import { mergeCodeAllTables, getTablesWithColumn, previewCodeMerge, + mergeCodeByValue, + previewMergeCodeByValue, } from "../controllers/codeMergeController"; import { authenticateToken } from "../middleware/authMiddleware"; @@ -13,7 +15,7 @@ router.use(authenticateToken); /** * POST /api/code-merge/merge-all-tables - * 코드 병합 실행 (모든 관련 테이블에 적용) + * 코드 병합 실행 (모든 관련 테이블에 적용 - 같은 컬럼명만) * Body: { columnName, oldValue, newValue } */ router.post("/merge-all-tables", mergeCodeAllTables); @@ -26,10 +28,24 @@ router.get("/tables-with-column/:columnName", getTablesWithColumn); /** * POST /api/code-merge/preview - * 코드 병합 미리보기 (실제 실행 없이 영향받을 데이터 확인) + * 코드 병합 미리보기 (같은 컬럼명 기준) * Body: { columnName, oldValue } */ router.post("/preview", previewCodeMerge); +/** + * POST /api/code-merge/merge-by-value + * 값 기반 코드 병합 (모든 테이블의 모든 컬럼에서 해당 값을 찾아 변경) + * Body: { oldValue, newValue } + */ +router.post("/merge-by-value", mergeCodeByValue); + +/** + * POST /api/code-merge/preview-by-value + * 값 기반 코드 병합 미리보기 (컬럼명 상관없이 값으로 검색) + * Body: { oldValue } + */ +router.post("/preview-by-value", previewMergeCodeByValue); + export default router; diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 4c9d638b..fdde7398 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -4920,26 +4920,35 @@ export class ButtonActionExecutor { const { oldValue, newValue } = confirmed; - // 미리보기 표시 (옵션) + // 미리보기 표시 (값 기반 검색 - 모든 테이블의 모든 컬럼에서 검색) if (config.mergeShowPreview !== false) { const { apiClient } = await import("@/lib/api/client"); - const previewResponse = await apiClient.post("/code-merge/preview", { - columnName, + toast.loading("영향받는 데이터 검색 중...", { duration: Infinity }); + + const previewResponse = await apiClient.post("/code-merge/preview-by-value", { oldValue, }); + toast.dismiss(); + if (previewResponse.data.success) { const preview = previewResponse.data.data; const totalRows = preview.totalAffectedRows; + // 상세 정보 생성 + const detailList = preview.preview + .map((p: any) => ` - ${p.tableName}.${p.columnName}: ${p.affectedRows}건`) + .join("\n"); + const confirmMerge = confirm( - "⚠️ 코드 병합 확인\n\n" + + "코드 병합 확인\n\n" + `${oldValue} → ${newValue}\n\n` + "영향받는 데이터:\n" + - `- 테이블 수: ${preview.preview.length}개\n` + + `- 테이블/컬럼 수: ${preview.preview.length}개\n` + `- 총 행 수: ${totalRows}개\n\n` + - `데이터는 삭제되지 않고, "${columnName}" 컬럼 값만 변경됩니다.\n\n` + + (preview.preview.length <= 10 ? `상세:\n${detailList}\n\n` : "") + + "모든 테이블에서 해당 값이 변경됩니다.\n\n" + "계속하시겠습니까?", ); @@ -4949,13 +4958,12 @@ export class ButtonActionExecutor { } } - // 병합 실행 + // 병합 실행 (값 기반 - 모든 테이블의 모든 컬럼) toast.loading("코드 병합 중...", { duration: Infinity }); const { apiClient } = await import("@/lib/api/client"); - const response = await apiClient.post("/code-merge/merge-all-tables", { - columnName, + const response = await apiClient.post("/code-merge/merge-by-value", { oldValue, newValue, }); @@ -4964,9 +4972,17 @@ export class ButtonActionExecutor { if (response.data.success) { const data = response.data.data; + + // 변경된 테이블/컬럼 목록 생성 + const changedList = data.affectedData + .map((d: any) => `${d.tableName}.${d.columnName}: ${d.rowsUpdated}건`) + .join(", "); + toast.success( - "코드 병합 완료!\n" + `${data.affectedTables.length}개 테이블, ${data.totalRowsUpdated}개 행 업데이트`, + `코드 병합 완료! ${data.affectedData.length}개 테이블/컬럼, ${data.totalRowsUpdated}개 행 업데이트`, ); + + console.log("코드 병합 결과:", data.affectedData); // 화면 새로고침 context.onRefresh?.();