import express from "express"; import { dataService } from "../services/dataService"; import { masterDetailExcelService } from "../services/masterDetailExcelService"; import { authenticateToken } from "../middleware/authMiddleware"; import { AuthenticatedRequest } from "../types/auth"; const router = express.Router(); // ================================ // 마스터-디테일 엑셀 API // ================================ /** * 마스터-디테일 관계 정보 조회 * GET /api/data/master-detail/relation/:screenId */ router.get( "/master-detail/relation/:screenId", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { screenId } = req.params; if (!screenId || isNaN(parseInt(screenId))) { return res.status(400).json({ success: false, message: "유효한 screenId가 필요합니다.", }); } console.log(`🔍 마스터-디테일 관계 조회: screenId=${screenId}`); const relation = await masterDetailExcelService.getMasterDetailRelation( parseInt(screenId) ); if (!relation) { return res.json({ success: true, data: null, message: "마스터-디테일 구조가 아닙니다.", }); } console.log(`✅ 마스터-디테일 관계 발견:`, { masterTable: relation.masterTable, detailTable: relation.detailTable, joinKey: relation.masterKeyColumn, }); return res.json({ success: true, data: relation, }); } catch (error: any) { console.error("마스터-디테일 관계 조회 오류:", error); return res.status(500).json({ success: false, message: "마스터-디테일 관계 조회 중 오류가 발생했습니다.", error: error.message, }); } } ); /** * 마스터-디테일 엑셀 다운로드 데이터 조회 * POST /api/data/master-detail/download */ router.post( "/master-detail/download", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { screenId, filters } = req.body; const companyCode = req.user?.companyCode || "*"; if (!screenId) { return res.status(400).json({ success: false, message: "screenId가 필요합니다.", }); } console.log(`📥 마스터-디테일 엑셀 다운로드: screenId=${screenId}`); // 1. 마스터-디테일 관계 조회 const relation = await masterDetailExcelService.getMasterDetailRelation( parseInt(screenId) ); if (!relation) { return res.status(400).json({ success: false, message: "마스터-디테일 구조가 아닙니다.", }); } // 2. JOIN 데이터 조회 const data = await masterDetailExcelService.getJoinedData( relation, companyCode, filters ); console.log(`✅ 마스터-디테일 데이터 조회 완료: ${data.data.length}행`); return res.json({ success: true, data, }); } catch (error: any) { console.error("마스터-디테일 다운로드 오류:", error); return res.status(500).json({ success: false, message: "마스터-디테일 다운로드 중 오류가 발생했습니다.", error: error.message, }); } } ); /** * 마스터-디테일 엑셀 업로드 * POST /api/data/master-detail/upload */ router.post( "/master-detail/upload", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { screenId, data } = req.body; const companyCode = req.user?.companyCode || "*"; const userId = req.user?.userId; if (!screenId || !data || !Array.isArray(data)) { return res.status(400).json({ success: false, message: "screenId와 data 배열이 필요합니다.", }); } console.log(`📤 마스터-디테일 엑셀 업로드: screenId=${screenId}, rows=${data.length}`); // 1. 마스터-디테일 관계 조회 const relation = await masterDetailExcelService.getMasterDetailRelation( parseInt(screenId) ); if (!relation) { return res.status(400).json({ success: false, message: "마스터-디테일 구조가 아닙니다.", }); } // 2. 데이터 업로드 const result = await masterDetailExcelService.uploadJoinedData( relation, data, companyCode, userId ); console.log(`✅ 마스터-디테일 업로드 완료:`, { masterInserted: result.masterInserted, masterUpdated: result.masterUpdated, detailInserted: result.detailInserted, errors: result.errors.length, }); return res.json({ success: result.success, data: result, message: result.success ? `마스터 ${result.masterInserted + result.masterUpdated}건, 디테일 ${result.detailInserted}건 처리되었습니다.` : "업로드 중 오류가 발생했습니다.", }); } catch (error: any) { console.error("마스터-디테일 업로드 오류:", error); return res.status(500).json({ success: false, message: "마스터-디테일 업로드 중 오류가 발생했습니다.", error: error.message, }); } } ); /** * 마스터-디테일 간단 모드 엑셀 업로드 * - 마스터 정보는 UI에서 선택 * - 디테일 정보만 엑셀에서 업로드 * - 채번 규칙을 통해 마스터 키 자동 생성 * * POST /api/data/master-detail/upload-simple */ router.post( "/master-detail/upload-simple", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { screenId, detailData, masterFieldValues, numberingRuleId, afterUploadFlowId, afterUploadFlows } = req.body; const companyCode = req.user?.companyCode || "*"; const userId = req.user?.userId || "system"; if (!screenId || !detailData || !Array.isArray(detailData)) { return res.status(400).json({ success: false, message: "screenId와 detailData 배열이 필요합니다.", }); } console.log(`📤 마스터-디테일 간단 모드 업로드: screenId=${screenId}, rows=${detailData.length}`); console.log(` 마스터 필드 값:`, masterFieldValues); console.log(` 채번 규칙 ID:`, numberingRuleId); console.log(` 업로드 후 제어:`, afterUploadFlows?.length > 0 ? `${afterUploadFlows.length}개` : afterUploadFlowId || "없음"); // 업로드 실행 const result = await masterDetailExcelService.uploadSimple( parseInt(screenId), detailData, masterFieldValues || {}, numberingRuleId, companyCode, userId, afterUploadFlowId, // 업로드 후 제어 실행 (단일, 하위 호환성) afterUploadFlows // 업로드 후 제어 실행 (다중) ); console.log(`✅ 마스터-디테일 간단 모드 업로드 완료:`, { masterInserted: result.masterInserted, detailInserted: result.detailInserted, generatedKey: result.generatedKey, errors: result.errors.length, }); return res.json({ success: result.success, data: result, message: result.success ? `마스터 1건(${result.generatedKey}), 디테일 ${result.detailInserted}건 처리되었습니다.` : "업로드 중 오류가 발생했습니다.", }); } catch (error: any) { console.error("마스터-디테일 간단 모드 업로드 오류:", error); return res.status(500).json({ success: false, message: "마스터-디테일 업로드 중 오류가 발생했습니다.", error: error.message, }); } } ); // ================================ // 기존 데이터 API // ================================ /** * 조인 데이터 조회 API (다른 라우트보다 먼저 정의) * GET /api/data/join?leftTable=...&rightTable=...&leftColumn=...&rightColumn=...&leftValue=... */ router.get( "/join", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { leftTable, rightTable, leftColumn, rightColumn, leftValue, dataFilter, enableEntityJoin, displayColumns, deduplication, } = req.query; // 입력값 검증 if (!leftTable || !rightTable || !leftColumn || !rightColumn) { return res.status(400).json({ success: false, message: "필수 파라미터가 누락되었습니다 (leftTable, rightTable, leftColumn, rightColumn).", error: "MISSING_PARAMETERS", }); } // dataFilter 파싱 (JSON 문자열로 전달됨) let parsedDataFilter = undefined; if (dataFilter && typeof dataFilter === "string") { try { parsedDataFilter = JSON.parse(dataFilter); } catch (error) { console.error("dataFilter 파싱 오류:", error); } } // 🆕 enableEntityJoin 파싱 const enableEntityJoinFlag = enableEntityJoin === "true" || (typeof enableEntityJoin === "boolean" && enableEntityJoin); // SQL 인젝션 방지를 위한 검증 const tables = [leftTable as string, rightTable as string]; const columns = [leftColumn as string, rightColumn as string]; for (const table of tables) { if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) { return res.status(400).json({ success: false, message: `유효하지 않은 테이블명입니다: ${table}`, error: "INVALID_TABLE_NAME", }); } } for (const column of columns) { if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(column)) { return res.status(400).json({ success: false, message: `유효하지 않은 컬럼명입니다: ${column}`, error: "INVALID_COLUMN_NAME", }); } } // 회사 코드 추출 (멀티테넌시 필터링) const userCompany = req.user?.companyCode; // displayColumns 파싱 (item_info.item_name 등) let parsedDisplayColumns: | Array<{ name: string; label?: string }> | undefined; if (displayColumns) { try { parsedDisplayColumns = JSON.parse(displayColumns as string); } catch (e) { console.error("displayColumns 파싱 실패:", e); } } // 🆕 deduplication 파싱 let parsedDeduplication: | { enabled: boolean; groupByColumn: string; keepStrategy: "latest" | "earliest" | "base_price" | "current_date"; sortColumn?: string; } | undefined; if (deduplication) { try { parsedDeduplication = JSON.parse(deduplication as string); } catch (e) { console.error("deduplication 파싱 실패:", e); } } console.log(`🔗 조인 데이터 조회:`, { leftTable, rightTable, leftColumn, rightColumn, leftValue, userCompany, dataFilter: parsedDataFilter, enableEntityJoin: enableEntityJoinFlag, displayColumns: parsedDisplayColumns, // 🆕 표시 컬럼 로그 deduplication: parsedDeduplication, // 🆕 중복 제거 로그 }); // 조인 데이터 조회 (회사 코드 + 데이터 필터 + Entity 조인 + 표시 컬럼 + 중복 제거 전달) const result = await dataService.getJoinedData( leftTable as string, rightTable as string, leftColumn as string, rightColumn as string, leftValue as string, userCompany, parsedDataFilter, enableEntityJoinFlag, parsedDisplayColumns, // 🆕 표시 컬럼 전달 parsedDeduplication // 🆕 중복 제거 설정 전달 ); if (!result.success) { return res.status(400).json(result); } console.log( `✅ 조인 데이터 조회 성공: ${result.data?.length || 0}개 항목` ); return res.json({ success: true, data: result.data, }); } catch (error) { console.error("조인 데이터 조회 오류:", error); return res.status(500).json({ success: false, message: "조인 데이터 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } ); /** * 동적 테이블 데이터 조회 API * GET /api/data/{tableName} */ router.get( "/:tableName", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { tableName } = req.params; const { limit, offset, page, size, orderBy, searchTerm, sortBy, sortOrder, userLang, ...filters } = req.query; // 입력값 검증 if (!tableName || typeof tableName !== "string") { return res.status(400).json({ success: false, message: "테이블명이 필요합니다.", error: "INVALID_TABLE_NAME", }); } // SQL 인젝션 방지를 위한 테이블명 검증 if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { return res.status(400).json({ success: false, message: "유효하지 않은 테이블명입니다.", error: "INVALID_TABLE_NAME", }); } // page/size 또는 limit/offset 방식 지원 let finalLimit = 100; let finalOffset = 0; if (page && size) { // page/size 방식 const pageNum = parseInt(page as string) || 1; const sizeNum = parseInt(size as string) || 100; finalLimit = sizeNum; finalOffset = (pageNum - 1) * sizeNum; } else if (limit || offset) { // limit/offset 방식 finalLimit = parseInt(limit as string) || 10; finalOffset = parseInt(offset as string) || 0; } console.log(`📊 데이터 조회 요청: ${tableName}`, { limit: finalLimit, offset: finalOffset, orderBy: orderBy || sortBy, searchTerm, filters, user: req.user?.userId, }); // filters에서 searchTerm과 sortOrder 제거 (이미 별도로 처리됨) const cleanFilters = { ...filters }; delete cleanFilters.searchTerm; delete cleanFilters.sortOrder; // 데이터 조회 const result = await dataService.getTableData({ tableName, limit: finalLimit, offset: finalOffset, orderBy: (orderBy || sortBy) as string, filters: cleanFilters as Record, userCompany: req.user?.companyCode, }); if (!result.success) { return res.status(400).json(result); } console.log( `✅ 데이터 조회 성공: ${tableName}, ${result.data?.length || 0}개 항목` ); // 페이징 정보 포함하여 반환 const total = result.data?.length || 0; const responsePage = finalLimit > 0 ? Math.floor(finalOffset / finalLimit) + 1 : 1; const responseSize = finalLimit; const totalPages = responseSize > 0 ? Math.ceil(total / responseSize) : 1; return res.json({ success: true, data: result.data, total, page: responsePage, size: responseSize, totalPages, }); } catch (error) { console.error("데이터 조회 오류:", error); return res.status(500).json({ success: false, message: "데이터 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } ); /** * 테이블 컬럼 정보 조회 API * GET /api/data/{tableName}/columns */ router.get( "/:tableName/columns", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { tableName } = req.params; // 입력값 검증 if (!tableName || typeof tableName !== "string") { return res.status(400).json({ success: false, message: "테이블명이 필요합니다.", error: "INVALID_TABLE_NAME", }); } // SQL 인젝션 방지를 위한 테이블명 검증 if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { return res.status(400).json({ success: false, message: "유효하지 않은 테이블명입니다.", error: "INVALID_TABLE_NAME", }); } console.log(`📋 컬럼 정보 조회: ${tableName}`); // 컬럼 정보 조회 const result = await dataService.getTableColumns(tableName); if (!result.success) { return res.status(400).json(result); } console.log( `✅ 컬럼 정보 조회 성공: ${tableName}, ${result.data?.length || 0}개 컬럼` ); return res.json(result); } catch (error) { console.error("컬럼 정보 조회 오류:", error); return res.status(500).json({ success: false, message: "컬럼 정보 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } ); /** * 레코드 상세 조회 API * GET /api/data/{tableName}/{id} */ router.get( "/:tableName/:id", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { tableName, id } = req.params; // 입력값 검증 if (!tableName || typeof tableName !== "string") { return res.status(400).json({ success: false, message: "테이블명이 필요합니다.", error: "INVALID_TABLE_NAME", }); } if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { return res.status(400).json({ success: false, message: "유효하지 않은 테이블명입니다.", error: "INVALID_TABLE_NAME", }); } const { enableEntityJoin, groupByColumns, primaryKeyColumn } = req.query; const enableEntityJoinFlag = enableEntityJoin === "true" || (typeof enableEntityJoin === "boolean" && enableEntityJoin); // groupByColumns 파싱 (JSON 문자열 또는 쉼표 구분) let groupByColumnsArray: string[] = []; if (groupByColumns) { try { if (typeof groupByColumns === "string") { // JSON 형식이면 파싱, 아니면 쉼표로 분리 groupByColumnsArray = groupByColumns.startsWith("[") ? JSON.parse(groupByColumns) : groupByColumns.split(",").map((c) => c.trim()); } } catch (error) { console.warn("groupByColumns 파싱 실패:", error); } } // 🆕 primaryKeyColumn 파싱 const primaryKeyColumnStr = typeof primaryKeyColumn === "string" ? primaryKeyColumn : undefined; console.log(`🔍 레코드 상세 조회: ${tableName}/${id}`, { enableEntityJoin: enableEntityJoinFlag, groupByColumns: groupByColumnsArray, primaryKeyColumn: primaryKeyColumnStr, }); // 레코드 상세 조회 (Entity Join 옵션 + 그룹핑 옵션 + Primary Key 컬럼 포함) const result = await dataService.getRecordDetail( tableName, id, enableEntityJoinFlag, groupByColumnsArray, primaryKeyColumnStr // 🆕 Primary Key 컬럼명 전달 ); if (!result.success) { return res.status(400).json(result); } if (!result.data) { return res.status(404).json({ success: false, message: "레코드를 찾을 수 없습니다.", }); } console.log(`✅ 레코드 조회 성공: ${tableName}/${id}`); return res.json({ success: true, data: result.data, }); } catch (error) { console.error("레코드 상세 조회 오류:", error); return res.status(500).json({ success: false, message: "레코드 조회 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } ); /** * 그룹화된 데이터 UPSERT API * POST /api/data/upsert-grouped * * 요청 본문: * { * tableName: string, * parentKeys: { customer_id: "CUST-0002", item_id: "SLI-2025-0002" }, * records: [ { customer_item_code: "84-44", start_date: "2025-11-18", ... }, ... ] * } */ router.post( "/upsert-grouped", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { tableName, parentKeys, records } = req.body; // 입력값 검증 if (!tableName || !parentKeys || !records || !Array.isArray(records)) { return res.status(400).json({ success: false, message: "필수 파라미터가 누락되었습니다 (tableName, parentKeys, records).", error: "MISSING_PARAMETERS", }); } // 테이블명 검증 if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { return res.status(400).json({ success: false, message: "유효하지 않은 테이블명입니다.", error: "INVALID_TABLE_NAME", }); } console.log(`🔄 그룹화된 데이터 UPSERT: ${tableName}`, { parentKeys, recordCount: records.length, userCompany: req.user?.companyCode, userId: req.user?.userId, }); // UPSERT 수행 const result = await dataService.upsertGroupedRecords( tableName, parentKeys, records, req.user?.companyCode, req.user?.userId ); if (!result.success) { return res.status(400).json(result); } console.log(`✅ 그룹화된 데이터 UPSERT 성공: ${tableName}`, { inserted: result.data?.inserted || 0, updated: result.data?.updated || 0, deleted: result.data?.deleted || 0, }); return res.json({ success: true, message: "데이터가 저장되었습니다.", inserted: result.data?.inserted || 0, updated: result.data?.updated || 0, deleted: result.data?.deleted || 0, }); } catch (error) { console.error("그룹화된 데이터 UPSERT 오류:", error); return res.status(500).json({ success: false, message: "데이터 저장 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } ); /** * 레코드 생성 API * POST /api/data/{tableName} */ router.post( "/:tableName", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { tableName } = req.params; const data = req.body; // 입력값 검증 if (!tableName || typeof tableName !== "string") { return res.status(400).json({ success: false, message: "테이블명이 필요합니다.", error: "INVALID_TABLE_NAME", }); } if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { return res.status(400).json({ success: false, message: "유효하지 않은 테이블명입니다.", error: "INVALID_TABLE_NAME", }); } console.log(`➕ 레코드 생성: ${tableName}`, data); // company_code와 company_name 자동 추가 (멀티테넌시) const enrichedData = { ...data }; // 테이블에 company_code 컬럼이 있는지 확인하고 자동으로 추가 const hasCompanyCode = await dataService.checkColumnExists( tableName, "company_code" ); if (hasCompanyCode && req.user?.companyCode) { enrichedData.company_code = req.user.companyCode; console.log(`🏢 company_code 자동 추가: ${req.user.companyCode}`); } // 테이블에 company_name 컬럼이 있는지 확인하고 자동으로 추가 const hasCompanyName = await dataService.checkColumnExists( tableName, "company_name" ); if (hasCompanyName && req.user?.companyName) { enrichedData.company_name = req.user.companyName; console.log(`🏢 company_name 자동 추가: ${req.user.companyName}`); } // 레코드 생성 const result = await dataService.createRecord(tableName, enrichedData); if (!result.success) { return res.status(400).json(result); } console.log(`✅ 레코드 생성 성공: ${tableName}`); return res.status(201).json({ success: true, data: result.data, message: "레코드가 생성되었습니다.", }); } catch (error) { console.error("레코드 생성 오류:", error); return res.status(500).json({ success: false, message: "레코드 생성 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } ); /** * 레코드 수정 API * PUT /api/data/{tableName}/{id} */ router.put( "/:tableName/:id", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { tableName, id } = req.params; const data = req.body; // 입력값 검증 if (!tableName || typeof tableName !== "string") { return res.status(400).json({ success: false, message: "테이블명이 필요합니다.", error: "INVALID_TABLE_NAME", }); } if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { return res.status(400).json({ success: false, message: "유효하지 않은 테이블명입니다.", error: "INVALID_TABLE_NAME", }); } console.log(`✏️ 레코드 수정: ${tableName}/${id}`, data); // 레코드 수정 const result = await dataService.updateRecord(tableName, id, data); if (!result.success) { return res.status(400).json(result); } console.log(`✅ 레코드 수정 성공: ${tableName}/${id}`); return res.json({ success: true, data: result.data, message: "레코드가 수정되었습니다.", }); } catch (error) { console.error("레코드 수정 오류:", error); return res.status(500).json({ success: false, message: "레코드 수정 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } ); /** * 레코드 삭제 API * DELETE /api/data/{tableName}/{id} */ /** * 복합키 레코드 삭제 API (POST) * POST /api/data/:tableName/delete * Body: { user_id: 'xxx', dept_code: 'yyy' } */ router.post( "/:tableName/delete", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { tableName } = req.params; const compositeKey = req.body; // 입력값 검증 if (!tableName || typeof tableName !== "string") { return res.status(400).json({ success: false, message: "테이블명이 필요합니다.", error: "INVALID_TABLE_NAME", }); } if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { return res.status(400).json({ success: false, message: "유효하지 않은 테이블명입니다.", error: "INVALID_TABLE_NAME", }); } console.log(`🗑️ 복합키 레코드 삭제: ${tableName}`, compositeKey); // 레코드 삭제 (복합키 객체 전달) const result = await dataService.deleteRecord(tableName, compositeKey); if (!result.success) { return res.status(400).json(result); } console.log(`✅ 레코드 삭제 성공: ${tableName}`); return res.json(result); } catch (error: any) { console.error(`레코드 삭제 오류 (${req.params.tableName}):`, error); return res.status(500).json({ success: false, message: "레코드 삭제 중 오류가 발생했습니다.", error: error.message, }); } } ); /** * 그룹 삭제 API * POST /api/data/:tableName/delete-group */ router.post( "/:tableName/delete-group", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { tableName } = req.params; const filterConditions = req.body; const userCompany = req.user?.companyCode; if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { return res.status(400).json({ success: false, message: "유효하지 않은 테이블명입니다.", }); } console.log(`🗑️ 그룹 삭제:`, { tableName, filterConditions, userCompany }); const result = await dataService.deleteGroupRecords( tableName, filterConditions, userCompany // 회사 코드 전달 ); if (!result.success) { return res.status(400).json(result); } console.log(`✅ 그룹 삭제: ${result.data?.deleted}개`); return res.json(result); } catch (error: any) { console.error("그룹 삭제 오류:", error); return res.status(500).json({ success: false, message: "그룹 삭제 실패", error: error.message, }); } } ); router.delete( "/:tableName/:id", authenticateToken, async (req: AuthenticatedRequest, res) => { try { const { tableName, id } = req.params; // 입력값 검증 if (!tableName || typeof tableName !== "string") { return res.status(400).json({ success: false, message: "테이블명이 필요합니다.", error: "INVALID_TABLE_NAME", }); } if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { return res.status(400).json({ success: false, message: "유효하지 않은 테이블명입니다.", error: "INVALID_TABLE_NAME", }); } console.log(`🗑️ 레코드 삭제: ${tableName}/${id}`); // 레코드 삭제 const result = await dataService.deleteRecord(tableName, id); if (!result.success) { return res.status(400).json(result); } console.log(`✅ 레코드 삭제 성공: ${tableName}/${id}`); return res.json({ success: true, message: "레코드가 삭제되었습니다.", }); } catch (error) { console.error("레코드 삭제 오류:", error); return res.status(500).json({ success: false, message: "레코드 삭제 중 오류가 발생했습니다.", error: error instanceof Error ? error.message : "Unknown error", }); } } ); export default router;