162 lines
4.8 KiB
TypeScript
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,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|