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:
kjs
2026-04-13 18:40:32 +09:00
parent f54d763373
commit 28bb443ee8
28 changed files with 3447 additions and 70 deletions
@@ -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>
{/* 수주 등록/수정 모달 */}