import { useState, useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { Card } from '../../../shared/ui/Card'; import { Button } from '../../../shared/ui/Button'; import { Input } from '../../../shared/ui/Input'; import { Search, Plus, Filter, Download, ChevronsLeft, ChevronsRight, ChevronLeft, ChevronRight } from 'lucide-react'; import './AssetListPage.css'; import { getCategories } from './AssetSettingsPage'; import { assetApi, type Asset } from '../../../shared/api/assetApi'; import { SERVER_URL } from '../../../shared/api/client'; import ExcelJS from 'exceljs'; import { saveAs } from 'file-saver'; // Mock Data Removed - Now using API export function AssetListPage() { const location = useLocation(); const navigate = useNavigate(); const [searchTerm, setSearchTerm] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [assets, setAssets] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const itemsPerPage = 8; // Fetch Assets useEffect(() => { loadAssets(); }, []); const loadAssets = async () => { try { setIsLoading(true); setError(null); const data = await assetApi.getAll(); if (Array.isArray(data)) { setAssets(data); } else { console.error("API returned non-array data:", data); setAssets([]); } } catch (error: any) { console.error("Failed to fetch assets:", error); setError("데이터를 불러오는 중 오류가 발생했습니다. 서버 연결을 확인해 주세요."); } finally { setIsLoading(false); } }; // 1. Determine Category based on URL & Dynamic Settings const getCategoryFromPath = () => { const categories = getCategories(); if (location.pathname.includes('facilities')) return categories.find(c => c.menuLink === 'facilities')?.name || '설비'; if (location.pathname.includes('tools')) return categories.find(c => c.menuLink === 'tools')?.name || '공구'; if (location.pathname.includes('instruments')) return categories.find(c => c.menuLink === 'instruments')?.name || '계측기'; if (location.pathname.includes('vehicles')) return categories.find(c => c.menuLink === 'vehicles')?.name || '차량/운반'; if (location.pathname.includes('general')) return categories.find(c => c.menuLink === 'general')?.name || '일반 자산'; if (location.pathname.includes('consumables')) return categories.find(c => c.menuLink === 'consumables')?.name || '소모품'; return null; // 'All' }; const currentCategory = getCategoryFromPath(); // 2. Determine Page Title const getPageTitle = () => { if (!currentCategory) return '전체 자산 조회'; return `${currentCategory} 현황`; }; const [isFilterOpen, setIsFilterOpen] = useState(false); const [activeFilters, setActiveFilters] = useState({ status: [] as string[], location: [] as string[] }); const toggleFilter = (type: 'status' | 'location', value: string) => { setActiveFilters(prev => { const current = prev[type]; const updated = current.includes(value) ? current.filter(item => item !== value) : [...current, value]; return { ...prev, [type]: updated }; }); }; const clearFilters = () => { setActiveFilters({ status: [], location: [] }); }; // 3. Main Filter Logic const filteredAssets = assets.filter(asset => { // Category Filter if (currentCategory) { // Strict match for category name // Note: If using multiple categories per item, use includes. For now, strict match. // If asset.category contains the currentCategory string (e.g. '자산' in '일반 자산'), it might pass loosely. // But we want strict filtering for specific menus. // If the asset category is exactly the current category, keep it. // Or if the asset category includes the main part (legacy logic). // Let's make it strict if possible, or robust for '설비' vs '설비 자산'. if (asset.category !== currentCategory) { // partial match fallback (e.g., '시설' in '시설 자산' or vice versa) const assetCat = asset.category || ''; const targetCat = currentCategory || ''; if (!assetCat.includes(targetCat) && !targetCat.includes(assetCat)) return false; } } // Search Term Filter if (searchTerm) { const lowerTerm = searchTerm.toLowerCase(); if (!asset.name.toLowerCase().includes(lowerTerm) && !asset.id.toLowerCase().includes(lowerTerm) && !asset.location.toLowerCase().includes(lowerTerm)) { return false; } } // Status Filter if (activeFilters.status.length > 0 && !activeFilters.status.includes(asset.status)) { return false; } // Location Filter if (activeFilters.location.length > 0 && !activeFilters.location.includes(asset.location)) { return false; } return true; }); // 4. Pagination const totalPages = Math.ceil(filteredAssets.length / itemsPerPage); const paginatedAssets = filteredAssets.slice( (currentPage - 1) * itemsPerPage, currentPage * itemsPerPage ); const getStatusBadge = (status: string) => { switch (status) { case 'active': return 정상 가동; case 'maintain': return 점검 중; case 'broken': return 수리 필요; default: return 미상; } }; const getStatusText = (status: string) => { switch (status) { case 'active': return '정상 가동'; case 'maintain': return '점검 중'; case 'broken': return '수리 필요'; case 'disposed': return '폐기 (말소)'; default: return '미상'; } }; const handleExcelDownload = async () => { setIsLoading(true); try { const workbook = new ExcelJS.Workbook(); const worksheet = workbook.addWorksheet('자산 목록'); // Columns worksheet.columns = [ { header: '이미지', key: 'image', width: 15 }, { header: '관리번호', key: 'id', width: 20 }, { header: '자산명', key: 'name', width: 30 }, { header: '카테고리', key: 'category', width: 15 }, { header: '모델명', key: 'model', width: 20 }, { header: 'S/N', key: 'serialNumber', width: 20 }, { header: '제작사', key: 'manufacturer', width: 20 }, { header: '설치 위치', key: 'location', width: 20 }, { header: '상태', key: 'status', width: 15 }, { header: '관리자', key: 'manager', width: 15 }, { header: '도입일', key: 'purchaseDate', width: 15 } ]; // Add rows first (Pass 1) to avoid empty row creation issues const rowsWithImages: { row: ExcelJS.Row, image: string | undefined, id: string }[] = []; for (const asset of filteredAssets) { const row = worksheet.addRow({ id: asset.id, name: asset.name, category: asset.category, model: asset.model, serialNumber: asset.serialNumber, manufacturer: asset.manufacturer, location: asset.location, status: getStatusText(asset.status), manager: asset.manager, purchaseDate: asset.purchaseDate }); // Set row height row.height = 60; if (asset.image) { rowsWithImages.push({ row, image: asset.image, id: asset.id }); } } // Embed Images (Pass 2) for (const { row, image, id } of rowsWithImages) { if (!image) continue; try { const imageUrl = image.startsWith('http') ? image : `${SERVER_URL}${image}`; const response = await fetch(imageUrl); const buffer = await response.arrayBuffer(); const imageId = workbook.addImage({ buffer: buffer, extension: 'png', }); worksheet.addImage(imageId, { tl: { col: 0, row: row.number - 1 } as any, br: { col: 1, row: row.number } as any, editAs: 'oneCell' }); } catch (err) { console.warn('Failed to embed image for asset:', id, err); } } // Headers Styling worksheet.getRow(1).font = { bold: true }; worksheet.getRow(1).alignment = { vertical: 'middle', horizontal: 'center' }; // Generate File const buffer = await workbook.xlsx.writeBuffer(); const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); const today = new Date().toISOString().split('T')[0].replace(/-/g, ''); saveAs(blob, `자산목록_${today}.xlsx`); } catch (error) { console.error("Excel generation failed:", error); alert("엑셀 다운로드 중 오류가 발생했습니다."); } finally { setIsLoading(false); } }; return (

{getPageTitle()}

{currentCategory ? `${currentCategory} 카테고리에 등록된 자산 목록입니다.` : '전체 자산의 실시간 현황을 조회합니다.'}

{/* Toolbar */}
} value={searchTerm} onChange={(e) => { setSearchTerm(e.target.value); setCurrentPage(1); }} />
{/* Filter Popup */} {isFilterOpen && (

상태

{[ { label: '정상 가동', value: 'active' }, { label: '점검 중', value: 'maintain' }, { label: '수리 필요', value: 'broken' }, { label: '폐기 (말소)', value: 'disposed' } ].map(opt => ( ))}

위치

{[ '제1공장 A라인', '제1공장 B라인', '제2공장 창고', '본사 사무실' ].map(loc => ( ))}
)}
{/* Table */}
{isLoading ? ( ) : paginatedAssets.length > 0 ? ( paginatedAssets.map((asset) => ( )) ) : error ? ( ) : ( )}
관리번호 자산명 카테고리 모델명 설치 위치 상태 관리자 도입일 관리
데이터를 불러오는 중입니다...
navigate(`/asset/detail/${asset.id}`)} title="상세 정보 보기" > {asset.id} navigate(`/asset/detail/${asset.id}`)} title="상세 정보 보기" > {asset.name} {asset.category} {asset.model} {asset.location}
{getStatusBadge(asset.status)}
{asset.manager} {asset.purchaseDate}
⚠️ {error}
데이터가 없습니다.
{/* Pagination */}
{filteredAssets.length}개 중 {filteredAssets.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} - {Math.min(currentPage * itemsPerPage, filteredAssets.length)}
{Array.from({ length: totalPages }, (_, i) => i + 1).map(page => ( ))}
); }