'use client'; import { useState, useMemo, useCallback, useEffect, useTransition } from 'react'; import { Files, Eye, EyeOff, BookOpen, } from 'lucide-react'; import { toast } from 'sonner'; import { getReferences, getReferenceSummary, markAsReadBulk, markAsUnreadBulk, } from './actions'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; import { Badge } from '@/components/ui/badge'; import { TableRow, TableCell } from '@/components/ui/table'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { IntegratedListTemplateV2, type TableColumn, type StatCard, type TabOption, } from '@/components/templates/IntegratedListTemplateV2'; import { DateRangeSelector } from '@/components/molecules/DateRangeSelector'; import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard'; import { DocumentDetailModal } from '@/components/approval/DocumentDetail'; import type { DocumentType, ProposalDocumentData, ExpenseReportDocumentData, ExpenseEstimateDocumentData } from '@/components/approval/DocumentDetail/types'; import type { ReferenceTabType, ReferenceRecord, SortOption, FilterOption, ApprovalType, } from './types'; import { REFERENCE_TAB_LABELS, SORT_OPTIONS, FILTER_OPTIONS, APPROVAL_TYPE_LABELS, READ_STATUS_LABELS, READ_STATUS_COLORS, } from './types'; // ===== 통계 타입 ===== interface ReferenceSummary { all: number; read: number; unread: number; } export function ReferenceBox() { const [isPending, startTransition] = useTransition(); // ===== 상태 관리 ===== const [activeTab, setActiveTab] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); const [filterOption, setFilterOption] = useState('all'); const [sortOption, setSortOption] = useState('latest'); const [selectedItems, setSelectedItems] = useState>(new Set()); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 20; // 날짜 범위 상태 const [startDate, setStartDate] = useState('2025-09-01'); const [endDate, setEndDate] = useState('2025-09-03'); // 다이얼로그 상태 const [markReadDialogOpen, setMarkReadDialogOpen] = useState(false); const [markUnreadDialogOpen, setMarkUnreadDialogOpen] = useState(false); // ===== 문서 상세 모달 상태 ===== const [isModalOpen, setIsModalOpen] = useState(false); const [selectedDocument, setSelectedDocument] = useState(null); // API 데이터 const [data, setData] = useState([]); const [totalCount, setTotalCount] = useState(0); const [totalPages, setTotalPages] = useState(1); const [isLoading, setIsLoading] = useState(true); // 통계 데이터 const [summary, setSummary] = useState(null); // ===== 데이터 로드 ===== const loadData = useCallback(async () => { setIsLoading(true); try { // 정렬 옵션 변환 const sortConfig: { sort_by: string; sort_dir: 'asc' | 'desc' } = (() => { switch (sortOption) { case 'latest': return { sort_by: 'created_at', sort_dir: 'desc' }; case 'oldest': return { sort_by: 'created_at', sort_dir: 'asc' }; case 'draftDateAsc': return { sort_by: 'created_at', sort_dir: 'asc' }; case 'draftDateDesc': return { sort_by: 'created_at', sort_dir: 'desc' }; default: return { sort_by: 'created_at', sort_dir: 'desc' }; } })(); // 탭에 따른 is_read 파라미터 const isReadParam = activeTab === 'all' ? undefined : activeTab === 'read'; const result = await getReferences({ page: currentPage, per_page: itemsPerPage, search: searchQuery || undefined, is_read: isReadParam, approval_type: filterOption !== 'all' ? filterOption : undefined, ...sortConfig, }); setData(result.data); setTotalCount(result.total); setTotalPages(result.lastPage); } catch (error) { console.error('Failed to load references:', error); toast.error('참조함 목록을 불러오는데 실패했습니다.'); } finally { setIsLoading(false); } }, [currentPage, itemsPerPage, searchQuery, filterOption, sortOption, activeTab]); // ===== 통계 로드 ===== const loadSummary = useCallback(async () => { try { const result = await getReferenceSummary(); setSummary(result); } catch (error) { console.error('Failed to load summary:', error); } }, []); // ===== 초기 로드 및 필터 변경 시 데이터 재로드 ===== useEffect(() => { loadData(); }, [loadData]); useEffect(() => { loadSummary(); }, [loadSummary]); // ===== 검색어/필터/탭 변경 시 페이지 초기화 ===== useEffect(() => { setCurrentPage(1); }, [searchQuery, filterOption, sortOption, activeTab]); // ===== 탭 변경 핸들러 ===== const handleTabChange = useCallback((value: string) => { setActiveTab(value as ReferenceTabType); setSelectedItems(new Set()); setSearchQuery(''); }, []); // ===== 체크박스 핸들러 ===== const toggleSelection = useCallback((id: string) => { setSelectedItems(prev => { const newSet = new Set(prev); if (newSet.has(id)) newSet.delete(id); else newSet.add(id); return newSet; }); }, []); const toggleSelectAll = useCallback(() => { if (selectedItems.size === data.length && data.length > 0) { setSelectedItems(new Set()); } else { setSelectedItems(new Set(data.map(item => item.id))); } }, [selectedItems.size, data]); // ===== 통계 데이터 (API summary 사용) ===== const stats = useMemo(() => { return { all: summary?.all ?? 0, read: summary?.read ?? 0, unread: summary?.unread ?? 0, }; }, [summary]); // ===== 열람/미열람 처리 핸들러 ===== const handleMarkReadClick = useCallback(() => { if (selectedItems.size === 0) return; setMarkReadDialogOpen(true); }, [selectedItems.size]); const handleMarkReadConfirm = useCallback(async () => { const ids = Array.from(selectedItems); startTransition(async () => { try { const result = await markAsReadBulk(ids); if (result.success) { toast.success('열람 처리 완료', { description: '열람 처리가 완료되었습니다.', }); setSelectedItems(new Set()); loadData(); loadSummary(); } else { toast.error(result.error || '열람 처리에 실패했습니다.'); } } catch (error) { console.error('Mark read error:', error); toast.error('열람 처리 중 오류가 발생했습니다.'); } }); setMarkReadDialogOpen(false); }, [selectedItems, loadData, loadSummary]); const handleMarkUnreadClick = useCallback(() => { if (selectedItems.size === 0) return; setMarkUnreadDialogOpen(true); }, [selectedItems.size]); const handleMarkUnreadConfirm = useCallback(async () => { const ids = Array.from(selectedItems); startTransition(async () => { try { const result = await markAsUnreadBulk(ids); if (result.success) { toast.success('미열람 처리 완료', { description: '미열람 처리가 완료되었습니다.', }); setSelectedItems(new Set()); loadData(); loadSummary(); } else { toast.error(result.error || '미열람 처리에 실패했습니다.'); } } catch (error) { console.error('Mark unread error:', error); toast.error('미열람 처리 중 오류가 발생했습니다.'); } }); setMarkUnreadDialogOpen(false); }, [selectedItems, loadData, loadSummary]); // ===== 문서 클릭/상세 보기 핸들러 ===== const handleDocumentClick = useCallback((item: ReferenceRecord) => { setSelectedDocument(item); setIsModalOpen(true); }, []); // ===== ApprovalType → DocumentType 변환 ===== const getDocumentType = (approvalType: ApprovalType): DocumentType => { switch (approvalType) { case 'expense_estimate': return 'expenseEstimate'; case 'expense_report': return 'expenseReport'; default: return 'proposal'; } }; // ===== ReferenceRecord → 모달용 데이터 변환 ===== const convertToModalData = (item: ReferenceRecord): ProposalDocumentData | ExpenseReportDocumentData | ExpenseEstimateDocumentData => { const docType = getDocumentType(item.approvalType); const drafter = { id: 'drafter-1', name: item.drafter, position: item.drafterPosition, department: item.drafterDepartment, status: 'approved' as const, }; const approvers = [{ id: 'approver-1', name: '결재자', position: '부장', department: '경영지원팀', status: 'approved' as const, }]; switch (docType) { case 'expenseEstimate': return { documentNo: item.documentNo, createdAt: item.draftDate, items: [ { id: '1', expectedPaymentDate: '2025-11-05', category: '통신 서비스', amount: 550000, vendor: 'KT', account: '국민 123-456-789012 홍길동' }, { id: '2', expectedPaymentDate: '2025-11-12', category: '인건 대행', amount: 2500000, vendor: '에이치알코리아', account: '신한 110-123-456789 (주)에이치알' }, ], totalExpense: 3050000, accountBalance: 25000000, finalDifference: 21950000, approvers, drafter, }; case 'expenseReport': return { documentNo: item.documentNo, createdAt: item.draftDate, requestDate: item.draftDate, paymentDate: item.draftDate, items: [ { id: '1', no: 1, description: '업무용 택시비', amount: 50000, note: '고객사 미팅' }, { id: '2', no: 2, description: '식대', amount: 30000, note: '팀 회식' }, ], cardInfo: '삼성카드 **** 1234', totalAmount: 80000, attachments: [], approvers, drafter, }; default: return { documentNo: item.documentNo, createdAt: item.draftDate, vendor: '거래처', vendorPaymentDate: item.draftDate, title: item.title, description: item.title, reason: '업무상 필요', estimatedCost: 1000000, attachments: [], approvers, drafter, }; } }; // ===== 통계 카드 ===== const statCards: StatCard[] = useMemo(() => [ { label: '전체', value: `${stats.all}건`, icon: Files, iconColor: 'text-blue-500' }, { label: '열람', value: `${stats.read}건`, icon: Eye, iconColor: 'text-green-500' }, { label: '미열람', value: `${stats.unread}건`, icon: EyeOff, iconColor: 'text-red-500' }, ], [stats]); // ===== 탭 옵션 (열람/미열람 토글 버튼 형태) ===== const tabs: TabOption[] = useMemo(() => [ { value: 'all', label: REFERENCE_TAB_LABELS.all, count: stats.all, color: 'blue' }, { value: 'read', label: REFERENCE_TAB_LABELS.read, count: stats.read, color: 'green' }, { value: 'unread', label: REFERENCE_TAB_LABELS.unread, count: stats.unread, color: 'red' }, ], [stats]); // ===== 테이블 컬럼 ===== // 문서번호, 문서유형, 제목, 기안자, 기안일시, 상태 const tableColumns: TableColumn[] = useMemo(() => [ { key: 'no', label: '번호', className: 'w-[60px] text-center' }, { key: 'documentNo', label: '문서번호' }, { key: 'approvalType', label: '문서유형' }, { key: 'title', label: '제목' }, { key: 'drafter', label: '기안자' }, { key: 'draftDate', label: '기안일시' }, { key: 'status', label: '상태', className: 'text-center' }, ], []); // ===== 테이블 행 렌더링 ===== const renderTableRow = useCallback((item: ReferenceRecord, index: number, globalIndex: number) => { const isSelected = selectedItems.has(item.id); return ( handleDocumentClick(item)} > e.stopPropagation()}> toggleSelection(item.id)} /> {globalIndex} {item.documentNo} {APPROVAL_TYPE_LABELS[item.approvalType]} {item.title} {item.drafter} {item.draftDate} {READ_STATUS_LABELS[item.readStatus]} ); }, [selectedItems, toggleSelection, handleDocumentClick]); // ===== 모바일 카드 렌더링 ===== const renderMobileCard = useCallback(( item: ReferenceRecord, index: number, globalIndex: number, isSelected: boolean, onToggle: () => void ) => { return ( {APPROVAL_TYPE_LABELS[item.approvalType]} {READ_STATUS_LABELS[item.readStatus]} } isSelected={isSelected} onToggleSelection={onToggle} infoGrid={
} actions={
{item.readStatus === 'unread' ? ( ) : ( )}
} /> ); }, []); // ===== 헤더 액션 (DateRangeSelector + 열람/미열람 버튼) ===== const headerActions = ( <> {selectedItems.size > 0 && (
)} ); // ===== 테이블 헤더 액션 (필터 + 정렬 셀렉트) ===== const tableHeaderActions = (
{/* 필터 셀렉트박스 */} {/* 정렬 셀렉트박스 */}
); return ( <> item.id} renderTableRow={renderTableRow} renderMobileCard={renderMobileCard} isLoading={isLoading || isPending} pagination={{ currentPage, totalPages, totalItems: totalCount, itemsPerPage, onPageChange: setCurrentPage, }} /> {/* 열람 처리 확인 다이얼로그 */} 열람 처리 정말 {selectedItems.size}건을 열람 처리하시겠습니까? 취소 확인 {/* 미열람 처리 확인 다이얼로그 */} 미열람 처리 정말 {selectedItems.size}건을 미열람 처리하시겠습니까? 취소 확인 {/* 문서 상세 모달 */} {selectedDocument && ( )} ); }