'use client'; /** * 재고현황 목록 - UniversalListPage 마이그레이션 * * 기존 IntegratedListTemplateV2 → UniversalListPage config 기반으로 변환 * - 서버 사이드 페이지네이션 (getStocks API) * - 통계 카드 (getStockStats API) * - 품목유형별 탭 필터 (getStockStatsByType API) * - 테이블 푸터 (요약 정보) */ import { useState, useEffect, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { Package, CheckCircle2, AlertCircle, Eye, AlertTriangle, } from 'lucide-react'; import type { ExcelColumn } from '@/lib/utils/excel-download'; 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 FilterFieldConfig, } from '@/components/templates/UniversalListPage'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; import { getStocks, getStockStats } from './actions'; import { USE_STATUS_LABELS, ITEM_TYPE_LABELS } from './types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import type { StockItem, StockStats, ItemType, StockStatusType } from './types'; // 페이지당 항목 수 const ITEMS_PER_PAGE = 20; export function StockStatusList() { const router = useRouter(); // ===== 통계 (외부 관리) ===== const [stockStats, setStockStats] = useState(null); // ===== 날짜 범위 상태 ===== const today = new Date(); const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); const [startDate, setStartDate] = useState(firstDayOfMonth.toISOString().split('T')[0]); const [endDate, setEndDate] = useState(today.toISOString().split('T')[0]); // ===== 데이터 상태 (수주관리 패턴) ===== const [stocks, setStocks] = useState([]); const [isLoading, setIsLoading] = useState(true); const [totalCount, setTotalCount] = useState(0); // ===== 검색 및 필터 상태 ===== const [searchTerm, setSearchTerm] = useState(''); const [filterValues, setFilterValues] = useState>({ useStatus: 'all', }); // 데이터 로드 함수 const loadData = useCallback(async () => { try { setIsLoading(true); const [stocksResult, statsResult] = await Promise.all([ getStocks({ page: 1, perPage: 9999, // 전체 데이터 로드 (클라이언트 사이드 필터링) startDate, endDate, }), getStockStats(), ]); if (stocksResult.success && stocksResult.data) { setStocks(stocksResult.data); setTotalCount(stocksResult.pagination.total); } if (statsResult.success && statsResult.data) { setStockStats(statsResult.data); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[StockStatusList] loadData error:', error); } finally { setIsLoading(false); } }, [startDate, endDate]); // 초기 데이터 로드 및 날짜 변경 시 재로드 useEffect(() => { loadData(); }, [loadData]); // 클라이언트 사이드 필터링 const filteredStocks = stocks.filter((stock) => { // 검색 필터 if (searchTerm) { const searchLower = searchTerm.toLowerCase(); const matchesSearch = stock.itemCode.toLowerCase().includes(searchLower) || stock.itemName.toLowerCase().includes(searchLower) || stock.stockNumber.toLowerCase().includes(searchLower); if (!matchesSearch) return false; } // 상태 필터 const useStatusFilter = filterValues.useStatus as string; if (useStatusFilter && useStatusFilter !== 'all') { if (stock.useStatus !== useStatusFilter) return false; } return true; }); // ===== 행 클릭 핸들러 ===== const handleRowClick = (item: StockItem) => { router.push(`/ko/material/stock-status/${item.id}?mode=view`); }; // ===== 엑셀 컬럼 정의 ===== const excelColumns: ExcelColumn[] = [ { header: '자재번호', key: 'stockNumber' }, { header: '품목코드', key: 'itemCode' }, { header: '품목유형', key: 'itemType', transform: (value) => ITEM_TYPE_LABELS[value as ItemType] || '-' }, { header: '품목명', key: 'itemName' }, { header: '규격', key: 'specification' }, { header: '단위', key: 'unit' }, { header: '재고량', key: 'calculatedQty' }, { header: '안전재고', key: 'safetyStock' }, { header: '상태', key: 'useStatus', transform: (value) => USE_STATUS_LABELS[value as 'active' | 'inactive'] || '-' }, ]; // ===== API 응답 매핑 함수 ===== const mapStockResponse = (result: unknown): StockItem[] => { const data = result as { data?: { data?: Record[] } }; const rawItems = data.data?.data ?? []; return rawItems.map((item: Record) => { const stock = item.stock as Record | null; const hasStock = !!stock; return { id: String(item.id ?? ''), stockNumber: hasStock ? (String(stock?.stock_number ?? stock?.id ?? item.id)) : String(item.id ?? ''), itemCode: (item.code ?? '') as string, itemName: (item.name ?? '') as string, itemType: (item.item_type ?? 'RM') as ItemType, specification: (item.specification ?? item.attributes ?? '') as string, unit: (item.unit ?? 'EA') as string, calculatedQty: hasStock ? (parseFloat(String(stock?.calculated_qty ?? stock?.stock_qty)) || 0) : 0, actualQty: hasStock ? (parseFloat(String(stock?.actual_qty ?? stock?.stock_qty)) || 0) : 0, stockQty: hasStock ? (parseFloat(String(stock?.stock_qty)) || 0) : 0, safetyStock: hasStock ? (parseFloat(String(stock?.safety_stock)) || 0) : 0, lotCount: hasStock ? (Number(stock?.lot_count) || 0) : 0, lotDaysElapsed: hasStock ? (Number(stock?.days_elapsed) || 0) : 0, status: hasStock ? (stock?.status as StockStatusType | null) : null, useStatus: (item.is_active === false || item.status === 'inactive') ? 'inactive' : 'active', location: hasStock ? ((stock?.location as string) || '-') : '-', hasStock, }; }); }; // ===== 통계 카드 ===== const stats = [ { label: '전체 품목', value: `${stockStats?.totalItems || 0}`, icon: Package, iconColor: 'text-gray-600', }, { label: '정상 재고', value: `${stockStats?.normalCount || 0}`, icon: CheckCircle2, iconColor: 'text-green-600', }, { label: '재고 부족', value: `${stockStats?.lowCount || 0}`, icon: AlertCircle, iconColor: 'text-red-600', }, { label: '안전재고 미달', value: `${stockStats?.outCount || 0}`, icon: AlertTriangle, iconColor: 'text-orange-600', }, ]; // ===== 필터 설정 (전체/사용/미사용) ===== const filterConfig: FilterFieldConfig[] = [ { key: 'useStatus', label: '상태', type: 'single', options: [ { value: 'all', label: '전체' }, { value: 'active', label: '사용' }, { value: 'inactive', label: '미사용' }, ], }, ]; // ===== 테이블 컬럼 ===== const tableColumns = [ { key: 'no', label: 'No.', className: 'w-[60px] text-center' }, { key: 'stockNumber', label: '자재번호', className: 'w-[100px]' }, { key: 'itemCode', label: '품목코드', className: 'min-w-[100px]' }, { key: 'itemType', label: '품목유형', className: 'w-[80px]' }, { key: 'itemName', label: '품목명', className: 'min-w-[150px]' }, { key: 'specification', label: '규격', className: 'w-[100px]' }, { key: 'unit', label: '단위', className: 'w-[60px] text-center' }, { key: 'calculatedQty', label: '재고량', className: 'w-[80px] text-center' }, { key: 'safetyStock', label: '안전재고', className: 'w-[80px] text-center' }, { key: 'useStatus', label: '상태', className: 'w-[80px] text-center' }, ]; // ===== 테이블 행 렌더링 ===== const renderTableRow = ( item: StockItem, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => { return ( handleRowClick(item)} > e.stopPropagation()}> {globalIndex} {item.stockNumber} {item.itemCode} {ITEM_TYPE_LABELS[item.itemType] || '-'} {item.itemName} {item.specification || '-'} {item.unit} {item.calculatedQty} {item.safetyStock} {USE_STATUS_LABELS[item.useStatus]} ); }; // ===== 모바일 카드 렌더링 ===== const renderMobileCard = ( item: StockItem, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => { return ( handleRowClick(item)} headerBadges={ <> #{globalIndex} {item.stockNumber} } title={item.itemName} statusBadge={ {USE_STATUS_LABELS[item.useStatus]} } infoGrid={
} actions={ handlers.isSelected && (
) } /> ); }; // ===== UniversalListPage Config (수주관리 패턴 - useMemo 없음) ===== const config: UniversalListConfig = { title: '재고 목록', description: '재고를 관리합니다', icon: Package, basePath: '/material/stock-status', idField: 'id', // 클라이언트 사이드 필터링 (수주관리 패턴) actions: { getList: async () => ({ success: true, data: filteredStocks, totalCount: filteredStocks.length, }), }, columns: tableColumns, // 클라이언트 사이드 필터링 clientSideFiltering: true, itemsPerPage: ITEMS_PER_PAGE, // 검색 searchPlaceholder: '품목코드, 품목명 검색...', // 검색 필터 함수 searchFilter: (stock, searchValue) => { const searchLower = searchValue.toLowerCase(); return ( stock.itemCode.toLowerCase().includes(searchLower) || stock.itemName.toLowerCase().includes(searchLower) || stock.stockNumber.toLowerCase().includes(searchLower) ); }, // 커스텀 필터 함수 customFilterFn: (items, fv) => { if (!items || items.length === 0) return items; return items.filter((item) => { const useStatusVal = fv.useStatus as string; if (useStatusVal && useStatusVal !== 'all' && item.useStatus !== useStatusVal) { return false; } return true; }); }, // 날짜 범위 필터 dateRangeSelector: { enabled: true, showPresets: true, startDate, endDate, onStartDateChange: setStartDate, onEndDateChange: setEndDate, }, // 필터 설정 filterConfig, initialFilters: filterValues, // 통계 computeStats: () => stats, // 테이블 푸터 tableFooter: ( 총 {filteredStocks.length}건 ), // 엑셀 다운로드 설정 excelDownload: { columns: excelColumns, filename: '재고현황', sheetName: '재고', fetchAllUrl: '/api/proxy/stocks', fetchAllParams: ({ searchValue, filters }) => { const params: Record = {}; if (filters?.useStatus && filters.useStatus !== 'all') { params.use_status = filters.useStatus as string; } if (searchValue) { params.search = searchValue; } params.start_date = startDate; params.end_date = endDate; return params; }, mapResponse: mapStockResponse, }, renderTableRow, renderMobileCard, }; // 로딩 상태 if (isLoading) { return (

재고 목록을 불러오는 중...

); } return ( config={config} initialData={filteredStocks} initialTotalCount={filteredStocks.length} onFilterChange={(newFilters) => setFilterValues(newFilters)} onSearchChange={setSearchTerm} /> ); }