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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user