/** * 단가 목록 클라이언트 컴포넌트 * * IntegratedListTemplateV2 공통 템플릿 활용 */ 'use client'; import { useState, useMemo } from 'react'; import { useRouter } from 'next/navigation'; import { DollarSign, Package, AlertCircle, CheckCircle2, RefreshCw, } from 'lucide-react'; 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 { UniversalListPage, type UniversalListConfig, type TabOption, type TableColumn, type StatCard, type SelectionHandlers, type RowClickHandlers, } from '@/components/templates/UniversalListPage'; import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard'; import type { PricingListItem, ItemType } from './types'; import { ITEM_TYPE_LABELS, ITEM_TYPE_COLORS } from './types'; interface PricingListClientProps { initialData: PricingListItem[]; } export function PricingListClient({ initialData, }: PricingListClientProps) { const router = useRouter(); const [data] = useState(initialData); const [searchTerm, setSearchTerm] = useState(''); const [activeTab, setActiveTab] = useState('all'); const pageSize = 20; // 탭 필터 함수 (UniversalListPage 내부에서 사용) const tabFilter = (item: PricingListItem, tab: string) => { if (tab === 'all') return true; return item.itemType === tab; }; // 검색 필터 함수 (UniversalListPage 내부에서 사용) const searchFilter = (item: PricingListItem, search: string) => { const searchLower = search.toLowerCase(); return ( (item.itemCode?.toLowerCase().includes(searchLower) ?? false) || (item.itemName?.toLowerCase().includes(searchLower) ?? false) || (item.specification?.toLowerCase().includes(searchLower) ?? false) ); }; // 탭별 데이터 수 계산 (통계용) const filteredData = useMemo(() => { let result = [...data]; // 탭 필터 if (activeTab !== 'all') { result = result.filter(item => item.itemType === activeTab); } // 검색 필터 if (searchTerm) { const search = searchTerm.toLowerCase(); result = result.filter(item => (item.itemCode?.toLowerCase().includes(search) ?? false) || (item.itemName?.toLowerCase().includes(search) ?? false) || (item.specification?.toLowerCase().includes(search) ?? false) ); } return result; }, [data, activeTab, searchTerm]); // 통계 계산 const totalStats = useMemo(() => { const totalAll = data.length; const totalFG = data.filter(d => d.itemType === 'FG').length; const totalPT = data.filter(d => d.itemType === 'PT').length; const totalSM = data.filter(d => d.itemType === 'SM').length; const totalRM = data.filter(d => d.itemType === 'RM').length; const totalCS = data.filter(d => d.itemType === 'CS').length; const registered = data.filter(d => d.status !== 'not_registered').length; const notRegistered = totalAll - registered; const finalized = data.filter(d => d.isFinal).length; return { totalAll, totalFG, totalPT, totalSM, totalRM, totalCS, registered, notRegistered, finalized }; }, [data]); // 금액 포맷팅 const formatPrice = (price?: number) => { if (price === undefined || price === null) return '-'; return `${price.toLocaleString()}원`; }; // 품목 유형 Badge 렌더링 const renderItemTypeBadge = (type: string) => { const colors = ITEM_TYPE_COLORS[type as ItemType]; const label = ITEM_TYPE_LABELS[type as ItemType] || type; if (!colors) { return {label}; } return ( {label} ); }; // 상태 Badge 렌더링 const renderStatusBadge = (item: PricingListItem) => { if (item.status === 'not_registered') { return 미등록; } if (item.isFinal) { return 확정; } if (item.status === 'active') { return 활성; } if (item.status === 'inactive') { return 비활성; } return 초안; }; // 마진율 Badge 렌더링 const renderMarginBadge = (marginRate?: number) => { if (marginRate === undefined || marginRate === null || marginRate === 0) { return -; } const colorClass = marginRate >= 30 ? 'bg-green-50 text-green-700 border-green-200' : marginRate >= 20 ? 'bg-blue-50 text-blue-700 border-blue-200' : marginRate >= 10 ? 'bg-orange-50 text-orange-700 border-orange-200' : 'bg-red-50 text-red-700 border-red-200'; return ( {marginRate.toFixed(1)}% ); }; // 네비게이션 핸들러 const handleRegister = (item: PricingListItem) => { // item_type_code는 품목 정보에서 자동으로 가져오므로 URL에 포함하지 않음 const params = new URLSearchParams(); params.set('mode', 'new'); if (item.itemId) params.set('itemId', item.itemId); if (item.itemCode) params.set('itemCode', item.itemCode); router.push(`/sales/pricing-management?${params.toString()}`); }; const handleEdit = (item: PricingListItem) => { router.push(`/sales/pricing-management/${item.id}?mode=edit`); }; const handleHistory = (item: PricingListItem) => { // TODO: 이력 다이얼로그 열기 console.log('이력 조회:', item.id); }; // 탭 옵션 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.registered, icon: DollarSign, iconColor: 'text-green-600' }, { label: '미등록', value: totalStats.notRegistered, icon: AlertCircle, iconColor: 'text-orange-600' }, { label: '확정', value: totalStats.finalized, icon: CheckCircle2, iconColor: 'text-purple-600' }, ]; // 테이블 컬럼 정의 const tableColumns: TableColumn[] = [ { key: 'rowNumber', label: '번호', className: 'w-[60px] text-center' }, { key: 'itemType', label: '품목유형', className: 'min-w-[100px]' }, { key: 'itemCode', label: '품목코드', className: 'min-w-[120px]' }, { key: 'itemName', label: '품목명', className: 'min-w-[150px]' }, { key: 'specification', label: '규격', className: 'min-w-[100px]', hideOnMobile: true }, { key: 'unit', label: '단위', className: 'min-w-[60px]', hideOnMobile: true }, { key: 'purchasePrice', label: '매입단가', className: 'min-w-[100px] text-right', hideOnTablet: true }, { key: 'processingCost', label: '가공비', className: 'min-w-[80px] text-right', hideOnTablet: true }, { key: 'salesPrice', label: '판매단가', className: 'min-w-[100px] text-right' }, { key: 'marginRate', label: '마진율', className: 'min-w-[80px] text-right', hideOnMobile: true }, { key: 'effectiveDate', label: '적용일', className: 'min-w-[100px]', hideOnMobile: true }, { key: 'status', label: '상태', className: 'min-w-[80px]' }, ]; // 테이블 행 렌더링 const renderTableRow = ( item: PricingListItem, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => { const { isSelected, onToggle } = handlers; // 행 클릭 핸들러: 등록되지 않은 항목은 등록, 등록된 항목은 수정 const handleRowClick = () => { if (item.status === 'not_registered') { handleRegister(item); } else { handleEdit(item); } }; return ( e.stopPropagation()}> {globalIndex} {renderItemTypeBadge(item.itemType)} {item.itemCode} {item.itemName} {item.specification || '-'} {item.unit || '-'} {formatPrice(item.purchasePrice)} {formatPrice(item.processingCost)} {formatPrice(item.salesPrice)} {renderMarginBadge(item.marginRate)} {item.effectiveDate ? new Date(item.effectiveDate).toLocaleDateString('ko-KR') : '-'} {renderStatusBadge(item)} ); }; // 모바일 카드 렌더링 const renderMobileCard = ( item: PricingListItem, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => { const { isSelected, onToggle } = handlers; return ( {item.itemCode} {renderItemTypeBadge(item.itemType)} } statusBadge={renderStatusBadge(item)} isSelected={isSelected} onToggleSelection={onToggle} onCardClick={() => item.status !== 'not_registered' ? handleEdit(item) : handleRegister(item)} infoGrid={
{item.specification && ( )} {item.unit && ( )}
} /> ); }; // 헤더 액션 (함수로 정의) const headerActions = () => ( ); // UniversalListPage 설정 const pricingConfig: UniversalListConfig = { title: '단가 목록', description: '품목별 매입단가, 판매단가 및 마진을 관리합니다', icon: DollarSign, basePath: '/sales/pricing-management', idField: 'id', actions: { getList: async () => ({ success: true, data: data, totalCount: data.length, }), }, columns: tableColumns, headerActions, stats, tabs, searchPlaceholder: '품목코드, 품목명, 규격 검색...', itemsPerPage: pageSize, clientSideFiltering: true, tabFilter, searchFilter, renderTableRow, renderMobileCard, }; return ( config={pricingConfig} initialData={data} initialTotalCount={data.length} onTabChange={setActiveTab} onSearchChange={setSearchTerm} /> ); } export default PricingListClient;