feat: Implement pagination functionality across logistics and production pages
- Added pagination state management to Outbound, Receiving, and Production Result pages, enhancing data navigation. - Introduced page size input for users to customize the number of items displayed per page. - Implemented logic to calculate total pages and manage current page state, improving user experience when handling large datasets. - Updated table rendering to reflect paginated data, ensuring efficient data display and interaction. - These changes aim to streamline data handling and improve usability across multiple company implementations.
This commit is contained in:
@@ -229,6 +229,11 @@ export default function OutboundPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -397,6 +402,37 @@ export default function OutboundPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredRows.slice(start, start + pageSize);
|
||||
}, [filteredRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -848,7 +884,7 @@ export default function OutboundPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -930,7 +966,7 @@ export default function OutboundPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRows.map((row) => {
|
||||
paginatedRows.map((row) => {
|
||||
const isChecked = checkedIds.includes(row.id);
|
||||
return (
|
||||
<TableRow
|
||||
@@ -989,6 +1025,85 @@ export default function OutboundPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 출고 등록 모달 */}
|
||||
|
||||
@@ -259,6 +259,11 @@ export default function ReceivingPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -430,6 +435,37 @@ export default function ReceivingPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredRows.slice(start, start + pageSize);
|
||||
}, [filteredRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -878,8 +914,8 @@ export default function ReceivingPage() {
|
||||
/>
|
||||
|
||||
{/* 입고 목록 테이블 (플랫 리스트) */}
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
||||
<div className="flex items-center justify-between border-b bg-muted/50 px-4 py-2.5">
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card flex flex-col">
|
||||
<div className="flex items-center justify-between border-b bg-muted/50 px-4 py-2.5 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-[13px] font-bold">입고 목록</h3>
|
||||
<span className="text-[11px] font-semibold text-primary bg-primary/10 border border-primary/15 px-2 py-0.5 rounded-full font-mono">
|
||||
@@ -891,7 +927,7 @@ export default function ReceivingPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="h-[calc(100%-44px)] overflow-auto">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -973,7 +1009,7 @@ export default function ReceivingPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRows.map((row) => {
|
||||
paginatedRows.map((row) => {
|
||||
const isChecked = checkedIds.includes(row.id);
|
||||
return (
|
||||
<TableRow
|
||||
@@ -1032,6 +1068,85 @@ export default function ReceivingPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 입고 등록 모달 */}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/componen
|
||||
import {
|
||||
Download, Loader2, Inbox, Search, BarChart3, AlertTriangle,
|
||||
CheckCircle2, Package, ChevronDown, ChevronRight, ClipboardList,
|
||||
ChevronLeft, ChevronsLeft, ChevronsRight,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
@@ -84,6 +85,11 @@ export default function ProductionResultPage() {
|
||||
const [groupBy, setGroupBy] = useState("none");
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// ── 우측: 실적 ──
|
||||
const [rightTab, setRightTab] = useState<"result" | "defect">("result");
|
||||
const [processData, setProcessData] = useState<any[]>([]);
|
||||
@@ -231,6 +237,55 @@ export default function ProductionResultPage() {
|
||||
return result;
|
||||
}, [wiList, groupBy]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(wiList.length / 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]);
|
||||
|
||||
const paginatedGroupedData = useMemo(() => {
|
||||
if (groupBy === "none") return paginatedRows;
|
||||
const groups = new Map<string, any[]>();
|
||||
for (const wi of paginatedRows) {
|
||||
let key = wi[groupBy] || "미지정";
|
||||
if (groupBy === "progress_status") key = PROGRESS_LABEL[key] || key;
|
||||
else if (groupBy === "work_team" || groupBy === "status") key = resolveCategory(groupBy, key) || key;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key)!.push(wi);
|
||||
}
|
||||
const result: any[] = [];
|
||||
groups.forEach((items, gk) => {
|
||||
result.push({ _group: true, _key: gk, _count: items.length });
|
||||
result.push(...items);
|
||||
});
|
||||
return result;
|
||||
}, [paginatedRows, groupBy]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [wiList.length]);
|
||||
|
||||
const toggleGroup = (key: string) => {
|
||||
setExpandedGroups((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -337,7 +392,7 @@ export default function ProductionResultPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{groupedData.map((row, idx) => {
|
||||
{paginatedGroupedData.map((row, idx) => {
|
||||
if (row._group) {
|
||||
const expanded = expandedGroups.has(row._key);
|
||||
return (
|
||||
@@ -398,6 +453,85 @@ export default function ProductionResultPage() {
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<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>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
|
||||
|
||||
@@ -159,6 +159,11 @@ export default function SalesOrderPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터 (DynamicSearchFilter에서 관리)
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -427,6 +432,37 @@ export default function SalesOrderPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredFlatRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredFlatRows.slice(start, start + pageSize);
|
||||
}, [filteredFlatRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredFlatRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -931,8 +967,8 @@ export default function SalesOrderPage() {
|
||||
</div>
|
||||
|
||||
{/* 데이터 테이블 (플랫 리스트) */}
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card flex flex-col">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -1013,7 +1049,7 @@ export default function SalesOrderPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
ts.groupData(filteredFlatRows).map((row: any) => {
|
||||
ts.groupData(paginatedRows).map((row: any) => {
|
||||
// 그룹 요약 행 렌더링
|
||||
if (row._isGroupSummary) {
|
||||
return (
|
||||
@@ -1073,6 +1109,85 @@ export default function SalesOrderPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredFlatRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 수주 등록/수정 모달 */}
|
||||
|
||||
@@ -229,6 +229,11 @@ export default function OutboundPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -397,6 +402,37 @@ export default function OutboundPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredRows.slice(start, start + pageSize);
|
||||
}, [filteredRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -848,7 +884,7 @@ export default function OutboundPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -930,7 +966,7 @@ export default function OutboundPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRows.map((row) => {
|
||||
paginatedRows.map((row) => {
|
||||
const isChecked = checkedIds.includes(row.id);
|
||||
return (
|
||||
<TableRow
|
||||
@@ -989,6 +1025,85 @@ export default function OutboundPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 출고 등록 모달 */}
|
||||
|
||||
@@ -259,6 +259,11 @@ export default function ReceivingPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -430,6 +435,37 @@ export default function ReceivingPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredRows.slice(start, start + pageSize);
|
||||
}, [filteredRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -878,8 +914,8 @@ export default function ReceivingPage() {
|
||||
/>
|
||||
|
||||
{/* 입고 목록 테이블 (플랫 리스트) */}
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
||||
<div className="flex items-center justify-between border-b bg-muted/50 px-4 py-2.5">
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card flex flex-col">
|
||||
<div className="flex items-center justify-between border-b bg-muted/50 px-4 py-2.5 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-[13px] font-bold">입고 목록</h3>
|
||||
<span className="text-[11px] font-semibold text-primary bg-primary/10 border border-primary/15 px-2 py-0.5 rounded-full font-mono">
|
||||
@@ -891,7 +927,7 @@ export default function ReceivingPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="h-[calc(100%-44px)] overflow-auto">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -973,7 +1009,7 @@ export default function ReceivingPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRows.map((row) => {
|
||||
paginatedRows.map((row) => {
|
||||
const isChecked = checkedIds.includes(row.id);
|
||||
return (
|
||||
<TableRow
|
||||
@@ -1032,6 +1068,85 @@ export default function ReceivingPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 입고 등록 모달 */}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/componen
|
||||
import {
|
||||
Download, Loader2, Inbox, Search, BarChart3, AlertTriangle,
|
||||
CheckCircle2, Package, ChevronDown, ChevronRight, ClipboardList,
|
||||
ChevronLeft, ChevronsLeft, ChevronsRight,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
@@ -84,6 +85,11 @@ export default function ProductionResultPage() {
|
||||
const [groupBy, setGroupBy] = useState("none");
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// ── 우측: 실적 ──
|
||||
const [rightTab, setRightTab] = useState<"result" | "defect">("result");
|
||||
const [processData, setProcessData] = useState<any[]>([]);
|
||||
@@ -231,6 +237,55 @@ export default function ProductionResultPage() {
|
||||
return result;
|
||||
}, [wiList, groupBy]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(wiList.length / 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]);
|
||||
|
||||
const paginatedGroupedData = useMemo(() => {
|
||||
if (groupBy === "none") return paginatedRows;
|
||||
const groups = new Map<string, any[]>();
|
||||
for (const wi of paginatedRows) {
|
||||
let key = wi[groupBy] || "미지정";
|
||||
if (groupBy === "progress_status") key = PROGRESS_LABEL[key] || key;
|
||||
else if (groupBy === "work_team" || groupBy === "status") key = resolveCategory(groupBy, key) || key;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key)!.push(wi);
|
||||
}
|
||||
const result: any[] = [];
|
||||
groups.forEach((items, gk) => {
|
||||
result.push({ _group: true, _key: gk, _count: items.length });
|
||||
result.push(...items);
|
||||
});
|
||||
return result;
|
||||
}, [paginatedRows, groupBy]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [wiList.length]);
|
||||
|
||||
const toggleGroup = (key: string) => {
|
||||
setExpandedGroups((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -337,7 +392,7 @@ export default function ProductionResultPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{groupedData.map((row, idx) => {
|
||||
{paginatedGroupedData.map((row, idx) => {
|
||||
if (row._group) {
|
||||
const expanded = expandedGroups.has(row._key);
|
||||
return (
|
||||
@@ -398,6 +453,85 @@ export default function ProductionResultPage() {
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<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>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
|
||||
|
||||
@@ -159,6 +159,11 @@ export default function SalesOrderPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터 (DynamicSearchFilter에서 관리)
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -427,6 +432,37 @@ export default function SalesOrderPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredFlatRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredFlatRows.slice(start, start + pageSize);
|
||||
}, [filteredFlatRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredFlatRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -931,8 +967,8 @@ export default function SalesOrderPage() {
|
||||
</div>
|
||||
|
||||
{/* 데이터 테이블 (플랫 리스트) */}
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card flex flex-col">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -1013,7 +1049,7 @@ export default function SalesOrderPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
ts.groupData(filteredFlatRows).map((row: any) => {
|
||||
ts.groupData(paginatedRows).map((row: any) => {
|
||||
// 그룹 요약 행 렌더링
|
||||
if (row._isGroupSummary) {
|
||||
return (
|
||||
@@ -1073,6 +1109,85 @@ export default function SalesOrderPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredFlatRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 수주 등록/수정 모달 */}
|
||||
|
||||
@@ -229,6 +229,11 @@ export default function OutboundPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -397,6 +402,37 @@ export default function OutboundPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredRows.slice(start, start + pageSize);
|
||||
}, [filteredRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -848,7 +884,7 @@ export default function OutboundPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -930,7 +966,7 @@ export default function OutboundPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRows.map((row) => {
|
||||
paginatedRows.map((row) => {
|
||||
const isChecked = checkedIds.includes(row.id);
|
||||
return (
|
||||
<TableRow
|
||||
@@ -989,6 +1025,85 @@ export default function OutboundPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 출고 등록 모달 */}
|
||||
|
||||
@@ -259,6 +259,11 @@ export default function ReceivingPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -430,6 +435,37 @@ export default function ReceivingPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredRows.slice(start, start + pageSize);
|
||||
}, [filteredRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -878,8 +914,8 @@ export default function ReceivingPage() {
|
||||
/>
|
||||
|
||||
{/* 입고 목록 테이블 (플랫 리스트) */}
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
||||
<div className="flex items-center justify-between border-b bg-muted/50 px-4 py-2.5">
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card flex flex-col">
|
||||
<div className="flex items-center justify-between border-b bg-muted/50 px-4 py-2.5 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-[13px] font-bold">입고 목록</h3>
|
||||
<span className="text-[11px] font-semibold text-primary bg-primary/10 border border-primary/15 px-2 py-0.5 rounded-full font-mono">
|
||||
@@ -891,7 +927,7 @@ export default function ReceivingPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="h-[calc(100%-44px)] overflow-auto">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -973,7 +1009,7 @@ export default function ReceivingPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRows.map((row) => {
|
||||
paginatedRows.map((row) => {
|
||||
const isChecked = checkedIds.includes(row.id);
|
||||
return (
|
||||
<TableRow
|
||||
@@ -1032,6 +1068,85 @@ export default function ReceivingPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 입고 등록 모달 */}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/componen
|
||||
import {
|
||||
Download, Loader2, Inbox, Search, BarChart3, AlertTriangle,
|
||||
CheckCircle2, Package, ChevronDown, ChevronRight, ClipboardList,
|
||||
ChevronLeft, ChevronsLeft, ChevronsRight,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
@@ -84,6 +85,11 @@ export default function ProductionResultPage() {
|
||||
const [groupBy, setGroupBy] = useState("none");
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// ── 우측: 실적 ──
|
||||
const [rightTab, setRightTab] = useState<"result" | "defect">("result");
|
||||
const [processData, setProcessData] = useState<any[]>([]);
|
||||
@@ -231,6 +237,55 @@ export default function ProductionResultPage() {
|
||||
return result;
|
||||
}, [wiList, groupBy]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(wiList.length / 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]);
|
||||
|
||||
const paginatedGroupedData = useMemo(() => {
|
||||
if (groupBy === "none") return paginatedRows;
|
||||
const groups = new Map<string, any[]>();
|
||||
for (const wi of paginatedRows) {
|
||||
let key = wi[groupBy] || "미지정";
|
||||
if (groupBy === "progress_status") key = PROGRESS_LABEL[key] || key;
|
||||
else if (groupBy === "work_team" || groupBy === "status") key = resolveCategory(groupBy, key) || key;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key)!.push(wi);
|
||||
}
|
||||
const result: any[] = [];
|
||||
groups.forEach((items, gk) => {
|
||||
result.push({ _group: true, _key: gk, _count: items.length });
|
||||
result.push(...items);
|
||||
});
|
||||
return result;
|
||||
}, [paginatedRows, groupBy]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [wiList.length]);
|
||||
|
||||
const toggleGroup = (key: string) => {
|
||||
setExpandedGroups((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -337,7 +392,7 @@ export default function ProductionResultPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{groupedData.map((row, idx) => {
|
||||
{paginatedGroupedData.map((row, idx) => {
|
||||
if (row._group) {
|
||||
const expanded = expandedGroups.has(row._key);
|
||||
return (
|
||||
@@ -398,6 +453,85 @@ export default function ProductionResultPage() {
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<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>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
|
||||
|
||||
@@ -159,6 +159,11 @@ export default function SalesOrderPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터 (DynamicSearchFilter에서 관리)
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -427,6 +432,37 @@ export default function SalesOrderPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredFlatRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredFlatRows.slice(start, start + pageSize);
|
||||
}, [filteredFlatRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredFlatRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -931,8 +967,8 @@ export default function SalesOrderPage() {
|
||||
</div>
|
||||
|
||||
{/* 데이터 테이블 (플랫 리스트) */}
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card flex flex-col">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -1013,7 +1049,7 @@ export default function SalesOrderPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
ts.groupData(filteredFlatRows).map((row: any) => {
|
||||
ts.groupData(paginatedRows).map((row: any) => {
|
||||
// 그룹 요약 행 렌더링
|
||||
if (row._isGroupSummary) {
|
||||
return (
|
||||
@@ -1073,6 +1109,85 @@ export default function SalesOrderPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredFlatRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 수주 등록/수정 모달 */}
|
||||
|
||||
@@ -229,6 +229,11 @@ export default function OutboundPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -397,6 +402,37 @@ export default function OutboundPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredRows.slice(start, start + pageSize);
|
||||
}, [filteredRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -848,7 +884,7 @@ export default function OutboundPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -930,7 +966,7 @@ export default function OutboundPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRows.map((row) => {
|
||||
paginatedRows.map((row) => {
|
||||
const isChecked = checkedIds.includes(row.id);
|
||||
return (
|
||||
<TableRow
|
||||
@@ -989,6 +1025,85 @@ export default function OutboundPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 출고 등록 모달 */}
|
||||
|
||||
@@ -259,6 +259,11 @@ export default function ReceivingPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -430,6 +435,37 @@ export default function ReceivingPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredRows.slice(start, start + pageSize);
|
||||
}, [filteredRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -878,8 +914,8 @@ export default function ReceivingPage() {
|
||||
/>
|
||||
|
||||
{/* 입고 목록 테이블 (플랫 리스트) */}
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
||||
<div className="flex items-center justify-between border-b bg-muted/50 px-4 py-2.5">
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card flex flex-col">
|
||||
<div className="flex items-center justify-between border-b bg-muted/50 px-4 py-2.5 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-[13px] font-bold">입고 목록</h3>
|
||||
<span className="text-[11px] font-semibold text-primary bg-primary/10 border border-primary/15 px-2 py-0.5 rounded-full font-mono">
|
||||
@@ -891,7 +927,7 @@ export default function ReceivingPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="h-[calc(100%-44px)] overflow-auto">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -973,7 +1009,7 @@ export default function ReceivingPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRows.map((row) => {
|
||||
paginatedRows.map((row) => {
|
||||
const isChecked = checkedIds.includes(row.id);
|
||||
return (
|
||||
<TableRow
|
||||
@@ -1032,6 +1068,85 @@ export default function ReceivingPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 입고 등록 모달 */}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/componen
|
||||
import {
|
||||
Download, Loader2, Inbox, Search, BarChart3, AlertTriangle,
|
||||
CheckCircle2, Package, ChevronDown, ChevronRight, ClipboardList,
|
||||
ChevronLeft, ChevronsLeft, ChevronsRight,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
@@ -84,6 +85,11 @@ export default function ProductionResultPage() {
|
||||
const [groupBy, setGroupBy] = useState("none");
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// ── 우측: 실적 ──
|
||||
const [rightTab, setRightTab] = useState<"result" | "defect">("result");
|
||||
const [processData, setProcessData] = useState<any[]>([]);
|
||||
@@ -231,6 +237,55 @@ export default function ProductionResultPage() {
|
||||
return result;
|
||||
}, [wiList, groupBy]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(wiList.length / 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]);
|
||||
|
||||
const paginatedGroupedData = useMemo(() => {
|
||||
if (groupBy === "none") return paginatedRows;
|
||||
const groups = new Map<string, any[]>();
|
||||
for (const wi of paginatedRows) {
|
||||
let key = wi[groupBy] || "미지정";
|
||||
if (groupBy === "progress_status") key = PROGRESS_LABEL[key] || key;
|
||||
else if (groupBy === "work_team" || groupBy === "status") key = resolveCategory(groupBy, key) || key;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key)!.push(wi);
|
||||
}
|
||||
const result: any[] = [];
|
||||
groups.forEach((items, gk) => {
|
||||
result.push({ _group: true, _key: gk, _count: items.length });
|
||||
result.push(...items);
|
||||
});
|
||||
return result;
|
||||
}, [paginatedRows, groupBy]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [wiList.length]);
|
||||
|
||||
const toggleGroup = (key: string) => {
|
||||
setExpandedGroups((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -337,7 +392,7 @@ export default function ProductionResultPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{groupedData.map((row, idx) => {
|
||||
{paginatedGroupedData.map((row, idx) => {
|
||||
if (row._group) {
|
||||
const expanded = expandedGroups.has(row._key);
|
||||
return (
|
||||
@@ -398,6 +453,85 @@ export default function ProductionResultPage() {
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<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>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
|
||||
|
||||
@@ -159,6 +159,11 @@ export default function SalesOrderPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터 (DynamicSearchFilter에서 관리)
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -427,6 +432,37 @@ export default function SalesOrderPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredFlatRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredFlatRows.slice(start, start + pageSize);
|
||||
}, [filteredFlatRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredFlatRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -931,8 +967,8 @@ export default function SalesOrderPage() {
|
||||
</div>
|
||||
|
||||
{/* 데이터 테이블 (플랫 리스트) */}
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card flex flex-col">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -1013,7 +1049,19 @@ export default function SalesOrderPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredFlatRows.map((row) => {
|
||||
ts.groupData(paginatedRows).map((row: any) => {
|
||||
// 그룹 요약 행 렌더링
|
||||
if (row._isGroupSummary) {
|
||||
return (
|
||||
<TableRow key={`summary-${row._groupKey || Math.random()}`} className="bg-muted/60 font-semibold border-t border-primary/20">
|
||||
<TableCell colSpan={TOTAL_COLS} className="py-2 px-3 text-[13px] text-primary">
|
||||
{row._groupLabel || "합계"}: {row._count ? `${row._count}건` : ""}
|
||||
{row.qty ? ` · 수량 ${Number(row.qty).toLocaleString()}` : ""}
|
||||
{row.amount ? ` · 금액 ${Number(row.amount).toLocaleString()}` : ""}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
const isChecked = checkedIds.includes(row.id);
|
||||
return (
|
||||
<TableRow
|
||||
@@ -1061,6 +1109,85 @@ export default function SalesOrderPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredFlatRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 수주 등록/수정 모달 */}
|
||||
|
||||
@@ -229,6 +229,11 @@ export default function OutboundPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -397,6 +402,37 @@ export default function OutboundPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredRows.slice(start, start + pageSize);
|
||||
}, [filteredRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -848,7 +884,7 @@ export default function OutboundPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -930,7 +966,7 @@ export default function OutboundPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRows.map((row) => {
|
||||
paginatedRows.map((row) => {
|
||||
const isChecked = checkedIds.includes(row.id);
|
||||
return (
|
||||
<TableRow
|
||||
@@ -989,6 +1025,85 @@ export default function OutboundPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 출고 등록 모달 */}
|
||||
|
||||
@@ -259,6 +259,11 @@ export default function ReceivingPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -430,6 +435,37 @@ export default function ReceivingPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredRows.slice(start, start + pageSize);
|
||||
}, [filteredRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -878,8 +914,8 @@ export default function ReceivingPage() {
|
||||
/>
|
||||
|
||||
{/* 입고 목록 테이블 (플랫 리스트) */}
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
||||
<div className="flex items-center justify-between border-b bg-muted/50 px-4 py-2.5">
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card flex flex-col">
|
||||
<div className="flex items-center justify-between border-b bg-muted/50 px-4 py-2.5 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-[13px] font-bold">입고 목록</h3>
|
||||
<span className="text-[11px] font-semibold text-primary bg-primary/10 border border-primary/15 px-2 py-0.5 rounded-full font-mono">
|
||||
@@ -891,7 +927,7 @@ export default function ReceivingPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="h-[calc(100%-44px)] overflow-auto">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -973,7 +1009,7 @@ export default function ReceivingPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRows.map((row) => {
|
||||
paginatedRows.map((row) => {
|
||||
const isChecked = checkedIds.includes(row.id);
|
||||
return (
|
||||
<TableRow
|
||||
@@ -1032,6 +1068,85 @@ export default function ReceivingPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 입고 등록 모달 */}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/componen
|
||||
import {
|
||||
Download, Loader2, Inbox, Search, BarChart3, AlertTriangle,
|
||||
CheckCircle2, Package, ChevronDown, ChevronRight, ClipboardList,
|
||||
ChevronLeft, ChevronsLeft, ChevronsRight,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
@@ -84,6 +85,11 @@ export default function ProductionResultPage() {
|
||||
const [groupBy, setGroupBy] = useState("none");
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// ── 우측: 실적 ──
|
||||
const [rightTab, setRightTab] = useState<"result" | "defect">("result");
|
||||
const [processData, setProcessData] = useState<any[]>([]);
|
||||
@@ -231,6 +237,55 @@ export default function ProductionResultPage() {
|
||||
return result;
|
||||
}, [wiList, groupBy]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(wiList.length / 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]);
|
||||
|
||||
const paginatedGroupedData = useMemo(() => {
|
||||
if (groupBy === "none") return paginatedRows;
|
||||
const groups = new Map<string, any[]>();
|
||||
for (const wi of paginatedRows) {
|
||||
let key = wi[groupBy] || "미지정";
|
||||
if (groupBy === "progress_status") key = PROGRESS_LABEL[key] || key;
|
||||
else if (groupBy === "work_team" || groupBy === "status") key = resolveCategory(groupBy, key) || key;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key)!.push(wi);
|
||||
}
|
||||
const result: any[] = [];
|
||||
groups.forEach((items, gk) => {
|
||||
result.push({ _group: true, _key: gk, _count: items.length });
|
||||
result.push(...items);
|
||||
});
|
||||
return result;
|
||||
}, [paginatedRows, groupBy]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [wiList.length]);
|
||||
|
||||
const toggleGroup = (key: string) => {
|
||||
setExpandedGroups((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -337,7 +392,7 @@ export default function ProductionResultPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{groupedData.map((row, idx) => {
|
||||
{paginatedGroupedData.map((row, idx) => {
|
||||
if (row._group) {
|
||||
const expanded = expandedGroups.has(row._key);
|
||||
return (
|
||||
@@ -398,6 +453,85 @@ export default function ProductionResultPage() {
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<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>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
|
||||
|
||||
@@ -159,6 +159,11 @@ export default function SalesOrderPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터 (DynamicSearchFilter에서 관리)
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -427,6 +432,37 @@ export default function SalesOrderPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredFlatRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredFlatRows.slice(start, start + pageSize);
|
||||
}, [filteredFlatRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredFlatRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -931,8 +967,8 @@ export default function SalesOrderPage() {
|
||||
</div>
|
||||
|
||||
{/* 데이터 테이블 (플랫 리스트) */}
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card flex flex-col">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -1013,7 +1049,7 @@ export default function SalesOrderPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
ts.groupData(filteredFlatRows).map((row: any) => {
|
||||
ts.groupData(paginatedRows).map((row: any) => {
|
||||
// 그룹 요약 행 렌더링
|
||||
if (row._isGroupSummary) {
|
||||
return (
|
||||
@@ -1073,6 +1109,85 @@ export default function SalesOrderPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredFlatRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 수주 등록/수정 모달 */}
|
||||
|
||||
@@ -229,6 +229,11 @@ export default function OutboundPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -397,6 +402,37 @@ export default function OutboundPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredRows.slice(start, start + pageSize);
|
||||
}, [filteredRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -848,7 +884,7 @@ export default function OutboundPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -930,7 +966,7 @@ export default function OutboundPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRows.map((row) => {
|
||||
paginatedRows.map((row) => {
|
||||
const isChecked = checkedIds.includes(row.id);
|
||||
return (
|
||||
<TableRow
|
||||
@@ -989,6 +1025,85 @@ export default function OutboundPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 출고 등록 모달 */}
|
||||
|
||||
@@ -259,6 +259,11 @@ export default function ReceivingPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -430,6 +435,37 @@ export default function ReceivingPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredRows.slice(start, start + pageSize);
|
||||
}, [filteredRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -878,8 +914,8 @@ export default function ReceivingPage() {
|
||||
/>
|
||||
|
||||
{/* 입고 목록 테이블 (플랫 리스트) */}
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
||||
<div className="flex items-center justify-between border-b bg-muted/50 px-4 py-2.5">
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card flex flex-col">
|
||||
<div className="flex items-center justify-between border-b bg-muted/50 px-4 py-2.5 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-[13px] font-bold">입고 목록</h3>
|
||||
<span className="text-[11px] font-semibold text-primary bg-primary/10 border border-primary/15 px-2 py-0.5 rounded-full font-mono">
|
||||
@@ -891,7 +927,7 @@ export default function ReceivingPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="h-[calc(100%-44px)] overflow-auto">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -973,7 +1009,7 @@ export default function ReceivingPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRows.map((row) => {
|
||||
paginatedRows.map((row) => {
|
||||
const isChecked = checkedIds.includes(row.id);
|
||||
return (
|
||||
<TableRow
|
||||
@@ -1032,6 +1068,85 @@ export default function ReceivingPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 입고 등록 모달 */}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/componen
|
||||
import {
|
||||
Download, Loader2, Inbox, Search, BarChart3, AlertTriangle,
|
||||
CheckCircle2, Package, ChevronDown, ChevronRight, ClipboardList,
|
||||
ChevronLeft, ChevronsLeft, ChevronsRight,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
@@ -84,6 +85,11 @@ export default function ProductionResultPage() {
|
||||
const [groupBy, setGroupBy] = useState("none");
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// ── 우측: 실적 ──
|
||||
const [rightTab, setRightTab] = useState<"result" | "defect">("result");
|
||||
const [processData, setProcessData] = useState<any[]>([]);
|
||||
@@ -231,6 +237,55 @@ export default function ProductionResultPage() {
|
||||
return result;
|
||||
}, [wiList, groupBy]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(wiList.length / 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]);
|
||||
|
||||
const paginatedGroupedData = useMemo(() => {
|
||||
if (groupBy === "none") return paginatedRows;
|
||||
const groups = new Map<string, any[]>();
|
||||
for (const wi of paginatedRows) {
|
||||
let key = wi[groupBy] || "미지정";
|
||||
if (groupBy === "progress_status") key = PROGRESS_LABEL[key] || key;
|
||||
else if (groupBy === "work_team" || groupBy === "status") key = resolveCategory(groupBy, key) || key;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key)!.push(wi);
|
||||
}
|
||||
const result: any[] = [];
|
||||
groups.forEach((items, gk) => {
|
||||
result.push({ _group: true, _key: gk, _count: items.length });
|
||||
result.push(...items);
|
||||
});
|
||||
return result;
|
||||
}, [paginatedRows, groupBy]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [wiList.length]);
|
||||
|
||||
const toggleGroup = (key: string) => {
|
||||
setExpandedGroups((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -337,7 +392,7 @@ export default function ProductionResultPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{groupedData.map((row, idx) => {
|
||||
{paginatedGroupedData.map((row, idx) => {
|
||||
if (row._group) {
|
||||
const expanded = expandedGroups.has(row._key);
|
||||
return (
|
||||
@@ -398,6 +453,85 @@ export default function ProductionResultPage() {
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<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>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
|
||||
|
||||
@@ -159,6 +159,11 @@ export default function SalesOrderPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터 (DynamicSearchFilter에서 관리)
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -427,6 +432,37 @@ export default function SalesOrderPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredFlatRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredFlatRows.slice(start, start + pageSize);
|
||||
}, [filteredFlatRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredFlatRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -931,8 +967,8 @@ export default function SalesOrderPage() {
|
||||
</div>
|
||||
|
||||
{/* 데이터 테이블 (플랫 리스트) */}
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card flex flex-col">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -1013,7 +1049,7 @@ export default function SalesOrderPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
ts.groupData(filteredFlatRows).map((row: any) => {
|
||||
ts.groupData(paginatedRows).map((row: any) => {
|
||||
// 그룹 요약 행 렌더링
|
||||
if (row._isGroupSummary) {
|
||||
return (
|
||||
@@ -1073,6 +1109,85 @@ export default function SalesOrderPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredFlatRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 수주 등록/수정 모달 */}
|
||||
|
||||
@@ -229,6 +229,11 @@ export default function OutboundPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -397,6 +402,37 @@ export default function OutboundPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredRows.slice(start, start + pageSize);
|
||||
}, [filteredRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -848,7 +884,7 @@ export default function OutboundPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -930,7 +966,7 @@ export default function OutboundPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRows.map((row) => {
|
||||
paginatedRows.map((row) => {
|
||||
const isChecked = checkedIds.includes(row.id);
|
||||
return (
|
||||
<TableRow
|
||||
@@ -989,6 +1025,85 @@ export default function OutboundPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 출고 등록 모달 */}
|
||||
|
||||
@@ -259,6 +259,11 @@ export default function ReceivingPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [checkedIds, setCheckedIds] = useState<string[]>([]);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -430,6 +435,37 @@ export default function ReceivingPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredRows.slice(start, start + pageSize);
|
||||
}, [filteredRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -878,8 +914,8 @@ export default function ReceivingPage() {
|
||||
/>
|
||||
|
||||
{/* 입고 목록 테이블 (플랫 리스트) */}
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
||||
<div className="flex items-center justify-between border-b bg-muted/50 px-4 py-2.5">
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card flex flex-col">
|
||||
<div className="flex items-center justify-between border-b bg-muted/50 px-4 py-2.5 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-[13px] font-bold">입고 목록</h3>
|
||||
<span className="text-[11px] font-semibold text-primary bg-primary/10 border border-primary/15 px-2 py-0.5 rounded-full font-mono">
|
||||
@@ -891,7 +927,7 @@ export default function ReceivingPage() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="h-[calc(100%-44px)] overflow-auto">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -973,7 +1009,7 @@ export default function ReceivingPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredRows.map((row) => {
|
||||
paginatedRows.map((row) => {
|
||||
const isChecked = checkedIds.includes(row.id);
|
||||
return (
|
||||
<TableRow
|
||||
@@ -1032,6 +1068,85 @@ export default function ReceivingPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 입고 등록 모달 */}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/componen
|
||||
import {
|
||||
Download, Loader2, Inbox, Search, BarChart3, AlertTriangle,
|
||||
CheckCircle2, Package, ChevronDown, ChevronRight, ClipboardList,
|
||||
ChevronLeft, ChevronsLeft, ChevronsRight,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
@@ -84,6 +85,11 @@ export default function ProductionResultPage() {
|
||||
const [groupBy, setGroupBy] = useState("none");
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// ── 우측: 실적 ──
|
||||
const [rightTab, setRightTab] = useState<"result" | "defect">("result");
|
||||
const [processData, setProcessData] = useState<any[]>([]);
|
||||
@@ -231,6 +237,55 @@ export default function ProductionResultPage() {
|
||||
return result;
|
||||
}, [wiList, groupBy]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(wiList.length / 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]);
|
||||
|
||||
const paginatedGroupedData = useMemo(() => {
|
||||
if (groupBy === "none") return paginatedRows;
|
||||
const groups = new Map<string, any[]>();
|
||||
for (const wi of paginatedRows) {
|
||||
let key = wi[groupBy] || "미지정";
|
||||
if (groupBy === "progress_status") key = PROGRESS_LABEL[key] || key;
|
||||
else if (groupBy === "work_team" || groupBy === "status") key = resolveCategory(groupBy, key) || key;
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key)!.push(wi);
|
||||
}
|
||||
const result: any[] = [];
|
||||
groups.forEach((items, gk) => {
|
||||
result.push({ _group: true, _key: gk, _count: items.length });
|
||||
result.push(...items);
|
||||
});
|
||||
return result;
|
||||
}, [paginatedRows, groupBy]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [wiList.length]);
|
||||
|
||||
const toggleGroup = (key: string) => {
|
||||
setExpandedGroups((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -337,7 +392,7 @@ export default function ProductionResultPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{groupedData.map((row, idx) => {
|
||||
{paginatedGroupedData.map((row, idx) => {
|
||||
if (row._group) {
|
||||
const expanded = expandedGroups.has(row._key);
|
||||
return (
|
||||
@@ -398,6 +453,85 @@ export default function ProductionResultPage() {
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<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>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
|
||||
|
||||
@@ -159,6 +159,11 @@ export default function SalesOrderPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
|
||||
// 페이지네이션
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [pageSizeInput, setPageSizeInput] = useState("20");
|
||||
|
||||
// 검색 필터 (DynamicSearchFilter에서 관리)
|
||||
const [searchFilters, setSearchFilters] = useState<FilterValue[]>([]);
|
||||
|
||||
@@ -427,6 +432,37 @@ export default function SalesOrderPage() {
|
||||
return rows;
|
||||
}, [flatRows, headerFilters, sortState]);
|
||||
|
||||
// 페이지네이션 계산
|
||||
const totalPages = Math.max(1, Math.ceil(filteredFlatRows.length / pageSize));
|
||||
const safePage = Math.min(Math.max(1, currentPage), totalPages);
|
||||
const paginatedRows = useMemo(() => {
|
||||
const start = (safePage - 1) * pageSize;
|
||||
return filteredFlatRows.slice(start, start + pageSize);
|
||||
}, [filteredFlatRows, safePage, pageSize]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) { setPageSize(n); setCurrentPage(1); }
|
||||
else setPageSizeInput(String(pageSize));
|
||||
};
|
||||
|
||||
const getPageNumbers = (): (number | "...")[] => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (safePage > 3) pages.push("...");
|
||||
for (let i = Math.max(2, safePage - 1); i <= Math.min(totalPages - 1, safePage + 1); i++) pages.push(i);
|
||||
if (safePage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 필터 변경 시 첫 페이지로 이동
|
||||
useEffect(() => { setCurrentPage(1); }, [headerFilters, sortState, filteredFlatRows.length]);
|
||||
|
||||
// 헤더 필터 토글/초기화
|
||||
const toggleHeaderFilter = (colKey: string, value: string) => {
|
||||
setHeaderFilters((prev) => {
|
||||
@@ -931,8 +967,8 @@ export default function SalesOrderPage() {
|
||||
</div>
|
||||
|
||||
{/* 데이터 테이블 (플랫 리스트) */}
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
||||
<div className="h-full overflow-auto">
|
||||
<div className="flex-1 overflow-hidden rounded-lg border border-border bg-card flex flex-col">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table noWrapper style={{ minWidth: "1500px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "40px" }} />
|
||||
@@ -1013,7 +1049,19 @@ export default function SalesOrderPage() {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredFlatRows.map((row) => {
|
||||
ts.groupData(paginatedRows).map((row: any) => {
|
||||
// 그룹 요약 행 렌더링
|
||||
if (row._isGroupSummary) {
|
||||
return (
|
||||
<TableRow key={`summary-${row._groupKey || Math.random()}`} className="bg-muted/60 font-semibold border-t border-primary/20">
|
||||
<TableCell colSpan={TOTAL_COLS} className="py-2 px-3 text-[13px] text-primary">
|
||||
{row._groupLabel || "합계"}: {row._count ? `${row._count}건` : ""}
|
||||
{row.qty ? ` · 수량 ${Number(row.qty).toLocaleString()}` : ""}
|
||||
{row.amount ? ` · 금액 ${Number(row.amount).toLocaleString()}` : ""}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
const isChecked = checkedIds.includes(row.id);
|
||||
return (
|
||||
<TableRow
|
||||
@@ -1061,6 +1109,85 @@ export default function SalesOrderPage() {
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* 페이지네이션 */}
|
||||
<div className="flex items-center justify-between border-t px-4 py-2.5 text-xs text-muted-foreground shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>전체</span>
|
||||
<span className="font-medium text-foreground">{filteredFlatRows.length.toLocaleString()}</span>
|
||||
<span>건</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
value={pageSizeInput}
|
||||
onChange={(e) => setPageSizeInput(e.target.value)}
|
||||
onBlur={applyPageSize}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); applyPageSize(); } }}
|
||||
className="h-7 w-16 text-center text-xs"
|
||||
/>
|
||||
<span>건씩 보기</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<button onClick={() => setCurrentPage(1)} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage((p) => Math.max(1, p - 1))} disabled={safePage === 1}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{getPageNumbers().map((page, idx) =>
|
||||
page === "..." ? (
|
||||
<span key={`dot-${idx}`} className="h-7 w-7 flex items-center justify-center text-muted-foreground">...</span>
|
||||
) : (
|
||||
<button key={page} onClick={() => setCurrentPage(page as number)}
|
||||
className={cn("h-7 w-7 flex items-center justify-center rounded text-xs",
|
||||
page === safePage ? "bg-primary text-primary-foreground font-medium" : "hover:bg-muted")}>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
<button onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(totalPages)} disabled={safePage === totalPages}
|
||||
className="h-7 w-7 flex items-center justify-center rounded hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed">
|
||||
<ChevronsRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
placeholder={String(safePage)}
|
||||
className="h-7 w-14 text-center text-xs"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
const val = parseInt((e.target as HTMLInputElement).value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
(e.target as HTMLInputElement).blur();
|
||||
}
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
if (!isNaN(val) && val >= 1 && val <= totalPages) {
|
||||
setCurrentPage(val);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<span>/ {totalPages} 페이지</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 수주 등록/수정 모달 */}
|
||||
|
||||
Reference in New Issue
Block a user