Implement server-side pagination for order summaries, plans, and shipping orders

- Added pagination parameters (page and size) to the getOrderSummary and getPlans functions in the productionController.
- Updated the getList function in shippingOrderController and shippingPlanController to support server-side pagination.
- Modified frontend components to handle pagination state and display total counts for orders and plans.
- Ensured compatibility with existing functionality by maintaining behavior when pagination is not used.
This commit is contained in:
kjs
2026-04-28 13:59:34 +09:00
parent 8bfa1f9838
commit 4afb0f5ca4
39 changed files with 1158 additions and 330 deletions

View File

@@ -15,7 +15,13 @@ import { logger } from "../utils/logger";
export async function getOrderSummary(
companyCode: string,
options?: { excludePlanned?: boolean; itemCode?: string; itemName?: string }
options?: {
excludePlanned?: boolean;
itemCode?: string;
itemName?: string;
page?: number;
size?: number;
}
) {
const pool = getPool();
const conditions: string[] = ["so.company_code = $1"];
@@ -35,6 +41,10 @@ export async function getOrderSummary(
const whereClause = conditions.join(" AND ");
const page = options?.page && options.page > 0 ? options.page : 1;
const size = options?.size && options.size > 0 ? options.size : 0; // 0 = 전체 (하위호환)
const usePaging = size > 0;
// 단일 쿼리로 요약 + 상세 + 재고 + 계획 통합 조회
const query = `
WITH all_orders AS (
@@ -122,38 +132,113 @@ export async function getOrderSummary(
LEFT JOIN plan_info pi ON os.item_code = pi.item_code
LEFT JOIN item_info_dedup ilt ON os.item_code = ilt.item_number
${options?.excludePlanned ? "WHERE COALESCE(pi.existing_plan_qty, 0) = 0" : ""}
ORDER BY os.item_code;
ORDER BY os.item_code
`;
const result = await pool.query(query, params);
// 페이징 시: total 별도 산출 + LIMIT/OFFSET 적용
let total = 0;
let pagedQuery = `${query};`;
let pagedParams: any[] = params;
// 상세 데이터: all_orders CTE와 동일 로직 (쿼리 재사용 위해 별도 실행)
const detailQuery = `
SELECT id::text, order_no, part_code, part_name,
COALESCE(order_qty::numeric, 0) AS order_qty,
COALESCE(ship_qty::numeric, 0) AS ship_qty,
COALESCE(balance_qty::numeric, 0) AS balance_qty,
due_date, status, partner_id, manager_name
FROM sales_order_mng
WHERE ${conditions.map(c => c.replace(/so\./g, "")).join(" AND ")}
AND part_code IS NOT NULL AND part_code != ''
AND NOT EXISTS (
SELECT 1 FROM sales_order_detail sd
WHERE sd.order_no = sales_order_mng.order_no AND sd.company_code = sales_order_mng.company_code
if (usePaging) {
// total: 그룹 수 = order_summary CTE의 행 수
const countQuery = `
WITH all_orders AS (
SELECT so.part_code, so.company_code
FROM sales_order_mng so
WHERE ${whereClause}
AND so.part_code IS NOT NULL AND so.part_code != ''
AND NOT EXISTS (
SELECT 1 FROM sales_order_detail sd
WHERE sd.order_no = so.order_no AND sd.company_code = so.company_code
)
UNION ALL
SELECT sd.part_code, sd.company_code
FROM sales_order_detail sd
INNER JOIN sales_order_mng so ON sd.order_no = so.order_no AND sd.company_code = so.company_code
WHERE sd.company_code = $1
AND sd.part_code IS NOT NULL AND sd.part_code != ''
)
UNION ALL
SELECT sd.id::text, sd.order_no, sd.part_code, sd.part_name,
COALESCE(sd.qty::numeric, 0) AS order_qty,
COALESCE(sd.ship_qty::numeric, 0) AS ship_qty,
COALESCE(sd.balance_qty::numeric, COALESCE(sd.qty::numeric, 0) - COALESCE(sd.ship_qty::numeric, 0), 0) AS balance_qty,
sd.due_date::date, so.status, so.partner_id, so.manager_name
FROM sales_order_detail sd
INNER JOIN sales_order_mng so ON sd.order_no = so.order_no AND sd.company_code = so.company_code
WHERE sd.company_code = $1
AND sd.part_code IS NOT NULL AND sd.part_code != ''
ORDER BY part_code, due_date;
`;
const detailResult = await pool.query(detailQuery, params);
SELECT COUNT(*)::int AS total FROM (
SELECT DISTINCT part_code FROM all_orders
) g;
`;
const countRes = await pool.query(countQuery, params);
total = countRes.rows[0]?.total ?? 0;
const offset = (page - 1) * size;
pagedQuery = `${query} LIMIT $${paramIdx} OFFSET $${paramIdx + 1};`;
pagedParams = [...params, size, offset];
}
const result = await pool.query(pagedQuery, pagedParams);
// 상세 데이터: 페이징 시 현재 페이지의 part_codes만, 미페이징 시 전체
let detailQuery: string;
let detailParams: any[];
if (usePaging) {
const partCodes = result.rows.map((r: any) => r.item_code).filter(Boolean);
if (partCodes.length === 0) {
const data = result.rows.map((g: any) => ({ ...g, orders: [] }));
return { data, total, page, size, totalPages: Math.max(1, Math.ceil(total / size)) };
}
detailQuery = `
SELECT id::text, order_no, part_code, part_name,
COALESCE(order_qty::numeric, 0) AS order_qty,
COALESCE(ship_qty::numeric, 0) AS ship_qty,
COALESCE(balance_qty::numeric, 0) AS balance_qty,
due_date, status, partner_id, manager_name
FROM sales_order_mng
WHERE company_code = $1
AND part_code = ANY($2::text[])
AND NOT EXISTS (
SELECT 1 FROM sales_order_detail sd
WHERE sd.order_no = sales_order_mng.order_no AND sd.company_code = sales_order_mng.company_code
)
UNION ALL
SELECT sd.id::text, sd.order_no, sd.part_code, sd.part_name,
COALESCE(sd.qty::numeric, 0) AS order_qty,
COALESCE(sd.ship_qty::numeric, 0) AS ship_qty,
COALESCE(sd.balance_qty::numeric, COALESCE(sd.qty::numeric, 0) - COALESCE(sd.ship_qty::numeric, 0), 0) AS balance_qty,
sd.due_date::date, so.status, so.partner_id, so.manager_name
FROM sales_order_detail sd
INNER JOIN sales_order_mng so ON sd.order_no = so.order_no AND sd.company_code = so.company_code
WHERE sd.company_code = $1
AND sd.part_code = ANY($2::text[])
ORDER BY part_code, due_date;
`;
detailParams = [companyCode, partCodes];
} else {
detailQuery = `
SELECT id::text, order_no, part_code, part_name,
COALESCE(order_qty::numeric, 0) AS order_qty,
COALESCE(ship_qty::numeric, 0) AS ship_qty,
COALESCE(balance_qty::numeric, 0) AS balance_qty,
due_date, status, partner_id, manager_name
FROM sales_order_mng
WHERE ${conditions.map(c => c.replace(/so\./g, "")).join(" AND ")}
AND part_code IS NOT NULL AND part_code != ''
AND NOT EXISTS (
SELECT 1 FROM sales_order_detail sd
WHERE sd.order_no = sales_order_mng.order_no AND sd.company_code = sales_order_mng.company_code
)
UNION ALL
SELECT sd.id::text, sd.order_no, sd.part_code, sd.part_name,
COALESCE(sd.qty::numeric, 0) AS order_qty,
COALESCE(sd.ship_qty::numeric, 0) AS ship_qty,
COALESCE(sd.balance_qty::numeric, COALESCE(sd.qty::numeric, 0) - COALESCE(sd.ship_qty::numeric, 0), 0) AS balance_qty,
sd.due_date::date, so.status, so.partner_id, so.manager_name
FROM sales_order_detail sd
INNER JOIN sales_order_mng so ON sd.order_no = so.order_no AND sd.company_code = so.company_code
WHERE sd.company_code = $1
AND sd.part_code IS NOT NULL AND sd.part_code != ''
ORDER BY part_code, due_date;
`;
detailParams = params;
}
const detailResult = await pool.query(detailQuery, detailParams);
// 그룹별로 상세 데이터 매핑
const ordersByItem: Record<string, any[]> = {};
@@ -168,7 +253,18 @@ export async function getOrderSummary(
orders: ordersByItem[group.item_code || "__null__"] || [],
}));
logger.info("수주 데이터 조회", { companyCode, groupCount: data.length });
logger.info("수주 데이터 조회", { companyCode, groupCount: data.length, page, size, total });
if (usePaging) {
return {
data,
total,
page,
size,
totalPages: Math.max(1, Math.ceil(total / size)),
};
}
// 하위호환: 페이징 미사용 시 기존 형태 (배열 그대로) 유지
return data;
}
@@ -210,6 +306,8 @@ export async function getPlans(
startDate?: string;
endDate?: string;
itemCode?: string;
page?: number;
size?: number;
}
) {
const pool = getPool();
@@ -217,6 +315,10 @@ export async function getPlans(
const params: any[] = [companyCode];
let paramIdx = 2;
const page = options?.page && options.page > 0 ? options.page : 1;
const size = options?.size && options.size > 0 ? options.size : 0;
const usePaging = size > 0;
if (companyCode !== "*") {
// 일반 회사: 자사 데이터만
} else {
@@ -269,6 +371,26 @@ export async function getPlans(
ORDER BY p.start_date ASC, p.item_code ASC
`;
if (usePaging) {
const countQuery = `SELECT COUNT(*)::int AS total FROM production_plan_mng p ${whereClause}`;
const countRes = await pool.query(countQuery, params);
const total = countRes.rows[0]?.total ?? 0;
const offset = (page - 1) * size;
const pagedQuery = `${query} LIMIT $${paramIdx} OFFSET $${paramIdx + 1}`;
const pagedRes = await pool.query(pagedQuery, [...params, size, offset]);
logger.info("생산계획 목록 조회 (페이징)", { companyCode, page, size, total });
return {
data: pagedRes.rows,
total,
page,
size,
totalPages: Math.max(1, Math.ceil(total / size)),
};
}
const result = await pool.query(query, params);
logger.info("생산계획 목록 조회", { companyCode, count: result.rowCount });
return result.rows;