'use client'; /** * 입고 목록 - 기획서 기준 마이그레이션 * * 기획서 기준: * - 날짜 범위 필터 * - 상태 셀렉트 필터 (전체, 입고대기, 입고완료, 검사완료) * - 통계 카드 (입고대기, 입고완료, 검사 중, 검사완료) * - 입고 등록 버튼 * - 테이블 헤더: 체크박스, 번호, 로트번호, 수입검사, 검사일, 발주처, 품목코드, 품목명, 규격, 단위, 입고수량, 입고일, 작성자, 상태 */ import { useState, useEffect, useMemo, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { Package, CheckCircle2, Clock, ClipboardCheck, Plus, Eye, Settings2, } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; import { UniversalListPage, type UniversalListConfig, type SelectionHandlers, type RowClickHandlers, type StatCard, type ListParams, type FilterFieldConfig, } from '@/components/templates/UniversalListPage'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; import { InventoryAdjustmentDialog } from './InventoryAdjustmentDialog'; import { getReceivings, getReceivingStats } from './actions'; import { RECEIVING_STATUS_LABELS, RECEIVING_STATUS_STYLES } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import type { ReceivingItem, ReceivingStats } from './types'; // 페이지당 항목 수 const ITEMS_PER_PAGE = 20; export function ReceivingList() { const router = useRouter(); // ===== 통계 데이터 (외부 관리) ===== const [stats, setStats] = useState(null); const [totalItems, setTotalItems] = useState(0); // ===== 재고 조정 팝업 상태 ===== const [isAdjustmentOpen, setIsAdjustmentOpen] = useState(false); // ===== 날짜 범위 상태 (최근 30일) ===== const today = new Date(); const thirtyDaysAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000); const [startDate, setStartDate] = useState(thirtyDaysAgo.toISOString().split('T')[0]); const [endDate, setEndDate] = useState(today.toISOString().split('T')[0]); // ===== 필터 상태 ===== const [filterValues, setFilterValues] = useState>({ status: 'all', }); // 초기 통계 로드 useEffect(() => { const loadStats = async () => { try { const result = await getReceivingStats(); if (result.success && result.data) { setStats(result.data); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[ReceivingList] loadStats error:', error); } }; loadStats(); }, []); // ===== 행 클릭 핸들러 ===== const handleRowClick = useCallback( (item: ReceivingItem) => { router.push(`/ko/material/receiving-management/${item.id}?mode=view`); }, [router] ); // ===== 입고 등록 핸들러 ===== const handleRegister = useCallback(() => { router.push('/ko/material/receiving-management/new?mode=new'); }, [router]); // ===== 통계 카드 ===== const statCards: StatCard[] = useMemo( () => [ { label: '입고대기', value: `${stats?.receivingPendingCount ?? 0}`, icon: Clock, iconColor: 'text-yellow-600', }, { label: '입고완료', value: `${stats?.receivingCompletedCount ?? 0}`, icon: CheckCircle2, iconColor: 'text-green-600', }, { label: '검사 중', value: `${stats?.inspectionPendingCount ?? 0}`, icon: ClipboardCheck, iconColor: 'text-orange-600', }, { label: '검사완료', value: `${stats?.inspectionCompletedCount ?? 0}`, icon: CheckCircle2, iconColor: 'text-blue-600', }, ], [stats] ); // ===== 필터 설정 ===== const filterConfig: FilterFieldConfig[] = [ { key: 'status', label: '상태', type: 'single', options: [ { value: 'receiving_pending', label: '입고대기' }, { value: 'completed', label: '입고완료' }, { value: 'inspection_completed', label: '검사완료' }, ], }, ]; // ===== 테이블 푸터 ===== const tableFooter = useMemo( () => ( 총 {totalItems}건 ), [totalItems] ); // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ // 페이지 기본 정보 title: '입고 목록', description: '입고를 관리합니다', icon: Package, basePath: '/material/receiving-management', // ID 추출 idField: 'id', // API 액션 (서버 사이드 페이지네이션) actions: { getList: async (params?: ListParams) => { try { const statusFilter = params?.filters?.status as string; const result = await getReceivings({ page: params?.page || 1, perPage: params?.pageSize || ITEMS_PER_PAGE, status: statusFilter !== 'all' ? statusFilter : undefined, search: params?.search || undefined, startDate, endDate, }); if (result.success) { // 통계 다시 로드 const statsResult = await getReceivingStats(); if (statsResult.success && statsResult.data) { setStats(statsResult.data); } // totalItems 업데이트 setTotalItems(result.pagination.total); return { success: true, data: result.data, totalCount: result.pagination.total, totalPages: result.pagination.lastPage, }; } return { success: false, error: result.error }; } catch (error) { if (isNextRedirectError(error)) throw error; return { success: false, error: '데이터 로드 중 오류가 발생했습니다.' }; } }, }, // 테이블 컬럼 (기획서 2026-02-03 순서) columns: [ { key: 'no', label: 'No.', className: 'w-[50px] text-center' }, { key: 'materialNo', label: '자재번호', className: 'w-[100px]' }, { key: 'lotNo', label: '로트번호', className: 'w-[120px]' }, { key: 'inspectionStatus', label: '수입검사', className: 'w-[70px] text-center' }, { key: 'inspectionDate', label: '검사일', className: 'w-[90px] text-center' }, { key: 'supplier', label: '발주처', className: 'min-w-[100px]' }, { key: 'manufacturer', label: '제조사', className: 'min-w-[100px]' }, { key: 'itemCode', label: '품목코드', className: 'min-w-[100px]' }, { key: 'itemType', label: '품목유형', className: 'w-[80px] text-center' }, { key: 'itemName', label: '품목명', className: 'min-w-[130px]' }, { key: 'specification', label: '규격', className: 'w-[90px]' }, { key: 'unit', label: '단위', className: 'w-[50px] text-center' }, { key: 'receivingQty', label: '수량', className: 'w-[60px] text-center' }, { key: 'receivingDate', label: '입고변경일', className: 'w-[100px] text-center' }, { key: 'createdBy', label: '작성자', className: 'w-[70px] text-center' }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, ], // 서버 사이드 페이지네이션 clientSideFiltering: false, itemsPerPage: ITEMS_PER_PAGE, // 검색 searchPlaceholder: '로트번호, 품목코드, 품목명 검색...', searchFilter: (item: ReceivingItem, search: string) => { const s = search.toLowerCase(); return ( item.lotNo?.toLowerCase().includes(s) || item.itemCode?.toLowerCase().includes(s) || item.itemName?.toLowerCase().includes(s) || false ); }, // 날짜 범위 필터 dateRangeSelector: { enabled: true, showPresets: true, startDate, endDate, onStartDateChange: setStartDate, onEndDateChange: setEndDate, }, // 필터 설정 filterConfig, initialFilters: filterValues, // 통계 카드 stats: statCards, // 헤더 액션 (재고 조정 + 입고 등록 버튼) headerActions: () => (
), // 테이블 푸터 tableFooter, // 테이블 행 렌더링 renderTableRow: ( item: ReceivingItem, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => { return ( handleRowClick(item)} > e.stopPropagation()}> {globalIndex} {item.materialNo || '-'} {item.lotNo || '-'} {item.inspectionStatus || '-'} {item.inspectionDate || '-'} {item.supplier} {item.manufacturer || '-'} {item.itemCode} {item.itemType || '-'} {item.itemName} {item.specification || '-'} {item.unit} {item.receivingQty !== undefined ? item.receivingQty : '-'} {item.receivingDate || '-'} {item.createdBy || '-'} {RECEIVING_STATUS_LABELS[item.status]} ); }, // 모바일 카드 렌더링 renderMobileCard: ( item: ReceivingItem, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => { return ( handleRowClick(item)} headerBadges={ <> #{globalIndex} {item.lotNo && ( {item.lotNo} )} } title={item.itemName} statusBadge={ {RECEIVING_STATUS_LABELS[item.status]} } infoGrid={
} actions={ handlers.isSelected && (
) } /> ); }, }), [statCards, filterConfig, filterValues, tableFooter, handleRowClick, handleRegister, startDate, endDate] ); return ( <> setFilterValues(newFilters)} /> {/* 재고 조정 팝업 */} ); }