diff --git a/frontend/app/(main)/COMPANY_29/logistics/outbound/page.tsx b/frontend/app/(main)/COMPANY_29/logistics/outbound/page.tsx index 57ea6455..b834b76a 100644 --- a/frontend/app/(main)/COMPANY_29/logistics/outbound/page.tsx +++ b/frontend/app/(main)/COMPANY_29/logistics/outbound/page.tsx @@ -37,6 +37,9 @@ import { X, Save, ChevronRight, + ChevronLeft, + ChevronsLeft, + ChevronsRight, } from "lucide-react"; import { cn } from "@/lib/utils"; import { @@ -131,6 +134,10 @@ export default function OutboundPage() { const [items, setItems] = useState([]); const [warehouses, setWarehouses] = useState([]); + // 소스 데이터 페이징 (클라이언트 사이드) + const [sourcePage, setSourcePage] = useState(1); + const [sourcePageSize, setSourcePageSize] = useState(20); + // 날짜 초기화 useEffect(() => { const today = new Date(); @@ -261,13 +268,44 @@ export default function OutboundPage() { }; const searchSourceData = useCallback(async () => { + setSourcePage(1); await loadSourceData(modalOutboundType, sourceKeyword || undefined); }, [modalOutboundType, sourceKeyword, loadSourceData]); + // 현재 출고유형에 따른 전체 소스 데이터 + const allSourceData = useMemo(() => { + if (modalOutboundType === "판매출고") return shipmentInstructions; + if (modalOutboundType === "반품출고") return purchaseOrders; + return items; + }, [modalOutboundType, shipmentInstructions, purchaseOrders, items]); + + const sourceTotalCount = allSourceData.length; + const sourceTotalPages = Math.max(1, Math.ceil(sourceTotalCount / sourcePageSize)); + + // 현재 페이지에 해당하는 slice + const pagedShipmentInstructions = useMemo(() => { + if (modalOutboundType !== "판매출고") return []; + const start = (sourcePage - 1) * sourcePageSize; + return shipmentInstructions.slice(start, start + sourcePageSize); + }, [modalOutboundType, shipmentInstructions, sourcePage, sourcePageSize]); + + const pagedPurchaseOrders = useMemo(() => { + if (modalOutboundType !== "반품출고") return []; + const start = (sourcePage - 1) * sourcePageSize; + return purchaseOrders.slice(start, start + sourcePageSize); + }, [modalOutboundType, purchaseOrders, sourcePage, sourcePageSize]); + + const pagedItems = useMemo(() => { + if (modalOutboundType !== "기타출고") return []; + const start = (sourcePage - 1) * sourcePageSize; + return items.slice(start, start + sourcePageSize); + }, [modalOutboundType, items, sourcePage, sourcePageSize]); + const handleOutboundTypeChange = useCallback( (type: string) => { setModalOutboundType(type); setSourceKeyword(""); + setSourcePage(1); setShipmentInstructions([]); setPurchaseOrders([]); setItems([]); @@ -686,6 +724,7 @@ export default function OutboundPage() { defaultMaxWidth="sm:max-w-[1600px]" defaultWidth="w-[95vw]" className="h-[90vh] p-0" + contentClassName="overflow-hidden flex flex-col" footer={
@@ -774,43 +813,87 @@ export default function OutboundPage() {
-
-

+
+

{modalOutboundType === "판매출고" ? "미출고 출하지시 목록" : modalOutboundType === "반품출고" ? "입고된 발주 목록" : "품목 목록"}

+ {sourceTotalCount > 0 && ( + 총 {sourceTotalCount}건 + )} +
+
{sourceLoading ? (
) : modalOutboundType === "판매출고" ? ( s.key)} /> ) : modalOutboundType === "반품출고" ? ( s.key)} /> ) : ( s.key)} /> )}
+ + {/* 페이징 바 */} + {sourceTotalCount > 0 && ( +
+
+ 표시: + { + const v = parseInt(e.target.value, 10); + if (v > 0) { setSourcePageSize(v); setSourcePage(1); } + }} + className="h-7 w-[60px] text-center text-[11px]" + /> +
+
+ + + {sourcePage} / {sourceTotalPages} + + +
+
+ )}

- + e.stopPropagation()} /> {/* 우측: 출고 정보 + 선택 품목 */} diff --git a/frontend/app/(main)/COMPANY_29/logistics/receiving/page.tsx b/frontend/app/(main)/COMPANY_29/logistics/receiving/page.tsx index a5de8c9d..6e493a5c 100644 --- a/frontend/app/(main)/COMPANY_29/logistics/receiving/page.tsx +++ b/frontend/app/(main)/COMPANY_29/logistics/receiving/page.tsx @@ -37,6 +37,9 @@ import { X, Save, ChevronRight, + ChevronLeft, + ChevronsLeft, + ChevronsRight, } from "lucide-react"; import { cn } from "@/lib/utils"; import { @@ -132,6 +135,11 @@ export default function ReceivingPage() { const [items, setItems] = useState([]); const [warehouses, setWarehouses] = useState([]); + // 소스 데이터 페이징 + const [sourcePage, setSourcePage] = useState(1); + const [sourcePageSize, setSourcePageSize] = useState(20); + const [sourceTotalCount, setSourceTotalCount] = useState(0); + // 날짜 초기화 useEffect(() => { const today = new Date(); @@ -214,18 +222,32 @@ export default function ReceivingPage() { // 소스 데이터 로드 함수 const loadSourceData = useCallback( - async (type: string, keyword?: string) => { + async (type: string, keyword?: string, pageOverride?: number) => { setSourceLoading(true); try { + const params = { + keyword: keyword || undefined, + page: pageOverride ?? sourcePage, + pageSize: sourcePageSize, + }; if (type === "구매입고") { - const res = await getPurchaseOrderSources(keyword || undefined); - if (res.success) setPurchaseOrders(res.data); + const res = await getPurchaseOrderSources(params); + if (res.success) { + setPurchaseOrders(res.data); + setSourceTotalCount(res.totalCount || 0); + } } else if (type === "반품입고") { - const res = await getShipmentSources(keyword || undefined); - if (res.success) setShipments(res.data); + const res = await getShipmentSources(params); + if (res.success) { + setShipments(res.data); + setSourceTotalCount(res.totalCount || 0); + } } else { - const res = await getItemSources(keyword || undefined); - if (res.success) setItems(res.data); + const res = await getItemSources(params); + if (res.success) { + setItems(res.data); + setSourceTotalCount(res.totalCount || 0); + } } } catch { // ignore @@ -233,7 +255,7 @@ export default function ReceivingPage() { setSourceLoading(false); } }, - [] + [sourcePage, sourcePageSize] ); const openRegisterModal = async () => { @@ -250,13 +272,15 @@ export default function ReceivingPage() { setPurchaseOrders([]); setShipments([]); setItems([]); + setSourcePage(1); + setSourceTotalCount(0); setIsModalOpen(true); // 입고번호 생성 + 발주 데이터 동시 로드 try { const [numRes] = await Promise.all([ generateReceivingNumber(), - loadSourceData(defaultType), + loadSourceData(defaultType, undefined, 1), ]); if (numRes.success) setModalInboundNo(numRes.data); } catch { @@ -266,7 +290,8 @@ export default function ReceivingPage() { // 검색 버튼 클릭 시 const searchSourceData = useCallback(async () => { - await loadSourceData(modalInboundType, sourceKeyword || undefined); + setSourcePage(1); + await loadSourceData(modalInboundType, sourceKeyword || undefined, 1); }, [modalInboundType, sourceKeyword, loadSourceData]); // 입고유형 변경 시 소스 데이터 자동 리로드 @@ -278,7 +303,9 @@ export default function ReceivingPage() { setShipments([]); setItems([]); setSelectedItems([]); - loadSourceData(type); + setSourcePage(1); + setSourceTotalCount(0); + loadSourceData(type, undefined, 1); }, [loadSourceData] ); @@ -303,7 +330,7 @@ export default function ReceivingPage() { inbound_qty: po.remain_qty, unit_price: po.unit_price, total_amount: po.remain_qty * po.unit_price, - source_table: "purchase_order_mng", + source_table: po.source_table || "purchase_order_mng", source_id: po.id, }, ]); @@ -694,6 +721,7 @@ export default function ReceivingPage() { defaultMaxWidth="sm:max-w-[1600px]" defaultWidth="w-[95vw]" className="h-[90vh] p-0" + contentClassName="overflow-hidden flex flex-col" footer={
@@ -817,10 +845,56 @@ export default function ReceivingPage() { /> )}
+ + {/* 페이징 */} + {sourceTotalCount > 0 && ( +
+
+ 표시: + { + const v = parseInt(e.target.value, 10); + if (v > 0) { + setSourcePageSize(v); + setSourcePage(1); + loadSourceData(modalInboundType, sourceKeyword || undefined, 1); + } + }} + className="h-7 w-[60px] text-center text-[11px]" + /> + + 총 {sourceTotalCount}건 + +
+
+ + + {sourcePage} / {Math.max(1, Math.ceil(sourceTotalCount / sourcePageSize))} + + +
+
+ )}
- + e.stopPropagation()} /> {/* 우측: 입고 정보 + 선택 품목 */} @@ -1030,7 +1104,7 @@ function SourcePurchaseOrderTable({ return ( - + 발주번호 @@ -1109,7 +1183,7 @@ function SourceShipmentTable({ return (
- + 출하번호 @@ -1186,7 +1260,7 @@ function SourceItemTable({ return (
- + 품목 diff --git a/frontend/app/(main)/COMPANY_29/sales/sales-item/page.tsx b/frontend/app/(main)/COMPANY_29/sales/sales-item/page.tsx index a5097b42..5da2b3b5 100644 --- a/frontend/app/(main)/COMPANY_29/sales/sales-item/page.tsx +++ b/frontend/app/(main)/COMPANY_29/sales/sales-item/page.tsx @@ -81,7 +81,7 @@ export default function SalesItemPage() { const [customerLoading, setCustomerLoading] = useState(false); // 카테고리 - const [categoryOptions, setCategoryOptions] = useState>({}); + const [categoryOptions, setCategoryOptions] = useState>({}); // 거래처 추가 모달 const [custSelectOpen, setCustSelectOpen] = useState(false); @@ -125,11 +125,11 @@ export default function SalesItemPage() { // 카테고리 로드 useEffect(() => { const load = async () => { - const optMap: Record = {}; - const flatten = (vals: any[]): { code: string; label: string }[] => { - const result: { code: string; label: string }[] = []; + const optMap: Record = {}; + const flatten = (vals: any[]): { code: string; label: string; isDefault?: boolean }[] => { + const result: { code: string; label: string; isDefault?: boolean }[] = []; for (const v of vals) { - result.push({ code: v.valueCode, label: v.valueLabel }); + result.push({ code: v.valueCode, label: v.valueLabel, isDefault: v.isDefault }); if (v.children?.length) result.push(...flatten(v.children)); } return result; @@ -164,7 +164,11 @@ export default function SalesItemPage() { const fetchItems = useCallback(async () => { setItemLoading(true); try { - const filters = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); + const filters: { columnName: string; operator: string; value: any }[] = searchFilters.map((f) => ({ columnName: f.columnName, operator: f.operator, value: f.value })); + + // 판매품목 division 필터 (다중값 컬럼이므로 contains로 매칭) + filters.push({ columnName: "division", operator: "contains", value: "CAT_DIV_SALES" }); + const res = await apiClient.post(`/table-management/tables/${ITEM_TABLE}/data`, { page: 1, size: 500, dataFilter: filters.length > 0 ? { enabled: true, filters } : undefined, diff --git a/frontend/app/(main)/COMPANY_29/sales/shipping-order/page.tsx b/frontend/app/(main)/COMPANY_29/sales/shipping-order/page.tsx index 9ffb45bd..5ff26159 100644 --- a/frontend/app/(main)/COMPANY_29/sales/shipping-order/page.tsx +++ b/frontend/app/(main)/COMPANY_29/sales/shipping-order/page.tsx @@ -12,7 +12,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogD import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; -import { Plus, Trash2, RotateCcw, Save, X, ChevronDown, ChevronRight, ChevronLeft, Truck, Search, Loader2, FileSpreadsheet } from "lucide-react"; +import { Plus, Trash2, RotateCcw, Save, X, ChevronDown, ChevronRight, ChevronLeft, ChevronsLeft, ChevronsRight, Truck, Search, Loader2, FileSpreadsheet } from "lucide-react"; import { cn } from "@/lib/utils"; import { FormDatePicker } from "@/components/screen/filters/FormDatePicker"; import { @@ -117,7 +117,7 @@ export default function ShippingOrderPage() { const [sourceLoading, setSourceLoading] = useState(false); const [selectedItems, setSelectedItems] = useState([]); const [sourcePage, setSourcePage] = useState(1); - const [sourcePageSize] = useState(20); + const [sourcePageSize, setSourcePageSize] = useState(20); const [sourceTotalCount, setSourceTotalCount] = useState(0); // 텍스트 입력 debounce (500ms) @@ -592,6 +592,8 @@ export default function ShippingOrderPage() { description={isEditMode ? "출하지시 정보를 수정합니다." : "왼쪽에서 데이터를 선택하고 오른쪽에서 출하지시 정보를 입력하세요."} defaultMaxWidth="max-w-[90vw]" defaultWidth="w-[1400px]" + className="h-[90vh]" + contentClassName="overflow-hidden flex flex-col" footer={ <> @@ -694,10 +696,28 @@ export default function ShippingOrderPage() { {/* 페이징 */} {sourceTotalCount > 0 && (
- - 총 {sourceTotalCount}건 중 {(sourcePage - 1) * sourcePageSize + 1}-{Math.min(sourcePage * sourcePageSize, sourceTotalCount)}건 - +
+ 표시: + { + const v = parseInt(e.target.value, 10); + if (v > 0) { setSourcePageSize(v); setSourcePage(1); fetchSourceData(1); } + }} + className="h-7 w-[60px] text-center text-[11px]" + /> + + 총 {sourceTotalCount}건 + +
+ +
)} - + e.stopPropagation()} /> {/* 오른쪽: 폼 */}