- receivingController: 헤더-디테일 JOIN 구조로 변경, 검색/조회 로직 개선 - materialStatusController: work_instruction 테이블 기반으로 쿼리 수정 - analyticsReportController: 구매 리포트 company_code 필터링 로직 개선 - material-status 페이지: COMPANY_29/COMPANY_7 프론트엔드 업데이트 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
536 lines
22 KiB
TypeScript
536 lines
22 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, "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<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(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<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, "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<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, "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<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 });
|
|
}
|
|
}
|