'use client'; import { useState, useMemo, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { AlertTriangle, Pencil, Plus, Inbox, Clock, CheckCircle, XCircle, Undo2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; import { MultiSelectOption } from '@/components/ui/multi-select-combobox'; import { IntegratedListTemplateV2, TableColumn, StatCard, FilterFieldConfig, FilterValues } from '@/components/templates/IntegratedListTemplateV2'; import { MobileCard } from '@/components/molecules/MobileCard'; import { DateRangeSelector } from '@/components/molecules/DateRangeSelector'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { toast } from 'sonner'; import type { Issue, IssueStats, } from './types'; import { ISSUE_STATUS_OPTIONS, ISSUE_PRIORITY_OPTIONS, ISSUE_CATEGORY_OPTIONS, ISSUE_SORT_OPTIONS, ISSUE_STATUS_STYLES, ISSUE_STATUS_LABELS, ISSUE_PRIORITY_STYLES, ISSUE_PRIORITY_LABELS, ISSUE_CATEGORY_LABELS, MOCK_ISSUE_PARTNERS, MOCK_ISSUE_SITES, MOCK_ISSUE_REPORTERS, MOCK_ISSUE_ASSIGNEES, } from './types'; import { getIssueList, getIssueStats, withdrawIssues, } from './actions'; // 테이블 컬럼 정의 // 체크박스, 번호, 이슈번호, 시공번호, 거래처, 현장, 구분, 제목, 보고자, 이슈보고일, 이슈해결일, 담당자, 중요도, 상태, 작업 const tableColumns: TableColumn[] = [ { key: 'no', label: '번호', className: 'w-[50px] text-center' }, { key: 'issueNumber', label: '이슈번호', className: 'w-[120px]' }, { key: 'constructionNumber', label: '시공번호', className: 'w-[100px]' }, { key: 'partnerName', label: '거래처', className: 'w-[100px]' }, { key: 'siteName', label: '현장', className: 'min-w-[120px]' }, { key: 'category', label: '구분', className: 'w-[80px] text-center' }, { key: 'title', label: '제목', className: 'min-w-[150px]' }, { key: 'reporter', label: '보고자', className: 'w-[80px]' }, { key: 'reportDate', label: '이슈보고일', className: 'w-[100px]' }, { key: 'resolvedDate', label: '이슈해결일', className: 'w-[100px]' }, { key: 'assignee', label: '담당자', className: 'w-[80px]' }, { key: 'priority', label: '중요도', className: 'w-[80px] text-center' }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ]; interface IssueManagementListClientProps { initialData?: Issue[]; initialStats?: IssueStats; } export default function IssueManagementListClient({ initialData = [], initialStats, }: IssueManagementListClientProps) { const router = useRouter(); // 상태 const [issues, setIssues] = useState(initialData); const [stats, setStats] = useState(initialStats || null); const [searchValue, setSearchValue] = useState(''); // 다중선택 필터 (빈 배열 = 전체) const [partnerFilters, setPartnerFilters] = useState([]); const [siteFilters, setSiteFilters] = useState([]); const [categoryFilters, setCategoryFilters] = useState([]); const [reporterFilters, setReporterFilters] = useState([]); const [assigneeFilters, setAssigneeFilters] = useState([]); // 단일선택 필터 const [priorityFilter, setPriorityFilter] = useState('all'); const [statusFilter, setStatusFilter] = useState('all'); const [sortBy, setSortBy] = useState('latest'); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [selectedItems, setSelectedItems] = useState>(new Set()); const [currentPage, setCurrentPage] = useState(1); const [isLoading, setIsLoading] = useState(false); const [activeStatTab, setActiveStatTab] = useState<'all' | 'received' | 'in_progress' | 'resolved' | 'unresolved'>('all'); const [withdrawDialogOpen, setWithdrawDialogOpen] = useState(false); const itemsPerPage = 20; // 데이터 로드 const loadData = useCallback(async () => { setIsLoading(true); try { const [listResult, statsResult] = await Promise.all([ getIssueList({ size: 1000, startDate: startDate || undefined, endDate: endDate || undefined, }), getIssueStats(), ]); if (listResult.success && listResult.data) { setIssues(listResult.data.items); } if (statsResult.success && statsResult.data) { setStats(statsResult.data); } } catch { toast.error('데이터 로드에 실패했습니다.'); } finally { setIsLoading(false); } }, [startDate, endDate]); // 초기 데이터가 없으면 로드 useEffect(() => { if (initialData.length === 0) { loadData(); } }, [initialData.length, loadData]); // 다중선택 필터 옵션 변환 (MultiSelectCombobox용) const partnerOptions: MultiSelectOption[] = useMemo(() => MOCK_ISSUE_PARTNERS.map(p => ({ value: p.value, label: p.label })), []); const siteOptions: MultiSelectOption[] = useMemo(() => MOCK_ISSUE_SITES.map(s => ({ value: s.value, label: s.label })), []); const categoryOptions: MultiSelectOption[] = useMemo(() => ISSUE_CATEGORY_OPTIONS.filter(c => c.value !== 'all').map(c => ({ value: c.value, label: c.label })), []); const reporterOptions: MultiSelectOption[] = useMemo(() => MOCK_ISSUE_REPORTERS.map(r => ({ value: r.value, label: r.label })), []); const assigneeOptions: MultiSelectOption[] = useMemo(() => MOCK_ISSUE_ASSIGNEES.map(a => ({ value: a.value, label: a.label })), []); // 필터링된 데이터 const filteredIssues = useMemo(() => { return issues.filter((item) => { // 상태 탭 필터 if (activeStatTab !== 'all' && item.status !== activeStatTab) return false; // 상태 필터 if (statusFilter !== 'all' && item.status !== statusFilter) return false; // 중요도 필터 if (priorityFilter !== 'all' && item.priority !== priorityFilter) return false; // 거래처 필터 (다중선택) if (partnerFilters.length > 0) { const matchingPartner = MOCK_ISSUE_PARTNERS.find((p) => p.label === item.partnerName); if (!matchingPartner || !partnerFilters.includes(matchingPartner.value)) { return false; } } // 현장 필터 (다중선택) if (siteFilters.length > 0) { const matchingSite = MOCK_ISSUE_SITES.find((s) => s.label === item.siteName); if (!matchingSite || !siteFilters.includes(matchingSite.value)) { return false; } } // 구분 필터 (다중선택) if (categoryFilters.length > 0) { if (!categoryFilters.includes(item.category)) { return false; } } // 보고자 필터 (다중선택) if (reporterFilters.length > 0) { const matchingReporter = MOCK_ISSUE_REPORTERS.find((r) => r.label === item.reporter); if (!matchingReporter || !reporterFilters.includes(matchingReporter.value)) { return false; } } // 담당자 필터 (다중선택) if (assigneeFilters.length > 0) { const matchingAssignee = MOCK_ISSUE_ASSIGNEES.find((a) => a.label === item.assignee); if (!matchingAssignee || !assigneeFilters.includes(matchingAssignee.value)) { return false; } } // 검색 필터 if (searchValue) { const search = searchValue.toLowerCase(); return ( item.issueNumber.toLowerCase().includes(search) || item.constructionNumber.toLowerCase().includes(search) || item.partnerName.toLowerCase().includes(search) || item.siteName.toLowerCase().includes(search) || item.title.toLowerCase().includes(search) || item.reporter.toLowerCase().includes(search) || item.assignee.toLowerCase().includes(search) ); } return true; }); }, [issues, activeStatTab, statusFilter, priorityFilter, partnerFilters, siteFilters, categoryFilters, reporterFilters, assigneeFilters, searchValue]); // 정렬 const sortedIssues = useMemo(() => { const sorted = [...filteredIssues]; switch (sortBy) { case 'latest': sorted.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); break; case 'oldest': sorted.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); break; case 'reportDate': sorted.sort((a, b) => new Date(b.reportDate).getTime() - new Date(a.reportDate).getTime()); break; case 'priorityHigh': const priorityOrder: Record = { urgent: 0, normal: 1 }; sorted.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]); break; case 'priorityLow': const priorityOrderLow: Record = { urgent: 1, normal: 0 }; sorted.sort((a, b) => priorityOrderLow[a.priority] - priorityOrderLow[b.priority]); break; } return sorted; }, [filteredIssues, sortBy]); // 페이지네이션 const totalPages = Math.ceil(sortedIssues.length / itemsPerPage); const paginatedData = useMemo(() => { const start = (currentPage - 1) * itemsPerPage; return sortedIssues.slice(start, start + itemsPerPage); }, [sortedIssues, currentPage, itemsPerPage]); // 핸들러 const handleSearchChange = useCallback((value: string) => { setSearchValue(value); setCurrentPage(1); }, []); const handleToggleSelection = useCallback((id: string) => { setSelectedItems((prev) => { const newSet = new Set(prev); if (newSet.has(id)) { newSet.delete(id); } else { newSet.add(id); } return newSet; }); }, []); const handleToggleSelectAll = useCallback(() => { if (selectedItems.size === paginatedData.length) { setSelectedItems(new Set()); } else { setSelectedItems(new Set(paginatedData.map((c) => c.id))); } }, [selectedItems.size, paginatedData]); const handleRowClick = useCallback( (item: Issue) => { router.push(`/ko/construction/project/issue-management/${item.id}`); }, [router] ); const handleEdit = useCallback( (e: React.MouseEvent, itemId: string) => { e.stopPropagation(); router.push(`/ko/construction/project/issue-management/${itemId}/edit`); }, [router] ); const handleCreateIssue = useCallback(() => { router.push('/ko/construction/project/issue-management/new'); }, [router]); // 철회 다이얼로그 열기 const handleWithdrawClick = useCallback(() => { if (selectedItems.size === 0) { toast.error('철회할 이슈를 선택해주세요.'); return; } setWithdrawDialogOpen(true); }, [selectedItems.size]); // 철회 실행 const handleWithdraw = useCallback(async () => { try { const ids = Array.from(selectedItems); const result = await withdrawIssues(ids); if (result.success) { toast.success(`${ids.length}건의 이슈가 철회되었습니다.`); setSelectedItems(new Set()); loadData(); } else { toast.error(result.error || '이슈 철회에 실패했습니다.'); } } catch { toast.error('이슈 철회에 실패했습니다.'); } finally { setWithdrawDialogOpen(false); } }, [selectedItems, loadData]); // 날짜 포맷 const formatDate = (dateStr: string | null) => { if (!dateStr) return '-'; return dateStr.split('T')[0]; }; // 테이블 행 렌더링 const renderTableRow = useCallback( (item: Issue, index: number, globalIndex: number) => { const isSelected = selectedItems.has(item.id); return ( handleRowClick(item)} > e.stopPropagation()}> handleToggleSelection(item.id)} /> {globalIndex} {item.issueNumber} {item.constructionNumber} {item.partnerName} {item.siteName} {ISSUE_CATEGORY_LABELS[item.category]} {item.title} {item.reporter} {formatDate(item.reportDate)} {formatDate(item.resolvedDate)} {item.assignee} {ISSUE_PRIORITY_LABELS[item.priority]} {ISSUE_STATUS_LABELS[item.status]} {isSelected && (
)}
); }, [selectedItems, handleToggleSelection, handleRowClick, handleEdit] ); // 모바일 카드 렌더링 const renderMobileCard = useCallback( (item: Issue, index: number, globalIndex: number, isSelected: boolean, onToggle: () => void) => { return ( handleRowClick(item)} details={[ { label: '거래처', value: item.partnerName }, { label: '현장', value: item.siteName }, { label: '보고일', value: formatDate(item.reportDate) }, { label: '중요도', value: ISSUE_PRIORITY_LABELS[item.priority] }, ]} /> ); }, [handleRowClick] ); // 헤더 액션 (DateRangeSelector + 이슈 등록 버튼) const headerActions = ( 이슈 등록 } /> ); // 통계 카드 클릭 핸들러 const handleStatClick = useCallback((tab: 'all' | 'received' | 'in_progress' | 'resolved' | 'unresolved') => { setActiveStatTab(tab); setCurrentPage(1); }, []); // 통계 카드 데이터 const statsCardsData: StatCard[] = [ { label: '접수', value: stats?.received ?? 0, icon: Inbox, iconColor: 'text-blue-600', onClick: () => handleStatClick('received'), isActive: activeStatTab === 'received', }, { label: '처리중', value: stats?.inProgress ?? 0, icon: Clock, iconColor: 'text-yellow-600', onClick: () => handleStatClick('in_progress'), isActive: activeStatTab === 'in_progress', }, { label: '해결완료', value: stats?.resolved ?? 0, icon: CheckCircle, iconColor: 'text-green-600', onClick: () => handleStatClick('resolved'), isActive: activeStatTab === 'resolved', }, { label: '미해결', value: stats?.unresolved ?? 0, icon: XCircle, iconColor: 'text-red-600', onClick: () => handleStatClick('unresolved'), isActive: activeStatTab === 'unresolved', }, ]; // ===== filterConfig 기반 통합 필터 시스템 ===== const filterConfig: FilterFieldConfig[] = useMemo(() => [ { key: 'partner', label: '거래처', type: 'multi', options: partnerOptions.map(o => ({ value: o.value, label: o.label, })), }, { key: 'site', label: '현장명', type: 'multi', options: siteOptions.map(o => ({ value: o.value, label: o.label, })), }, { key: 'category', label: '구분', type: 'multi', options: categoryOptions.map(o => ({ value: o.value, label: o.label, })), }, { key: 'reporter', label: '보고자', type: 'multi', options: reporterOptions.map(o => ({ value: o.value, label: o.label, })), }, { key: 'assignee', label: '담당자', type: 'multi', options: assigneeOptions.map(o => ({ value: o.value, label: o.label, })), }, { key: 'priority', label: '중요도', type: 'single', options: ISSUE_PRIORITY_OPTIONS.filter(o => o.value !== 'all').map(o => ({ value: o.value, label: o.label, })), allOptionLabel: '전체', }, { key: 'status', label: '상태', type: 'single', options: ISSUE_STATUS_OPTIONS.filter(o => o.value !== 'all').map(o => ({ value: o.value, label: o.label, })), allOptionLabel: '전체', }, { key: 'sortBy', label: '정렬', type: 'single', options: ISSUE_SORT_OPTIONS.map(o => ({ value: o.value, label: o.label, })), allOptionLabel: '최신순', }, ], [partnerOptions, siteOptions, categoryOptions, reporterOptions, assigneeOptions]); const filterValues: FilterValues = useMemo(() => ({ partner: partnerFilters, site: siteFilters, category: categoryFilters, reporter: reporterFilters, assignee: assigneeFilters, priority: priorityFilter, status: statusFilter, sortBy: sortBy, }), [partnerFilters, siteFilters, categoryFilters, reporterFilters, assigneeFilters, priorityFilter, statusFilter, sortBy]); const handleFilterChange = useCallback((key: string, value: string | string[]) => { switch (key) { case 'partner': setPartnerFilters(value as string[]); break; case 'site': setSiteFilters(value as string[]); break; case 'category': setCategoryFilters(value as string[]); break; case 'reporter': setReporterFilters(value as string[]); break; case 'assignee': setAssigneeFilters(value as string[]); break; case 'priority': setPriorityFilter(value as string); break; case 'status': setStatusFilter(value as string); break; case 'sortBy': setSortBy(value as string); break; } setCurrentPage(1); }, []); const handleFilterReset = useCallback(() => { setPartnerFilters([]); setSiteFilters([]); setCategoryFilters([]); setReporterFilters([]); setAssigneeFilters([]); setPriorityFilter('all'); setStatusFilter('all'); setSortBy('latest'); setCurrentPage(1); }, []); // 철회 버튼 (bulkActions용) const bulkActions = selectedItems.size > 0 ? ( ) : null; return ( <> item.id} renderTableRow={renderTableRow} renderMobileCard={renderMobileCard} selectedItems={selectedItems} onToggleSelection={handleToggleSelection} onToggleSelectAll={handleToggleSelectAll} pagination={{ currentPage, totalPages, totalItems: sortedIssues.length, itemsPerPage, onPageChange: setCurrentPage, }} /> {/* 철회 확인 다이얼로그 */} 이슈 철회 선택한 {selectedItems.size}건의 이슈를 철회하시겠습니까?
철회된 이슈는 복구할 수 없습니다.
취소 철회
); }