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, "wi", idx); if (cf.condition) { conditions.push(cf.condition); params.push(...cf.params); idx = cf.nextIdx; } const df = buildDateFilter(startDate, endDate, "COALESCE(wi.start_date, wi.created_date::date::text)", idx); conditions.push(...df.conditions); params.push(...df.params); idx = df.nextIdx; const whereClause = buildWhereClause(conditions); const dataQuery = ` SELECT COALESCE(wi.start_date, wi.created_date::date::text) as date, COALESCE(wi.routing, '미지정') as process, COALESCE(ei.equipment_name, wi.equipment_id, '미지정') as equipment, COALESCE(ii.item_name, wi.item_id, '미지정') as item, COALESCE(wi.worker, '미지정') as worker, CAST(COALESCE(NULLIF(wi.qty, ''), '0') AS numeric) as "planQty", COALESCE(pr.production_qty, 0) as "prodQty", COALESCE(pr.defect_qty, 0) as "defectQty", 0 as "runTime", 0 as "downTime", wi.status, wi.company_code FROM work_instruction wi LEFT JOIN ( SELECT wo_id, company_code, SUM(CAST(COALESCE(NULLIF(production_qty, ''), '0') AS numeric)) as production_qty, SUM(CAST(COALESCE(NULLIF(defect_qty, ''), '0') AS numeric)) as defect_qty FROM production_record GROUP BY wo_id, company_code ) pr ON wi.id = pr.wo_id AND wi.company_code = pr.company_code LEFT JOIN ( SELECT DISTINCT ON (equipment_code, company_code) equipment_code, equipment_name, equipment_type, company_code FROM equipment_info ORDER BY equipment_code, company_code, created_date DESC ) ei ON wi.equipment_id = ei.equipment_code AND wi.company_code = ei.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 ${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(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(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, "pr", idx); if (cf.condition) { conditions.push(cf.condition); params.push(...cf.params); idx = cf.nextIdx; } const df = buildDateFilter(startDate, endDate, "COALESCE(pr.production_date, pr.created_date::date::text)", idx); conditions.push(...df.conditions); params.push(...df.params); idx = df.nextIdx; const whereClause = buildWhereClause(conditions); const dataQuery = ` SELECT COALESCE(pr.production_date, pr.created_date::date::text) as date, COALESCE(ii.item_name, wi.item_id, '미지정') as item, '일반검사' as "defectType", COALESCE(wi.routing, '미지정') as process, COALESCE(pr.worker_name, '미지정') as inspector, CAST(COALESCE(NULLIF(pr.production_qty, ''), '0') AS numeric) as "inspQty", CAST(COALESCE(NULLIF(pr.production_qty, ''), '0') AS numeric) - CAST(COALESCE(NULLIF(pr.defect_qty, ''), '0') AS numeric) as "passQty", CAST(COALESCE(NULLIF(pr.defect_qty, ''), '0') AS numeric) as "defectQty", 0 as "reworkQty", 0 as "scrapQty", 0 as "claimCnt", 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 ${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"), defectTypes: [ { value: "외관불량", label: "외관불량" }, { value: "치수불량", label: "치수불량" }, { value: "기능불량", label: "기능불량" }, { value: "재질불량", label: "재질불량" }, { value: "일반검사", label: "일반검사" }, ], 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 conditions: string[] = []; const params: any[] = []; let idx = 1; const cf = buildCompanyFilter(companyCode, "ei", idx); if (cf.condition) { conditions.push(cf.condition); params.push(...cf.params); idx = cf.nextIdx; } const whereClause = buildWhereClause(conditions); const dataQuery = ` 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 `; const dataRows = await query(dataQuery, params); logger.info("설비 리포트 데이터 조회", { companyCode, rowCount: dataRows.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, }, }); } catch (error: any) { logger.error("설비 리포트 조회 실패", { error: error.message }); res.status(500).json({ success: false, message: "설비 리포트 데이터 조회에 실패했습니다", error: error.message }); } } // ============================================ // 금형 리포트 // ============================================ 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 }); } }