/** * 품목 목록 Client Component * * 품목기준관리 API 연동 * - 품목 목록: useItemList 훅으로 API 조회 (서버 사이드 검색/필터링) * - IntegratedListTemplateV2 기반 공통 UI 적용 */ 'use client'; import { useState, useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; import type { ItemMaster } from '@/types/item'; import { ITEM_TYPE_LABELS } from '@/types/item'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; import { TableRow, TableCell } from '@/components/ui/table'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Search, Plus, Edit, Trash2, Package, Loader2 } from 'lucide-react'; import { useItemList } from '@/hooks/useItemList'; import { IntegratedListTemplateV2, type TabOption, type TableColumn, type StatCard, } from '@/components/templates/IntegratedListTemplateV2'; import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard'; // Debounce 훅 function useDebounce(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const timer = setTimeout(() => setDebouncedValue(value), delay); return () => clearTimeout(timer); }, [value, delay]); return debouncedValue; } /** * 품목 유형별 Badge 색상 반환 */ function getItemTypeBadge(itemType: string) { const badges: Record = { FG: { variant: 'default', className: 'bg-purple-100 text-purple-700 border-purple-200' }, PT: { variant: 'default', className: 'bg-orange-100 text-orange-700 border-orange-200' }, SM: { variant: 'default', className: 'bg-green-100 text-green-700 border-green-200' }, RM: { variant: 'default', className: 'bg-blue-100 text-blue-700 border-blue-200' }, CS: { variant: 'default', className: 'bg-gray-100 text-gray-700 border-gray-200' }, }; const config = badges[itemType] || { variant: 'outline' as const, className: '' }; return ( {ITEM_TYPE_LABELS[itemType as keyof typeof ITEM_TYPE_LABELS]} ); } /** * 부품 유형 라벨 반환 */ function getPartTypeLabel(partType: string | undefined): string { if (!partType) return ''; const labels: Record = { ASSEMBLY: '조립', BENDING: '절곡', PURCHASED: '구매', }; return labels[partType] || ''; } export default function ItemListClient() { const router = useRouter(); const [searchTerm, setSearchTerm] = useState(''); const [selectedType, setSelectedType] = useState('all'); const [selectedItems, setSelectedItems] = useState>(new Set()); // 삭제 다이얼로그 상태 const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [itemToDelete, setItemToDelete] = useState<{ id: string; code: string; itemType: string } | null>(null); // Materials 타입 (SM, RM, CS는 Material 테이블 사용) const MATERIAL_TYPES = ['SM', 'RM', 'CS']; // API에서 품목 목록 및 테이블 컬럼 조회 (서버 사이드 검색/필터링) const { items, pagination, totalStats, isLoading, isSearching, refresh, search, } = useItemList(); // 디바운스된 검색어 (300ms 딜레이) const debouncedSearchTerm = useDebounce(searchTerm, 300); // 검색 상태 추적용 ref const isFirstRender = useRef(true); // 디바운스된 검색어 변경 시 서버 검색 실행 useEffect(() => { // 첫 렌더링에서는 검색하지 않음 if (isFirstRender.current) { isFirstRender.current = false; return; } search({ search: debouncedSearchTerm, type: selectedType, page: 1, }); }, [debouncedSearchTerm, selectedType, search]); // 로딩 상태 if (isLoading) { return (
품목 목록 로딩 중...
); } // 유형 변경 핸들러 const handleTypeChange = (value: string) => { setSelectedType(value); }; // 페이지 변경 핸들러 const handlePageChange = (page: number) => { search({ search: searchTerm, type: selectedType, page, }); }; const handleView = (itemCode: string, itemType: string, itemId: string) => { // itemType을 query param으로 전달 (Materials 조회를 위해) router.push(`/items/${encodeURIComponent(itemCode)}?type=${itemType}&id=${itemId}`); }; const handleEdit = (itemCode: string, itemType: string, itemId: string) => { // itemType을 query param으로 전달 (Materials 조회를 위해) router.push(`/items/${encodeURIComponent(itemCode)}/edit?type=${itemType}&id=${itemId}`); }; // 삭제 확인 다이얼로그 열기 const openDeleteDialog = (itemId: string, itemCode: string, itemType: string) => { setItemToDelete({ id: itemId, code: itemCode, itemType }); setDeleteDialogOpen(true); }; // 삭제 실행 const handleConfirmDelete = async () => { if (!itemToDelete) return; try { console.log('[Delete] 삭제 요청:', itemToDelete); // Materials (SM, RM, CS)는 /products/materials 엔드포인트 사용 // Products (FG, PT)는 /items 엔드포인트 사용 // 2025-12-10: item_type 파라미터를 실제 코드(SM, RM, CS)로 전달 const isMaterial = MATERIAL_TYPES.includes(itemToDelete.itemType); const deleteUrl = isMaterial ? `/api/proxy/products/materials/${itemToDelete.id}?item_type=${itemToDelete.itemType}` : `/api/proxy/items/${itemToDelete.id}`; console.log('[Delete] URL:', deleteUrl, '(isMaterial:', isMaterial, ', itemType:', itemToDelete.itemType, ')'); const response = await fetch(deleteUrl, { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, }); const result = await response.json(); console.log('[Delete] 응답:', { status: response.status, result }); if (response.ok && result.success) { refresh(); } else { throw new Error(result.message || '삭제에 실패했습니다.'); } } catch (error) { console.error('품목 삭제 실패:', error); alert(error instanceof Error ? error.message : '품목 삭제에 실패했습니다.'); } finally { setDeleteDialogOpen(false); setItemToDelete(null); } }; // 체크박스 전체 선택/해제 const toggleSelectAll = () => { if (selectedItems.size === items.length && items.length > 0) { setSelectedItems(new Set()); } else { const allIds = new Set(items.map((item) => item.id)); setSelectedItems(allIds); } }; // 개별 체크박스 선택/해제 const toggleSelection = (itemId: string) => { const newSelected = new Set(selectedItems); if (newSelected.has(itemId)) { newSelected.delete(itemId); } else { newSelected.add(itemId); } setSelectedItems(newSelected); }; // 일괄 삭제 핸들러 // 2025-12-10: item_type 파라미터를 실제 코드(SM, RM, CS)로 전달 const handleBulkDelete = async () => { const itemIds = Array.from(selectedItems); let successCount = 0; let failCount = 0; for (const id of itemIds) { try { // 해당 품목의 itemType 찾기 const item = items.find((i) => i.id === id); const isMaterial = item ? MATERIAL_TYPES.includes(item.itemType) : false; // Materials는 /products/materials 엔드포인트 + item_type, Products는 /items 엔드포인트 const deleteUrl = isMaterial ? `/api/proxy/products/materials/${id}?item_type=${item?.itemType}` : `/api/proxy/items/${id}`; const response = await fetch(deleteUrl, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, }); const result = await response.json(); if (response.ok && result.success) { successCount++; } else { failCount++; } } catch { failCount++; } } if (successCount > 0) { alert(`${successCount}개 품목이 삭제되었습니다.${failCount > 0 ? ` (${failCount}개 실패)` : ''}`); setSelectedItems(new Set()); refresh(); } else { alert('품목 삭제에 실패했습니다.'); } }; // 탭 옵션 (품목 유형별) const tabs: TabOption[] = [ { value: 'all', label: '전체', count: totalStats.totalAll, color: 'gray' }, { value: 'FG', label: '제품', count: totalStats.totalFG, color: 'purple' }, { value: 'PT', label: '부품', count: totalStats.totalPT, color: 'orange' }, { value: 'SM', label: '부자재', count: totalStats.totalSM, color: 'green' }, { value: 'RM', label: '원자재', count: totalStats.totalRM, color: 'blue' }, { value: 'CS', label: '소모품', count: totalStats.totalCS, color: 'gray' }, ]; // 통계 카드 (전체 통계) const stats: StatCard[] = [ { label: '전체 품목', value: totalStats.totalAll, icon: Package, iconColor: 'text-blue-600' }, { label: '제품', value: totalStats.totalFG, icon: Package, iconColor: 'text-purple-600' }, { label: '부품', value: totalStats.totalPT, icon: Package, iconColor: 'text-orange-600' }, { label: '부자재', value: totalStats.totalSM, icon: Package, iconColor: 'text-green-600' }, { label: '원자재', value: totalStats.totalRM, icon: Package, iconColor: 'text-cyan-600' }, { label: '소모품', value: totalStats.totalCS, icon: Package, iconColor: 'text-gray-600' }, ]; // 테이블 컬럼 정의 const tableColumns: TableColumn[] = [ { key: 'rowNumber', label: '번호', className: 'w-[60px] text-center' }, { key: 'itemCode', label: '품목코드', className: 'min-w-[120px]' }, { key: 'itemType', label: '품목유형', className: 'min-w-[100px]' }, { key: 'itemName', label: '품목명', className: 'min-w-[150px]' }, { key: 'specification', label: '규격', className: 'min-w-[100px]' }, { key: 'unit', label: '단위', className: 'min-w-[60px]' }, { key: 'status', label: '품목상태', className: 'min-w-[80px]' }, { key: 'actions', label: '작업', className: 'w-[120px] text-right' }, ]; // 테이블 행 렌더링 const renderTableRow = (item: ItemMaster, index: number, globalIndex: number) => { const isSelected = selectedItems.has(item.id); return ( toggleSelection(item.id)} /> {globalIndex} {item.itemCode || '-'}
{getItemTypeBadge(item.itemType)} {item.itemType === 'PT' && item.partType && ( {getPartTypeLabel(item.partType)} )}
{item.itemName} {item.specification || '-'} {item.unit || '-'} {item.isActive ? '활성' : '비활성'}
); }; // 모바일 카드 렌더링 const renderMobileCard = ( item: ItemMaster, index: number, globalIndex: number, isSelected: boolean, onToggle: () => void ) => { return ( {item.itemCode} {getItemTypeBadge(item.itemType)} {item.itemType === 'PT' && item.partType && ( {getPartTypeLabel(item.partType)} )} } statusBadge={ {item.isActive ? '활성' : '비활성'} } isSelected={isSelected} onToggleSelection={onToggle} onCardClick={() => handleView(item.itemCode, item.itemType, item.id)} infoGrid={
{item.specification && ( )} {item.unit && ( )}
} actions={ isSelected ? (
) : undefined } /> ); }; // 헤더 액션 (검색 중 로딩 + 품목 등록 버튼) const headerActions = (
{isSearching && (
검색 중...
)}
); return ( <> title="품목 관리" description="제품, 부품, 부자재, 원자재, 소모품 등록 및 관리" icon={Package} headerActions={headerActions} stats={stats} searchValue={searchTerm} onSearchChange={setSearchTerm} searchPlaceholder="품목코드, 품목명, 규격 검색..." tabs={tabs} activeTab={selectedType} onTabChange={handleTypeChange} tableColumns={tableColumns} data={items} totalCount={pagination.totalItems} allData={items} selectedItems={selectedItems} onToggleSelection={toggleSelection} onToggleSelectAll={toggleSelectAll} onBulkDelete={handleBulkDelete} getItemId={(item) => item.id} renderTableRow={renderTableRow} renderMobileCard={renderMobileCard} pagination={{ currentPage: pagination.currentPage, totalPages: pagination.totalPages, totalItems: pagination.totalItems, itemsPerPage: pagination.perPage, onPageChange: handlePageChange, }} /> {/* 개별 삭제 확인 다이얼로그 */} 품목 삭제 품목 "{itemToDelete?.code}"을(를) 삭제하시겠습니까?
이 작업은 되돌릴 수 없습니다.
취소 삭제
); }