Refactor analytics report data retrieval and enhance equipment report logic
- Updated the `getQualityReportData` function to improve item name retrieval logic by using `NULLIF` for better handling of empty values. - Refactored the `getEquipmentReportData` function to include company and date filters for equipment statistics, ensuring accurate data aggregation. - Enhanced the SQL queries for both quality and equipment reports to utilize lateral joins and improve performance. - Improved loading states in frontend components for inspection records and inspection results across multiple companies. This refactor enhances data accuracy and user experience in the analytics module.
This commit is contained in:
@@ -378,7 +378,7 @@ export async function getQualityReportData(req: any, res: Response): Promise<voi
|
||||
const dataQuery = `
|
||||
SELECT
|
||||
COALESCE(pr.production_date, pr.created_date::date::text) as date,
|
||||
COALESCE(ii.item_name, wi.item_id, '미지정') as item,
|
||||
COALESCE(NULLIF(ii.item_name, ''), NULLIF(ii.item_number, ''), '미지정') as item,
|
||||
'일반검사' as "defectType",
|
||||
COALESCE(wi.routing, '미지정') as process,
|
||||
COALESCE(pr.worker_name, '미지정') as inspector,
|
||||
@@ -392,11 +392,30 @@ export async function getQualityReportData(req: any, res: Response): Promise<voi
|
||||
pr.company_code
|
||||
FROM production_record pr
|
||||
LEFT JOIN work_instruction wi ON pr.wo_id = wi.id AND pr.company_code = wi.company_code
|
||||
LEFT JOIN (
|
||||
SELECT DISTINCT ON (item_number, company_code)
|
||||
item_number, item_name, company_code
|
||||
FROM item_info ORDER BY item_number, company_code, created_date DESC
|
||||
) ii ON wi.item_id = ii.item_number AND wi.company_code = ii.company_code
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT ii_inner.item_number, ii_inner.item_name
|
||||
FROM item_info ii_inner
|
||||
WHERE ii_inner.company_code = wi.company_code
|
||||
AND (
|
||||
(NULLIF(wi.item_id, '') IS NOT NULL
|
||||
AND (ii_inner.id = wi.item_id OR ii_inner.item_number = wi.item_id))
|
||||
OR ii_inner.item_number = (
|
||||
SELECT wid.item_number
|
||||
FROM work_instruction_detail wid
|
||||
WHERE wid.work_instruction_id = wi.id
|
||||
AND wid.company_code = wi.company_code
|
||||
AND NULLIF(wid.item_number, '') IS NOT NULL
|
||||
ORDER BY wid.created_date ASC
|
||||
LIMIT 1
|
||||
)
|
||||
)
|
||||
ORDER BY
|
||||
CASE WHEN ii_inner.id = wi.item_id THEN 1
|
||||
WHEN ii_inner.item_number = wi.item_id THEN 2
|
||||
ELSE 3 END,
|
||||
ii_inner.created_date DESC
|
||||
LIMIT 1
|
||||
) ii ON true
|
||||
${whereClause}
|
||||
ORDER BY date DESC NULLS LAST
|
||||
`;
|
||||
@@ -436,44 +455,165 @@ export async function getEquipmentReportData(req: any, res: Response): Promise<v
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) { res.status(401).json({ success: false, message: "인증 정보가 없습니다" }); return; }
|
||||
|
||||
const conditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let idx = 1;
|
||||
const { startDate, endDate } = req.query;
|
||||
|
||||
const cf = buildCompanyFilter(companyCode, "ei", idx);
|
||||
if (cf.condition) { conditions.push(cf.condition); params.push(...cf.params); idx = cf.nextIdx; }
|
||||
// 회사 필터
|
||||
const equipParams: any[] = [];
|
||||
let equipWhere = "";
|
||||
if (companyCode !== "*") {
|
||||
equipParams.push(companyCode);
|
||||
equipWhere = `WHERE ei.company_code = $1`;
|
||||
}
|
||||
|
||||
const whereClause = buildWhereClause(conditions);
|
||||
// wopr(가동 실적) 집계: 회사 + 날짜 필터
|
||||
const woprParams: any[] = [];
|
||||
const woprConds: string[] = [
|
||||
"NULLIF(wopr.started_at, '') IS NOT NULL",
|
||||
"NULLIF(wopr.equipment_code, '') IS NOT NULL",
|
||||
];
|
||||
if (companyCode !== "*") {
|
||||
woprParams.push(companyCode);
|
||||
woprConds.push(`wopr.company_code = $${woprParams.length}`);
|
||||
}
|
||||
if (startDate) {
|
||||
woprParams.push(startDate);
|
||||
woprConds.push(`wopr.started_at::date >= $${woprParams.length}::date`);
|
||||
}
|
||||
if (endDate) {
|
||||
woprParams.push(endDate);
|
||||
woprConds.push(`wopr.started_at::date <= $${woprParams.length}::date`);
|
||||
}
|
||||
const woprWhere = woprConds.join(" AND ");
|
||||
|
||||
const dataQuery = `
|
||||
// 1-A) wopr(가동시간) 집계 — equipment_code 또는 em.id 매칭
|
||||
const woprStatsQuery = `
|
||||
SELECT
|
||||
COALESCE(ei.updated_date, ei.created_date)::date::text as date,
|
||||
ei.equipment_code,
|
||||
COALESCE(ei.equipment_name, ei.equipment_code) as equipment,
|
||||
COALESCE(ei.equipment_type, '미지정') as "equipType",
|
||||
COALESCE(ei.location, '미지정') as line,
|
||||
COALESCE(ui.user_name, ei.manager_id, '미지정') as manager,
|
||||
ei.status,
|
||||
CAST(COALESCE(NULLIF(ei.capacity_per_day::text, ''), '0') AS numeric) as "runTime",
|
||||
0 as "downTime",
|
||||
100 as "opRate",
|
||||
0 as "faultCnt",
|
||||
0 as "mtbf",
|
||||
0 as "mttr",
|
||||
0 as "maintCost",
|
||||
CAST(COALESCE(NULLIF(ei.capacity_per_day::text, ''), '0') AS numeric) as "prodQty",
|
||||
ei.company_code
|
||||
FROM equipment_info ei
|
||||
LEFT JOIN (
|
||||
SELECT DISTINCT ON (user_id) user_id, user_name FROM user_info
|
||||
) ui ON ei.manager_id = ui.user_id
|
||||
${whereClause}
|
||||
ORDER BY equipment ASC
|
||||
em.equipment_code,
|
||||
em.company_code,
|
||||
SUM(GREATEST(
|
||||
CASE
|
||||
WHEN NULLIF(wopr.completed_at, '') IS NOT NULL
|
||||
AND NULLIF(wopr.started_at, '') IS NOT NULL
|
||||
THEN EXTRACT(EPOCH FROM (wopr.completed_at::timestamp - wopr.started_at::timestamp)) / 3600.0
|
||||
ELSE 0
|
||||
END, 0)) AS run_hours,
|
||||
SUM(CAST(COALESCE(NULLIF(wopr.total_paused_time, ''), '0') AS numeric)) / 3600.0 AS down_hours,
|
||||
COUNT(*) FILTER (
|
||||
WHERE wopr.status IN ('fault','error','breakdown','수리','고장')
|
||||
) AS fault_cnt,
|
||||
MAX(wopr.started_at) AS last_started
|
||||
FROM equipment_mng em
|
||||
JOIN work_order_process_result wopr
|
||||
ON wopr.company_code = em.company_code
|
||||
AND (wopr.equipment_code = em.equipment_code OR wopr.equipment_code = em.id::text)
|
||||
WHERE ${woprWhere}
|
||||
${companyCode !== "*" ? `AND em.company_code = $1` : ""}
|
||||
GROUP BY em.equipment_code, em.company_code
|
||||
`;
|
||||
const woprStatsRows = await query(woprStatsQuery, woprParams);
|
||||
|
||||
const dataRows = await query(dataQuery, params);
|
||||
// 1-B) production_record(생산량/불량) 집계 — wo_id → wi.equipment_id → em
|
||||
// pr.production_date(date string) 기준 날짜 필터
|
||||
const prParams: any[] = [];
|
||||
const prConds: string[] = [];
|
||||
if (companyCode !== "*") {
|
||||
prParams.push(companyCode);
|
||||
prConds.push(`pr.company_code = $${prParams.length}`);
|
||||
}
|
||||
if (startDate) {
|
||||
prParams.push(startDate);
|
||||
prConds.push(`COALESCE(pr.production_date, pr.created_date::date::text) >= $${prParams.length}`);
|
||||
}
|
||||
if (endDate) {
|
||||
prParams.push(endDate);
|
||||
prConds.push(`COALESCE(pr.production_date, pr.created_date::date::text) <= $${prParams.length}`);
|
||||
}
|
||||
const prWhere = prConds.length > 0 ? `WHERE ${prConds.join(" AND ")}` : "";
|
||||
const prStatsQuery = `
|
||||
SELECT
|
||||
em.equipment_code,
|
||||
em.company_code,
|
||||
SUM(CAST(COALESCE(NULLIF(pr.production_qty, ''), '0') AS numeric)) AS prod_qty,
|
||||
SUM(CAST(COALESCE(NULLIF(pr.defect_qty, ''), '0') AS numeric)) AS defect_qty,
|
||||
MAX(COALESCE(pr.production_date, pr.created_date::date::text)) AS last_prod_date
|
||||
FROM equipment_mng em
|
||||
JOIN work_instruction wi
|
||||
ON wi.company_code = em.company_code
|
||||
AND (wi.equipment_id = em.id::text OR wi.equipment_id = em.equipment_code)
|
||||
JOIN production_record pr
|
||||
ON pr.wo_id = wi.id AND pr.company_code = wi.company_code
|
||||
${prWhere}
|
||||
GROUP BY em.equipment_code, em.company_code
|
||||
`;
|
||||
const prStatsRows = await query(prStatsQuery, prParams);
|
||||
|
||||
logger.info("설비 리포트 데이터 조회", { companyCode, rowCount: dataRows.length });
|
||||
// 두 집계를 회사+설비코드 키로 병합
|
||||
const statsByCode = new Map<string, any>();
|
||||
for (const r of woprStatsRows) {
|
||||
const k = `${r.company_code}::${r.equipment_code}`;
|
||||
statsByCode.set(k, { ...(statsByCode.get(k) || {}), ...r });
|
||||
}
|
||||
for (const r of prStatsRows) {
|
||||
const k = `${r.company_code}::${r.equipment_code}`;
|
||||
statsByCode.set(k, { ...(statsByCode.get(k) || {}), ...r });
|
||||
}
|
||||
|
||||
// 2) 설비 마스터 조회 (전체) + 집계 매핑
|
||||
const equipQuery = `
|
||||
SELECT
|
||||
ei.id::text AS em_id,
|
||||
ei.equipment_code,
|
||||
ei.equipment_name,
|
||||
ei.equipment_type,
|
||||
ei.installation_location,
|
||||
ei.manufacturer,
|
||||
ei.operation_status,
|
||||
ei.created_date,
|
||||
ei.updated_date,
|
||||
ei.company_code
|
||||
FROM equipment_mng ei
|
||||
${equipWhere}
|
||||
ORDER BY ei.equipment_name ASC, ei.equipment_code ASC
|
||||
`;
|
||||
const equipRows = await query(equipQuery, equipParams);
|
||||
|
||||
const dataRows = equipRows.map((e: any) => {
|
||||
const s = statsByCode.get(`${e.company_code}::${e.equipment_code}`);
|
||||
const runHours = Number(s?.run_hours ?? 0);
|
||||
const downHours = Number(s?.down_hours ?? 0);
|
||||
const totalHours = runHours + downHours;
|
||||
const opRate = totalHours > 0
|
||||
? Math.round((runHours / totalHours) * 1000) / 10
|
||||
: 0;
|
||||
const lastDate = (s?.last_prod_date || s?.last_started || e.updated_date || e.created_date || "")
|
||||
.toString().slice(0, 10);
|
||||
return {
|
||||
date: lastDate,
|
||||
equipment_code: e.equipment_code,
|
||||
equipment: (e.equipment_name && e.equipment_name.trim()) || e.equipment_code || "미지정",
|
||||
equipType: (e.equipment_type && e.equipment_type.trim()) || "미지정",
|
||||
line: (e.installation_location && e.installation_location.trim()) || "미지정",
|
||||
manager: (e.manufacturer && e.manufacturer.trim()) || "미지정",
|
||||
status: (e.operation_status && e.operation_status.trim()) || "미지정",
|
||||
runTime: Math.round(runHours * 10) / 10,
|
||||
downTime: Math.round(downHours * 10) / 10,
|
||||
opRate,
|
||||
faultCnt: Number(s?.fault_cnt ?? 0),
|
||||
mtbf: 0,
|
||||
mttr: 0,
|
||||
maintCost: 0,
|
||||
prodQty: Number(s?.prod_qty ?? 0),
|
||||
defectQty: Number(s?.defect_qty ?? 0),
|
||||
company_code: e.company_code,
|
||||
};
|
||||
});
|
||||
|
||||
logger.info("설비 리포트 데이터 조회", {
|
||||
companyCode,
|
||||
rowCount: dataRows.length,
|
||||
woprStatsCount: woprStatsRows.length,
|
||||
prStatsCount: prStatsRows.length,
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
@@ -488,9 +628,11 @@ export async function getEquipmentReportData(req: any, res: Response): Promise<v
|
||||
totalCount: dataRows.length,
|
||||
},
|
||||
});
|
||||
return;
|
||||
} catch (error: any) {
|
||||
logger.error("설비 리포트 조회 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: "설비 리포트 데이터 조회에 실패했습니다", error: error.message });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user