diff --git a/backend-node/src/controllers/productionController.ts b/backend-node/src/controllers/productionController.ts index 73aeb53f..f832a3ad 100644 --- a/backend-node/src/controllers/productionController.ts +++ b/backend-node/src/controllers/productionController.ts @@ -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 }); diff --git a/backend-node/src/controllers/shippingOrderController.ts b/backend-node/src/controllers/shippingOrderController.ts index 896f68f0..0f875ec5 100644 --- a/backend-node/src/controllers/shippingOrderController.ts +++ b/backend-node/src/controllers/shippingOrderController.ts @@ -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 }); diff --git a/backend-node/src/controllers/shippingPlanController.ts b/backend-node/src/controllers/shippingPlanController.ts index 64f63f76..0ac09b35 100644 --- a/backend-node/src/controllers/shippingPlanController.ts +++ b/backend-node/src/controllers/shippingPlanController.ts @@ -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, diff --git a/backend-node/src/services/productionPlanService.ts b/backend-node/src/services/productionPlanService.ts index 8959d0ad..570816f6 100644 --- a/backend-node/src/services/productionPlanService.ts +++ b/backend-node/src/services/productionPlanService.ts @@ -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 = {}; @@ -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; diff --git a/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx index 6eae857e..cd159a0b 100644 --- a/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_10/production/plan-management/page.tsx @@ -139,10 +139,11 @@ export default function ProductionPlanManagementPage() { // 데이터 상태 const [orderItems, setOrderItems] = useState([]); - // 좌측 수주목록 페이지네이션 + // 좌측 수주목록 페이지네이션 (서버 페이징) const [orderPage, setOrderPage] = useState(1); const [orderPageSize, setOrderPageSize] = useState(20); const [orderPageSizeInput, setOrderPageSizeInput] = useState("20"); + const [orderTotalCount, setOrderTotalCount] = useState(0); const [stockItems, setStockItems] = useState([]); const [finishedPlans, setFinishedPlans] = useState([]); const [semiPlans, setSemiPlans] = useState([]); @@ -210,22 +211,25 @@ export default function ProductionPlanManagementPage() { const res = await getOrderSummary({ excludePlanned: filterUnplannedOrdersOnly, itemCode: searchItemCode || undefined, + page: orderPage, + size: orderPageSize, }); - if (res.success) setOrderItems(res.data || []); + if (res.success) { + setOrderItems(res.data || []); + setOrderTotalCount(res.total ?? res.data?.length ?? 0); + } } catch (err: any) { toast.error("수주 데이터 조회 실패: " + (err.message || "")); } finally { setLoadingOrders(false); } - }, [filterUnplannedOrdersOnly, searchItemCode]); + }, [filterUnplannedOrdersOnly, searchItemCode, orderPage, orderPageSize]); // 수주목록 페이지네이션 계산 - const orderTotalPages = Math.max(1, Math.ceil(orderItems.length / orderPageSize)); + const orderTotalPages = Math.max(1, Math.ceil(orderTotalCount / orderPageSize)); const orderSafePage = Math.min(Math.max(1, orderPage), orderTotalPages); - const paginatedOrderItems = useMemo(() => { - const start = (orderSafePage - 1) * orderPageSize; - return orderItems.slice(start, start + orderPageSize); - }, [orderItems, orderSafePage, orderPageSize]); + // 서버 페이징: 응답 자체가 페이지 데이터이므로 slice 불필요 + const paginatedOrderItems = orderItems; const applyOrderPageSize = () => { const n = parseInt(orderPageSizeInput, 10); @@ -247,8 +251,8 @@ export default function ProductionPlanManagementPage() { return pages; }; - // orderItems 변경 시 1페이지로 리셋 - useEffect(() => { setOrderPage(1); }, [orderItems.length]); + // 검색 필터 변경 시 1페이지로 리셋 + useEffect(() => { setOrderPage(1); }, [filterUnplannedOrdersOnly, searchItemCode]); const fetchStockShortage = useCallback(async () => { setLoadingStock(true); @@ -265,19 +269,28 @@ export default function ProductionPlanManagementPage() { const fetchPlans = useCallback(async () => { setLoadingPlans(true); try { + // 타임라인 성능: 기간 필터 미입력 시 기본 오늘 ~ +60일 자동 적용 + // 이전 기록은 검색에서 시작일/종료일을 직접 지정하면 조회됨 + const today = new Date(); + const fmt = (d: Date) => d.toISOString().slice(0, 10); + const defaultStart = fmt(today); + const defaultEnd = fmt(new Date(today.getTime() + 60 * 86400000)); + const effectiveStart = searchStartDate || defaultStart; + const effectiveEnd = searchEndDate || defaultEnd; + const [finRes, semiRes] = await Promise.all([ getPlans({ productType: "완제품", status: searchStatus !== "all" ? searchStatus : undefined, - startDate: searchStartDate || undefined, - endDate: searchEndDate || undefined, + startDate: effectiveStart, + endDate: effectiveEnd, itemCode: searchItemCode || undefined, }), getPlans({ productType: "반제품", status: searchStatus !== "all" ? searchStatus : undefined, - startDate: searchStartDate || undefined, - endDate: searchEndDate || undefined, + startDate: effectiveStart, + endDate: effectiveEnd, }), ]); if (finRes.success) setFinishedPlans(finRes.data || []); @@ -329,11 +342,10 @@ export default function ProductionPlanManagementPage() { }, []); useEffect(() => { - if (searchFilters.length > 0) { - fetchOrderSummary(); - fetchPlans(); - } - }, [searchItemCode, searchStatus, searchStartDate, searchEndDate]); + // 검색 state 변경 시 자동 재조회 (필터 비어있어도 default 기간으로 재조회) + fetchOrderSummary(); + fetchPlans(); + }, [searchItemCode, searchStatus, searchStartDate, searchEndDate]); // eslint-disable-line react-hooks/exhaustive-deps // ========== 토글/선택 핸들러 ========== @@ -1516,6 +1528,7 @@ export default function ProductionPlanManagementPage() { onEventClick={openScheduleDetail} onEventMove={handleEventMove} onEventResize={handleEventResize} + onRangeChange={(s, e) => { setSearchStartDate(s); setSearchEndDate(e); }} /> @@ -1585,6 +1598,7 @@ export default function ProductionPlanManagementPage() { onEventClick={openScheduleDetail} onEventMove={handleEventMove} onEventResize={handleEventResize} + onRangeChange={(s, e) => { setSearchStartDate(s); setSearchEndDate(e); }} /> diff --git a/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx index 74fb5981..45042ac7 100644 --- a/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_10/quality/item-inspection/page.tsx @@ -33,6 +33,7 @@ const INSPECTION_TABLE = "inspection_standard"; const GRID_COLUMNS = [ { key: "item_code", label: "품목코드" }, { key: "item_name", label: "품명" }, + { key: "size", label: "규격" }, { key: "inspection_type", label: "검사유형" }, { key: "is_active", label: "사용여부" }, ]; @@ -74,7 +75,7 @@ export default function ItemInspectionInfoPage() { const [saving, setSaving] = useState(false); // FK 옵션 - const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; unit: string }[]>([]); + const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; size: string; unit: string }[]>([]); const [inspOptions, setInspOptions] = useState<{ code: string; label: string; detail: string; method: string; judgment_criteria: string; selection_options: string; unit: string; types: string[] }[]>([]); const [inspTypeCatOptions, setInspTypeCatOptions] = useState<{ code: string; label: string }[]>([]); @@ -130,6 +131,7 @@ export default function ItemInspectionInfoPage() { code: r.item_number || r.item_code || "", name: r.item_name || "", item_type: r.type || r.item_type || "", + size: r.size || "", unit: r.inventory_unit || "", }))); @@ -239,7 +241,7 @@ export default function ItemInspectionInfoPage() { const resData = res.data?.data; const rows = resData?.data || resData?.rows || []; const cm = itemCatMapRef.current; - setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: cm["type"]?.[r.type] || r.type || "", unit: cm["inventory_unit"]?.[r.inventory_unit] || r.inventory_unit || "" }))); + setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: cm["type"]?.[r.type] || r.type || "", size: r.size || "", unit: cm["inventory_unit"]?.[r.inventory_unit] || r.inventory_unit || "" }))); setItemTotal(resData?.total || resData?.totalCount || rows.length); } catch { /* skip */ } finally { setItemSearchLoading(false); } }; @@ -459,11 +461,13 @@ export default function ItemInspectionInfoPage() { // item_code별 그룹핑 const groupedData = useMemo(() => { - const map: Record = {}; + const itemSizeMap: Record = {}; + for (const it of itemOptions) itemSizeMap[it.code] = it.size || ""; + const map: Record = {}; for (const row of data) { const key = row.item_code || row.id; if (!map[key]) { - map[key] = { item_code: row.item_code, item_name: row.item_name, is_active: row.is_active || "", types: [], rows: [] }; + map[key] = { item_code: row.item_code, item_name: row.item_name, size: itemSizeMap[row.item_code] || "", is_active: row.is_active || "", types: [], rows: [] }; } map[key].rows.push(row); if (row.inspection_type && !map[key].types.includes(row.inspection_type)) { @@ -471,7 +475,7 @@ export default function ItemInspectionInfoPage() { } } return Object.values(map); - }, [data]); + }, [data, itemOptions]); // 좌측 품목 목록 정렬 (컬럼 헤더 클릭 → asc → desc → 해제 순환) const [sortConfig, setSortConfig] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -1052,12 +1056,13 @@ export default function ItemInspectionInfoPage() { switch (col.key) { case "item_code": return {group.item_code}; case "item_name": return {group.item_name}; + case "size": return {group.size}; case "inspection_type": return ( -
+
{group.types.map((t: string) => { const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t; - return {label}; + return {label}; })}
@@ -1252,17 +1257,19 @@ export default function ItemInspectionInfoPage() { 품목코드 품목명 + 규격 품목유형 단위 {filteredItems.length === 0 ? ( - {itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"} + {itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"} ) : filteredItems.map((item) => ( selectItem(item)}> {item.code} {item.name} + {item.size} {item.item_type} {item.unit} diff --git a/frontend/app/(main)/COMPANY_10/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_10/sales/shipping-order/page.tsx index 2ed29b40..a8c95d2e 100644 --- a/frontend/app/(main)/COMPANY_10/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_10/sales/shipping-order/page.tsx @@ -97,6 +97,11 @@ export default function ShippingOrderPage() { // 검색 필터 (DynamicSearchFilter에서 관리) const [searchFilters, setSearchFilters] = useState([]); + // 서버 페이징 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(50); + const [totalCount, setTotalCount] = useState(0); + // 엑셀 업로드 const [excelUploadOpen, setExcelUploadOpen] = useState(false); @@ -136,7 +141,7 @@ export default function ShippingOrderPage() { const fetchOrders = useCallback(async () => { setLoading(true); try { - const params: any = {}; + const params: any = { page: currentPage, size: pageSize }; for (const f of searchFilters) { if (f.columnName === "ship_date" && f.operator === "between" && f.value) { const [from, to] = f.value.split(","); @@ -152,18 +157,25 @@ export default function ShippingOrderPage() { } const result = await getShippingOrderList(params); - if (result.success) setOrders(result.data || []); + if (result.success) { + setOrders(result.data || []); + setTotalCount(result.total ?? result.data?.length ?? 0); + } } catch (err) { console.error("출하지시 조회 실패:", err); } finally { setLoading(false); } - }, [searchFilters]); + }, [searchFilters, currentPage, pageSize]); useEffect(() => { fetchOrders(); }, [fetchOrders]); + useEffect(() => { + setCurrentPage(1); + }, [searchFilters]); + // 소스 데이터 조회 const fetchSourceData = useCallback(async (pageOverride?: number) => { setSourceLoading(true); @@ -473,7 +485,7 @@ export default function ShippingOrderPage() { tableName={ts.tableName} filterId="c16-shipping-order" onFilterChange={setSearchFilters} - dataCount={orders.length} + dataCount={totalCount} externalFilterConfig={ts.filterConfig} /> @@ -482,7 +494,7 @@ export default function ShippingOrderPage() {

출하지시 관리

- {orders.length}건 + {totalCount}건 {loading && }
@@ -548,6 +560,13 @@ export default function ShippingOrderPage() { onRowClick={(row) => setSelectedOrderId(row._orderId)} onRowDoubleClick={(row) => openModal(row._order)} showPagination + serverPagination + serverCurrentPage={currentPage} + serverPageSize={pageSize} + serverTotalCount={totalCount} + onServerPageChange={setCurrentPage} + onServerPageSizeChange={(s) => { setPageSize(s); setCurrentPage(1); }} + defaultPageSize={pageSize} draggableColumns={false} columnOrderKey="c16-shipping-order" /> diff --git a/frontend/app/(main)/COMPANY_10/sales/shipping-plan/page.tsx b/frontend/app/(main)/COMPANY_10/sales/shipping-plan/page.tsx index 747ac23d..9e0398f9 100644 --- a/frontend/app/(main)/COMPANY_10/sales/shipping-plan/page.tsx +++ b/frontend/app/(main)/COMPANY_10/sales/shipping-plan/page.tsx @@ -69,6 +69,11 @@ export default function ShippingPlanPage() { // 검색 필터 (DynamicSearchFilter에서 관리) const [searchFilters, setSearchFilters] = useState([]); + // 서버 페이징 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(50); + const [totalCount, setTotalCount] = useState(0); + // 상세 패널 편집 const [editPlanQty, setEditPlanQty] = useState(""); const [editPlanDate, setEditPlanDate] = useState(""); @@ -81,7 +86,7 @@ export default function ShippingPlanPage() { const fetchData = useCallback(async () => { setLoading(true); try { - const params: any = {}; + const params: any = { page: currentPage, size: pageSize }; for (const f of searchFilters) { if (f.columnName === "plan_date" && f.operator === "between" && f.value) { const [from, to] = f.value.split(","); @@ -99,19 +104,24 @@ export default function ShippingPlanPage() { const result = await getShipmentPlanList(params); if (result.success) { setData(result.data || []); + setTotalCount(result.total ?? result.data?.length ?? 0); } } catch (err) { console.error("출하계획 조회 실패:", err); } finally { setLoading(false); } - }, [searchFilters]); + }, [searchFilters, currentPage, pageSize]); - // searchFilters 변경 시 자동 조회 + // searchFilters 변경 시 자동 조회 + 1페이지로 리셋 useEffect(() => { fetchData(); }, [fetchData]); + useEffect(() => { + setCurrentPage(1); + }, [searchFilters]); + const selectedPlan = useMemo(() => data.find(p => p.id === selectedId), [data, selectedId]); const groupedData = useMemo(() => { @@ -209,7 +219,7 @@ export default function ShippingPlanPage() { tableName="shipment_plan" filterId="c16-shipping-plan" onFilterChange={setSearchFilters} - dataCount={data.length} + dataCount={totalCount} externalFilterConfig={ts.filterConfig} /> @@ -224,7 +234,7 @@ export default function ShippingPlanPage() {
출하계획 목록 - {data.length}건 + {totalCount}건 {loading && }
@@ -262,7 +272,14 @@ export default function ShippingPlanPage() { showCheckbox checkedIds={checkedIds.map(String)} onCheckedChange={(ids) => setCheckedIds(ids.map(Number))} - showPagination={false} + showPagination + serverPagination + serverCurrentPage={currentPage} + serverPageSize={pageSize} + serverTotalCount={totalCount} + onServerPageChange={setCurrentPage} + onServerPageSizeChange={(s) => { setPageSize(s); setCurrentPage(1); }} + defaultPageSize={pageSize} draggableColumns={false} />
diff --git a/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx index 6eae857e..cd159a0b 100644 --- a/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_16/production/plan-management/page.tsx @@ -139,10 +139,11 @@ export default function ProductionPlanManagementPage() { // 데이터 상태 const [orderItems, setOrderItems] = useState([]); - // 좌측 수주목록 페이지네이션 + // 좌측 수주목록 페이지네이션 (서버 페이징) const [orderPage, setOrderPage] = useState(1); const [orderPageSize, setOrderPageSize] = useState(20); const [orderPageSizeInput, setOrderPageSizeInput] = useState("20"); + const [orderTotalCount, setOrderTotalCount] = useState(0); const [stockItems, setStockItems] = useState([]); const [finishedPlans, setFinishedPlans] = useState([]); const [semiPlans, setSemiPlans] = useState([]); @@ -210,22 +211,25 @@ export default function ProductionPlanManagementPage() { const res = await getOrderSummary({ excludePlanned: filterUnplannedOrdersOnly, itemCode: searchItemCode || undefined, + page: orderPage, + size: orderPageSize, }); - if (res.success) setOrderItems(res.data || []); + if (res.success) { + setOrderItems(res.data || []); + setOrderTotalCount(res.total ?? res.data?.length ?? 0); + } } catch (err: any) { toast.error("수주 데이터 조회 실패: " + (err.message || "")); } finally { setLoadingOrders(false); } - }, [filterUnplannedOrdersOnly, searchItemCode]); + }, [filterUnplannedOrdersOnly, searchItemCode, orderPage, orderPageSize]); // 수주목록 페이지네이션 계산 - const orderTotalPages = Math.max(1, Math.ceil(orderItems.length / orderPageSize)); + const orderTotalPages = Math.max(1, Math.ceil(orderTotalCount / orderPageSize)); const orderSafePage = Math.min(Math.max(1, orderPage), orderTotalPages); - const paginatedOrderItems = useMemo(() => { - const start = (orderSafePage - 1) * orderPageSize; - return orderItems.slice(start, start + orderPageSize); - }, [orderItems, orderSafePage, orderPageSize]); + // 서버 페이징: 응답 자체가 페이지 데이터이므로 slice 불필요 + const paginatedOrderItems = orderItems; const applyOrderPageSize = () => { const n = parseInt(orderPageSizeInput, 10); @@ -247,8 +251,8 @@ export default function ProductionPlanManagementPage() { return pages; }; - // orderItems 변경 시 1페이지로 리셋 - useEffect(() => { setOrderPage(1); }, [orderItems.length]); + // 검색 필터 변경 시 1페이지로 리셋 + useEffect(() => { setOrderPage(1); }, [filterUnplannedOrdersOnly, searchItemCode]); const fetchStockShortage = useCallback(async () => { setLoadingStock(true); @@ -265,19 +269,28 @@ export default function ProductionPlanManagementPage() { const fetchPlans = useCallback(async () => { setLoadingPlans(true); try { + // 타임라인 성능: 기간 필터 미입력 시 기본 오늘 ~ +60일 자동 적용 + // 이전 기록은 검색에서 시작일/종료일을 직접 지정하면 조회됨 + const today = new Date(); + const fmt = (d: Date) => d.toISOString().slice(0, 10); + const defaultStart = fmt(today); + const defaultEnd = fmt(new Date(today.getTime() + 60 * 86400000)); + const effectiveStart = searchStartDate || defaultStart; + const effectiveEnd = searchEndDate || defaultEnd; + const [finRes, semiRes] = await Promise.all([ getPlans({ productType: "완제품", status: searchStatus !== "all" ? searchStatus : undefined, - startDate: searchStartDate || undefined, - endDate: searchEndDate || undefined, + startDate: effectiveStart, + endDate: effectiveEnd, itemCode: searchItemCode || undefined, }), getPlans({ productType: "반제품", status: searchStatus !== "all" ? searchStatus : undefined, - startDate: searchStartDate || undefined, - endDate: searchEndDate || undefined, + startDate: effectiveStart, + endDate: effectiveEnd, }), ]); if (finRes.success) setFinishedPlans(finRes.data || []); @@ -329,11 +342,10 @@ export default function ProductionPlanManagementPage() { }, []); useEffect(() => { - if (searchFilters.length > 0) { - fetchOrderSummary(); - fetchPlans(); - } - }, [searchItemCode, searchStatus, searchStartDate, searchEndDate]); + // 검색 state 변경 시 자동 재조회 (필터 비어있어도 default 기간으로 재조회) + fetchOrderSummary(); + fetchPlans(); + }, [searchItemCode, searchStatus, searchStartDate, searchEndDate]); // eslint-disable-line react-hooks/exhaustive-deps // ========== 토글/선택 핸들러 ========== @@ -1516,6 +1528,7 @@ export default function ProductionPlanManagementPage() { onEventClick={openScheduleDetail} onEventMove={handleEventMove} onEventResize={handleEventResize} + onRangeChange={(s, e) => { setSearchStartDate(s); setSearchEndDate(e); }} /> @@ -1585,6 +1598,7 @@ export default function ProductionPlanManagementPage() { onEventClick={openScheduleDetail} onEventMove={handleEventMove} onEventResize={handleEventResize} + onRangeChange={(s, e) => { setSearchStartDate(s); setSearchEndDate(e); }} /> diff --git a/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx index 6bdf9f93..71f21a18 100644 --- a/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_16/quality/item-inspection/page.tsx @@ -33,6 +33,7 @@ const INSPECTION_TABLE = "inspection_standard"; const GRID_COLUMNS = [ { key: "item_code", label: "품목코드" }, { key: "item_name", label: "품명" }, + { key: "size", label: "규격" }, { key: "inspection_type", label: "검사유형" }, { key: "is_active", label: "사용여부" }, ]; @@ -453,11 +454,13 @@ export default function ItemInspectionInfoPage() { // item_code별 그룹핑 const groupedData = useMemo(() => { - const map: Record = {}; + const itemSizeMap: Record = {}; + for (const it of itemOptions) itemSizeMap[it.code] = it.size || ""; + const map: Record = {}; for (const row of data) { const key = row.item_code || row.id; if (!map[key]) { - map[key] = { item_code: row.item_code, item_name: row.item_name, is_active: row.is_active || "", types: [], rows: [] }; + map[key] = { item_code: row.item_code, item_name: row.item_name, size: itemSizeMap[row.item_code] || "", is_active: row.is_active || "", types: [], rows: [] }; } map[key].rows.push(row); if (row.inspection_type && !map[key].types.includes(row.inspection_type)) { @@ -465,7 +468,7 @@ export default function ItemInspectionInfoPage() { } } return Object.values(map); - }, [data]); + }, [data, itemOptions]); // 좌측 품목 목록 정렬 (컬럼 헤더 클릭 → asc → desc → 해제 순환) const [sortConfig, setSortConfig] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -1046,12 +1049,13 @@ export default function ItemInspectionInfoPage() { switch (col.key) { case "item_code": return {group.item_code}; case "item_name": return {group.item_name}; + case "size": return {group.size}; case "inspection_type": return ( -
+
{group.types.map((t: string) => { const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t; - return {label}; + return {label}; })}
diff --git a/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx index 2ed29b40..a8c95d2e 100644 --- a/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/shipping-order/page.tsx @@ -97,6 +97,11 @@ export default function ShippingOrderPage() { // 검색 필터 (DynamicSearchFilter에서 관리) const [searchFilters, setSearchFilters] = useState([]); + // 서버 페이징 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(50); + const [totalCount, setTotalCount] = useState(0); + // 엑셀 업로드 const [excelUploadOpen, setExcelUploadOpen] = useState(false); @@ -136,7 +141,7 @@ export default function ShippingOrderPage() { const fetchOrders = useCallback(async () => { setLoading(true); try { - const params: any = {}; + const params: any = { page: currentPage, size: pageSize }; for (const f of searchFilters) { if (f.columnName === "ship_date" && f.operator === "between" && f.value) { const [from, to] = f.value.split(","); @@ -152,18 +157,25 @@ export default function ShippingOrderPage() { } const result = await getShippingOrderList(params); - if (result.success) setOrders(result.data || []); + if (result.success) { + setOrders(result.data || []); + setTotalCount(result.total ?? result.data?.length ?? 0); + } } catch (err) { console.error("출하지시 조회 실패:", err); } finally { setLoading(false); } - }, [searchFilters]); + }, [searchFilters, currentPage, pageSize]); useEffect(() => { fetchOrders(); }, [fetchOrders]); + useEffect(() => { + setCurrentPage(1); + }, [searchFilters]); + // 소스 데이터 조회 const fetchSourceData = useCallback(async (pageOverride?: number) => { setSourceLoading(true); @@ -473,7 +485,7 @@ export default function ShippingOrderPage() { tableName={ts.tableName} filterId="c16-shipping-order" onFilterChange={setSearchFilters} - dataCount={orders.length} + dataCount={totalCount} externalFilterConfig={ts.filterConfig} /> @@ -482,7 +494,7 @@ export default function ShippingOrderPage() {

출하지시 관리

- {orders.length}건 + {totalCount}건 {loading && }
@@ -548,6 +560,13 @@ export default function ShippingOrderPage() { onRowClick={(row) => setSelectedOrderId(row._orderId)} onRowDoubleClick={(row) => openModal(row._order)} showPagination + serverPagination + serverCurrentPage={currentPage} + serverPageSize={pageSize} + serverTotalCount={totalCount} + onServerPageChange={setCurrentPage} + onServerPageSizeChange={(s) => { setPageSize(s); setCurrentPage(1); }} + defaultPageSize={pageSize} draggableColumns={false} columnOrderKey="c16-shipping-order" /> diff --git a/frontend/app/(main)/COMPANY_16/sales/shipping-plan/page.tsx b/frontend/app/(main)/COMPANY_16/sales/shipping-plan/page.tsx index 747ac23d..9e0398f9 100644 --- a/frontend/app/(main)/COMPANY_16/sales/shipping-plan/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/shipping-plan/page.tsx @@ -69,6 +69,11 @@ export default function ShippingPlanPage() { // 검색 필터 (DynamicSearchFilter에서 관리) const [searchFilters, setSearchFilters] = useState([]); + // 서버 페이징 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(50); + const [totalCount, setTotalCount] = useState(0); + // 상세 패널 편집 const [editPlanQty, setEditPlanQty] = useState(""); const [editPlanDate, setEditPlanDate] = useState(""); @@ -81,7 +86,7 @@ export default function ShippingPlanPage() { const fetchData = useCallback(async () => { setLoading(true); try { - const params: any = {}; + const params: any = { page: currentPage, size: pageSize }; for (const f of searchFilters) { if (f.columnName === "plan_date" && f.operator === "between" && f.value) { const [from, to] = f.value.split(","); @@ -99,19 +104,24 @@ export default function ShippingPlanPage() { const result = await getShipmentPlanList(params); if (result.success) { setData(result.data || []); + setTotalCount(result.total ?? result.data?.length ?? 0); } } catch (err) { console.error("출하계획 조회 실패:", err); } finally { setLoading(false); } - }, [searchFilters]); + }, [searchFilters, currentPage, pageSize]); - // searchFilters 변경 시 자동 조회 + // searchFilters 변경 시 자동 조회 + 1페이지로 리셋 useEffect(() => { fetchData(); }, [fetchData]); + useEffect(() => { + setCurrentPage(1); + }, [searchFilters]); + const selectedPlan = useMemo(() => data.find(p => p.id === selectedId), [data, selectedId]); const groupedData = useMemo(() => { @@ -209,7 +219,7 @@ export default function ShippingPlanPage() { tableName="shipment_plan" filterId="c16-shipping-plan" onFilterChange={setSearchFilters} - dataCount={data.length} + dataCount={totalCount} externalFilterConfig={ts.filterConfig} /> @@ -224,7 +234,7 @@ export default function ShippingPlanPage() {
출하계획 목록 - {data.length}건 + {totalCount}건 {loading && }
@@ -262,7 +272,14 @@ export default function ShippingPlanPage() { showCheckbox checkedIds={checkedIds.map(String)} onCheckedChange={(ids) => setCheckedIds(ids.map(Number))} - showPagination={false} + showPagination + serverPagination + serverCurrentPage={currentPage} + serverPageSize={pageSize} + serverTotalCount={totalCount} + onServerPageChange={setCurrentPage} + onServerPageSizeChange={(s) => { setPageSize(s); setCurrentPage(1); }} + defaultPageSize={pageSize} draggableColumns={false} />
diff --git a/frontend/app/(main)/COMPANY_29/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_29/production/plan-management/page.tsx index 6eae857e..cd159a0b 100644 --- a/frontend/app/(main)/COMPANY_29/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_29/production/plan-management/page.tsx @@ -139,10 +139,11 @@ export default function ProductionPlanManagementPage() { // 데이터 상태 const [orderItems, setOrderItems] = useState([]); - // 좌측 수주목록 페이지네이션 + // 좌측 수주목록 페이지네이션 (서버 페이징) const [orderPage, setOrderPage] = useState(1); const [orderPageSize, setOrderPageSize] = useState(20); const [orderPageSizeInput, setOrderPageSizeInput] = useState("20"); + const [orderTotalCount, setOrderTotalCount] = useState(0); const [stockItems, setStockItems] = useState([]); const [finishedPlans, setFinishedPlans] = useState([]); const [semiPlans, setSemiPlans] = useState([]); @@ -210,22 +211,25 @@ export default function ProductionPlanManagementPage() { const res = await getOrderSummary({ excludePlanned: filterUnplannedOrdersOnly, itemCode: searchItemCode || undefined, + page: orderPage, + size: orderPageSize, }); - if (res.success) setOrderItems(res.data || []); + if (res.success) { + setOrderItems(res.data || []); + setOrderTotalCount(res.total ?? res.data?.length ?? 0); + } } catch (err: any) { toast.error("수주 데이터 조회 실패: " + (err.message || "")); } finally { setLoadingOrders(false); } - }, [filterUnplannedOrdersOnly, searchItemCode]); + }, [filterUnplannedOrdersOnly, searchItemCode, orderPage, orderPageSize]); // 수주목록 페이지네이션 계산 - const orderTotalPages = Math.max(1, Math.ceil(orderItems.length / orderPageSize)); + const orderTotalPages = Math.max(1, Math.ceil(orderTotalCount / orderPageSize)); const orderSafePage = Math.min(Math.max(1, orderPage), orderTotalPages); - const paginatedOrderItems = useMemo(() => { - const start = (orderSafePage - 1) * orderPageSize; - return orderItems.slice(start, start + orderPageSize); - }, [orderItems, orderSafePage, orderPageSize]); + // 서버 페이징: 응답 자체가 페이지 데이터이므로 slice 불필요 + const paginatedOrderItems = orderItems; const applyOrderPageSize = () => { const n = parseInt(orderPageSizeInput, 10); @@ -247,8 +251,8 @@ export default function ProductionPlanManagementPage() { return pages; }; - // orderItems 변경 시 1페이지로 리셋 - useEffect(() => { setOrderPage(1); }, [orderItems.length]); + // 검색 필터 변경 시 1페이지로 리셋 + useEffect(() => { setOrderPage(1); }, [filterUnplannedOrdersOnly, searchItemCode]); const fetchStockShortage = useCallback(async () => { setLoadingStock(true); @@ -265,19 +269,28 @@ export default function ProductionPlanManagementPage() { const fetchPlans = useCallback(async () => { setLoadingPlans(true); try { + // 타임라인 성능: 기간 필터 미입력 시 기본 오늘 ~ +60일 자동 적용 + // 이전 기록은 검색에서 시작일/종료일을 직접 지정하면 조회됨 + const today = new Date(); + const fmt = (d: Date) => d.toISOString().slice(0, 10); + const defaultStart = fmt(today); + const defaultEnd = fmt(new Date(today.getTime() + 60 * 86400000)); + const effectiveStart = searchStartDate || defaultStart; + const effectiveEnd = searchEndDate || defaultEnd; + const [finRes, semiRes] = await Promise.all([ getPlans({ productType: "완제품", status: searchStatus !== "all" ? searchStatus : undefined, - startDate: searchStartDate || undefined, - endDate: searchEndDate || undefined, + startDate: effectiveStart, + endDate: effectiveEnd, itemCode: searchItemCode || undefined, }), getPlans({ productType: "반제품", status: searchStatus !== "all" ? searchStatus : undefined, - startDate: searchStartDate || undefined, - endDate: searchEndDate || undefined, + startDate: effectiveStart, + endDate: effectiveEnd, }), ]); if (finRes.success) setFinishedPlans(finRes.data || []); @@ -329,11 +342,10 @@ export default function ProductionPlanManagementPage() { }, []); useEffect(() => { - if (searchFilters.length > 0) { - fetchOrderSummary(); - fetchPlans(); - } - }, [searchItemCode, searchStatus, searchStartDate, searchEndDate]); + // 검색 state 변경 시 자동 재조회 (필터 비어있어도 default 기간으로 재조회) + fetchOrderSummary(); + fetchPlans(); + }, [searchItemCode, searchStatus, searchStartDate, searchEndDate]); // eslint-disable-line react-hooks/exhaustive-deps // ========== 토글/선택 핸들러 ========== @@ -1516,6 +1528,7 @@ export default function ProductionPlanManagementPage() { onEventClick={openScheduleDetail} onEventMove={handleEventMove} onEventResize={handleEventResize} + onRangeChange={(s, e) => { setSearchStartDate(s); setSearchEndDate(e); }} /> @@ -1585,6 +1598,7 @@ export default function ProductionPlanManagementPage() { onEventClick={openScheduleDetail} onEventMove={handleEventMove} onEventResize={handleEventResize} + onRangeChange={(s, e) => { setSearchStartDate(s); setSearchEndDate(e); }} /> diff --git a/frontend/app/(main)/COMPANY_29/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_29/quality/item-inspection/page.tsx index 74fb5981..45042ac7 100644 --- a/frontend/app/(main)/COMPANY_29/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_29/quality/item-inspection/page.tsx @@ -33,6 +33,7 @@ const INSPECTION_TABLE = "inspection_standard"; const GRID_COLUMNS = [ { key: "item_code", label: "품목코드" }, { key: "item_name", label: "품명" }, + { key: "size", label: "규격" }, { key: "inspection_type", label: "검사유형" }, { key: "is_active", label: "사용여부" }, ]; @@ -74,7 +75,7 @@ export default function ItemInspectionInfoPage() { const [saving, setSaving] = useState(false); // FK 옵션 - const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; unit: string }[]>([]); + const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; size: string; unit: string }[]>([]); const [inspOptions, setInspOptions] = useState<{ code: string; label: string; detail: string; method: string; judgment_criteria: string; selection_options: string; unit: string; types: string[] }[]>([]); const [inspTypeCatOptions, setInspTypeCatOptions] = useState<{ code: string; label: string }[]>([]); @@ -130,6 +131,7 @@ export default function ItemInspectionInfoPage() { code: r.item_number || r.item_code || "", name: r.item_name || "", item_type: r.type || r.item_type || "", + size: r.size || "", unit: r.inventory_unit || "", }))); @@ -239,7 +241,7 @@ export default function ItemInspectionInfoPage() { const resData = res.data?.data; const rows = resData?.data || resData?.rows || []; const cm = itemCatMapRef.current; - setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: cm["type"]?.[r.type] || r.type || "", unit: cm["inventory_unit"]?.[r.inventory_unit] || r.inventory_unit || "" }))); + setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: cm["type"]?.[r.type] || r.type || "", size: r.size || "", unit: cm["inventory_unit"]?.[r.inventory_unit] || r.inventory_unit || "" }))); setItemTotal(resData?.total || resData?.totalCount || rows.length); } catch { /* skip */ } finally { setItemSearchLoading(false); } }; @@ -459,11 +461,13 @@ export default function ItemInspectionInfoPage() { // item_code별 그룹핑 const groupedData = useMemo(() => { - const map: Record = {}; + const itemSizeMap: Record = {}; + for (const it of itemOptions) itemSizeMap[it.code] = it.size || ""; + const map: Record = {}; for (const row of data) { const key = row.item_code || row.id; if (!map[key]) { - map[key] = { item_code: row.item_code, item_name: row.item_name, is_active: row.is_active || "", types: [], rows: [] }; + map[key] = { item_code: row.item_code, item_name: row.item_name, size: itemSizeMap[row.item_code] || "", is_active: row.is_active || "", types: [], rows: [] }; } map[key].rows.push(row); if (row.inspection_type && !map[key].types.includes(row.inspection_type)) { @@ -471,7 +475,7 @@ export default function ItemInspectionInfoPage() { } } return Object.values(map); - }, [data]); + }, [data, itemOptions]); // 좌측 품목 목록 정렬 (컬럼 헤더 클릭 → asc → desc → 해제 순환) const [sortConfig, setSortConfig] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -1052,12 +1056,13 @@ export default function ItemInspectionInfoPage() { switch (col.key) { case "item_code": return {group.item_code}; case "item_name": return {group.item_name}; + case "size": return {group.size}; case "inspection_type": return ( -
+
{group.types.map((t: string) => { const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t; - return {label}; + return {label}; })}
@@ -1252,17 +1257,19 @@ export default function ItemInspectionInfoPage() { 품목코드 품목명 + 규격 품목유형 단위 {filteredItems.length === 0 ? ( - {itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"} + {itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"} ) : filteredItems.map((item) => ( selectItem(item)}> {item.code} {item.name} + {item.size} {item.item_type} {item.unit} diff --git a/frontend/app/(main)/COMPANY_29/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_29/sales/shipping-order/page.tsx index 2ed29b40..a8c95d2e 100644 --- a/frontend/app/(main)/COMPANY_29/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_29/sales/shipping-order/page.tsx @@ -97,6 +97,11 @@ export default function ShippingOrderPage() { // 검색 필터 (DynamicSearchFilter에서 관리) const [searchFilters, setSearchFilters] = useState([]); + // 서버 페이징 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(50); + const [totalCount, setTotalCount] = useState(0); + // 엑셀 업로드 const [excelUploadOpen, setExcelUploadOpen] = useState(false); @@ -136,7 +141,7 @@ export default function ShippingOrderPage() { const fetchOrders = useCallback(async () => { setLoading(true); try { - const params: any = {}; + const params: any = { page: currentPage, size: pageSize }; for (const f of searchFilters) { if (f.columnName === "ship_date" && f.operator === "between" && f.value) { const [from, to] = f.value.split(","); @@ -152,18 +157,25 @@ export default function ShippingOrderPage() { } const result = await getShippingOrderList(params); - if (result.success) setOrders(result.data || []); + if (result.success) { + setOrders(result.data || []); + setTotalCount(result.total ?? result.data?.length ?? 0); + } } catch (err) { console.error("출하지시 조회 실패:", err); } finally { setLoading(false); } - }, [searchFilters]); + }, [searchFilters, currentPage, pageSize]); useEffect(() => { fetchOrders(); }, [fetchOrders]); + useEffect(() => { + setCurrentPage(1); + }, [searchFilters]); + // 소스 데이터 조회 const fetchSourceData = useCallback(async (pageOverride?: number) => { setSourceLoading(true); @@ -473,7 +485,7 @@ export default function ShippingOrderPage() { tableName={ts.tableName} filterId="c16-shipping-order" onFilterChange={setSearchFilters} - dataCount={orders.length} + dataCount={totalCount} externalFilterConfig={ts.filterConfig} /> @@ -482,7 +494,7 @@ export default function ShippingOrderPage() {

출하지시 관리

- {orders.length}건 + {totalCount}건 {loading && }
@@ -548,6 +560,13 @@ export default function ShippingOrderPage() { onRowClick={(row) => setSelectedOrderId(row._orderId)} onRowDoubleClick={(row) => openModal(row._order)} showPagination + serverPagination + serverCurrentPage={currentPage} + serverPageSize={pageSize} + serverTotalCount={totalCount} + onServerPageChange={setCurrentPage} + onServerPageSizeChange={(s) => { setPageSize(s); setCurrentPage(1); }} + defaultPageSize={pageSize} draggableColumns={false} columnOrderKey="c16-shipping-order" /> diff --git a/frontend/app/(main)/COMPANY_29/sales/shipping-plan/page.tsx b/frontend/app/(main)/COMPANY_29/sales/shipping-plan/page.tsx index 747ac23d..9e0398f9 100644 --- a/frontend/app/(main)/COMPANY_29/sales/shipping-plan/page.tsx +++ b/frontend/app/(main)/COMPANY_29/sales/shipping-plan/page.tsx @@ -69,6 +69,11 @@ export default function ShippingPlanPage() { // 검색 필터 (DynamicSearchFilter에서 관리) const [searchFilters, setSearchFilters] = useState([]); + // 서버 페이징 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(50); + const [totalCount, setTotalCount] = useState(0); + // 상세 패널 편집 const [editPlanQty, setEditPlanQty] = useState(""); const [editPlanDate, setEditPlanDate] = useState(""); @@ -81,7 +86,7 @@ export default function ShippingPlanPage() { const fetchData = useCallback(async () => { setLoading(true); try { - const params: any = {}; + const params: any = { page: currentPage, size: pageSize }; for (const f of searchFilters) { if (f.columnName === "plan_date" && f.operator === "between" && f.value) { const [from, to] = f.value.split(","); @@ -99,19 +104,24 @@ export default function ShippingPlanPage() { const result = await getShipmentPlanList(params); if (result.success) { setData(result.data || []); + setTotalCount(result.total ?? result.data?.length ?? 0); } } catch (err) { console.error("출하계획 조회 실패:", err); } finally { setLoading(false); } - }, [searchFilters]); + }, [searchFilters, currentPage, pageSize]); - // searchFilters 변경 시 자동 조회 + // searchFilters 변경 시 자동 조회 + 1페이지로 리셋 useEffect(() => { fetchData(); }, [fetchData]); + useEffect(() => { + setCurrentPage(1); + }, [searchFilters]); + const selectedPlan = useMemo(() => data.find(p => p.id === selectedId), [data, selectedId]); const groupedData = useMemo(() => { @@ -209,7 +219,7 @@ export default function ShippingPlanPage() { tableName="shipment_plan" filterId="c16-shipping-plan" onFilterChange={setSearchFilters} - dataCount={data.length} + dataCount={totalCount} externalFilterConfig={ts.filterConfig} /> @@ -224,7 +234,7 @@ export default function ShippingPlanPage() {
출하계획 목록 - {data.length}건 + {totalCount}건 {loading && }
@@ -262,7 +272,14 @@ export default function ShippingPlanPage() { showCheckbox checkedIds={checkedIds.map(String)} onCheckedChange={(ids) => setCheckedIds(ids.map(Number))} - showPagination={false} + showPagination + serverPagination + serverCurrentPage={currentPage} + serverPageSize={pageSize} + serverTotalCount={totalCount} + onServerPageChange={setCurrentPage} + onServerPageSizeChange={(s) => { setPageSize(s); setCurrentPage(1); }} + defaultPageSize={pageSize} draggableColumns={false} />
diff --git a/frontend/app/(main)/COMPANY_30/monitoring/equipment/page.tsx b/frontend/app/(main)/COMPANY_30/monitoring/equipment/page.tsx index b165ff65..692b2ca5 100644 --- a/frontend/app/(main)/COMPANY_30/monitoring/equipment/page.tsx +++ b/frontend/app/(main)/COMPANY_30/monitoring/equipment/page.tsx @@ -151,6 +151,27 @@ export default function EquipmentMonitoringPage() { const [filterStatus, setFilterStatus] = useState("all"); const autoRefreshRef = useRef(autoRefresh); + // 기간 필터 (기본: 오늘) + const todayStr = new Date().toISOString().slice(0, 10); + const [dateFrom, setDateFrom] = useState(todayStr); + const [dateTo, setDateTo] = useState(todayStr); + const setRangeToday = () => { const t = new Date().toISOString().slice(0, 10); setDateFrom(t); setDateTo(t); }; + const setRangeThisWeek = () => { + const now = new Date(); + const day = now.getDay() || 7; + const mon = new Date(now); mon.setDate(now.getDate() - (day - 1)); + const sun = new Date(mon); sun.setDate(mon.getDate() + 6); + setDateFrom(mon.toISOString().slice(0, 10)); + setDateTo(sun.toISOString().slice(0, 10)); + }; + const setRangeThisMonth = () => { + const now = new Date(); + const first = new Date(now.getFullYear(), now.getMonth(), 1); + const last = new Date(now.getFullYear(), now.getMonth() + 1, 0); + setDateFrom(first.toISOString().slice(0, 10)); + setDateTo(last.toISOString().slice(0, 10)); + }; + // autoRefreshRef 동기화 useEffect(() => { autoRefreshRef.current = autoRefresh; @@ -167,16 +188,27 @@ export default function EquipmentMonitoringPage() { try { setLoading(true); const [equipRes, wiRes, procRes] = await Promise.all([ + // 설비 마스터 (기간 무관, 전체) apiClient.post("/table-management/tables/equipment_mng/data", { autoFilter: true, page: 1, size: 500, }), - apiClient.get("/work-instruction/list").catch(() => ({ data: { data: [] } })), + // 작업지시 (시작일 기준 기간) + apiClient.get("/work-instruction/list", { params: { dateFrom, dateTo } }) + .catch(() => ({ data: { data: [] } })), + // 작업공정 (생성일 기준 기간) apiClient.post("/table-management/tables/work_order_process/data", { page: 1, size: 2000, autoFilter: true, + dataFilter: { + enabled: true, + filters: [ + { columnName: "created_date", operator: "greater_or_equal", value: `${dateFrom} 00:00:00` }, + { columnName: "created_date", operator: "less_or_equal", value: `${dateTo} 23:59:59` }, + ], + }, }).catch(() => ({ data: { data: { data: [] } } })), ]); @@ -193,7 +225,7 @@ export default function EquipmentMonitoringPage() { } finally { setLoading(false); } - }, []); + }, [dateFrom, dateTo]); useEffect(() => { fetchData(); @@ -384,9 +416,9 @@ export default function EquipmentMonitoringPage() { /* ────────────── 렌더 ────────────── */ return ( -
+
{/* ── 헤더 ── */} -
+

설비운영모니터링

@@ -443,8 +475,21 @@ export default function EquipmentMonitoringPage() {
+ {/* ── 기간 필터 ── */} +
+ 조회 기간 + setDateFrom(e.target.value)} + className={cn("h-8 rounded border px-2 text-sm", theme.cardBorder, theme.card, theme.text)} /> + ~ + setDateTo(e.target.value)} + className={cn("h-8 rounded border px-2 text-sm", theme.cardBorder, theme.card, theme.text)} /> + + + +
+ {/* ── 요약 카드 5개 ── */} -
+
{summaryCards.map((card) => (
- {/* ── 로딩 ── */} + {/* ── 스크롤 영역: 로딩 / 데이터 없음 / 설비 카드 그리드 ── */} +
{loading && equipments.length === 0 && (
@@ -494,7 +540,6 @@ export default function EquipmentMonitoringPage() {
)} - {/* ── 데이터 없음 ── */} {!loading && equipments.length === 0 && (
@@ -502,7 +547,6 @@ export default function EquipmentMonitoringPage() {
)} - {/* ── 설비 카드 그리드 ── */} {filteredEquipments.length > 0 && (
{filteredEquipments.map((eq) => { @@ -661,6 +705,7 @@ export default function EquipmentMonitoringPage() {

해당 상태의 설비가 없습니다.

)} +
); } diff --git a/frontend/app/(main)/COMPANY_30/monitoring/production/page.tsx b/frontend/app/(main)/COMPANY_30/monitoring/production/page.tsx index 820a30d5..d9fa0fa8 100644 --- a/frontend/app/(main)/COMPANY_30/monitoring/production/page.tsx +++ b/frontend/app/(main)/COMPANY_30/monitoring/production/page.tsx @@ -120,6 +120,27 @@ export default function ProductionMonitoringPage() { const [autoRefresh, setAutoRefresh] = useState(settings.autoRefresh); const [activeTab, setActiveTab] = useState("전체"); + // ─── 기간 필터 (기본: 오늘) ────────────────────────────────── + const todayStr = new Date().toISOString().slice(0, 10); + const [dateFrom, setDateFrom] = useState(todayStr); + const [dateTo, setDateTo] = useState(todayStr); + const setRangeToday = () => { const t = new Date().toISOString().slice(0, 10); setDateFrom(t); setDateTo(t); }; + const setRangeThisWeek = () => { + const now = new Date(); + const day = now.getDay() || 7; // Sun=0 → 7 + const mon = new Date(now); mon.setDate(now.getDate() - (day - 1)); + const sun = new Date(mon); sun.setDate(mon.getDate() + 6); + setDateFrom(mon.toISOString().slice(0, 10)); + setDateTo(sun.toISOString().slice(0, 10)); + }; + const setRangeThisMonth = () => { + const now = new Date(); + const first = new Date(now.getFullYear(), now.getMonth(), 1); + const last = new Date(now.getFullYear(), now.getMonth() + 1, 0); + setDateFrom(first.toISOString().slice(0, 10)); + setDateTo(last.toISOString().slice(0, 10)); + }; + // ─── 실시간 시계 ───────────────────────────────────────── useEffect(() => { const timer = setInterval(() => setCurrentTime(new Date()), 1000); @@ -131,8 +152,10 @@ export default function ProductionMonitoringPage() { try { setLoading(true); - // 작업지시 목록 조회 - const wiRes = await apiClient.get("/work-instruction/list"); + // 작업지시 목록 조회 — 기간(시작일) 필터 적용 + const wiRes = await apiClient.get("/work-instruction/list", { + params: { dateFrom, dateTo }, + }); const wiRaw: WorkInstruction[] = wiRes.data?.success && Array.isArray(wiRes.data.data) ? wiRes.data.data : []; // 작업지시번호 기준 중복 제거 (디테일 JOIN으로 중복 발생 가능) const seen = new Set(); @@ -144,12 +167,19 @@ export default function ProductionMonitoringPage() { }); setWorkInstructions(wiData); - // 공정현황 조회 (실패해도 작업지시는 표시) + // 공정현황 조회 (실패해도 작업지시는 표시) — 생성일(created_date) 기준 기간 필터 try { const procRes = await apiClient.post("/table-management/tables/work_order_process/data", { page: 1, size: 1000, autoFilter: true, + dataFilter: { + enabled: true, + filters: [ + { columnName: "created_date", operator: "greater_or_equal", value: `${dateFrom} 00:00:00` }, + { columnName: "created_date", operator: "less_or_equal", value: `${dateTo} 23:59:59` }, + ], + }, }); const rows: ProcessStep[] = procRes.data?.data?.data || procRes.data?.data?.rows || procRes.data?.data || []; @@ -195,7 +225,7 @@ export default function ProductionMonitoringPage() { } finally { setLoading(false); } - }, []); + }, [dateFrom, dateTo]); useEffect(() => { fetchData(); @@ -241,9 +271,9 @@ export default function ProductionMonitoringPage() { // ─── 렌더링 ────────────────────────────────────────────── return ( -
+
{/* 헤더 */} -
+

생산모니터링

@@ -275,8 +305,21 @@ export default function ProductionMonitoringPage() {
+ {/* 기간 필터 */} +
+ 조회 기간 + setDateFrom(e.target.value)} + className={cn("h-8 rounded border px-2 text-sm", theme.cardBorder, theme.card, theme.text)} /> + ~ + setDateTo(e.target.value)} + className={cn("h-8 rounded border px-2 text-sm", theme.cardBorder, theme.card, theme.text)} /> + + + +
+ {/* 요약 카드 */} -
+
} label="대기중" @@ -304,7 +347,7 @@ export default function ProductionMonitoringPage() {
{/* 탭 필터 */} -
+
{(["전체", "대기", "진행중", "완료"] as FilterTab[]).map((tab) => ( + + +
+ {/* 요약 카드 */} -
+
{summaryCards.map((card) => (

{card.label}

@@ -280,7 +323,7 @@ export default function QualityMonitoringPage() {
{/* 검사유형 탭 */} -
+
{TABS.map((tab) => (
diff --git a/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx index 5e9874ac..76e88f1c 100644 --- a/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_7/quality/item-inspection/page.tsx @@ -36,6 +36,7 @@ const INSPECTION_TABLE = "inspection_standard"; const GRID_COLUMNS = [ { key: "item_code", label: "품목코드" }, { key: "item_name", label: "품명" }, + { key: "size", label: "규격" }, { key: "inspection_type", label: "검사유형" }, { key: "is_active", label: "사용여부" }, ]; @@ -103,7 +104,7 @@ export default function ItemInspectionInfoPage() { const [saving, setSaving] = useState(false); // FK 옵션 - const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; unit: string }[]>([]); + const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; size: string; unit: string }[]>([]); const [inspOptions, setInspOptions] = useState<{ code: string; label: string; detail: string; method: string; judgment_criteria: string; selection_options: string; unit: string; types: string[] }[]>([]); const [inspTypeCatOptions, setInspTypeCatOptions] = useState<{ code: string; label: string }[]>([]); @@ -159,6 +160,7 @@ export default function ItemInspectionInfoPage() { code: r.item_number || r.item_code || "", name: r.item_name || "", item_type: r.type || r.item_type || "", + size: r.size || "", unit: r.inventory_unit || "", }))); @@ -268,7 +270,7 @@ export default function ItemInspectionInfoPage() { const resData = res.data?.data; const rows = resData?.data || resData?.rows || []; const cm = itemCatMapRef.current; - setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: cm["type"]?.[r.type] || r.type || "", unit: cm["inventory_unit"]?.[r.inventory_unit] || r.inventory_unit || "" }))); + setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: cm["type"]?.[r.type] || r.type || "", size: r.size || "", unit: cm["inventory_unit"]?.[r.inventory_unit] || r.inventory_unit || "" }))); setItemTotal(resData?.total || resData?.totalCount || rows.length); } catch { /* skip */ } finally { setItemSearchLoading(false); } }; @@ -488,11 +490,13 @@ export default function ItemInspectionInfoPage() { // item_code별 그룹핑 const groupedData = useMemo(() => { - const map: Record = {}; + const itemSizeMap: Record = {}; + for (const it of itemOptions) itemSizeMap[it.code] = it.size || ""; + const map: Record = {}; for (const row of data) { const key = row.item_code || row.id; if (!map[key]) { - map[key] = { item_code: row.item_code, item_name: row.item_name, is_active: row.is_active || "", types: [], rows: [] }; + map[key] = { item_code: row.item_code, item_name: row.item_name, size: itemSizeMap[row.item_code] || "", is_active: row.is_active || "", types: [], rows: [] }; } map[key].rows.push(row); if (row.inspection_type && !map[key].types.includes(row.inspection_type)) { @@ -500,7 +504,7 @@ export default function ItemInspectionInfoPage() { } } return Object.values(map); - }, [data]); + }, [data, itemOptions]); // 좌측 품목 목록 정렬 (컬럼 헤더 클릭 → asc → desc → 해제 순환) const [sortConfig, setSortConfig] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -1090,12 +1094,13 @@ export default function ItemInspectionInfoPage() { switch (col.key) { case "item_code": return {group.item_code}; case "item_name": return {group.item_name}; + case "size": return {group.size}; case "inspection_type": return ( -
+
{group.types.map((t: string) => { const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t; - return {label}; + return {label}; })}
@@ -1290,17 +1295,19 @@ export default function ItemInspectionInfoPage() { 품목코드 품목명 + 규격 품목유형 단위 {filteredItems.length === 0 ? ( - {itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"} + {itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"} ) : filteredItems.map((item) => ( selectItem(item)}> {item.code} {item.name} + {item.size} {item.item_type} {item.unit} diff --git a/frontend/app/(main)/COMPANY_7/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_7/sales/shipping-order/page.tsx index 2ed29b40..a8c95d2e 100644 --- a/frontend/app/(main)/COMPANY_7/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_7/sales/shipping-order/page.tsx @@ -97,6 +97,11 @@ export default function ShippingOrderPage() { // 검색 필터 (DynamicSearchFilter에서 관리) const [searchFilters, setSearchFilters] = useState([]); + // 서버 페이징 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(50); + const [totalCount, setTotalCount] = useState(0); + // 엑셀 업로드 const [excelUploadOpen, setExcelUploadOpen] = useState(false); @@ -136,7 +141,7 @@ export default function ShippingOrderPage() { const fetchOrders = useCallback(async () => { setLoading(true); try { - const params: any = {}; + const params: any = { page: currentPage, size: pageSize }; for (const f of searchFilters) { if (f.columnName === "ship_date" && f.operator === "between" && f.value) { const [from, to] = f.value.split(","); @@ -152,18 +157,25 @@ export default function ShippingOrderPage() { } const result = await getShippingOrderList(params); - if (result.success) setOrders(result.data || []); + if (result.success) { + setOrders(result.data || []); + setTotalCount(result.total ?? result.data?.length ?? 0); + } } catch (err) { console.error("출하지시 조회 실패:", err); } finally { setLoading(false); } - }, [searchFilters]); + }, [searchFilters, currentPage, pageSize]); useEffect(() => { fetchOrders(); }, [fetchOrders]); + useEffect(() => { + setCurrentPage(1); + }, [searchFilters]); + // 소스 데이터 조회 const fetchSourceData = useCallback(async (pageOverride?: number) => { setSourceLoading(true); @@ -473,7 +485,7 @@ export default function ShippingOrderPage() { tableName={ts.tableName} filterId="c16-shipping-order" onFilterChange={setSearchFilters} - dataCount={orders.length} + dataCount={totalCount} externalFilterConfig={ts.filterConfig} /> @@ -482,7 +494,7 @@ export default function ShippingOrderPage() {

출하지시 관리

- {orders.length}건 + {totalCount}건 {loading && }
@@ -548,6 +560,13 @@ export default function ShippingOrderPage() { onRowClick={(row) => setSelectedOrderId(row._orderId)} onRowDoubleClick={(row) => openModal(row._order)} showPagination + serverPagination + serverCurrentPage={currentPage} + serverPageSize={pageSize} + serverTotalCount={totalCount} + onServerPageChange={setCurrentPage} + onServerPageSizeChange={(s) => { setPageSize(s); setCurrentPage(1); }} + defaultPageSize={pageSize} draggableColumns={false} columnOrderKey="c16-shipping-order" /> diff --git a/frontend/app/(main)/COMPANY_7/sales/shipping-plan/page.tsx b/frontend/app/(main)/COMPANY_7/sales/shipping-plan/page.tsx index 747ac23d..9e0398f9 100644 --- a/frontend/app/(main)/COMPANY_7/sales/shipping-plan/page.tsx +++ b/frontend/app/(main)/COMPANY_7/sales/shipping-plan/page.tsx @@ -69,6 +69,11 @@ export default function ShippingPlanPage() { // 검색 필터 (DynamicSearchFilter에서 관리) const [searchFilters, setSearchFilters] = useState([]); + // 서버 페이징 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(50); + const [totalCount, setTotalCount] = useState(0); + // 상세 패널 편집 const [editPlanQty, setEditPlanQty] = useState(""); const [editPlanDate, setEditPlanDate] = useState(""); @@ -81,7 +86,7 @@ export default function ShippingPlanPage() { const fetchData = useCallback(async () => { setLoading(true); try { - const params: any = {}; + const params: any = { page: currentPage, size: pageSize }; for (const f of searchFilters) { if (f.columnName === "plan_date" && f.operator === "between" && f.value) { const [from, to] = f.value.split(","); @@ -99,19 +104,24 @@ export default function ShippingPlanPage() { const result = await getShipmentPlanList(params); if (result.success) { setData(result.data || []); + setTotalCount(result.total ?? result.data?.length ?? 0); } } catch (err) { console.error("출하계획 조회 실패:", err); } finally { setLoading(false); } - }, [searchFilters]); + }, [searchFilters, currentPage, pageSize]); - // searchFilters 변경 시 자동 조회 + // searchFilters 변경 시 자동 조회 + 1페이지로 리셋 useEffect(() => { fetchData(); }, [fetchData]); + useEffect(() => { + setCurrentPage(1); + }, [searchFilters]); + const selectedPlan = useMemo(() => data.find(p => p.id === selectedId), [data, selectedId]); const groupedData = useMemo(() => { @@ -209,7 +219,7 @@ export default function ShippingPlanPage() { tableName="shipment_plan" filterId="c16-shipping-plan" onFilterChange={setSearchFilters} - dataCount={data.length} + dataCount={totalCount} externalFilterConfig={ts.filterConfig} /> @@ -224,7 +234,7 @@ export default function ShippingPlanPage() {
출하계획 목록 - {data.length}건 + {totalCount}건 {loading && }
@@ -262,7 +272,14 @@ export default function ShippingPlanPage() { showCheckbox checkedIds={checkedIds.map(String)} onCheckedChange={(ids) => setCheckedIds(ids.map(Number))} - showPagination={false} + showPagination + serverPagination + serverCurrentPage={currentPage} + serverPageSize={pageSize} + serverTotalCount={totalCount} + onServerPageChange={setCurrentPage} + onServerPageSizeChange={(s) => { setPageSize(s); setCurrentPage(1); }} + defaultPageSize={pageSize} draggableColumns={false} />
diff --git a/frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx index 6eae857e..cd159a0b 100644 --- a/frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_8/production/plan-management/page.tsx @@ -139,10 +139,11 @@ export default function ProductionPlanManagementPage() { // 데이터 상태 const [orderItems, setOrderItems] = useState([]); - // 좌측 수주목록 페이지네이션 + // 좌측 수주목록 페이지네이션 (서버 페이징) const [orderPage, setOrderPage] = useState(1); const [orderPageSize, setOrderPageSize] = useState(20); const [orderPageSizeInput, setOrderPageSizeInput] = useState("20"); + const [orderTotalCount, setOrderTotalCount] = useState(0); const [stockItems, setStockItems] = useState([]); const [finishedPlans, setFinishedPlans] = useState([]); const [semiPlans, setSemiPlans] = useState([]); @@ -210,22 +211,25 @@ export default function ProductionPlanManagementPage() { const res = await getOrderSummary({ excludePlanned: filterUnplannedOrdersOnly, itemCode: searchItemCode || undefined, + page: orderPage, + size: orderPageSize, }); - if (res.success) setOrderItems(res.data || []); + if (res.success) { + setOrderItems(res.data || []); + setOrderTotalCount(res.total ?? res.data?.length ?? 0); + } } catch (err: any) { toast.error("수주 데이터 조회 실패: " + (err.message || "")); } finally { setLoadingOrders(false); } - }, [filterUnplannedOrdersOnly, searchItemCode]); + }, [filterUnplannedOrdersOnly, searchItemCode, orderPage, orderPageSize]); // 수주목록 페이지네이션 계산 - const orderTotalPages = Math.max(1, Math.ceil(orderItems.length / orderPageSize)); + const orderTotalPages = Math.max(1, Math.ceil(orderTotalCount / orderPageSize)); const orderSafePage = Math.min(Math.max(1, orderPage), orderTotalPages); - const paginatedOrderItems = useMemo(() => { - const start = (orderSafePage - 1) * orderPageSize; - return orderItems.slice(start, start + orderPageSize); - }, [orderItems, orderSafePage, orderPageSize]); + // 서버 페이징: 응답 자체가 페이지 데이터이므로 slice 불필요 + const paginatedOrderItems = orderItems; const applyOrderPageSize = () => { const n = parseInt(orderPageSizeInput, 10); @@ -247,8 +251,8 @@ export default function ProductionPlanManagementPage() { return pages; }; - // orderItems 변경 시 1페이지로 리셋 - useEffect(() => { setOrderPage(1); }, [orderItems.length]); + // 검색 필터 변경 시 1페이지로 리셋 + useEffect(() => { setOrderPage(1); }, [filterUnplannedOrdersOnly, searchItemCode]); const fetchStockShortage = useCallback(async () => { setLoadingStock(true); @@ -265,19 +269,28 @@ export default function ProductionPlanManagementPage() { const fetchPlans = useCallback(async () => { setLoadingPlans(true); try { + // 타임라인 성능: 기간 필터 미입력 시 기본 오늘 ~ +60일 자동 적용 + // 이전 기록은 검색에서 시작일/종료일을 직접 지정하면 조회됨 + const today = new Date(); + const fmt = (d: Date) => d.toISOString().slice(0, 10); + const defaultStart = fmt(today); + const defaultEnd = fmt(new Date(today.getTime() + 60 * 86400000)); + const effectiveStart = searchStartDate || defaultStart; + const effectiveEnd = searchEndDate || defaultEnd; + const [finRes, semiRes] = await Promise.all([ getPlans({ productType: "완제품", status: searchStatus !== "all" ? searchStatus : undefined, - startDate: searchStartDate || undefined, - endDate: searchEndDate || undefined, + startDate: effectiveStart, + endDate: effectiveEnd, itemCode: searchItemCode || undefined, }), getPlans({ productType: "반제품", status: searchStatus !== "all" ? searchStatus : undefined, - startDate: searchStartDate || undefined, - endDate: searchEndDate || undefined, + startDate: effectiveStart, + endDate: effectiveEnd, }), ]); if (finRes.success) setFinishedPlans(finRes.data || []); @@ -329,11 +342,10 @@ export default function ProductionPlanManagementPage() { }, []); useEffect(() => { - if (searchFilters.length > 0) { - fetchOrderSummary(); - fetchPlans(); - } - }, [searchItemCode, searchStatus, searchStartDate, searchEndDate]); + // 검색 state 변경 시 자동 재조회 (필터 비어있어도 default 기간으로 재조회) + fetchOrderSummary(); + fetchPlans(); + }, [searchItemCode, searchStatus, searchStartDate, searchEndDate]); // eslint-disable-line react-hooks/exhaustive-deps // ========== 토글/선택 핸들러 ========== @@ -1516,6 +1528,7 @@ export default function ProductionPlanManagementPage() { onEventClick={openScheduleDetail} onEventMove={handleEventMove} onEventResize={handleEventResize} + onRangeChange={(s, e) => { setSearchStartDate(s); setSearchEndDate(e); }} />
@@ -1585,6 +1598,7 @@ export default function ProductionPlanManagementPage() { onEventClick={openScheduleDetail} onEventMove={handleEventMove} onEventResize={handleEventResize} + onRangeChange={(s, e) => { setSearchStartDate(s); setSearchEndDate(e); }} />
diff --git a/frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx index 74fb5981..45042ac7 100644 --- a/frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_8/quality/item-inspection/page.tsx @@ -33,6 +33,7 @@ const INSPECTION_TABLE = "inspection_standard"; const GRID_COLUMNS = [ { key: "item_code", label: "품목코드" }, { key: "item_name", label: "품명" }, + { key: "size", label: "규격" }, { key: "inspection_type", label: "검사유형" }, { key: "is_active", label: "사용여부" }, ]; @@ -74,7 +75,7 @@ export default function ItemInspectionInfoPage() { const [saving, setSaving] = useState(false); // FK 옵션 - const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; unit: string }[]>([]); + const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; size: string; unit: string }[]>([]); const [inspOptions, setInspOptions] = useState<{ code: string; label: string; detail: string; method: string; judgment_criteria: string; selection_options: string; unit: string; types: string[] }[]>([]); const [inspTypeCatOptions, setInspTypeCatOptions] = useState<{ code: string; label: string }[]>([]); @@ -130,6 +131,7 @@ export default function ItemInspectionInfoPage() { code: r.item_number || r.item_code || "", name: r.item_name || "", item_type: r.type || r.item_type || "", + size: r.size || "", unit: r.inventory_unit || "", }))); @@ -239,7 +241,7 @@ export default function ItemInspectionInfoPage() { const resData = res.data?.data; const rows = resData?.data || resData?.rows || []; const cm = itemCatMapRef.current; - setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: cm["type"]?.[r.type] || r.type || "", unit: cm["inventory_unit"]?.[r.inventory_unit] || r.inventory_unit || "" }))); + setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: cm["type"]?.[r.type] || r.type || "", size: r.size || "", unit: cm["inventory_unit"]?.[r.inventory_unit] || r.inventory_unit || "" }))); setItemTotal(resData?.total || resData?.totalCount || rows.length); } catch { /* skip */ } finally { setItemSearchLoading(false); } }; @@ -459,11 +461,13 @@ export default function ItemInspectionInfoPage() { // item_code별 그룹핑 const groupedData = useMemo(() => { - const map: Record = {}; + const itemSizeMap: Record = {}; + for (const it of itemOptions) itemSizeMap[it.code] = it.size || ""; + const map: Record = {}; for (const row of data) { const key = row.item_code || row.id; if (!map[key]) { - map[key] = { item_code: row.item_code, item_name: row.item_name, is_active: row.is_active || "", types: [], rows: [] }; + map[key] = { item_code: row.item_code, item_name: row.item_name, size: itemSizeMap[row.item_code] || "", is_active: row.is_active || "", types: [], rows: [] }; } map[key].rows.push(row); if (row.inspection_type && !map[key].types.includes(row.inspection_type)) { @@ -471,7 +475,7 @@ export default function ItemInspectionInfoPage() { } } return Object.values(map); - }, [data]); + }, [data, itemOptions]); // 좌측 품목 목록 정렬 (컬럼 헤더 클릭 → asc → desc → 해제 순환) const [sortConfig, setSortConfig] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -1052,12 +1056,13 @@ export default function ItemInspectionInfoPage() { switch (col.key) { case "item_code": return {group.item_code}; case "item_name": return {group.item_name}; + case "size": return {group.size}; case "inspection_type": return ( -
+
{group.types.map((t: string) => { const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t; - return {label}; + return {label}; })}
@@ -1252,17 +1257,19 @@ export default function ItemInspectionInfoPage() { 품목코드 품목명 + 규격 품목유형 단위 {filteredItems.length === 0 ? ( - {itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"} + {itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"} ) : filteredItems.map((item) => ( selectItem(item)}> {item.code} {item.name} + {item.size} {item.item_type} {item.unit} diff --git a/frontend/app/(main)/COMPANY_8/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_8/sales/shipping-order/page.tsx index 2ed29b40..a8c95d2e 100644 --- a/frontend/app/(main)/COMPANY_8/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_8/sales/shipping-order/page.tsx @@ -97,6 +97,11 @@ export default function ShippingOrderPage() { // 검색 필터 (DynamicSearchFilter에서 관리) const [searchFilters, setSearchFilters] = useState([]); + // 서버 페이징 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(50); + const [totalCount, setTotalCount] = useState(0); + // 엑셀 업로드 const [excelUploadOpen, setExcelUploadOpen] = useState(false); @@ -136,7 +141,7 @@ export default function ShippingOrderPage() { const fetchOrders = useCallback(async () => { setLoading(true); try { - const params: any = {}; + const params: any = { page: currentPage, size: pageSize }; for (const f of searchFilters) { if (f.columnName === "ship_date" && f.operator === "between" && f.value) { const [from, to] = f.value.split(","); @@ -152,18 +157,25 @@ export default function ShippingOrderPage() { } const result = await getShippingOrderList(params); - if (result.success) setOrders(result.data || []); + if (result.success) { + setOrders(result.data || []); + setTotalCount(result.total ?? result.data?.length ?? 0); + } } catch (err) { console.error("출하지시 조회 실패:", err); } finally { setLoading(false); } - }, [searchFilters]); + }, [searchFilters, currentPage, pageSize]); useEffect(() => { fetchOrders(); }, [fetchOrders]); + useEffect(() => { + setCurrentPage(1); + }, [searchFilters]); + // 소스 데이터 조회 const fetchSourceData = useCallback(async (pageOverride?: number) => { setSourceLoading(true); @@ -473,7 +485,7 @@ export default function ShippingOrderPage() { tableName={ts.tableName} filterId="c16-shipping-order" onFilterChange={setSearchFilters} - dataCount={orders.length} + dataCount={totalCount} externalFilterConfig={ts.filterConfig} /> @@ -482,7 +494,7 @@ export default function ShippingOrderPage() {

출하지시 관리

- {orders.length}건 + {totalCount}건 {loading && }
@@ -548,6 +560,13 @@ export default function ShippingOrderPage() { onRowClick={(row) => setSelectedOrderId(row._orderId)} onRowDoubleClick={(row) => openModal(row._order)} showPagination + serverPagination + serverCurrentPage={currentPage} + serverPageSize={pageSize} + serverTotalCount={totalCount} + onServerPageChange={setCurrentPage} + onServerPageSizeChange={(s) => { setPageSize(s); setCurrentPage(1); }} + defaultPageSize={pageSize} draggableColumns={false} columnOrderKey="c16-shipping-order" /> diff --git a/frontend/app/(main)/COMPANY_8/sales/shipping-plan/page.tsx b/frontend/app/(main)/COMPANY_8/sales/shipping-plan/page.tsx index 747ac23d..9e0398f9 100644 --- a/frontend/app/(main)/COMPANY_8/sales/shipping-plan/page.tsx +++ b/frontend/app/(main)/COMPANY_8/sales/shipping-plan/page.tsx @@ -69,6 +69,11 @@ export default function ShippingPlanPage() { // 검색 필터 (DynamicSearchFilter에서 관리) const [searchFilters, setSearchFilters] = useState([]); + // 서버 페이징 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(50); + const [totalCount, setTotalCount] = useState(0); + // 상세 패널 편집 const [editPlanQty, setEditPlanQty] = useState(""); const [editPlanDate, setEditPlanDate] = useState(""); @@ -81,7 +86,7 @@ export default function ShippingPlanPage() { const fetchData = useCallback(async () => { setLoading(true); try { - const params: any = {}; + const params: any = { page: currentPage, size: pageSize }; for (const f of searchFilters) { if (f.columnName === "plan_date" && f.operator === "between" && f.value) { const [from, to] = f.value.split(","); @@ -99,19 +104,24 @@ export default function ShippingPlanPage() { const result = await getShipmentPlanList(params); if (result.success) { setData(result.data || []); + setTotalCount(result.total ?? result.data?.length ?? 0); } } catch (err) { console.error("출하계획 조회 실패:", err); } finally { setLoading(false); } - }, [searchFilters]); + }, [searchFilters, currentPage, pageSize]); - // searchFilters 변경 시 자동 조회 + // searchFilters 변경 시 자동 조회 + 1페이지로 리셋 useEffect(() => { fetchData(); }, [fetchData]); + useEffect(() => { + setCurrentPage(1); + }, [searchFilters]); + const selectedPlan = useMemo(() => data.find(p => p.id === selectedId), [data, selectedId]); const groupedData = useMemo(() => { @@ -209,7 +219,7 @@ export default function ShippingPlanPage() { tableName="shipment_plan" filterId="c16-shipping-plan" onFilterChange={setSearchFilters} - dataCount={data.length} + dataCount={totalCount} externalFilterConfig={ts.filterConfig} /> @@ -224,7 +234,7 @@ export default function ShippingPlanPage() {
출하계획 목록 - {data.length}건 + {totalCount}건 {loading && }
@@ -262,7 +272,14 @@ export default function ShippingPlanPage() { showCheckbox checkedIds={checkedIds.map(String)} onCheckedChange={(ids) => setCheckedIds(ids.map(Number))} - showPagination={false} + showPagination + serverPagination + serverCurrentPage={currentPage} + serverPageSize={pageSize} + serverTotalCount={totalCount} + onServerPageChange={setCurrentPage} + onServerPageSizeChange={(s) => { setPageSize(s); setCurrentPage(1); }} + defaultPageSize={pageSize} draggableColumns={false} />
diff --git a/frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx b/frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx index 6eae857e..cd159a0b 100644 --- a/frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx +++ b/frontend/app/(main)/COMPANY_9/production/plan-management/page.tsx @@ -139,10 +139,11 @@ export default function ProductionPlanManagementPage() { // 데이터 상태 const [orderItems, setOrderItems] = useState([]); - // 좌측 수주목록 페이지네이션 + // 좌측 수주목록 페이지네이션 (서버 페이징) const [orderPage, setOrderPage] = useState(1); const [orderPageSize, setOrderPageSize] = useState(20); const [orderPageSizeInput, setOrderPageSizeInput] = useState("20"); + const [orderTotalCount, setOrderTotalCount] = useState(0); const [stockItems, setStockItems] = useState([]); const [finishedPlans, setFinishedPlans] = useState([]); const [semiPlans, setSemiPlans] = useState([]); @@ -210,22 +211,25 @@ export default function ProductionPlanManagementPage() { const res = await getOrderSummary({ excludePlanned: filterUnplannedOrdersOnly, itemCode: searchItemCode || undefined, + page: orderPage, + size: orderPageSize, }); - if (res.success) setOrderItems(res.data || []); + if (res.success) { + setOrderItems(res.data || []); + setOrderTotalCount(res.total ?? res.data?.length ?? 0); + } } catch (err: any) { toast.error("수주 데이터 조회 실패: " + (err.message || "")); } finally { setLoadingOrders(false); } - }, [filterUnplannedOrdersOnly, searchItemCode]); + }, [filterUnplannedOrdersOnly, searchItemCode, orderPage, orderPageSize]); // 수주목록 페이지네이션 계산 - const orderTotalPages = Math.max(1, Math.ceil(orderItems.length / orderPageSize)); + const orderTotalPages = Math.max(1, Math.ceil(orderTotalCount / orderPageSize)); const orderSafePage = Math.min(Math.max(1, orderPage), orderTotalPages); - const paginatedOrderItems = useMemo(() => { - const start = (orderSafePage - 1) * orderPageSize; - return orderItems.slice(start, start + orderPageSize); - }, [orderItems, orderSafePage, orderPageSize]); + // 서버 페이징: 응답 자체가 페이지 데이터이므로 slice 불필요 + const paginatedOrderItems = orderItems; const applyOrderPageSize = () => { const n = parseInt(orderPageSizeInput, 10); @@ -247,8 +251,8 @@ export default function ProductionPlanManagementPage() { return pages; }; - // orderItems 변경 시 1페이지로 리셋 - useEffect(() => { setOrderPage(1); }, [orderItems.length]); + // 검색 필터 변경 시 1페이지로 리셋 + useEffect(() => { setOrderPage(1); }, [filterUnplannedOrdersOnly, searchItemCode]); const fetchStockShortage = useCallback(async () => { setLoadingStock(true); @@ -265,19 +269,28 @@ export default function ProductionPlanManagementPage() { const fetchPlans = useCallback(async () => { setLoadingPlans(true); try { + // 타임라인 성능: 기간 필터 미입력 시 기본 오늘 ~ +60일 자동 적용 + // 이전 기록은 검색에서 시작일/종료일을 직접 지정하면 조회됨 + const today = new Date(); + const fmt = (d: Date) => d.toISOString().slice(0, 10); + const defaultStart = fmt(today); + const defaultEnd = fmt(new Date(today.getTime() + 60 * 86400000)); + const effectiveStart = searchStartDate || defaultStart; + const effectiveEnd = searchEndDate || defaultEnd; + const [finRes, semiRes] = await Promise.all([ getPlans({ productType: "완제품", status: searchStatus !== "all" ? searchStatus : undefined, - startDate: searchStartDate || undefined, - endDate: searchEndDate || undefined, + startDate: effectiveStart, + endDate: effectiveEnd, itemCode: searchItemCode || undefined, }), getPlans({ productType: "반제품", status: searchStatus !== "all" ? searchStatus : undefined, - startDate: searchStartDate || undefined, - endDate: searchEndDate || undefined, + startDate: effectiveStart, + endDate: effectiveEnd, }), ]); if (finRes.success) setFinishedPlans(finRes.data || []); @@ -329,11 +342,10 @@ export default function ProductionPlanManagementPage() { }, []); useEffect(() => { - if (searchFilters.length > 0) { - fetchOrderSummary(); - fetchPlans(); - } - }, [searchItemCode, searchStatus, searchStartDate, searchEndDate]); + // 검색 state 변경 시 자동 재조회 (필터 비어있어도 default 기간으로 재조회) + fetchOrderSummary(); + fetchPlans(); + }, [searchItemCode, searchStatus, searchStartDate, searchEndDate]); // eslint-disable-line react-hooks/exhaustive-deps // ========== 토글/선택 핸들러 ========== @@ -1516,6 +1528,7 @@ export default function ProductionPlanManagementPage() { onEventClick={openScheduleDetail} onEventMove={handleEventMove} onEventResize={handleEventResize} + onRangeChange={(s, e) => { setSearchStartDate(s); setSearchEndDate(e); }} />
@@ -1585,6 +1598,7 @@ export default function ProductionPlanManagementPage() { onEventClick={openScheduleDetail} onEventMove={handleEventMove} onEventResize={handleEventResize} + onRangeChange={(s, e) => { setSearchStartDate(s); setSearchEndDate(e); }} />
diff --git a/frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx b/frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx index 6bb55a70..7c6df883 100644 --- a/frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx +++ b/frontend/app/(main)/COMPANY_9/quality/item-inspection/page.tsx @@ -33,6 +33,7 @@ const INSPECTION_TABLE = "inspection_standard"; const GRID_COLUMNS = [ { key: "item_code", label: "품목코드" }, { key: "item_name", label: "품명" }, + { key: "size", label: "규격" }, { key: "inspection_type", label: "검사유형" }, { key: "is_active", label: "사용여부" }, ]; @@ -74,7 +75,7 @@ export default function ItemInspectionInfoPage() { const [saving, setSaving] = useState(false); // FK 옵션 - const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; unit: string }[]>([]); + const [itemOptions, setItemOptions] = useState<{ code: string; name: string; item_type: string; size: string; unit: string }[]>([]); const [inspOptions, setInspOptions] = useState<{ code: string; label: string; detail: string; method: string; judgment_criteria: string; selection_options: string; unit: string; types: string[] }[]>([]); const [inspTypeCatOptions, setInspTypeCatOptions] = useState<{ code: string; label: string }[]>([]); @@ -130,6 +131,7 @@ export default function ItemInspectionInfoPage() { code: r.item_number || r.item_code || "", name: r.item_name || "", item_type: r.type || r.item_type || "", + size: r.size || "", unit: r.inventory_unit || "", }))); @@ -239,7 +241,7 @@ export default function ItemInspectionInfoPage() { const resData = res.data?.data; const rows = resData?.data || resData?.rows || []; const cm = itemCatMapRef.current; - setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: cm["type"]?.[r.type] || r.type || "", unit: cm["inventory_unit"]?.[r.inventory_unit] || r.inventory_unit || "" }))); + setFilteredItems(rows.map((r: any) => ({ code: r.item_number, name: r.item_name, item_type: cm["type"]?.[r.type] || r.type || "", size: r.size || "", unit: cm["inventory_unit"]?.[r.inventory_unit] || r.inventory_unit || "" }))); setItemTotal(resData?.total || resData?.totalCount || rows.length); } catch { /* skip */ } finally { setItemSearchLoading(false); } }; @@ -459,11 +461,13 @@ export default function ItemInspectionInfoPage() { // item_code별 그룹핑 const groupedData = useMemo(() => { - const map: Record = {}; + const itemSizeMap: Record = {}; + for (const it of itemOptions) itemSizeMap[it.code] = it.size || ""; + const map: Record = {}; for (const row of data) { const key = row.item_code || row.id; if (!map[key]) { - map[key] = { item_code: row.item_code, item_name: row.item_name, is_active: row.is_active || "", types: [], rows: [] }; + map[key] = { item_code: row.item_code, item_name: row.item_name, size: itemSizeMap[row.item_code] || "", is_active: row.is_active || "", types: [], rows: [] }; } map[key].rows.push(row); if (row.inspection_type && !map[key].types.includes(row.inspection_type)) { @@ -471,7 +475,7 @@ export default function ItemInspectionInfoPage() { } } return Object.values(map); - }, [data]); + }, [data, itemOptions]); // 좌측 품목 목록 정렬 (컬럼 헤더 클릭 → asc → desc → 해제 순환) const [sortConfig, setSortConfig] = useState<{ key: string; direction: "asc" | "desc" } | null>(null); @@ -1052,12 +1056,13 @@ export default function ItemInspectionInfoPage() { switch (col.key) { case "item_code": return {group.item_code}; case "item_name": return {group.item_name}; + case "size": return {group.size}; case "inspection_type": return ( -
+
{group.types.map((t: string) => { const label = inspTypeCatOptions.find((o) => o.code === t)?.label || t; - return {label}; + return {label}; })}
@@ -1252,17 +1257,19 @@ export default function ItemInspectionInfoPage() { 품목코드 품목명 + 규격 품목유형 단위 {filteredItems.length === 0 ? ( - {itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"} + {itemSearchLoading ? "검색 중..." : "검색 결과가 없어요"} ) : filteredItems.map((item) => ( selectItem(item)}> {item.code} {item.name} + {item.size} {item.item_type} {item.unit} diff --git a/frontend/app/(main)/COMPANY_9/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_9/sales/shipping-order/page.tsx index 2ed29b40..a8c95d2e 100644 --- a/frontend/app/(main)/COMPANY_9/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_9/sales/shipping-order/page.tsx @@ -97,6 +97,11 @@ export default function ShippingOrderPage() { // 검색 필터 (DynamicSearchFilter에서 관리) const [searchFilters, setSearchFilters] = useState([]); + // 서버 페이징 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(50); + const [totalCount, setTotalCount] = useState(0); + // 엑셀 업로드 const [excelUploadOpen, setExcelUploadOpen] = useState(false); @@ -136,7 +141,7 @@ export default function ShippingOrderPage() { const fetchOrders = useCallback(async () => { setLoading(true); try { - const params: any = {}; + const params: any = { page: currentPage, size: pageSize }; for (const f of searchFilters) { if (f.columnName === "ship_date" && f.operator === "between" && f.value) { const [from, to] = f.value.split(","); @@ -152,18 +157,25 @@ export default function ShippingOrderPage() { } const result = await getShippingOrderList(params); - if (result.success) setOrders(result.data || []); + if (result.success) { + setOrders(result.data || []); + setTotalCount(result.total ?? result.data?.length ?? 0); + } } catch (err) { console.error("출하지시 조회 실패:", err); } finally { setLoading(false); } - }, [searchFilters]); + }, [searchFilters, currentPage, pageSize]); useEffect(() => { fetchOrders(); }, [fetchOrders]); + useEffect(() => { + setCurrentPage(1); + }, [searchFilters]); + // 소스 데이터 조회 const fetchSourceData = useCallback(async (pageOverride?: number) => { setSourceLoading(true); @@ -473,7 +485,7 @@ export default function ShippingOrderPage() { tableName={ts.tableName} filterId="c16-shipping-order" onFilterChange={setSearchFilters} - dataCount={orders.length} + dataCount={totalCount} externalFilterConfig={ts.filterConfig} /> @@ -482,7 +494,7 @@ export default function ShippingOrderPage() {

출하지시 관리

- {orders.length}건 + {totalCount}건 {loading && }
@@ -548,6 +560,13 @@ export default function ShippingOrderPage() { onRowClick={(row) => setSelectedOrderId(row._orderId)} onRowDoubleClick={(row) => openModal(row._order)} showPagination + serverPagination + serverCurrentPage={currentPage} + serverPageSize={pageSize} + serverTotalCount={totalCount} + onServerPageChange={setCurrentPage} + onServerPageSizeChange={(s) => { setPageSize(s); setCurrentPage(1); }} + defaultPageSize={pageSize} draggableColumns={false} columnOrderKey="c16-shipping-order" /> diff --git a/frontend/app/(main)/COMPANY_9/sales/shipping-plan/page.tsx b/frontend/app/(main)/COMPANY_9/sales/shipping-plan/page.tsx index 747ac23d..9e0398f9 100644 --- a/frontend/app/(main)/COMPANY_9/sales/shipping-plan/page.tsx +++ b/frontend/app/(main)/COMPANY_9/sales/shipping-plan/page.tsx @@ -69,6 +69,11 @@ export default function ShippingPlanPage() { // 검색 필터 (DynamicSearchFilter에서 관리) const [searchFilters, setSearchFilters] = useState([]); + // 서버 페이징 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(50); + const [totalCount, setTotalCount] = useState(0); + // 상세 패널 편집 const [editPlanQty, setEditPlanQty] = useState(""); const [editPlanDate, setEditPlanDate] = useState(""); @@ -81,7 +86,7 @@ export default function ShippingPlanPage() { const fetchData = useCallback(async () => { setLoading(true); try { - const params: any = {}; + const params: any = { page: currentPage, size: pageSize }; for (const f of searchFilters) { if (f.columnName === "plan_date" && f.operator === "between" && f.value) { const [from, to] = f.value.split(","); @@ -99,19 +104,24 @@ export default function ShippingPlanPage() { const result = await getShipmentPlanList(params); if (result.success) { setData(result.data || []); + setTotalCount(result.total ?? result.data?.length ?? 0); } } catch (err) { console.error("출하계획 조회 실패:", err); } finally { setLoading(false); } - }, [searchFilters]); + }, [searchFilters, currentPage, pageSize]); - // searchFilters 변경 시 자동 조회 + // searchFilters 변경 시 자동 조회 + 1페이지로 리셋 useEffect(() => { fetchData(); }, [fetchData]); + useEffect(() => { + setCurrentPage(1); + }, [searchFilters]); + const selectedPlan = useMemo(() => data.find(p => p.id === selectedId), [data, selectedId]); const groupedData = useMemo(() => { @@ -209,7 +219,7 @@ export default function ShippingPlanPage() { tableName="shipment_plan" filterId="c16-shipping-plan" onFilterChange={setSearchFilters} - dataCount={data.length} + dataCount={totalCount} externalFilterConfig={ts.filterConfig} /> @@ -224,7 +234,7 @@ export default function ShippingPlanPage() {
출하계획 목록 - {data.length}건 + {totalCount}건 {loading && }
@@ -262,7 +272,14 @@ export default function ShippingPlanPage() { showCheckbox checkedIds={checkedIds.map(String)} onCheckedChange={(ids) => setCheckedIds(ids.map(Number))} - showPagination={false} + showPagination + serverPagination + serverCurrentPage={currentPage} + serverPageSize={pageSize} + serverTotalCount={totalCount} + onServerPageChange={setCurrentPage} + onServerPageSizeChange={(s) => { setPageSize(s); setCurrentPage(1); }} + defaultPageSize={pageSize} draggableColumns={false} />
diff --git a/frontend/components/common/TimelineScheduler.tsx b/frontend/components/common/TimelineScheduler.tsx index edad9de2..c768492f 100644 --- a/frontend/components/common/TimelineScheduler.tsx +++ b/frontend/components/common/TimelineScheduler.tsx @@ -70,6 +70,8 @@ export interface TimelineSchedulerProps { onEventMove?: (eventId: string | number, newStartDate: string, newEndDate: string) => void; /** 리사이즈 완료 */ onEventResize?: (eventId: string | number, newStartDate: string, newEndDate: string) => void; + /** 표시 기간(이전/다음/오늘 또는 줌 변경) 변경 시 호출 — 부모가 데이터 재조회 등에 사용 */ + onRangeChange?: (startDate: string, endDate: string) => void; /** 상태별 색상 배열 */ statusColors?: StatusColor[]; /** 진행률 바 표시 여부 */ @@ -191,6 +193,7 @@ export default function TimelineScheduler({ onEventClick, onEventMove, onEventResize, + onRangeChange, statusColors = DEFAULT_STATUS_COLORS, showProgress = true, showMilestones = true, @@ -249,6 +252,14 @@ export default function TimelineScheduler({ return arr; }, [baseDate, config.spanDays]); + // 표시 범위 변경 시 부모에 알림 (데이터 재조회 트리거용) + useEffect(() => { + if (!onRangeChange) return; + const start = toDateStr(baseDate); + const end = toDateStr(addDays(baseDate, config.spanDays - 1)); + onRangeChange(start, end); + }, [baseDate, config.spanDays, onRangeChange]); + const totalWidth = config.cellWidth * config.spanDays; // 충돌 ID 집합 diff --git a/frontend/lib/api/production.ts b/frontend/lib/api/production.ts index 48d3e894..4b6caad1 100644 --- a/frontend/lib/api/production.ts +++ b/frontend/lib/api/production.ts @@ -108,6 +108,8 @@ export async function getPlans(params?: { startDate?: string; endDate?: string; itemCode?: string; + page?: number; + size?: number; }) { const queryParams = new URLSearchParams(); if (params?.productType) queryParams.set("productType", params.productType); @@ -115,11 +117,20 @@ export async function getPlans(params?: { if (params?.startDate) queryParams.set("startDate", params.startDate); if (params?.endDate) queryParams.set("endDate", params.endDate); if (params?.itemCode) queryParams.set("itemCode", params.itemCode); + if (params?.page != null) queryParams.set("page", String(params.page)); + if (params?.size != null) queryParams.set("size", String(params.size)); const qs = queryParams.toString(); const url = `/production/plans${qs ? `?${qs}` : ""}`; const response = await apiClient.get(url); - return response.data as { success: boolean; data: ProductionPlan[] }; + return response.data as { + success: boolean; + data: ProductionPlan[]; + total?: number; + page?: number; + size?: number; + totalPages?: number; + }; } /** 자동 스케줄 미리보기 (DB 변경 없이 예상 결과) */ @@ -145,16 +156,27 @@ export async function getOrderSummary(params?: { excludePlanned?: boolean; itemCode?: string; itemName?: string; + page?: number; + size?: number; }) { const queryParams = new URLSearchParams(); if (params?.excludePlanned) queryParams.set("excludePlanned", "true"); if (params?.itemCode) queryParams.set("itemCode", params.itemCode); if (params?.itemName) queryParams.set("itemName", params.itemName); + if (params?.page != null) queryParams.set("page", String(params.page)); + if (params?.size != null) queryParams.set("size", String(params.size)); const qs = queryParams.toString(); const url = `/production/order-summary${qs ? `?${qs}` : ""}`; const response = await apiClient.get(url); - return response.data as { success: boolean; data: OrderSummaryItem[] }; + return response.data as { + success: boolean; + data: OrderSummaryItem[]; + total?: number; + page?: number; + size?: number; + totalPages?: number; + }; } /** 안전재고 부족분 조회 */ diff --git a/frontend/lib/api/shipping.ts b/frontend/lib/api/shipping.ts index 28608d84..bfbfaba6 100644 --- a/frontend/lib/api/shipping.ts +++ b/frontend/lib/api/shipping.ts @@ -90,11 +90,20 @@ export interface ShipmentPlanListParams { status?: string; customer?: string; keyword?: string; + page?: number; + size?: number; } export async function getShipmentPlanList(params: ShipmentPlanListParams) { const res = await apiClient.get("/shipping-plan/list", { params }); - return res.data as { success: boolean; data: ShipmentPlanListItem[] }; + return res.data as { + success: boolean; + data: ShipmentPlanListItem[]; + total?: number; + page?: number; + size?: number; + totalPages?: number; + }; } // 출하계획 단건 수정 @@ -114,9 +123,18 @@ export async function getShippingOrderList(params?: { status?: string; customer?: string; keyword?: string; + page?: number; + size?: number; }) { const res = await apiClient.get("/shipping-order/list", { params }); - return res.data as { success: boolean; data: any[] }; + return res.data as { + success: boolean; + data: any[]; + total?: number; + page?: number; + size?: number; + totalPages?: number; + }; } export async function saveShippingOrder(data: any) { diff --git a/frontend/lib/registry/components/v2-process-work-standard/hooks/useProcessWorkStandard.ts b/frontend/lib/registry/components/v2-process-work-standard/hooks/useProcessWorkStandard.ts index 4bdbf5a4..3b955114 100644 --- a/frontend/lib/registry/components/v2-process-work-standard/hooks/useProcessWorkStandard.ts +++ b/frontend/lib/registry/components/v2-process-work-standard/hooks/useProcessWorkStandard.ts @@ -135,7 +135,7 @@ export function useProcessWorkStandard(config: ProcessWorkStandardConfig) { [config.dataSource] ); - // 작업 항목 조회 + // 작업 항목 조회 + 각 phase별 첫 항목 자동 선택 (상세 영역이 비어 보이는 오해 방지) const fetchWorkItems = useCallback(async (routingDetailId: string) => { try { setLoading(true); @@ -143,7 +143,26 @@ export function useProcessWorkStandard(config: ProcessWorkStandardConfig) { `${API_BASE}/routing-detail/${routingDetailId}/work-items` ); if (res.data?.success) { - setWorkItems(res.data.items || []); + const items: WorkItem[] = res.data.items || []; + setWorkItems(items); + + // 각 phase별 첫 번째 항목 자동 선택 + 상세 병렬 로드 + const firstByPhase: Record = {}; + for (const it of items) { + const phase = (it as any).work_phase; + if (phase && !firstByPhase[phase]) firstByPhase[phase] = it; + } + await Promise.all( + Object.entries(firstByPhase).map(async ([phaseKey, item]) => { + try { + const dr = await apiClient.get(`${API_BASE}/work-items/${item.id}/details`); + if (dr.data?.success) { + setSelectedDetailsByPhase((prev) => ({ ...prev, [phaseKey]: dr.data.data })); + setSelectedWorkItemIdByPhase((prev) => ({ ...prev, [phaseKey]: item.id })); + } + } catch { /* skip */ } + }) + ); } } catch (err) { console.error("작업 항목 조회 실패", err);