From bcb583e8227fb4571ddd3a060d3fff310e82a80c Mon Sep 17 00:00:00 2001 From: Johngreen Date: Wed, 25 Feb 2026 17:56:12 +0900 Subject: [PATCH] feat: add POST /api/data/equipment_mng/validate bulk validation endpoint Adds equipment ID bulk validation API for collector-config-manager integration. Accepts array of equipment IDs (max 500), returns existing equipment with names. Includes multi-tenancy company_code filtering and parameterized query for SQL injection prevention. --- backend-node/src/routes/dataRoutes.ts | 168 ++++++++++++++++++++------ 1 file changed, 132 insertions(+), 36 deletions(-) diff --git a/backend-node/src/routes/dataRoutes.ts b/backend-node/src/routes/dataRoutes.ts index c4c80e19..f671cafe 100644 --- a/backend-node/src/routes/dataRoutes.ts +++ b/backend-node/src/routes/dataRoutes.ts @@ -3,6 +3,7 @@ import { dataService } from "../services/dataService"; import { masterDetailExcelService } from "../services/masterDetailExcelService"; import { authenticateToken } from "../middleware/authMiddleware"; import { AuthenticatedRequest } from "../types/auth"; +import { getPool } from "../database/db"; const router = express.Router(); @@ -31,7 +32,7 @@ router.get( console.log(`πŸ” λ§ˆμŠ€ν„°-λ””ν…ŒμΌ 관계 쑰회: screenId=${screenId}`); const relation = await masterDetailExcelService.getMasterDetailRelation( - parseInt(screenId) + parseInt(screenId), ); if (!relation) { @@ -60,7 +61,7 @@ router.get( error: error.message, }); } - } + }, ); /** @@ -86,7 +87,7 @@ router.post( // 1. λ§ˆμŠ€ν„°-λ””ν…ŒμΌ 관계 쑰회 const relation = await masterDetailExcelService.getMasterDetailRelation( - parseInt(screenId) + parseInt(screenId), ); if (!relation) { @@ -100,7 +101,7 @@ router.post( const data = await masterDetailExcelService.getJoinedData( relation, companyCode, - filters + filters, ); console.log(`βœ… λ§ˆμŠ€ν„°-λ””ν…ŒμΌ 데이터 쑰회 μ™„λ£Œ: ${data.data.length}ν–‰`); @@ -117,7 +118,7 @@ router.post( error: error.message, }); } - } + }, ); /** @@ -140,11 +141,13 @@ router.post( }); } - console.log(`πŸ“€ λ§ˆμŠ€ν„°-λ””ν…ŒμΌ μ—‘μ…€ μ—…λ‘œλ“œ: screenId=${screenId}, rows=${data.length}`); + console.log( + `πŸ“€ λ§ˆμŠ€ν„°-λ””ν…ŒμΌ μ—‘μ…€ μ—…λ‘œλ“œ: screenId=${screenId}, rows=${data.length}`, + ); // 1. λ§ˆμŠ€ν„°-λ””ν…ŒμΌ 관계 쑰회 const relation = await masterDetailExcelService.getMasterDetailRelation( - parseInt(screenId) + parseInt(screenId), ); if (!relation) { @@ -159,7 +162,7 @@ router.post( relation, data, companyCode, - userId + userId, ); console.log(`βœ… λ§ˆμŠ€ν„°-λ””ν…ŒμΌ μ—…λ‘œλ“œ μ™„λ£Œ:`, { @@ -190,7 +193,7 @@ router.post( error: error.message, }); } - } + }, ); /** @@ -198,7 +201,7 @@ router.post( * - λ§ˆμŠ€ν„° μ •λ³΄λŠ” UIμ—μ„œ 선택 * - λ””ν…ŒμΌ μ •λ³΄λ§Œ μ—‘μ…€μ—μ„œ μ—…λ‘œλ“œ * - μ±„λ²ˆ κ·œμΉ™μ„ 톡해 λ§ˆμŠ€ν„° ν‚€ μžλ™ 생성 - * + * * POST /api/data/master-detail/upload-simple */ router.post( @@ -206,7 +209,14 @@ router.post( authenticateToken, async (req: AuthenticatedRequest, res) => { try { - const { screenId, detailData, masterFieldValues, numberingRuleId, afterUploadFlowId, afterUploadFlows } = req.body; + const { + screenId, + detailData, + masterFieldValues, + numberingRuleId, + afterUploadFlowId, + afterUploadFlows, + } = req.body; const companyCode = req.user?.companyCode || "*"; const userId = req.user?.userId || "system"; @@ -217,10 +227,17 @@ router.post( }); } - console.log(`πŸ“€ λ§ˆμŠ€ν„°-λ””ν…ŒμΌ 간단 λͺ¨λ“œ μ—…λ‘œλ“œ: screenId=${screenId}, rows=${detailData.length}`); + console.log( + `πŸ“€ λ§ˆμŠ€ν„°-λ””ν…ŒμΌ 간단 λͺ¨λ“œ μ—…λ‘œλ“œ: screenId=${screenId}, rows=${detailData.length}`, + ); console.log(` λ§ˆμŠ€ν„° ν•„λ“œ κ°’:`, masterFieldValues); console.log(` μ±„λ²ˆ κ·œμΉ™ ID:`, numberingRuleId); - console.log(` μ—…λ‘œλ“œ ν›„ μ œμ–΄:`, afterUploadFlows?.length > 0 ? `${afterUploadFlows.length}개` : afterUploadFlowId || "μ—†μŒ"); + console.log( + ` μ—…λ‘œλ“œ ν›„ μ œμ–΄:`, + afterUploadFlows?.length > 0 + ? `${afterUploadFlows.length}개` + : afterUploadFlowId || "μ—†μŒ", + ); // μ—…λ‘œλ“œ μ‹€ν–‰ const result = await masterDetailExcelService.uploadSimple( @@ -231,7 +248,7 @@ router.post( companyCode, userId, afterUploadFlowId, // μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰ (단일, ν•˜μœ„ ν˜Έν™˜μ„±) - afterUploadFlows // μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰ (닀쀑) + afterUploadFlows, // μ—…λ‘œλ“œ ν›„ μ œμ–΄ μ‹€ν–‰ (닀쀑) ); console.log(`βœ… λ§ˆμŠ€ν„°-λ””ν…ŒμΌ 간단 λͺ¨λ“œ μ—…λ‘œλ“œ μ™„λ£Œ:`, { @@ -256,7 +273,7 @@ router.post( error: error.message, }); } - } + }, ); // ================================ @@ -389,7 +406,7 @@ router.get( parsedDataFilter, enableEntityJoinFlag, parsedDisplayColumns, // πŸ†• ν‘œμ‹œ 컬럼 전달 - parsedDeduplication // πŸ†• 쀑볡 제거 μ„€μ • 전달 + parsedDeduplication, // πŸ†• 쀑볡 제거 μ„€μ • 전달 ); if (!result.success) { @@ -397,7 +414,7 @@ router.get( } console.log( - `βœ… 쑰인 데이터 쑰회 성곡: ${result.data?.length || 0}개 ν•­λͺ©` + `βœ… 쑰인 데이터 쑰회 성곡: ${result.data?.length || 0}개 ν•­λͺ©`, ); return res.json({ @@ -412,7 +429,7 @@ router.get( error: error instanceof Error ? error.message : "Unknown error", }); } - } + }, ); /** @@ -501,7 +518,7 @@ router.get( } console.log( - `βœ… 데이터 쑰회 성곡: ${tableName}, ${result.data?.length || 0}개 ν•­λͺ©` + `βœ… 데이터 쑰회 성곡: ${tableName}, ${result.data?.length || 0}개 ν•­λͺ©`, ); // νŽ˜μ΄μ§• 정보 ν¬ν•¨ν•˜μ—¬ λ°˜ν™˜ @@ -527,7 +544,7 @@ router.get( error: error instanceof Error ? error.message : "Unknown error", }); } - } + }, ); /** @@ -569,7 +586,7 @@ router.get( } console.log( - `βœ… 컬럼 정보 쑰회 성곡: ${tableName}, ${result.data?.length || 0}개 컬럼` + `βœ… 컬럼 정보 쑰회 성곡: ${tableName}, ${result.data?.length || 0}개 컬럼`, ); return res.json(result); @@ -581,7 +598,7 @@ router.get( error: error instanceof Error ? error.message : "Unknown error", }); } - } + }, ); /** @@ -633,7 +650,8 @@ router.get( } // πŸ†• primaryKeyColumn νŒŒμ‹± - const primaryKeyColumnStr = typeof primaryKeyColumn === "string" ? primaryKeyColumn : undefined; + const primaryKeyColumnStr = + typeof primaryKeyColumn === "string" ? primaryKeyColumn : undefined; console.log(`πŸ” λ ˆμ½”λ“œ 상세 쑰회: ${tableName}/${id}`, { enableEntityJoin: enableEntityJoinFlag, @@ -647,7 +665,7 @@ router.get( id, enableEntityJoinFlag, groupByColumnsArray, - primaryKeyColumnStr // πŸ†• Primary Key 컬럼λͺ… 전달 + primaryKeyColumnStr, // πŸ†• Primary Key 컬럼λͺ… 전달 ); if (!result.success) { @@ -675,7 +693,7 @@ router.get( error: error instanceof Error ? error.message : "Unknown error", }); } - } + }, ); /** @@ -729,7 +747,7 @@ router.post( records, req.user?.companyCode, req.user?.userId, - deleteOrphans + deleteOrphans, ); if (!result.success) { @@ -758,7 +776,81 @@ router.post( error: error instanceof Error ? error.message : "Unknown error", }); } - } + }, +); + +/** + * μ„€λΉ„ ID 일괄 검증 API + * POST /api/data/equipment_mng/validate + * + * μš”μ²­: { "equipmentIds": ["EQ_001", "EQ_002", "EQ_003"] } + * 응닡: { "success": true, "data": [{ "equipment_id": "EQ_001", "equipment_name": "ν”„λ ˆμŠ€ 1호기" }, ...] } + * + * - μ‘΄μž¬ν•˜λŠ” μ„€λΉ„λ§Œ λ°˜ν™˜ (μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” IDλŠ” μ‘λ‹΅μ—μ„œ μ œμ™Έ) + * - λ©€ν‹°ν…Œλ„Œμ‹œ: company_code 필터링 적용 + */ +router.post( + "/equipment_mng/validate", + authenticateToken, + async (req: AuthenticatedRequest, res) => { + try { + const { equipmentIds } = req.body; + + if (!Array.isArray(equipmentIds) || equipmentIds.length === 0) { + return res.status(400).json({ + success: false, + message: "equipmentIds 배열이 ν•„μš”ν•©λ‹ˆλ‹€.", + }); + } + + // μ΅œλŒ€ 500개 μ œν•œ + if (equipmentIds.length > 500) { + return res.status(400).json({ + success: false, + message: "ν•œ λ²ˆμ— μ΅œλŒ€ 500κ°œκΉŒμ§€ 검증할 수 μžˆμŠ΅λ‹ˆλ‹€.", + }); + } + + const companyCode = req.user?.companyCode; + const pool = getPool(); + + // Parameterized query둜 SQL Injection λ°©μ§€ + const placeholders = equipmentIds.map((_, i) => `$${i + 1}`).join(", "); + const params: any[] = [...equipmentIds]; + + let whereClause = `WHERE equipment_id IN (${placeholders})`; + + // λ©€ν‹°ν…Œλ„Œμ‹œ 필터링 (company_codeκ°€ '*'이 μ•„λ‹Œ 경우) + if (companyCode && companyCode !== "*") { + params.push(companyCode); + whereClause += ` AND company_code = $${params.length}`; + } + + const query = ` + SELECT equipment_id, equipment_name + FROM equipment_mng + ${whereClause} + `; + + const result = await pool.query(query, params); + + console.log( + `βœ… μ„€λΉ„ 일괄 검증: ${result.rowCount}/${equipmentIds.length}개 확인`, + ); + + return res.json({ + success: true, + data: result.rows, + }); + } catch (error: any) { + console.error("μ„€λΉ„ 일괄 검증 였λ₯˜:", error); + return res.status(500).json({ + success: false, + message: "μ„€λΉ„ 검증 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.", + error: error.message, + }); + } + }, ); /** @@ -798,7 +890,7 @@ router.post( // ν…Œμ΄λΈ”μ— company_code 컬럼이 μžˆλŠ”μ§€ ν™•μΈν•˜κ³  μžλ™μœΌλ‘œ μΆ”κ°€ const hasCompanyCode = await dataService.checkColumnExists( tableName, - "company_code" + "company_code", ); if (hasCompanyCode && req.user?.companyCode) { enrichedData.company_code = req.user.companyCode; @@ -808,7 +900,7 @@ router.post( // ν…Œμ΄λΈ”μ— company_name 컬럼이 μžˆλŠ”μ§€ ν™•μΈν•˜κ³  μžλ™μœΌλ‘œ μΆ”κ°€ const hasCompanyName = await dataService.checkColumnExists( tableName, - "company_name" + "company_name", ); if (hasCompanyName && req.user?.companyName) { enrichedData.company_name = req.user.companyName; @@ -837,7 +929,7 @@ router.post( error: error instanceof Error ? error.message : "Unknown error", }); } - } + }, ); /** @@ -893,7 +985,7 @@ router.put( error: error instanceof Error ? error.message : "Unknown error", }); } - } + }, ); /** @@ -949,7 +1041,7 @@ router.post( error: error.message, }); } - } + }, ); /** @@ -972,12 +1064,16 @@ router.post( }); } - console.log(`πŸ—‘οΈ κ·Έλ£Ή μ‚­μ œ:`, { tableName, filterConditions, userCompany }); + console.log(`πŸ—‘οΈ κ·Έλ£Ή μ‚­μ œ:`, { + tableName, + filterConditions, + userCompany, + }); const result = await dataService.deleteGroupRecords( tableName, filterConditions, - userCompany // νšŒμ‚¬ μ½”λ“œ 전달 + userCompany, // νšŒμ‚¬ μ½”λ“œ 전달 ); if (!result.success) { @@ -994,7 +1090,7 @@ router.post( error: error.message, }); } - } + }, ); router.delete( @@ -1044,7 +1140,7 @@ router.delete( error: error instanceof Error ? error.message : "Unknown error", }); } - } + }, ); export default router;