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.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
<div className="grid grid-cols-2 gap-3.5">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold text-muted-foreground tracking-wide">공급업체</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
options={categoryOptions["supplier_code"] || []}
|
||||
value={masterForm.supplier_code || ""}
|
||||
onValueChange={(v) => {
|
||||
const supp = categoryOptions["supplier_code"]?.find((o) => o.code === v);
|
||||
@@ -1034,15 +1036,9 @@ export default function PurchaseOrderPage() {
|
||||
setMasterForm((p) => ({ ...p, supplier_code: v, supplier_name: name }));
|
||||
recalcPrices(masterForm.price_mode || "", v);
|
||||
}}
|
||||
placeholder="공급업체 선택"
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<SelectTrigger className="h-9"><SelectValue placeholder="공급업체 선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["supplier_code"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
<div className="grid grid-cols-1 gap-3.5 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">거래처</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
options={categoryOptions["partner_id"] || []}
|
||||
value={masterForm.partner_id || ""}
|
||||
onValueChange={(v) => { setMasterForm((p) => ({ ...p, partner_id: v, delivery_partner_id: "" })); loadDeliveryOptions(v); recalcPrices(masterForm.price_mode || "", v); }}
|
||||
>
|
||||
<SelectTrigger className="h-9"><SelectValue placeholder="거래처 선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["partner_id"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
placeholder="거래처 선택"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">담당자</Label>
|
||||
|
||||
@@ -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() {
|
||||
<div className="grid grid-cols-2 gap-3.5">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold text-muted-foreground tracking-wide">공급업체</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
options={categoryOptions["supplier_code"] || []}
|
||||
value={masterForm.supplier_code || ""}
|
||||
onValueChange={(v) => {
|
||||
const supp = categoryOptions["supplier_code"]?.find((o) => o.code === v);
|
||||
@@ -1036,15 +1038,9 @@ export default function PurchaseOrderPage() {
|
||||
setMasterForm((p) => ({ ...p, supplier_code: v, supplier_name: name }));
|
||||
recalcPrices(masterForm.price_mode || "", v);
|
||||
}}
|
||||
placeholder="공급업체 선택"
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<SelectTrigger className="h-9"><SelectValue placeholder="공급업체 선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["supplier_code"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
<div className="grid grid-cols-1 gap-3.5 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">거래처</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
options={categoryOptions["partner_id"] || []}
|
||||
value={masterForm.partner_id || ""}
|
||||
onValueChange={(v) => { setMasterForm((p) => ({ ...p, partner_id: v, delivery_partner_id: "" })); loadDeliveryOptions(v); recalcPrices(masterForm.price_mode || "", v); }}
|
||||
>
|
||||
<SelectTrigger className="h-9"><SelectValue placeholder="거래처 선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["partner_id"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
placeholder="거래처 선택"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">담당자</Label>
|
||||
|
||||
@@ -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() {
|
||||
<div className="grid grid-cols-2 gap-3.5">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold text-muted-foreground tracking-wide">공급업체</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
options={categoryOptions["supplier_code"] || []}
|
||||
value={masterForm.supplier_code || ""}
|
||||
onValueChange={(v) => {
|
||||
const supp = categoryOptions["supplier_code"]?.find((o) => o.code === v);
|
||||
@@ -1034,15 +1036,9 @@ export default function PurchaseOrderPage() {
|
||||
setMasterForm((p) => ({ ...p, supplier_code: v, supplier_name: name }));
|
||||
recalcPrices(masterForm.price_mode || "", v);
|
||||
}}
|
||||
placeholder="공급업체 선택"
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<SelectTrigger className="h-9"><SelectValue placeholder="공급업체 선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["supplier_code"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
<div className="grid grid-cols-1 gap-3.5 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">거래처</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
options={categoryOptions["partner_id"] || []}
|
||||
value={masterForm.partner_id || ""}
|
||||
onValueChange={(v) => { setMasterForm((p) => ({ ...p, partner_id: v, delivery_partner_id: "" })); loadDeliveryOptions(v); recalcPrices(masterForm.price_mode || "", v); }}
|
||||
>
|
||||
<SelectTrigger className="h-9"><SelectValue placeholder="거래처 선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["partner_id"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
placeholder="거래처 선택"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">담당자</Label>
|
||||
|
||||
@@ -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<string, string> = {};
|
||||
const params: Record<string, string> = { 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<string>();
|
||||
@@ -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() {
|
||||
<div className="flex items-center gap-2">
|
||||
<ClipboardList className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-semibold">작업지시 목록</span>
|
||||
<Badge variant="secondary" className="font-mono text-xs">{wiList.length}건</Badge>
|
||||
<Badge variant="secondary" className="font-mono text-xs">{wiTotalCount}건</Badge>
|
||||
</div>
|
||||
<Select value={groupBy} onValueChange={(v) => { setGroupBy(v); setExpandedGroups(new Set()); }}>
|
||||
<SelectTrigger className="h-8 w-[120px] text-xs">
|
||||
@@ -459,7 +462,7 @@ export default function ProductionResultPage() {
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{wiList.length.toLocaleString()}</span>
|
||||
<span className="font-medium text-foreground">{wiTotalCount.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
|
||||
@@ -65,6 +65,10 @@ export default function WorkInstructionPage() {
|
||||
const ts = useTableSettings("c16-work-instruction", "work_instruction", GRID_COLUMNS);
|
||||
const [orders, setOrders] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
// 서버사이드 페이지네이션 (WI 단위)
|
||||
const [wiPage, setWiPage] = useState(1);
|
||||
const [wiPageSize, setWiPageSize] = useState(20);
|
||||
const [wiTotalCount, setWiTotalCount] = useState(0);
|
||||
const [equipmentOptions, setEquipmentOptions] = useState<EquipmentOption[]>([]);
|
||||
const [employeeOptions, setEmployeeOptions] = useState<EmployeeOption[]>([]);
|
||||
|
||||
@@ -143,7 +147,7 @@ export default function WorkInstructionPage() {
|
||||
const fetchOrders = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: any = {};
|
||||
const params: any = { page: wiPage, pageSize: wiPageSize };
|
||||
for (const f of searchFilters) {
|
||||
if (f.columnName === "start_date" && f.operator === "between" && f.value) {
|
||||
const [from, to] = f.value.split("|");
|
||||
@@ -160,12 +164,18 @@ export default function WorkInstructionPage() {
|
||||
}
|
||||
}
|
||||
const r = await getWorkInstructionList(params);
|
||||
if (r.success) setOrders(r.data || []);
|
||||
if (r.success) {
|
||||
setOrders(r.data || []);
|
||||
setWiTotalCount(r.totalCount ?? (r.data?.length || 0));
|
||||
}
|
||||
} catch {} finally { setLoading(false); }
|
||||
}, [searchFilters]);
|
||||
}, [searchFilters, wiPage, wiPageSize]);
|
||||
|
||||
useEffect(() => { fetchOrders(); }, [fetchOrders]);
|
||||
|
||||
// 검색 조건 변경 시 1페이지로 리셋
|
||||
useEffect(() => { setWiPage(1); }, [searchFilters]);
|
||||
|
||||
// ─── 1단계 등록 ───
|
||||
const openRegModal = () => {
|
||||
setRegSourceType("production"); setRegSourceData([]); setRegKeyword(""); setRegCheckedIds(new Set());
|
||||
@@ -520,6 +530,12 @@ export default function WorkInstructionPage() {
|
||||
showPagination
|
||||
draggableColumns
|
||||
columnOrderKey="c16-work-instruction"
|
||||
serverPagination
|
||||
serverCurrentPage={wiPage}
|
||||
serverPageSize={wiPageSize}
|
||||
serverTotalCount={wiTotalCount}
|
||||
onServerPageChange={setWiPage}
|
||||
onServerPageSizeChange={(n) => { setWiPageSize(n); setWiPage(1); }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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";
|
||||
import { previewNumberingCode, allocateNumberingCode } from "@/lib/api/numberingRule";
|
||||
|
||||
@@ -1070,7 +1071,8 @@ export default function PurchaseOrderPage() {
|
||||
<div className="grid grid-cols-2 gap-3.5">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold text-muted-foreground tracking-wide">공급업체</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
options={categoryOptions["supplier_code"] || []}
|
||||
value={masterForm.supplier_code || ""}
|
||||
onValueChange={(v) => {
|
||||
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}
|
||||
>
|
||||
<SelectTrigger className="h-9"><SelectValue placeholder="공급업체 선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["supplier_code"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm">거래처</Label>
|
||||
<Select value={masterForm.partner_id || ""} onValueChange={(v) => setMasterForm((p) => ({ ...p, partner_id: v }))}>
|
||||
<SelectTrigger className="h-9"><SelectValue placeholder="거래처 선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["partner_id"] || []).map((o) => <SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<SmartSelect
|
||||
options={categoryOptions["partner_id"] || []}
|
||||
value={masterForm.partner_id || ""}
|
||||
onValueChange={(v) => setMasterForm((p) => ({ ...p, partner_id: v }))}
|
||||
placeholder="거래처 선택"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm">상태</Label>
|
||||
|
||||
@@ -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() {
|
||||
<div className="grid grid-cols-2 gap-3.5">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold text-muted-foreground tracking-wide">공급업체</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
options={categoryOptions["supplier_code"] || []}
|
||||
value={masterForm.supplier_code || ""}
|
||||
onValueChange={(v) => {
|
||||
const supp = categoryOptions["supplier_code"]?.find((o) => o.code === v);
|
||||
@@ -1034,15 +1036,9 @@ export default function PurchaseOrderPage() {
|
||||
setMasterForm((p) => ({ ...p, supplier_code: v, supplier_name: name }));
|
||||
recalcPrices(masterForm.price_mode || "", v);
|
||||
}}
|
||||
placeholder="공급업체 선택"
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<SelectTrigger className="h-9"><SelectValue placeholder="공급업체 선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["supplier_code"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
<div className="grid grid-cols-1 gap-3.5 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">거래처</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
options={categoryOptions["partner_id"] || []}
|
||||
value={masterForm.partner_id || ""}
|
||||
onValueChange={(v) => { setMasterForm((p) => ({ ...p, partner_id: v, delivery_partner_id: "" })); loadDeliveryOptions(v); recalcPrices(masterForm.price_mode || "", v); }}
|
||||
>
|
||||
<SelectTrigger className="h-9"><SelectValue placeholder="거래처 선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["partner_id"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
placeholder="거래처 선택"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">담당자</Label>
|
||||
|
||||
@@ -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() {
|
||||
<div className="grid grid-cols-2 gap-3.5">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold text-muted-foreground tracking-wide">공급업체</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
options={categoryOptions["supplier_code"] || []}
|
||||
value={masterForm.supplier_code || ""}
|
||||
onValueChange={(v) => {
|
||||
const supp = categoryOptions["supplier_code"]?.find((o) => o.code === v);
|
||||
@@ -1034,15 +1036,9 @@ export default function PurchaseOrderPage() {
|
||||
setMasterForm((p) => ({ ...p, supplier_code: v, supplier_name: name }));
|
||||
recalcPrices(masterForm.price_mode || "", v);
|
||||
}}
|
||||
placeholder="공급업체 선택"
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<SelectTrigger className="h-9"><SelectValue placeholder="공급업체 선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["supplier_code"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
<div className="grid grid-cols-1 gap-3.5 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">거래처</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
options={categoryOptions["partner_id"] || []}
|
||||
value={masterForm.partner_id || ""}
|
||||
onValueChange={(v) => { setMasterForm((p) => ({ ...p, partner_id: v, delivery_partner_id: "" })); loadDeliveryOptions(v); recalcPrices(masterForm.price_mode || "", v); }}
|
||||
>
|
||||
<SelectTrigger className="h-9"><SelectValue placeholder="거래처 선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["partner_id"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
placeholder="거래처 선택"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">담당자</Label>
|
||||
|
||||
@@ -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() {
|
||||
<div className="grid grid-cols-2 gap-3.5">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[11px] font-semibold text-muted-foreground tracking-wide">공급업체</Label>
|
||||
<Select
|
||||
<SmartSelect
|
||||
options={categoryOptions["supplier_code"] || []}
|
||||
value={masterForm.supplier_code || ""}
|
||||
onValueChange={(v) => {
|
||||
const supp = categoryOptions["supplier_code"]?.find((o) => o.code === v);
|
||||
@@ -1046,15 +1048,9 @@ export default function PurchaseOrderPage() {
|
||||
setMasterForm((p) => ({ ...p, supplier_code: v, supplier_name: name }));
|
||||
recalcPrices(masterForm.price_mode || "", v);
|
||||
}}
|
||||
placeholder="공급업체 선택"
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<SelectTrigger className="h-9"><SelectValue placeholder="공급업체 선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["supplier_code"] || []).map((o) => (
|
||||
<SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm">거래처</Label>
|
||||
<Select value={masterForm.partner_id || ""} onValueChange={(v) => setMasterForm((p) => ({ ...p, partner_id: v }))}>
|
||||
<SelectTrigger className="h-9"><SelectValue placeholder="거래처 선택" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(categoryOptions["partner_id"] || []).map((o) => <SelectItem key={o.code} value={o.code}>{o.label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<SmartSelect
|
||||
options={categoryOptions["partner_id"] || []}
|
||||
value={masterForm.partner_id || ""}
|
||||
onValueChange={(v) => setMasterForm((p) => ({ ...p, partner_id: v }))}
|
||||
placeholder="거래처 선택"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm">상태</Label>
|
||||
|
||||
@@ -83,6 +83,15 @@ export interface EDataTableProps<T extends Record<string, any> = 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<T extends Record<string, any> = any>({
|
||||
showRowNumber = false,
|
||||
showPagination = true,
|
||||
defaultPageSize = 50,
|
||||
serverPagination = false,
|
||||
serverCurrentPage,
|
||||
serverPageSize,
|
||||
serverTotalCount,
|
||||
onServerPageChange,
|
||||
onServerPageSizeChange,
|
||||
className,
|
||||
}: EDataTableProps<T>) {
|
||||
const [columns, setColumns] = useState(initialColumns);
|
||||
@@ -287,10 +302,21 @@ export function EDataTable<T extends Record<string, any> = any>({
|
||||
// 헤더 필터
|
||||
const [headerFilters, setHeaderFilters] = useState<Record<string, Set<string>>>({});
|
||||
|
||||
// 페이지네이션
|
||||
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<Set<string>>(new Set());
|
||||
@@ -394,8 +420,9 @@ export function EDataTable<T extends Record<string, any> = any>({
|
||||
});
|
||||
};
|
||||
|
||||
// 필터 + 정렬
|
||||
// 필터 + 정렬 (서버사이드 모드면 원본 data 그대로 사용)
|
||||
const processedData = useMemo(() => {
|
||||
if (serverPagination) return data;
|
||||
let result = [...data];
|
||||
|
||||
// 헤더 필터
|
||||
@@ -425,24 +452,28 @@ export function EDataTable<T extends Record<string, any> = 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(() => {
|
||||
|
||||
@@ -4,7 +4,7 @@ export interface PaginatedResponse { success: boolean; data: any[]; totalCount:
|
||||
|
||||
export async function getWorkInstructionList(params?: Record<string, any>) {
|
||||
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() {
|
||||
|
||||
Reference in New Issue
Block a user