123
This commit is contained in:
161
backend-node/src/controllers/salesReportController.ts
Normal file
161
backend-node/src/controllers/salesReportController.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user