Files
vexplor_dev/backend-node/src/controllers/salesReportController.ts
DDD1542 5715e67ba9 123
2026-03-19 17:18:14 +09:00

162 lines
4.8 KiB
TypeScript

import { Response } from "express";
import { query } from "../database/db";
import { logger } from "../utils/logger";
/**
* 영업 리포트 컨트롤러
* - 수주 데이터를 기반으로 집계/분석용 원본 데이터를 반환
* - 프론트엔드에서 그룹핑/집계/필터링 처리
*/
export async function getSalesReportData(
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 paramIdx = 1;
// 멀티테넌시: 최고관리자는 전체, 일반 회사는 자기 데이터만
if (companyCode !== "*") {
conditions.push(`som.company_code = $${paramIdx}`);
params.push(companyCode);
paramIdx++;
}
// 날짜 필터 (due_date 또는 order_date 기준)
if (startDate) {
conditions.push(
`COALESCE(sod.due_date, som.order_date::text, som.created_date::date::text) >= $${paramIdx}`
);
params.push(startDate);
paramIdx++;
}
if (endDate) {
conditions.push(
`COALESCE(sod.due_date, som.order_date::text, som.created_date::date::text) <= $${paramIdx}`
);
params.push(endDate);
paramIdx++;
}
const whereClause =
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
const dataQuery = `
SELECT
som.order_no,
COALESCE(sod.due_date, som.order_date::text, som.created_date::date::text) as date,
som.order_date,
som.partner_id,
COALESCE(cm.customer_name, som.partner_id, '미지정') as customer,
sod.part_code,
COALESCE(ii.item_name, sod.part_name, sod.part_code, '미지정') as item,
CAST(COALESCE(NULLIF(sod.qty, ''), '0') AS numeric) as "orderQty",
CAST(COALESCE(NULLIF(sod.ship_qty, ''), '0') AS numeric) as "shipQty",
CAST(COALESCE(NULLIF(sod.unit_price, ''), '0') AS numeric) as "unitPrice",
CAST(COALESCE(NULLIF(sod.amount, ''), '0') AS numeric) as "orderAmt",
1 as "orderCount",
som.status,
som.company_code
FROM sales_order_mng som
JOIN sales_order_detail sod
ON som.order_no = sod.order_no
AND som.company_code = sod.company_code
LEFT JOIN customer_mng cm
ON som.partner_id = cm.customer_code
AND som.company_code = cm.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 sod.part_code = ii.item_number
AND sod.company_code = ii.company_code
${whereClause}
ORDER BY date DESC NULLS LAST
`;
// query()는 rows 배열을 직접 반환
const dataRows = await query(dataQuery, params);
// 필터 옵션 조회 (거래처, 품목, 상태)
const filterParams: any[] = [];
let filterWhere = "";
if (companyCode !== "*") {
filterWhere = `WHERE company_code = $1`;
filterParams.push(companyCode);
}
const statusWhere = filterWhere
? `${filterWhere} AND status IS NOT NULL`
: `WHERE status IS NOT NULL`;
const [customersRows, statusRows] = await Promise.all([
query(
`SELECT DISTINCT customer_code as value, customer_name as label
FROM customer_mng ${filterWhere}
ORDER BY customer_name`,
filterParams
),
query(
`SELECT DISTINCT status as value, status as label
FROM sales_order_mng ${statusWhere}
ORDER BY status`,
filterParams
),
]);
// 품목은 데이터에서 추출 (실제 수주에 사용된 품목만)
const itemSet = new Map<string, string>();
dataRows.forEach((row: any) => {
if (row.part_code && !itemSet.has(row.part_code)) {
itemSet.set(row.part_code, row.item);
}
});
const items = Array.from(itemSet.entries()).map(([value, label]) => ({
value,
label,
}));
logger.info("영업 리포트 데이터 조회", {
companyCode,
rowCount: dataRows.length,
startDate,
endDate,
});
res.status(200).json({
success: true,
data: {
rows: dataRows,
filterOptions: {
customers: customersRows,
items,
statuses: statusRows,
},
totalCount: dataRows.length,
},
});
} catch (error: any) {
logger.error("영업 리포트 데이터 조회 실패", {
error: error.message,
stack: error.stack,
});
res.status(500).json({
success: false,
message: "영업 리포트 데이터 조회에 실패했습니다",
error: error.message,
});
}
}