117 lines
4.1 KiB
TypeScript
117 lines
4.1 KiB
TypeScript
|
|
/**
|
||
|
|
* 품질 모니터링 데이터 조회 (서버 페이징 + 통계 합산)
|
||
|
|
* work_order_process(공정 메타) + work_order_process_result(실적) JOIN
|
||
|
|
* - 페이지: 화면 표 표시용
|
||
|
|
* - summary: KPI 카드용 (전체 합산)
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { Response } from "express";
|
||
|
|
import { AuthenticatedRequest } from "../types/auth";
|
||
|
|
import { getPool } from "../database/db";
|
||
|
|
import { logger } from "../utils/logger";
|
||
|
|
|
||
|
|
export async function getQualityMonitoringData(req: AuthenticatedRequest, res: Response) {
|
||
|
|
try {
|
||
|
|
const companyCode = req.user!.companyCode;
|
||
|
|
const from = (req.query.from as string | undefined)?.trim() || "";
|
||
|
|
const to = (req.query.to as string | undefined)?.trim() || "";
|
||
|
|
const page = Math.max(1, parseInt(String(req.query.page ?? "1"), 10) || 1);
|
||
|
|
const size = Math.max(1, Math.min(500, parseInt(String(req.query.size ?? "50"), 10) || 50));
|
||
|
|
const offset = (page - 1) * size;
|
||
|
|
|
||
|
|
const params: any[] = [companyCode];
|
||
|
|
const conds: string[] = ["wopr.company_code = $1"];
|
||
|
|
if (from) {
|
||
|
|
params.push(`${from} 00:00:00`);
|
||
|
|
conds.push(`wopr.created_date >= $${params.length}::timestamp`);
|
||
|
|
}
|
||
|
|
if (to) {
|
||
|
|
params.push(`${to} 23:59:59`);
|
||
|
|
conds.push(`wopr.created_date <= $${params.length}::timestamp`);
|
||
|
|
}
|
||
|
|
const whereClause = `WHERE ${conds.join(" AND ")}`;
|
||
|
|
|
||
|
|
const pool = getPool();
|
||
|
|
|
||
|
|
// 1) total + summary (KPI 카드)
|
||
|
|
const summaryQuery = `
|
||
|
|
SELECT
|
||
|
|
COUNT(*)::int AS total,
|
||
|
|
SUM(CASE WHEN wopr.status = 'completed' AND COALESCE(CAST(NULLIF(wopr.defect_qty, '') AS numeric), 0) = 0 THEN 1 ELSE 0 END)::int AS passed,
|
||
|
|
SUM(CASE WHEN wopr.status = 'completed' AND COALESCE(CAST(NULLIF(wopr.defect_qty, '') AS numeric), 0) > 0 THEN 1 ELSE 0 END)::int AS failed,
|
||
|
|
SUM(CASE WHEN wopr.status <> 'completed' OR wopr.status IS NULL THEN 1 ELSE 0 END)::int AS pending
|
||
|
|
FROM work_order_process_result wopr
|
||
|
|
${whereClause}
|
||
|
|
`;
|
||
|
|
const summaryRes = await pool.query(summaryQuery, params);
|
||
|
|
const summaryRow = summaryRes.rows[0] || { total: 0, passed: 0, failed: 0, pending: 0 };
|
||
|
|
const total = summaryRow.total || 0;
|
||
|
|
const passRate = total > 0 ? Math.round((summaryRow.passed / total) * 1000) / 10 : 0;
|
||
|
|
|
||
|
|
// 2) 페이지 데이터
|
||
|
|
const pageParams = [...params, size, offset];
|
||
|
|
const dataQuery = `
|
||
|
|
SELECT
|
||
|
|
wopr.id,
|
||
|
|
wopr.wop_id,
|
||
|
|
wopr.status,
|
||
|
|
wopr.input_qty,
|
||
|
|
wopr.good_qty,
|
||
|
|
wopr.defect_qty,
|
||
|
|
wopr.started_at,
|
||
|
|
wopr.completed_at,
|
||
|
|
wopr.completed_by,
|
||
|
|
wopr.accepted_by,
|
||
|
|
wop.wo_id,
|
||
|
|
wop.process_code,
|
||
|
|
wop.process_name,
|
||
|
|
wop.plan_qty
|
||
|
|
FROM work_order_process_result wopr
|
||
|
|
LEFT JOIN work_order_process wop
|
||
|
|
ON wop.id = wopr.wop_id AND wop.company_code = wopr.company_code
|
||
|
|
${whereClause}
|
||
|
|
ORDER BY wopr.created_date DESC
|
||
|
|
LIMIT $${params.length + 1} OFFSET $${params.length + 2}
|
||
|
|
`;
|
||
|
|
const dataRes = await pool.query(dataQuery, pageParams);
|
||
|
|
|
||
|
|
const rows = dataRes.rows.map((r: any) => ({
|
||
|
|
id: r.id,
|
||
|
|
wo_id: r.wo_id,
|
||
|
|
process_code: r.process_code || "",
|
||
|
|
process_name: r.process_name || "",
|
||
|
|
status: r.status || "",
|
||
|
|
plan_qty: Number(r.plan_qty) || 0,
|
||
|
|
input_qty: Number(r.input_qty) || 0,
|
||
|
|
good_qty: Number(r.good_qty) || 0,
|
||
|
|
defect_qty: Number(r.defect_qty) || 0,
|
||
|
|
started_at: r.started_at || null,
|
||
|
|
completed_at: r.completed_at || null,
|
||
|
|
worker_name: r.completed_by || r.accepted_by || "",
|
||
|
|
}));
|
||
|
|
|
||
|
|
logger.info("품질 모니터링 조회", {
|
||
|
|
companyCode, from, to, page, size, total,
|
||
|
|
});
|
||
|
|
|
||
|
|
return res.json({
|
||
|
|
success: true,
|
||
|
|
rows,
|
||
|
|
total,
|
||
|
|
page,
|
||
|
|
size,
|
||
|
|
totalPages: Math.max(1, Math.ceil(total / size)),
|
||
|
|
summary: {
|
||
|
|
total,
|
||
|
|
passed: summaryRow.passed || 0,
|
||
|
|
failed: summaryRow.failed || 0,
|
||
|
|
pending: summaryRow.pending || 0,
|
||
|
|
passRate,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
} catch (error: any) {
|
||
|
|
logger.error("품질 모니터링 조회 실패", { error: error.message });
|
||
|
|
return res.status(500).json({ success: false, message: error.message });
|
||
|
|
}
|
||
|
|
}
|