From 68bc857eaedb898f82aad448ebe76c522fbb3dd5 Mon Sep 17 00:00:00 2001 From: kjs Date: Mon, 20 Apr 2026 14:51:32 +0900 Subject: [PATCH] feat: Implement pagination and enhanced keyword search in work instruction retrieval - Added pagination support to the `getList` function in the work instruction controller, allowing for efficient data retrieval with `page` and `pageSize` parameters. - Enhanced the keyword search functionality to include checks for item numbers in the work instruction details, improving search accuracy. - Updated the frontend components to utilize the new `SmartSelect` component for supplier and partner selection, enhancing user experience. - Adjusted the `EDataTable` component to support server-side pagination, ensuring better performance with large datasets. --- .../controllers/workInstructionController.ts | 112 ++++++++++++++++-- .../(main)/COMPANY_10/purchase/order/page.tsx | 14 +-- .../(main)/COMPANY_10/sales/order/page.tsx | 14 +-- .../(main)/COMPANY_16/purchase/order/page.tsx | 14 +-- .../(main)/COMPANY_16/sales/order/page.tsx | 14 +-- .../(main)/COMPANY_29/purchase/order/page.tsx | 14 +-- .../(main)/COMPANY_29/sales/order/page.tsx | 14 +-- .../COMPANY_30/production/result/page.tsx | 29 +++-- .../production/work-instruction/page.tsx | 22 +++- .../(main)/COMPANY_30/purchase/order/page.tsx | 14 +-- .../(main)/COMPANY_30/sales/order/page.tsx | 13 +- .../(main)/COMPANY_7/purchase/order/page.tsx | 14 +-- .../app/(main)/COMPANY_7/sales/order/page.tsx | 14 +-- .../(main)/COMPANY_8/purchase/order/page.tsx | 14 +-- .../app/(main)/COMPANY_8/sales/order/page.tsx | 14 +-- .../(main)/COMPANY_9/purchase/order/page.tsx | 14 +-- .../app/(main)/COMPANY_9/sales/order/page.tsx | 13 +- frontend/components/common/EDataTable.tsx | 59 ++++++--- frontend/lib/api/workInstruction.ts | 2 +- 19 files changed, 260 insertions(+), 158 deletions(-) diff --git a/backend-node/src/controllers/workInstructionController.ts b/backend-node/src/controllers/workInstructionController.ts index 9d3341d2..9c88f858 100644 --- a/backend-node/src/controllers/workInstructionController.ts +++ b/backend-node/src/controllers/workInstructionController.ts @@ -23,7 +23,12 @@ export async function getList(req: AuthenticatedRequest, res: Response) { try { await ensureDetailRoutingColumn(); const companyCode = req.user!.companyCode; - const { dateFrom, dateTo, status, progressStatus, keyword } = req.query; + const { dateFrom, dateTo, status, progressStatus, keyword, page, pageSize } = req.query; + + // 페이지네이션 파라미터 파싱 (page 없으면 전체 반환 — 하위호환) + const pageNum = page ? Math.max(1, parseInt(page as string, 10) || 1) : null; + const sizeNum = pageSize ? Math.max(1, Math.min(1000, parseInt(pageSize as string, 10) || 20)) : null; + const paginated = pageNum !== null && sizeNum !== null; const conditions: string[] = []; const params: any[] = []; @@ -54,14 +59,110 @@ export async function getList(req: AuthenticatedRequest, res: Response) { params.push(progressStatus); idx++; } + // keyword 검색: wi 자체 필드 + detail.item_number 존재 여부로 EXISTS if (keyword) { - conditions.push(`(wi.work_instruction_no ILIKE $${idx} OR wi.worker ILIKE $${idx} OR COALESCE(itm.item_name,'') ILIKE $${idx} OR COALESCE(d.item_number,'') ILIKE $${idx})`); + conditions.push(`( + wi.work_instruction_no ILIKE $${idx} + OR wi.worker ILIKE $${idx} + OR EXISTS ( + SELECT 1 FROM work_instruction_detail dd + LEFT JOIN item_info ii ON ii.item_number = dd.item_number AND ii.company_code = wi.company_code + WHERE dd.work_instruction_id = wi.id + AND (dd.item_number ILIKE $${idx} OR COALESCE(ii.item_name,'') ILIKE $${idx}) + ) + )`); params.push(`%${keyword}%`); idx++; } const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""; + const pool = getPool(); + + // 페이지네이션 모드: WI 단위로 페이지 잘라낸 뒤 detail과 JOIN + if (paginated) { + // 1) 총 WI 개수 카운트 + const countSql = ` + SELECT COUNT(*)::int AS cnt + FROM work_instruction wi + ${whereClause} + `; + const countRes = await pool.query(countSql, params); + const totalCount = countRes.rows[0]?.cnt ?? 0; + + // 2) 현재 페이지 WI id 목록 + const offset = (pageNum! - 1) * sizeNum!; + const pageSql = ` + SELECT wi.id + FROM work_instruction wi + ${whereClause} + ORDER BY wi.created_date DESC, wi.id DESC + LIMIT ${sizeNum} OFFSET ${offset} + `; + const pageRes = await pool.query(pageSql, params); + const wiIds = pageRes.rows.map((r) => r.id); + + if (wiIds.length === 0) { + return res.json({ success: true, data: [], totalCount, page: pageNum, pageSize: sizeNum }); + } + + // 3) 해당 WI들의 detail + 품목/설비/라우팅 JOIN + const dataSql = ` + SELECT + wi.id AS wi_id, + wi.work_instruction_no, + wi.status, + wi.progress_status, + wi.qty AS total_qty, + wi.completed_qty, + wi.start_date, + wi.end_date, + wi.equipment_id, + wi.work_team, + wi.worker, + wi.remark AS wi_remark, + wi.created_date, + d.id AS detail_id, + d.item_number, + d.qty AS detail_qty, + d.remark AS detail_remark, + d.part_code, + d.source_table, + d.source_id, + d.routing_version_id AS detail_routing_version_id, + COALESCE(itm.item_name, '') AS item_name, + COALESCE(itm.type, '') AS item_type, + COALESCE(itm.size, '') AS item_spec, + COALESCE(e.equipment_name, '') AS equipment_name, + COALESCE(e.equipment_code, '') AS equipment_code, + wi.routing AS routing_version_id, + COALESCE(rv.version_name, '') AS routing_name, + ROW_NUMBER() OVER (PARTITION BY wi.work_instruction_no ORDER BY d.created_date) AS detail_seq, + COUNT(*) OVER (PARTITION BY wi.work_instruction_no) AS detail_count + FROM work_instruction wi + INNER JOIN work_instruction_detail d + ON d.work_instruction_id = wi.id + LEFT JOIN item_info itm + ON itm.item_number = d.item_number AND itm.company_code = wi.company_code + LEFT JOIN equipment_mng e + ON wi.equipment_id = e.id AND wi.company_code = e.company_code + LEFT JOIN item_routing_version rv + ON wi.routing = rv.id AND rv.company_code = wi.company_code + WHERE wi.id = ANY($1::varchar[]) + ORDER BY wi.created_date DESC, wi.id DESC, d.created_date ASC + `; + const dataRes = await pool.query(dataSql, [wiIds]); + + return res.json({ + success: true, + data: dataRes.rows, + totalCount, + page: pageNum, + pageSize: sizeNum, + }); + } + + // 비페이지 모드 (하위호환): 기존 방식 유지, LATERAL만 LEFT JOIN으로 교체 const query = ` SELECT wi.id AS wi_id, @@ -97,17 +198,14 @@ export async function getList(req: AuthenticatedRequest, res: Response) { FROM work_instruction wi INNER JOIN work_instruction_detail d ON d.work_instruction_id = wi.id - LEFT JOIN LATERAL ( - SELECT item_name, size, type FROM item_info - WHERE item_number = d.item_number AND company_code = wi.company_code LIMIT 1 - ) itm ON true + LEFT JOIN item_info itm + ON itm.item_number = d.item_number AND itm.company_code = wi.company_code LEFT JOIN equipment_mng e ON wi.equipment_id = e.id AND wi.company_code = e.company_code LEFT JOIN item_routing_version rv ON wi.routing = rv.id AND rv.company_code = wi.company_code ${whereClause} ORDER BY wi.created_date DESC, d.created_date ASC `; - const pool = getPool(); const result = await pool.query(query, params); return res.json({ success: true, data: result.rows }); } catch (error: any) { diff --git a/frontend/app/(main)/COMPANY_10/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_10/purchase/order/page.tsx index bf01613e..9ec4e88e 100644 --- a/frontend/app/(main)/COMPANY_10/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_10/purchase/order/page.tsx @@ -30,6 +30,7 @@ import { toast } from "sonner"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { SmartSelect } from "@/components/common/SmartSelect"; import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const MASTER_TABLE = "purchase_order_mng"; @@ -1026,7 +1027,8 @@ export default function PurchaseOrderPage() {
- + />
diff --git a/frontend/app/(main)/COMPANY_10/sales/order/page.tsx b/frontend/app/(main)/COMPANY_10/sales/order/page.tsx index f62967f1..e6ec842b 100644 --- a/frontend/app/(main)/COMPANY_10/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_10/sales/order/page.tsx @@ -28,6 +28,7 @@ import { ShippingPlanBatchModal } from "@/components/common/ShippingPlanBatchMod import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { SmartSelect } from "@/components/common/SmartSelect"; import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; const DETAIL_TABLE = "sales_order_detail"; @@ -1481,17 +1482,12 @@ export default function SalesOrderPage() {
- + placeholder="거래처 선택" + />
diff --git a/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx index e32841d2..dd66b1e5 100644 --- a/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/purchase/order/page.tsx @@ -30,6 +30,7 @@ import { toast } from "sonner"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { SmartSelect } from "@/components/common/SmartSelect"; import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const MASTER_TABLE = "purchase_order_mng"; @@ -1028,7 +1029,8 @@ export default function PurchaseOrderPage() {
- + />
diff --git a/frontend/app/(main)/COMPANY_16/sales/order/page.tsx b/frontend/app/(main)/COMPANY_16/sales/order/page.tsx index ef5b10aa..766a867c 100644 --- a/frontend/app/(main)/COMPANY_16/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_16/sales/order/page.tsx @@ -28,6 +28,7 @@ import { ShippingPlanBatchModal } from "@/components/common/ShippingPlanBatchMod import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { SmartSelect } from "@/components/common/SmartSelect"; import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; const DETAIL_TABLE = "sales_order_detail"; @@ -1481,17 +1482,12 @@ export default function SalesOrderPage() {
- + placeholder="거래처 선택" + />
diff --git a/frontend/app/(main)/COMPANY_29/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_29/purchase/order/page.tsx index bf01613e..9ec4e88e 100644 --- a/frontend/app/(main)/COMPANY_29/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_29/purchase/order/page.tsx @@ -30,6 +30,7 @@ import { toast } from "sonner"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { SmartSelect } from "@/components/common/SmartSelect"; import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const MASTER_TABLE = "purchase_order_mng"; @@ -1026,7 +1027,8 @@ export default function PurchaseOrderPage() {
- + />
diff --git a/frontend/app/(main)/COMPANY_29/sales/order/page.tsx b/frontend/app/(main)/COMPANY_29/sales/order/page.tsx index f62967f1..e6ec842b 100644 --- a/frontend/app/(main)/COMPANY_29/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_29/sales/order/page.tsx @@ -28,6 +28,7 @@ import { ShippingPlanBatchModal } from "@/components/common/ShippingPlanBatchMod import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { SmartSelect } from "@/components/common/SmartSelect"; import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; const DETAIL_TABLE = "sales_order_detail"; @@ -1481,17 +1482,12 @@ export default function SalesOrderPage() {
- + placeholder="거래처 선택" + />
diff --git a/frontend/app/(main)/COMPANY_30/production/result/page.tsx b/frontend/app/(main)/COMPANY_30/production/result/page.tsx index b2176781..e150ab8a 100644 --- a/frontend/app/(main)/COMPANY_30/production/result/page.tsx +++ b/frontend/app/(main)/COMPANY_30/production/result/page.tsx @@ -89,6 +89,7 @@ export default function ProductionResultPage() { const [currentPage, setCurrentPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [pageSizeInput, setPageSizeInput] = useState("20"); + const [wiTotalCount, setWiTotalCount] = useState(0); // ── 우측: 실적 ── const [rightTab, setRightTab] = useState<"result" | "defect">("result"); @@ -135,7 +136,7 @@ export default function ProductionResultPage() { const fetchWiList = useCallback(async () => { setWiLoading(true); try { - const params: Record = {}; + const params: Record = { page: String(currentPage), pageSize: String(pageSize) }; for (const f of searchFilters) { if (f.value) { if (f.columnName === "progress_status") params.progressStatus = f.value; @@ -145,6 +146,7 @@ export default function ProductionResultPage() { } const res = await apiClient.get("/work-instruction/list", { params }); const raw: any[] = res.data?.data || []; + const total: number = res.data?.totalCount ?? raw.length; // work_instruction_no 기준 중복 제거 (detail JOIN으로 여러 행 반환) const seen = new Set(); @@ -167,15 +169,19 @@ export default function ProductionResultPage() { }; }); setWiList(enriched); + setWiTotalCount(total); } catch { toast.error("작업지시 목록 조회 실패"); } finally { setWiLoading(false); } - }, [searchFilters]); + }, [searchFilters, currentPage, pageSize]); useEffect(() => { fetchWiList(); }, [fetchWiList]); + // 검색 조건 변경 시 1페이지로 리셋 + useEffect(() => { setCurrentPage(1); }, [searchFilters]); + // 실적 로드 useEffect(() => { if (!selectedWiId) { setProcessData([]); return; } @@ -237,13 +243,11 @@ export default function ProductionResultPage() { return result; }, [wiList, groupBy]); - // 페이지네이션 계산 - const totalPages = Math.max(1, Math.ceil(wiList.length / pageSize)); + // 페이지네이션 계산 (서버사이드) + const totalPages = Math.max(1, Math.ceil(wiTotalCount / pageSize)); const safePage = Math.min(Math.max(1, currentPage), totalPages); - const paginatedRows = useMemo(() => { - const start = (safePage - 1) * pageSize; - return wiList.slice(start, start + pageSize); - }, [wiList, safePage, pageSize]); + // 서버가 이미 페이지 분량만 반환하므로 slice 불필요 + const paginatedRows = wiList; const paginatedGroupedData = useMemo(() => { if (groupBy === "none") return paginatedRows; @@ -283,8 +287,7 @@ export default function ProductionResultPage() { return pages; }; - // 필터 변경 시 첫 페이지로 이동 - useEffect(() => { setCurrentPage(1); }, [wiList.length]); + // (검색 조건 변경 시 1페이지 리셋은 위 useEffect에서 처리) const toggleGroup = (key: string) => { setExpandedGroups((prev) => { @@ -337,7 +340,7 @@ export default function ProductionResultPage() { tableName={WI_TABLE} filterId="c16-production-result" onFilterChange={setSearchFilters} - dataCount={wiList.length} + dataCount={wiTotalCount} /> {/* 메인 */} @@ -351,7 +354,7 @@ export default function ProductionResultPage() {
작업지시 목록 - {wiList.length}건 + {wiTotalCount}건
{ const supp = categoryOptions["supplier_code"]?.find((o) => o.code === v); @@ -1078,15 +1080,9 @@ export default function PurchaseOrderPage() { setMasterForm((p) => ({ ...p, supplier_code: v, supplier_name: name })); recalcPrices(masterForm.price_mode || "", v); }} + placeholder="공급업체 선택" disabled={isReadOnly} - > - - - {(categoryOptions["supplier_code"] || []).map((o) => ( - {o.label} - ))} - - + />
diff --git a/frontend/app/(main)/COMPANY_30/sales/order/page.tsx b/frontend/app/(main)/COMPANY_30/sales/order/page.tsx index 55a4dcbc..97b09f52 100644 --- a/frontend/app/(main)/COMPANY_30/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_30/sales/order/page.tsx @@ -34,6 +34,7 @@ import { apiClient } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { SmartSelect } from "@/components/common/SmartSelect"; import { DataGrid, DataGridColumn } from "@/components/common/DataGrid"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { FullscreenDialog } from "@/components/common/FullscreenDialog"; @@ -1151,12 +1152,12 @@ export default function ChunganSalesOrderPage() {
- + setMasterForm((p) => ({ ...p, partner_id: v }))} + placeholder="거래처 선택" + />
diff --git a/frontend/app/(main)/COMPANY_7/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_7/purchase/order/page.tsx index bf01613e..9ec4e88e 100644 --- a/frontend/app/(main)/COMPANY_7/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_7/purchase/order/page.tsx @@ -30,6 +30,7 @@ import { toast } from "sonner"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { SmartSelect } from "@/components/common/SmartSelect"; import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const MASTER_TABLE = "purchase_order_mng"; @@ -1026,7 +1027,8 @@ export default function PurchaseOrderPage() {
- + />
diff --git a/frontend/app/(main)/COMPANY_7/sales/order/page.tsx b/frontend/app/(main)/COMPANY_7/sales/order/page.tsx index f62967f1..e6ec842b 100644 --- a/frontend/app/(main)/COMPANY_7/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_7/sales/order/page.tsx @@ -28,6 +28,7 @@ import { ShippingPlanBatchModal } from "@/components/common/ShippingPlanBatchMod import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { SmartSelect } from "@/components/common/SmartSelect"; import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; const DETAIL_TABLE = "sales_order_detail"; @@ -1481,17 +1482,12 @@ export default function SalesOrderPage() {
- + placeholder="거래처 선택" + />
diff --git a/frontend/app/(main)/COMPANY_8/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_8/purchase/order/page.tsx index bf01613e..9ec4e88e 100644 --- a/frontend/app/(main)/COMPANY_8/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_8/purchase/order/page.tsx @@ -30,6 +30,7 @@ import { toast } from "sonner"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { SmartSelect } from "@/components/common/SmartSelect"; import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const MASTER_TABLE = "purchase_order_mng"; @@ -1026,7 +1027,8 @@ export default function PurchaseOrderPage() {
- + />
diff --git a/frontend/app/(main)/COMPANY_8/sales/order/page.tsx b/frontend/app/(main)/COMPANY_8/sales/order/page.tsx index f62967f1..e6ec842b 100644 --- a/frontend/app/(main)/COMPANY_8/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_8/sales/order/page.tsx @@ -28,6 +28,7 @@ import { ShippingPlanBatchModal } from "@/components/common/ShippingPlanBatchMod import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { SmartSelect } from "@/components/common/SmartSelect"; import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule"; const DETAIL_TABLE = "sales_order_detail"; @@ -1481,17 +1482,12 @@ export default function SalesOrderPage() {
- + placeholder="거래처 선택" + />
diff --git a/frontend/app/(main)/COMPANY_9/purchase/order/page.tsx b/frontend/app/(main)/COMPANY_9/purchase/order/page.tsx index 840d336b..e47834ef 100644 --- a/frontend/app/(main)/COMPANY_9/purchase/order/page.tsx +++ b/frontend/app/(main)/COMPANY_9/purchase/order/page.tsx @@ -30,6 +30,7 @@ import { toast } from "sonner"; import { useTableSettings } from "@/hooks/useTableSettings"; import { TableSettingsModal } from "@/components/common/TableSettingsModal"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { SmartSelect } from "@/components/common/SmartSelect"; import { EDataTable, EDataTableColumn } from "@/components/common/EDataTable"; const MASTER_TABLE = "purchase_order_mng"; @@ -1038,7 +1039,8 @@ export default function PurchaseOrderPage() {
- + />
diff --git a/frontend/app/(main)/COMPANY_9/sales/order/page.tsx b/frontend/app/(main)/COMPANY_9/sales/order/page.tsx index 5cd46fb5..c7be925b 100644 --- a/frontend/app/(main)/COMPANY_9/sales/order/page.tsx +++ b/frontend/app/(main)/COMPANY_9/sales/order/page.tsx @@ -31,6 +31,7 @@ import { apiClient } from "@/lib/api/client"; import { useAuth } from "@/hooks/useAuth"; import { toast } from "sonner"; import { DynamicSearchFilter, FilterValue } from "@/components/common/DynamicSearchFilter"; +import { SmartSelect } from "@/components/common/SmartSelect"; import { DataGrid, DataGridColumn } from "@/components/common/DataGrid"; import { useConfirmDialog } from "@/components/common/ConfirmDialog"; import { FullscreenDialog } from "@/components/common/FullscreenDialog"; @@ -936,12 +937,12 @@ export default function JeilGlassOrderPage() {
- + setMasterForm((p) => ({ ...p, partner_id: v }))} + placeholder="거래처 선택" + />
diff --git a/frontend/components/common/EDataTable.tsx b/frontend/components/common/EDataTable.tsx index 00bf3486..9e9fac41 100644 --- a/frontend/components/common/EDataTable.tsx +++ b/frontend/components/common/EDataTable.tsx @@ -83,6 +83,15 @@ export interface EDataTableProps = any> { showPagination?: boolean; defaultPageSize?: number; + // ─── 서버사이드 페이지네이션 모드 ─── + // serverPagination=true 일 때: 내부 slice/filter/sort 미사용, data는 이미 해당 페이지 분량 + serverPagination?: boolean; + serverCurrentPage?: number; + serverPageSize?: number; + serverTotalCount?: number; + onServerPageChange?: (page: number) => void; + onServerPageSizeChange?: (size: number) => void; + className?: string; } @@ -275,6 +284,12 @@ export function EDataTable = any>({ showRowNumber = false, showPagination = true, defaultPageSize = 50, + serverPagination = false, + serverCurrentPage, + serverPageSize, + serverTotalCount, + onServerPageChange, + onServerPageSizeChange, className, }: EDataTableProps) { const [columns, setColumns] = useState(initialColumns); @@ -287,10 +302,21 @@ export function EDataTable = any>({ // 헤더 필터 const [headerFilters, setHeaderFilters] = useState>>({}); - // 페이지네이션 - const [currentPage, setCurrentPage] = useState(1); - const [pageSize, setPageSize] = useState(defaultPageSize); - const [pageSizeInput, setPageSizeInput] = useState(String(defaultPageSize)); + // 페이지네이션 — 서버사이드 모드면 외부 state 사용 + const [internalCurrentPage, setInternalCurrentPage] = useState(1); + const [internalPageSize, setInternalPageSize] = useState(defaultPageSize); + const currentPage = serverPagination ? (serverCurrentPage ?? 1) : internalCurrentPage; + const pageSize = serverPagination ? (serverPageSize ?? defaultPageSize) : internalPageSize; + const setCurrentPage = (next: number | ((prev: number) => number)) => { + const resolved = typeof next === "function" ? (next as (p: number) => number)(currentPage) : next; + if (serverPagination) onServerPageChange?.(resolved); + else setInternalCurrentPage(resolved); + }; + const setPageSize = (n: number) => { + if (serverPagination) onServerPageSizeChange?.(n); + else setInternalPageSize(n); + }; + const [pageSizeInput, setPageSizeInput] = useState(String(serverPagination ? (serverPageSize ?? defaultPageSize) : defaultPageSize)); // 그룹 접기/펼치기 const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); @@ -394,8 +420,9 @@ export function EDataTable = any>({ }); }; - // 필터 + 정렬 + // 필터 + 정렬 (서버사이드 모드면 원본 data 그대로 사용) const processedData = useMemo(() => { + if (serverPagination) return data; let result = [...data]; // 헤더 필터 @@ -425,24 +452,28 @@ export function EDataTable = any>({ } return result; - }, [data, headerFilters, sortState, onSortChange]); + }, [data, headerFilters, sortState, onSortChange, serverPagination]); - // 필터/데이터 건수 변경 시 1페이지 리셋 (참조만 바뀐 경우는 리셋 안 함) - useEffect(() => { setCurrentPage(1); }, [data.length, headerFilters]); + // 필터/데이터 건수 변경 시 1페이지 리셋 (서버사이드에선 외부가 제어) + useEffect(() => { + if (!serverPagination) setCurrentPage(1); + }, [data.length, headerFilters, serverPagination]); // 페이지네이션 - const totalItems = processedData.length; + const totalItems = serverPagination ? (serverTotalCount ?? data.length) : processedData.length; const totalPages = Math.max(1, Math.ceil(totalItems / pageSize)); const safePage = Math.min(currentPage, totalPages); useEffect(() => { - if (currentPage > totalPages) setCurrentPage(totalPages); - }, [currentPage, totalPages]); + if (!serverPagination && currentPage > totalPages) setCurrentPage(totalPages); + }, [currentPage, totalPages, serverPagination]); const pageOffset = (safePage - 1) * pageSize; - const paginatedDataRaw = showPagination - ? processedData.slice(pageOffset, pageOffset + pageSize) - : processedData; + const paginatedDataRaw = serverPagination + ? processedData + : showPagination + ? processedData.slice(pageOffset, pageOffset + pageSize) + : processedData; // 접힌 그룹의 데이터 행 숨김 const paginatedData = useMemo(() => { diff --git a/frontend/lib/api/workInstruction.ts b/frontend/lib/api/workInstruction.ts index 20db0997..59a75443 100644 --- a/frontend/lib/api/workInstruction.ts +++ b/frontend/lib/api/workInstruction.ts @@ -4,7 +4,7 @@ export interface PaginatedResponse { success: boolean; data: any[]; totalCount: export async function getWorkInstructionList(params?: Record) { const res = await apiClient.get("/work-instruction/list", { params }); - return res.data as { success: boolean; data: any[] }; + return res.data as { success: boolean; data: any[]; totalCount?: number; page?: number; pageSize?: number }; } export async function previewWorkInstructionNo() {