Files
vexplor_dev/backend-node/src/controllers/analyticsReportController.ts
kjs d549947fac Refactor analytics report and production plan services for improved data handling
- Updated the `getQualityReportData` function to utilize `inspection_result_mng` for quality report generation, enhancing data accuracy by aggregating inspection results.
- Refined date handling in the `getOrderSummary` function to improve filtering logic and ensure accurate stock calculations.
- Implemented virtual scrolling in the `TimelineScheduler` component to optimize performance when rendering large datasets.

These changes enhance data retrieval efficiency and user experience across analytics and production planning modules.
2026-04-29 21:05:38 +09:00

682 lines
28 KiB
TypeScript

import { Response } from "express";
import { query } from "../database/db";
import { logger } from "../utils/logger";
function buildCompanyFilter(companyCode: string, alias: string, paramIdx: number) {
if (companyCode === "*") return { condition: "", params: [] as any[], nextIdx: paramIdx };
return {
condition: `${alias}.company_code = $${paramIdx}`,
params: [companyCode],
nextIdx: paramIdx + 1,
};
}
function buildDateFilter(startDate: string | undefined, endDate: string | undefined, dateExpr: string, paramIdx: number) {
const conditions: string[] = [];
const params: any[] = [];
let idx = paramIdx;
if (startDate) {
conditions.push(`${dateExpr} >= $${idx}`);
params.push(startDate);
idx++;
}
if (endDate) {
conditions.push(`${dateExpr} <= $${idx}`);
params.push(endDate);
idx++;
}
return { conditions, params, nextIdx: idx };
}
function buildWhereClause(conditions: string[]): string {
return conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
}
function extractFilterSet(rows: any[], field: string, labelField?: string): { value: string; label: string }[] {
const set = new Map<string, string>();
rows.forEach((r: any) => {
const val = r[field];
if (val && val !== "미지정") set.set(val, r[labelField || field] || val);
});
return [...set.entries()].map(([value, label]) => ({ value, label }));
}
// ============================================
// 생산 리포트
// ============================================
export async function getProductionReportData(req: any, res: Response): Promise<void> {
try {
const companyCode = req.user?.companyCode;
if (!companyCode) { res.status(401).json({ success: false, message: "인증 정보가 없습니다" }); return; }
const { startDate, endDate } = req.query;
const conditions: string[] = [];
const params: any[] = [];
let idx = 1;
const cf = buildCompanyFilter(companyCode, "wop", idx);
if (cf.condition) { conditions.push(cf.condition); params.push(...cf.params); idx = cf.nextIdx; }
const dateExpr = "COALESCE(NULLIF(wopr.started_at, ''), wop.created_date::date::text)";
const df = buildDateFilter(startDate, endDate, dateExpr, idx);
conditions.push(...df.conditions); params.push(...df.params); idx = df.nextIdx;
const whereClause = buildWhereClause(conditions);
// 공정 메타(process_code/name/plan_qty)는 work_order_process,
// 실적(started_at/completed_at/good_qty/defect_qty/equipment_code 등)은 work_order_process_result에 있음
const dataQuery = `
SELECT
COALESCE(NULLIF(wopr.started_at, ''), wop.created_date::date::text) as date,
COALESCE(NULLIF(wop.process_name, ''), NULLIF(wop.process_code, ''), '미지정') as process,
COALESCE(NULLIF(em.equipment_name, ''), NULLIF(em.equipment_code, ''), '미지정') as equipment,
COALESCE(NULLIF(ii.item_name, ''), NULLIF(ii.item_number, ''), '미지정') as item,
COALESCE(NULLIF(wi.worker, ''), '미지정') as worker,
CAST(COALESCE(NULLIF(wop.plan_qty, ''), '0') AS numeric) as "planQty",
CAST(COALESCE(NULLIF(wopr.good_qty, ''), '0') AS numeric) as "prodQty",
CAST(COALESCE(NULLIF(wopr.defect_qty, ''), '0') AS numeric) as "defectQty",
CASE
WHEN NULLIF(wopr.started_at, '') IS NOT NULL
AND NULLIF(wopr.completed_at, '') IS NOT NULL
THEN GREATEST(
EXTRACT(EPOCH FROM (wopr.completed_at::timestamp - wopr.started_at::timestamp)) / 3600.0,
0
)
ELSE 0
END as "runTime",
CAST(COALESCE(NULLIF(wopr.total_paused_time, ''), '0') AS numeric) / 3600.0 as "downTime",
wopr.status,
wop.company_code
FROM work_order_process wop
LEFT JOIN work_order_process_result wopr
ON wopr.wop_id = wop.id AND wopr.company_code = wop.company_code
LEFT JOIN work_instruction wi
ON wop.wo_id = wi.id AND wop.company_code = wi.company_code
LEFT JOIN LATERAL (
SELECT equipment_code, equipment_name
FROM equipment_mng
WHERE company_code = wi.company_code
AND (id = wi.equipment_id OR equipment_code = wi.equipment_id
OR id = wopr.equipment_code OR equipment_code = wopr.equipment_code)
ORDER BY (id = wi.equipment_id OR id = wopr.equipment_code) DESC, created_date DESC
LIMIT 1
) em ON true
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
`;
const dataRows = await query(dataQuery, params);
logger.info("생산 리포트 데이터 조회", { companyCode, rowCount: dataRows.length });
res.status(200).json({
success: true,
data: {
rows: dataRows,
filterOptions: {
processes: extractFilterSet(dataRows, "process"),
equipment: extractFilterSet(dataRows, "equipment"),
items: extractFilterSet(dataRows, "item"),
workers: extractFilterSet(dataRows, "worker"),
},
totalCount: dataRows.length,
},
});
} catch (error: any) {
logger.error("생산 리포트 조회 실패", { error: error.message });
res.status(500).json({ success: false, message: "생산 리포트 데이터 조회에 실패했습니다", error: error.message });
}
}
// ============================================
// 재고 리포트
// ============================================
export async function getInventoryReportData(req: any, res: Response): Promise<void> {
try {
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 cf = buildCompanyFilter(companyCode, "ist", idx);
if (cf.condition) { conditions.push(cf.condition); params.push(...cf.params); idx = cf.nextIdx; }
const whereClause = buildWhereClause(conditions);
const dataQuery = `
SELECT
COALESCE(ist.updated_date, ist.created_date)::date::text as date,
ist.item_code,
COALESCE(ii.item_name, ist.item_code, '미지정') as item,
COALESCE(wi.warehouse_name, ist.warehouse_code, '미지정') as warehouse,
'일반' as category,
CAST(COALESCE(NULLIF(ist.current_qty::text, ''), '0') AS numeric) as "currentQty",
CAST(COALESCE(NULLIF(ist.safety_qty::text, ''), '0') AS numeric) as "safetyQty",
COALESCE(ih_in.in_qty, 0) as "inQty",
COALESCE(ih_out.out_qty, 0) as "outQty",
0 as "stockValue",
GREATEST(CAST(COALESCE(NULLIF(ist.safety_qty::text, ''), '0') AS numeric)
- CAST(COALESCE(NULLIF(ist.current_qty::text, ''), '0') AS numeric), 0) as "shortageQty",
CASE WHEN CAST(COALESCE(NULLIF(ist.current_qty::text, ''), '0') AS numeric) > 0
AND COALESCE(ih_out.out_qty, 0) > 0
THEN ROUND(COALESCE(ih_out.out_qty, 0)::numeric
/ CAST(COALESCE(NULLIF(ist.current_qty::text, ''), '1') AS numeric), 2)
ELSE 0 END as "turnover",
ist.company_code
FROM inventory_stock ist
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 ist.item_code = ii.item_number AND ist.company_code = ii.company_code
LEFT JOIN warehouse_info wi ON ist.warehouse_code = wi.warehouse_code
AND ist.company_code = wi.company_code
LEFT JOIN (
SELECT item_code, company_code,
SUM(CAST(COALESCE(NULLIF(quantity::text, ''), '0') AS numeric)) as in_qty
FROM inventory_history WHERE transaction_type = 'IN'
GROUP BY item_code, company_code
) ih_in ON ist.item_code = ih_in.item_code AND ist.company_code = ih_in.company_code
LEFT JOIN (
SELECT item_code, company_code,
SUM(CAST(COALESCE(NULLIF(quantity::text, ''), '0') AS numeric)) as out_qty
FROM inventory_history WHERE transaction_type = 'OUT'
GROUP BY item_code, company_code
) ih_out ON ist.item_code = ih_out.item_code AND ist.company_code = ih_out.company_code
${whereClause}
ORDER BY date DESC NULLS LAST
`;
const dataRows = await query(dataQuery, params);
logger.info("재고 리포트 데이터 조회", { companyCode, rowCount: dataRows.length });
res.status(200).json({
success: true,
data: {
rows: dataRows,
filterOptions: {
items: extractFilterSet(dataRows, "item"),
warehouses: extractFilterSet(dataRows, "warehouse"),
categories: [
{ value: "원자재", label: "원자재" }, { value: "부자재", label: "부자재" },
{ value: "반제품", label: "반제품" }, { value: "완제품", label: "완제품" },
{ value: "일반", label: "일반" },
],
},
totalCount: dataRows.length,
},
});
} catch (error: any) {
logger.error("재고 리포트 조회 실패", { error: error.message });
res.status(500).json({ success: false, message: "재고 리포트 데이터 조회에 실패했습니다", error: error.message });
}
}
// ============================================
// 구매 리포트
// ============================================
export async function getPurchaseReportData(req: any, res: Response): Promise<void> {
try {
const companyCode = req.user?.companyCode;
if (!companyCode) { res.status(401).json({ success: false, message: "인증 정보가 없습니다" }); return; }
const { startDate, endDate } = req.query;
const params: any[] = [];
let idx = 1;
// company_code 필터 파라미터 ($1 또는 없음)
const cf = buildCompanyFilter(companyCode, "po", idx);
let companyConditionDetail = "";
let companyConditionLegacy = "";
if (cf.condition) {
// purchase_detail 쪽: pd.company_code
companyConditionDetail = `pd.company_code = $${idx}`;
// purchase_order_mng 쪽: po.company_code
companyConditionLegacy = `po.company_code = $${idx}`;
// NOT EXISTS 내부에서도 동일 파라미터 재사용
params.push(...cf.params);
idx = cf.nextIdx;
}
// 날짜 필터는 외부 쿼리에서 적용
const outerConditions: string[] = [];
const df = buildDateFilter(startDate, endDate, "date", idx);
outerConditions.push(...df.conditions);
params.push(...df.params);
idx = df.nextIdx;
const outerWhereClause = buildWhereClause(outerConditions);
const dataQuery = `
WITH combined AS (
-- 신규: purchase_detail 기반 (헤더는 purchase_order_mng LEFT JOIN)
SELECT
COALESCE(po.order_date, po.created_date::date::text, pd.created_date::date::text) as date,
COALESCE(po.purchase_no, pd.purchase_no) as purchase_no,
COALESCE(pd.supplier_name, pd.supplier_code, po.supplier_name, po.supplier_code, '미지정') as supplier,
COALESCE(NULLIF(pd.item_name, ''), po.item_name, NULLIF(pd.item_code, ''), po.item_code, '미지정') as item,
COALESCE(NULLIF(pd.item_code, ''), po.item_code) as item_code,
COALESCE(po.manager, '미지정') as manager,
COALESCE(po.status, '') as status,
CAST(COALESCE(NULLIF(pd.order_qty, ''), '0') AS numeric) as "orderQty",
CAST(COALESCE(NULLIF(pd.received_qty, ''), NULLIF(po.received_qty, ''), '0') AS numeric) as "receiveQty",
CAST(COALESCE(NULLIF(pd.unit_price, ''), '0') AS numeric) as "unitPrice",
CAST(COALESCE(NULLIF(pd.order_qty, ''), '0') AS numeric)
* CAST(COALESCE(NULLIF(pd.unit_price, ''), '0') AS numeric) as "orderAmt",
CAST(COALESCE(NULLIF(pd.received_qty, ''), NULLIF(po.received_qty, ''), '0') AS numeric)
* CAST(COALESCE(NULLIF(pd.unit_price, ''), '0') AS numeric) as "receiveAmt",
1 as "orderCnt",
pd.company_code
FROM purchase_detail pd
LEFT JOIN purchase_order_mng po
ON pd.purchase_no = po.purchase_no AND pd.company_code = po.company_code
${companyConditionDetail ? `WHERE ${companyConditionDetail}` : ""}
UNION ALL
-- 레거시: purchase_detail에 없는 purchase_order_mng 데이터
SELECT
COALESCE(po.order_date, po.created_date::date::text) as date,
po.purchase_no,
COALESCE(po.supplier_name, po.supplier_code, '미지정') as supplier,
COALESCE(po.item_name, po.item_code, '미지정') as item,
po.item_code,
COALESCE(po.manager, '미지정') as manager,
po.status,
CAST(COALESCE(NULLIF(po.order_qty, ''), '0') AS numeric) as "orderQty",
CAST(COALESCE(NULLIF(po.received_qty, ''), '0') AS numeric) as "receiveQty",
CAST(COALESCE(NULLIF(po.unit_price, ''), '0') AS numeric) as "unitPrice",
CAST(COALESCE(NULLIF(po.amount, ''), '0') AS numeric) as "orderAmt",
CAST(COALESCE(NULLIF(po.received_qty, ''), '0') AS numeric)
* CAST(COALESCE(NULLIF(po.unit_price, ''), '0') AS numeric) as "receiveAmt",
1 as "orderCnt",
po.company_code
FROM purchase_order_mng po
WHERE ${companyConditionLegacy ? `${companyConditionLegacy} AND ` : ""}NOT EXISTS (
SELECT 1 FROM purchase_detail pd
WHERE pd.purchase_no = po.purchase_no AND pd.company_code = po.company_code
)
)
SELECT * FROM combined
${outerWhereClause}
ORDER BY date DESC NULLS LAST
`;
const dataRows = await query(dataQuery, params);
logger.info("구매 리포트 데이터 조회", { companyCode, rowCount: dataRows.length });
res.status(200).json({
success: true,
data: {
rows: dataRows,
filterOptions: {
suppliers: extractFilterSet(dataRows, "supplier"),
items: extractFilterSet(dataRows, "item"),
managers: extractFilterSet(dataRows, "manager"),
statuses: extractFilterSet(dataRows, "status"),
},
totalCount: dataRows.length,
},
});
} catch (error: any) {
logger.error("구매 리포트 조회 실패", { error: error.message });
res.status(500).json({ success: false, message: "구매 리포트 데이터 조회에 실패했습니다", error: error.message });
}
}
// ============================================
// 품질 리포트
// ============================================
export async function getQualityReportData(req: any, res: Response): Promise<void> {
try {
const companyCode = req.user?.companyCode;
if (!companyCode) { res.status(401).json({ success: false, message: "인증 정보가 없습니다" }); return; }
const { startDate, endDate } = req.query;
const conditions: string[] = [];
const params: any[] = [];
let idx = 1;
const cf = buildCompanyFilter(companyCode, "irm", idx);
if (cf.condition) { conditions.push(cf.condition); params.push(...cf.params); idx = cf.nextIdx; }
const df = buildDateFilter(startDate, endDate, "TO_CHAR(irm.inspection_date, 'YYYY-MM-DD')", idx);
conditions.push(...df.conditions); params.push(...df.params); idx = df.nextIdx;
const whereClause = buildWhereClause(conditions);
// 품질 리포트는 실제 검사 결과(inspection_result_mng) 기반으로 집계.
// 작업/생산실적이 아닌 검사 단위에서 합격/불량 수량을 산출.
const dataQuery = `
SELECT
TO_CHAR(irm.inspection_date, 'YYYY-MM-DD') AS date,
COALESCE(NULLIF(REGEXP_REPLACE(COALESCE(irm.item_name, ''), '^[[:space:]]+|[[:space:]]+$', '', 'g'), ''), NULLIF(irm.item_code, ''), '미지정') AS item,
COALESCE(NULLIF(irm.inspection_type, ''), '일반검사') AS "defectType",
COALESCE(NULLIF(irm.inspection_type, ''), '미지정') AS process,
COALESCE(NULLIF(irm.inspector, ''), '미지정') AS inspector,
COALESCE(irm.total_qty, 0) AS "inspQty",
COALESCE(irm.good_qty, 0) AS "passQty",
COALESCE(irm.bad_qty, 0) AS "defectQty",
0 AS "reworkQty",
0 AS "scrapQty",
0 AS "claimCnt",
irm.company_code
FROM inspection_result_mng irm
${whereClause}
ORDER BY irm.inspection_date DESC NULLS LAST
`;
const dataRows = await query(dataQuery, params);
logger.info("품질 리포트 데이터 조회", { companyCode, rowCount: dataRows.length });
res.status(200).json({
success: true,
data: {
rows: dataRows,
filterOptions: {
items: extractFilterSet(dataRows, "item"),
defectTypes: extractFilterSet(dataRows, "defectType"),
processes: extractFilterSet(dataRows, "process"),
inspectors: extractFilterSet(dataRows, "inspector"),
},
totalCount: dataRows.length,
},
});
} catch (error: any) {
logger.error("품질 리포트 조회 실패", { error: error.message });
res.status(500).json({ success: false, message: "품질 리포트 데이터 조회에 실패했습니다", error: error.message });
}
}
// ============================================
// 설비 리포트
// ============================================
export async function getEquipmentReportData(req: any, res: Response): Promise<void> {
try {
const companyCode = req.user?.companyCode;
if (!companyCode) { res.status(401).json({ success: false, message: "인증 정보가 없습니다" }); return; }
const { startDate, endDate } = req.query;
// 회사 필터
const equipParams: any[] = [];
let equipWhere = "";
if (companyCode !== "*") {
equipParams.push(companyCode);
equipWhere = `WHERE ei.company_code = $1`;
}
// 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 ");
// 1-A) wopr(가동시간) 집계 — equipment_code 또는 em.id 매칭
const woprStatsQuery = `
SELECT
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);
// 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);
// 두 집계를 회사+설비코드 키로 병합
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,
data: {
rows: dataRows,
filterOptions: {
equipment: extractFilterSet(dataRows, "equipment"),
equipTypes: extractFilterSet(dataRows, "equipType"),
lines: extractFilterSet(dataRows, "line"),
managers: extractFilterSet(dataRows, "manager"),
},
totalCount: dataRows.length,
},
});
return;
} catch (error: any) {
logger.error("설비 리포트 조회 실패", { error: error.message });
res.status(500).json({ success: false, message: "설비 리포트 데이터 조회에 실패했습니다", error: error.message });
return;
}
}
// ============================================
// 금형 리포트
// ============================================
export async function getMoldReportData(req: any, res: Response): Promise<void> {
try {
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 cf = buildCompanyFilter(companyCode, "mm", idx);
if (cf.condition) { conditions.push(cf.condition); params.push(...cf.params); idx = cf.nextIdx; }
const whereClause = buildWhereClause(conditions);
const dataQuery = `
SELECT
COALESCE(mm.updated_date, mm.created_date)::date::text as date,
mm.mold_code,
COALESCE(mm.mold_name, mm.mold_code) as mold,
COALESCE(mm.mold_type, mm.category, '미지정') as "moldType",
COALESCE(ii.item_name, '미지정') as item,
COALESCE(mm.manufacturer, '미지정') as maker,
mm.operation_status as status,
CAST(COALESCE(NULLIF(mm.shot_count::text, ''), '0') AS numeric) as "shotCnt",
CAST(COALESCE(NULLIF(mm.warranty_shot_count::text, ''), '0') AS numeric) as "guaranteeShot",
CASE WHEN CAST(COALESCE(NULLIF(mm.warranty_shot_count::text, ''), '0') AS numeric) > 0
THEN ROUND(
CAST(COALESCE(NULLIF(mm.shot_count::text, ''), '0') AS numeric) * 100.0
/ CAST(COALESCE(NULLIF(mm.warranty_shot_count::text, ''), '1') AS numeric), 1)
ELSE 0 END as "lifeRate",
0 as "repairCnt",
0 as "repairCost",
0 as "prodQty",
0 as "defectRate",
CAST(COALESCE(NULLIF(mm.cavity_count::text, ''), '0') AS numeric) as "cavityUse",
mm.company_code
FROM mold_mng mm
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 mm.mold_code = ii.item_number AND mm.company_code = ii.company_code
${whereClause}
ORDER BY mold ASC
`;
const dataRows = await query(dataQuery, params);
logger.info("금형 리포트 데이터 조회", { companyCode, rowCount: dataRows.length });
res.status(200).json({
success: true,
data: {
rows: dataRows,
filterOptions: {
molds: extractFilterSet(dataRows, "mold"),
moldTypes: extractFilterSet(dataRows, "moldType"),
items: extractFilterSet(dataRows, "item"),
makers: extractFilterSet(dataRows, "maker"),
},
totalCount: dataRows.length,
},
});
} catch (error: any) {
logger.error("금형 리포트 조회 실패", { error: error.message });
res.status(500).json({ success: false, message: "금형 리포트 데이터 조회에 실패했습니다", error: error.message });
}
}