From c78ba865b694477711fb8dff710e0de471518f2b Mon Sep 17 00:00:00 2001 From: kjs Date: Fri, 28 Nov 2025 15:15:35 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=95=88=ED=92=80=EB=A6=AC=EB=8A=94=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tableCategoryValueController.ts | 46 ++++++ .../src/routes/tableCategoryValueRoutes.ts | 7 +- .../src/services/tableCategoryValueService.ts | 60 +++++++ frontend/app/(main)/admin/tableMng/page.tsx | 153 +++++++++++------- frontend/lib/api/tableCategoryValue.ts | 22 +++ 5 files changed, 226 insertions(+), 62 deletions(-) diff --git a/backend-node/src/controllers/tableCategoryValueController.ts b/backend-node/src/controllers/tableCategoryValueController.ts index c25b4127..248bb867 100644 --- a/backend-node/src/controllers/tableCategoryValueController.ts +++ b/backend-node/src/controllers/tableCategoryValueController.ts @@ -481,6 +481,52 @@ export const deleteColumnMapping = async (req: AuthenticatedRequest, res: Respon } }; +/** + * 테이블+컬럼 기준으로 모든 매핑 삭제 + * + * DELETE /api/categories/column-mapping/:tableName/:columnName + * + * 메뉴 선택 변경 시 기존 매핑을 모두 삭제하고 새로운 매핑만 추가하기 위해 사용 + */ +export const deleteColumnMappingsByColumn = async (req: AuthenticatedRequest, res: Response) => { + try { + const companyCode = req.user!.companyCode; + const { tableName, columnName } = req.params; + + if (!tableName || !columnName) { + return res.status(400).json({ + success: false, + message: "tableName과 columnName은 필수입니다", + }); + } + + logger.info("테이블+컬럼 기준 매핑 삭제", { + tableName, + columnName, + companyCode, + }); + + const deletedCount = await tableCategoryValueService.deleteColumnMappingsByColumn( + tableName, + columnName, + companyCode + ); + + return res.json({ + success: true, + message: `${deletedCount}개의 컬럼 매핑이 삭제되었습니다`, + deletedCount, + }); + } catch (error: any) { + logger.error(`테이블+컬럼 기준 매핑 삭제 실패: ${error.message}`); + return res.status(500).json({ + success: false, + message: error.message || "컬럼 매핑 삭제 중 오류가 발생했습니다", + error: error.message, + }); + } +}; + /** * 2레벨 메뉴 목록 조회 * diff --git a/backend-node/src/routes/tableCategoryValueRoutes.ts b/backend-node/src/routes/tableCategoryValueRoutes.ts index c4afe66e..b79aab75 100644 --- a/backend-node/src/routes/tableCategoryValueRoutes.ts +++ b/backend-node/src/routes/tableCategoryValueRoutes.ts @@ -11,6 +11,7 @@ import { createColumnMapping, getLogicalColumns, deleteColumnMapping, + deleteColumnMappingsByColumn, getSecondLevelMenus, } from "../controllers/tableCategoryValueController"; import { authenticateToken } from "../middleware/authMiddleware"; @@ -57,7 +58,11 @@ router.get("/logical-columns/:tableName/:menuObjid", getLogicalColumns); // 컬럼 매핑 생성/수정 router.post("/column-mapping", createColumnMapping); -// 컬럼 매핑 삭제 +// 테이블+컬럼 기준 매핑 삭제 (메뉴 선택 변경 시 기존 매핑 모두 삭제용) +// 주의: 더 구체적인 라우트가 먼저 와야 함 (3개 세그먼트 > 1개 세그먼트) +router.delete("/column-mapping/:tableName/:columnName/all", deleteColumnMappingsByColumn); + +// 컬럼 매핑 삭제 (단일) router.delete("/column-mapping/:mappingId", deleteColumnMapping); export default router; diff --git a/backend-node/src/services/tableCategoryValueService.ts b/backend-node/src/services/tableCategoryValueService.ts index 2a379ae0..b68d5f05 100644 --- a/backend-node/src/services/tableCategoryValueService.ts +++ b/backend-node/src/services/tableCategoryValueService.ts @@ -1066,6 +1066,66 @@ class TableCategoryValueService { } } + /** + * 테이블+컬럼 기준으로 모든 매핑 삭제 + * + * 메뉴 선택 변경 시 기존 매핑을 모두 삭제하고 새로운 매핑만 추가하기 위해 사용 + * + * @param tableName - 테이블명 + * @param columnName - 컬럼명 + * @param companyCode - 회사 코드 + * @returns 삭제된 매핑 수 + */ + async deleteColumnMappingsByColumn( + tableName: string, + columnName: string, + companyCode: string + ): Promise { + const pool = getPool(); + + try { + logger.info("테이블+컬럼 기준 매핑 삭제", { tableName, columnName, companyCode }); + + // 멀티테넌시 적용 + let deleteQuery: string; + let deleteParams: any[]; + + if (companyCode === "*") { + // 최고 관리자: 해당 테이블+컬럼의 모든 매핑 삭제 + deleteQuery = ` + DELETE FROM category_column_mapping + WHERE table_name = $1 + AND logical_column_name = $2 + `; + deleteParams = [tableName, columnName]; + } else { + // 일반 회사: 자신의 매핑만 삭제 + deleteQuery = ` + DELETE FROM category_column_mapping + WHERE table_name = $1 + AND logical_column_name = $2 + AND company_code = $3 + `; + deleteParams = [tableName, columnName, companyCode]; + } + + const result = await pool.query(deleteQuery, deleteParams); + const deletedCount = result.rowCount || 0; + + logger.info("테이블+컬럼 기준 매핑 삭제 완료", { + tableName, + columnName, + companyCode, + deletedCount + }); + + return deletedCount; + } catch (error: any) { + logger.error(`테이블+컬럼 기준 매핑 삭제 실패: ${error.message}`); + throw error; + } + } + /** * 논리적 컬럼명을 물리적 컬럼명으로 변환 * diff --git a/frontend/app/(main)/admin/tableMng/page.tsx b/frontend/app/(main)/admin/tableMng/page.tsx index 2fb83df4..abc71fd1 100644 --- a/frontend/app/(main)/admin/tableMng/page.tsx +++ b/frontend/app/(main)/admin/tableMng/page.tsx @@ -17,7 +17,7 @@ import { apiClient } from "@/lib/api/client"; import { commonCodeApi } from "@/lib/api/commonCode"; import { entityJoinApi, ReferenceTableColumn } from "@/lib/api/entityJoin"; import { ddlApi } from "@/lib/api/ddl"; -import { getSecondLevelMenus, createColumnMapping } from "@/lib/api/tableCategoryValue"; +import { getSecondLevelMenus, createColumnMapping, deleteColumnMappingsByColumn } from "@/lib/api/tableCategoryValue"; import { CreateTableModal } from "@/components/admin/CreateTableModal"; import { AddColumnModal } from "@/components/admin/AddColumnModal"; import { DDLLogViewer } from "@/components/admin/DDLLogViewer"; @@ -488,52 +488,69 @@ export default function TableManagementPage() { if (response.data.success) { console.log("✅ 컬럼 설정 저장 성공"); - // 🆕 Category 타입인 경우 컬럼 매핑 생성 + // 🆕 Category 타입인 경우 컬럼 매핑 처리 console.log("🔍 카테고리 조건 체크:", { isCategory: column.inputType === "category", hasCategoryMenus: !!column.categoryMenus, length: column.categoryMenus?.length || 0, }); - if (column.inputType === "category" && column.categoryMenus && column.categoryMenus.length > 0) { - console.log("📥 카테고리 메뉴 매핑 시작:", { + if (column.inputType === "category") { + // 1. 먼저 기존 매핑 모두 삭제 + console.log("🗑️ 기존 카테고리 메뉴 매핑 삭제 시작:", { + tableName: selectedTable, columnName: column.columnName, - categoryMenus: column.categoryMenus, - count: column.categoryMenus.length, }); - let successCount = 0; - let failCount = 0; - - for (const menuObjid of column.categoryMenus) { - try { - const mappingResponse = await createColumnMapping({ - tableName: selectedTable, - logicalColumnName: column.columnName, - physicalColumnName: column.columnName, - menuObjid, - description: `${column.displayName} (메뉴별 카테고리)`, - }); - - if (mappingResponse.success) { - successCount++; - } else { - console.error("❌ 매핑 생성 실패:", mappingResponse); - failCount++; - } - } catch (error) { - console.error(`❌ 메뉴 ${menuObjid}에 대한 매핑 생성 실패:`, error); - failCount++; - } + try { + const deleteResponse = await deleteColumnMappingsByColumn(selectedTable, column.columnName); + console.log("🗑️ 기존 매핑 삭제 결과:", deleteResponse); + } catch (error) { + console.error("❌ 기존 매핑 삭제 실패:", error); } + // 2. 새로운 매핑 추가 (선택된 메뉴가 있는 경우만) + if (column.categoryMenus && column.categoryMenus.length > 0) { + console.log("📥 카테고리 메뉴 매핑 시작:", { + columnName: column.columnName, + categoryMenus: column.categoryMenus, + count: column.categoryMenus.length, + }); - if (successCount > 0 && failCount === 0) { - toast.success(`컬럼 설정 및 ${successCount}개 메뉴 매핑이 저장되었습니다.`); - } else if (successCount > 0 && failCount > 0) { - toast.warning(`컬럼 설정 저장 성공. ${successCount}개 메뉴 매핑 성공, ${failCount}개 실패.`); - } else if (failCount > 0) { - toast.error(`컬럼 설정 저장 성공. 메뉴 매핑 생성 실패.`); + let successCount = 0; + let failCount = 0; + + for (const menuObjid of column.categoryMenus) { + try { + const mappingResponse = await createColumnMapping({ + tableName: selectedTable, + logicalColumnName: column.columnName, + physicalColumnName: column.columnName, + menuObjid, + description: `${column.displayName} (메뉴별 카테고리)`, + }); + + if (mappingResponse.success) { + successCount++; + } else { + console.error("❌ 매핑 생성 실패:", mappingResponse); + failCount++; + } + } catch (error) { + console.error(`❌ 메뉴 ${menuObjid}에 대한 매핑 생성 실패:`, error); + failCount++; + } + } + + if (successCount > 0 && failCount === 0) { + toast.success(`컬럼 설정 및 ${successCount}개 메뉴 매핑이 저장되었습니다.`); + } else if (successCount > 0 && failCount > 0) { + toast.warning(`컬럼 설정 저장 성공. ${successCount}개 메뉴 매핑 성공, ${failCount}개 실패.`); + } else if (failCount > 0) { + toast.error(`컬럼 설정 저장 성공. 메뉴 매핑 생성 실패.`); + } + } else { + toast.success("컬럼 설정이 저장되었습니다. (메뉴 매핑 없음)"); } } else { toast.success("컬럼 설정이 성공적으로 저장되었습니다."); @@ -596,10 +613,8 @@ export default function TableManagementPage() { ); if (response.data.success) { - // 🆕 Category 타입 컬럼들의 메뉴 매핑 생성 - const categoryColumns = columns.filter( - (col) => col.inputType === "category" && col.categoryMenus && col.categoryMenus.length > 0 - ); + // 🆕 Category 타입 컬럼들의 메뉴 매핑 처리 + const categoryColumns = columns.filter((col) => col.inputType === "category"); console.log("📥 전체 저장: 카테고리 컬럼 확인", { totalColumns: columns.length, @@ -615,33 +630,49 @@ export default function TableManagementPage() { let totalFailCount = 0; for (const column of categoryColumns) { - for (const menuObjid of column.categoryMenus!) { - try { - console.log("🔄 매핑 API 호출:", { - tableName: selectedTable, - columnName: column.columnName, - menuObjid, - }); + // 1. 먼저 기존 매핑 모두 삭제 + console.log("🗑️ 기존 카테고리 메뉴 매핑 삭제:", { + tableName: selectedTable, + columnName: column.columnName, + }); - const mappingResponse = await createColumnMapping({ - tableName: selectedTable, - logicalColumnName: column.columnName, - physicalColumnName: column.columnName, - menuObjid, - description: `${column.displayName} (메뉴별 카테고리)`, - }); + try { + const deleteResponse = await deleteColumnMappingsByColumn(selectedTable, column.columnName); + console.log("🗑️ 기존 매핑 삭제 결과:", deleteResponse); + } catch (error) { + console.error("❌ 기존 매핑 삭제 실패:", error); + } - console.log("✅ 매핑 API 응답:", mappingResponse); + // 2. 새로운 매핑 추가 (선택된 메뉴가 있는 경우만) + if (column.categoryMenus && column.categoryMenus.length > 0) { + for (const menuObjid of column.categoryMenus) { + try { + console.log("🔄 매핑 API 호출:", { + tableName: selectedTable, + columnName: column.columnName, + menuObjid, + }); - if (mappingResponse.success) { - totalSuccessCount++; - } else { - console.error("❌ 매핑 생성 실패:", mappingResponse); + const mappingResponse = await createColumnMapping({ + tableName: selectedTable, + logicalColumnName: column.columnName, + physicalColumnName: column.columnName, + menuObjid, + description: `${column.displayName} (메뉴별 카테고리)`, + }); + + console.log("✅ 매핑 API 응답:", mappingResponse); + + if (mappingResponse.success) { + totalSuccessCount++; + } else { + console.error("❌ 매핑 생성 실패:", mappingResponse); + totalFailCount++; + } + } catch (error) { + console.error(`❌ 메뉴 ${menuObjid}에 대한 매핑 생성 실패:`, error); totalFailCount++; } - } catch (error) { - console.error(`❌ 메뉴 ${menuObjid}에 대한 매핑 생성 실패:`, error); - totalFailCount++; } } } diff --git a/frontend/lib/api/tableCategoryValue.ts b/frontend/lib/api/tableCategoryValue.ts index ba830457..3c5380d1 100644 --- a/frontend/lib/api/tableCategoryValue.ts +++ b/frontend/lib/api/tableCategoryValue.ts @@ -259,6 +259,28 @@ export async function deleteColumnMapping(mappingId: number) { } } +/** + * 테이블+컬럼 기준으로 모든 매핑 삭제 + * + * 메뉴 선택 변경 시 기존 매핑을 모두 삭제하고 새로운 매핑만 추가하기 위해 사용 + * + * @param tableName - 테이블명 + * @param columnName - 컬럼명 + */ +export async function deleteColumnMappingsByColumn(tableName: string, columnName: string) { + try { + const response = await apiClient.delete<{ + success: boolean; + message: string; + deletedCount: number; + }>(`/table-categories/column-mapping/${tableName}/${columnName}/all`); + return response.data; + } catch (error: any) { + console.error("테이블+컬럼 기준 매핑 삭제 실패:", error); + return { success: false, error: error.message, deletedCount: 0 }; + } +} + /** * 2레벨 메뉴 목록 조회 *