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.
This commit is contained in:
Johngreen
2026-02-25 17:56:12 +09:00
parent c997926c2b
commit bcb583e822

View File

@@ -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;