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(); 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 { 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 { 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 { 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 { 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 { 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(); 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 { 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 }); } }