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:
@@ -14,13 +14,23 @@ export async function getOrderSummary(req: AuthenticatedRequest, res: Response)
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { excludePlanned, itemCode, itemName } = req.query;
|
||||
|
||||
const data = await productionService.getOrderSummary(companyCode, {
|
||||
// 서버 페이징 (size 미지정 시 기존 동작 유지: 전체 반환)
|
||||
const page = parseInt(String(req.query.page ?? "1"), 10) || 1;
|
||||
const size = parseInt(String(req.query.size ?? "0"), 10) || 0;
|
||||
|
||||
const result = await productionService.getOrderSummary(companyCode, {
|
||||
excludePlanned: excludePlanned === "true",
|
||||
itemCode: itemCode as string,
|
||||
itemName: itemName as string,
|
||||
page,
|
||||
size,
|
||||
});
|
||||
|
||||
return res.json({ success: true, data });
|
||||
// 페이징 사용 시 result는 { data, total, page, size, totalPages } 객체
|
||||
if (size > 0 && !Array.isArray(result)) {
|
||||
return res.json({ success: true, ...result });
|
||||
}
|
||||
return res.json({ success: true, data: result });
|
||||
} catch (error: any) {
|
||||
logger.error("수주 데이터 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
@@ -47,15 +57,23 @@ export async function getPlans(req: AuthenticatedRequest, res: Response) {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { productType, status, startDate, endDate, itemCode } = req.query;
|
||||
|
||||
const data = await productionService.getPlans(companyCode, {
|
||||
const page = parseInt(String(req.query.page ?? "1"), 10) || 1;
|
||||
const size = parseInt(String(req.query.size ?? "0"), 10) || 0;
|
||||
|
||||
const result = await productionService.getPlans(companyCode, {
|
||||
productType: productType as string,
|
||||
status: status as string,
|
||||
startDate: startDate as string,
|
||||
endDate: endDate as string,
|
||||
itemCode: itemCode as string,
|
||||
page,
|
||||
size,
|
||||
});
|
||||
|
||||
return res.json({ success: true, data });
|
||||
if (size > 0 && !Array.isArray(result)) {
|
||||
return res.json({ success: true, ...result });
|
||||
}
|
||||
return res.json({ success: true, data: result });
|
||||
} catch (error: any) {
|
||||
logger.error("생산계획 목록 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
|
||||
@@ -13,6 +13,11 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { dateFrom, dateTo, status, customer, keyword } = req.query;
|
||||
|
||||
// 서버 페이징 파라미터 (없으면 기존 동작 유지: 전체 조회)
|
||||
const page = parseInt(String(req.query.page ?? "1"), 10) || 1;
|
||||
const size = parseInt(String(req.query.size ?? "0"), 10) || 0;
|
||||
const usePaging = size > 0;
|
||||
|
||||
const conditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let idx = 1;
|
||||
@@ -89,10 +94,41 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
`;
|
||||
|
||||
const pool = getPool();
|
||||
|
||||
if (usePaging) {
|
||||
// total 카운트 — JOIN/GROUP 없이 si 기준 distinct count
|
||||
const countQuery = `
|
||||
SELECT COUNT(DISTINCT si.id)::int AS total
|
||||
FROM shipment_instruction si
|
||||
LEFT JOIN customer_mng c
|
||||
ON si.partner_id = c.customer_code AND si.company_code = c.company_code
|
||||
${where}
|
||||
`;
|
||||
const countResult = await pool.query(countQuery, params);
|
||||
const total = countResult.rows[0]?.total ?? 0;
|
||||
|
||||
const offset = (page - 1) * size;
|
||||
const pagedQuery = `${query} LIMIT $${idx} OFFSET $${idx + 1}`;
|
||||
const pagedResult = await pool.query(pagedQuery, [...params, size, offset]);
|
||||
|
||||
logger.info("출하지시 목록 조회 (페이징)", {
|
||||
companyCode, page, size, total, count: pagedResult.rowCount,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: pagedResult.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 res.json({ success: true, data: result.rows });
|
||||
return res.json({ success: true, data: result.rows, total: result.rowCount });
|
||||
} catch (error: any) {
|
||||
logger.error("출하지시 목록 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
|
||||
@@ -151,6 +151,11 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { dateFrom, dateTo, status, customer, keyword } = req.query;
|
||||
|
||||
// 서버 페이징 파라미터 (없으면 기존 동작 유지: 전체 조회)
|
||||
const page = parseInt(String(req.query.page ?? "1"), 10) || 1;
|
||||
const size = parseInt(String(req.query.size ?? "0"), 10) || 0;
|
||||
const usePaging = size > 0;
|
||||
|
||||
const conditions: string[] = [];
|
||||
const params: any[] = [];
|
||||
let paramIndex = 1;
|
||||
@@ -239,6 +244,53 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
`;
|
||||
|
||||
const pool = getPool();
|
||||
|
||||
// 서버 페이징 적용 시: COUNT + LIMIT/OFFSET
|
||||
if (usePaging) {
|
||||
const countQuery = `
|
||||
SELECT COUNT(*)::int AS total
|
||||
FROM shipment_plan sp
|
||||
LEFT JOIN sales_order_detail d
|
||||
ON sp.detail_id = d.id AND sp.company_code = d.company_code
|
||||
LEFT JOIN sales_order_mng m
|
||||
ON sp.sales_order_id = m.id AND sp.company_code = m.company_code
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT item_name FROM item_info
|
||||
WHERE item_number = COALESCE(d.part_code, m.part_code)
|
||||
AND company_code = sp.company_code
|
||||
LIMIT 1
|
||||
) i ON true
|
||||
LEFT JOIN customer_mng c
|
||||
ON COALESCE(NULLIF(m.partner_id, ''), NULLIF(d.delivery_partner_code, '')) = c.customer_code
|
||||
AND sp.company_code = c.company_code
|
||||
${whereClause}
|
||||
`;
|
||||
const countResult = await pool.query(countQuery, params);
|
||||
const total = countResult.rows[0]?.total ?? 0;
|
||||
|
||||
const offset = (page - 1) * size;
|
||||
const pagedQuery = `${query} LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
|
||||
const pagedResult = await pool.query(pagedQuery, [...params, size, offset]);
|
||||
|
||||
logger.info("출하계획 목록 조회 (페이징)", {
|
||||
companyCode,
|
||||
page,
|
||||
size,
|
||||
total,
|
||||
rowCount: pagedResult.rowCount,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: pagedResult.rows,
|
||||
total,
|
||||
page,
|
||||
size,
|
||||
totalPages: Math.max(1, Math.ceil(total / size)),
|
||||
});
|
||||
}
|
||||
|
||||
// 페이징 미사용: 기존 동작 (전체 조회)
|
||||
const result = await pool.query(query, params);
|
||||
|
||||
logger.info("출하계획 목록 조회", {
|
||||
@@ -246,7 +298,7 @@ export async function getList(req: AuthenticatedRequest, res: Response) {
|
||||
rowCount: result.rowCount,
|
||||
});
|
||||
|
||||
return res.json({ success: true, data: result.rows });
|
||||
return res.json({ success: true, data: result.rows, total: result.rowCount });
|
||||
} catch (error: any) {
|
||||
logger.error("출하계획 목록 조회 실패", {
|
||||
error: error.message,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user