'use client'; /** * 시공관리 - UniversalListPage 마이그레이션 * * 기존 IntegratedListTemplateV2 → UniversalListPage config 기반으로 변환 * - 클라이언트 사이드 필터링 (검색, 필터, 정렬) * - ScheduleCalendar (beforeTableContent) * - 달력 전용 필터 (calendarSiteFilters, calendarWorkTeamFilters) * - 달력 날짜 선택 필터링 (selectedCalendarDate) * - Stats 카드 클릭 필터링 (activeStatTab) * - DateRangeSelector (dateRangeSelector config) * - filterConfig (multi 4개 + single 2개) * - 삭제 기능 없음 (수정만 가능) */ import { useState, useMemo, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { HardHat, Pencil, Clock, CheckCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { TableCell, TableRow } from '@/components/ui/table'; import { Checkbox } from '@/components/ui/checkbox'; import { MultiSelectCombobox, MultiSelectOption } from '@/components/ui/multi-select-combobox'; import { MobileCard } from '@/components/organisms/MobileCard'; import { ScheduleCalendar, ScheduleEvent, DayBadge } from '@/components/common/ScheduleCalendar'; import { UniversalListPage, type UniversalListConfig, type SelectionHandlers, type RowClickHandlers, type StatCard, } from '@/components/templates/UniversalListPage'; import { format, isSameDay, startOfDay, parseISO } from 'date-fns'; import type { ConstructionManagement, ConstructionManagementStats, } from './types'; import { CONSTRUCTION_MANAGEMENT_STATUS_OPTIONS, CONSTRUCTION_MANAGEMENT_SORT_OPTIONS, CONSTRUCTION_MANAGEMENT_STATUS_STYLES, CONSTRUCTION_MANAGEMENT_STATUS_LABELS, MOCK_CM_PARTNERS, MOCK_CM_SITES, MOCK_CM_CONSTRUCTION_PM, MOCK_CM_WORK_TEAM_LEADERS, getConstructionScheduleColor, } from './types'; import { getConstructionManagementList, getConstructionManagementStats, } from './actions'; // 테이블 컬럼 정의 const tableColumns = [ { key: 'no', label: '번호', className: 'w-[50px] text-center' }, { key: 'constructionNumber', label: '시공번호', className: 'w-[100px]' }, { key: 'partnerName', label: '거래처', className: 'w-[100px]' }, { key: 'siteName', label: '현장명', className: 'min-w-[120px]' }, { key: 'constructionPM', label: '공사PM', className: 'w-[80px]' }, { key: 'workTeamLeader', label: '작업반장', className: 'w-[80px]' }, { key: 'worker', label: '작업자', className: 'w-[80px]' }, { key: 'constructionStartDate', label: '시공투입일', className: 'w-[100px]' }, { key: 'constructionEndDate', label: '시공완료일', className: 'w-[100px]' }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ]; interface ConstructionManagementListClientProps { initialData?: ConstructionManagement[]; initialStats?: ConstructionManagementStats; } export default function ConstructionManagementListClient({ initialData = [], initialStats, }: ConstructionManagementListClientProps) { const router = useRouter(); // ===== 외부 상태 (UniversalListPage 외부에서 관리) ===== const [activeStatTab, setActiveStatTab] = useState<'all' | 'in_progress' | 'completed'>('all'); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [stats, setStats] = useState(initialStats || null); const [searchQuery, setSearchQuery] = useState(''); // 달력 관련 상태 const [selectedCalendarDate, setSelectedCalendarDate] = useState(null); const [calendarDate, setCalendarDate] = useState(new Date()); // 달력 전용 필터 (테이블 필터와 별도) const [calendarSiteFilters, setCalendarSiteFilters] = useState([]); const [calendarWorkTeamFilters, setCalendarWorkTeamFilters] = useState([]); // 전체 데이터 (달력 이벤트용) const [allConstructions, setAllConstructions] = useState(initialData); // Stats 로드 useEffect(() => { if (!initialStats) { getConstructionManagementStats().then((result) => { if (result.success && result.data) { setStats(result.data); } }); } }, [initialStats]); // 필터 옵션 (memo) const siteOptions: MultiSelectOption[] = useMemo(() => MOCK_CM_SITES.map(s => ({ value: s.value, label: s.label })), []); const workTeamOptions: MultiSelectOption[] = useMemo(() => MOCK_CM_WORK_TEAM_LEADERS.map(l => ({ value: l.value, label: l.label })), []); const partnerOptions: MultiSelectOption[] = useMemo(() => MOCK_CM_PARTNERS.map(p => ({ value: p.value, label: p.label })), []); const constructionPMOptions: MultiSelectOption[] = useMemo(() => MOCK_CM_CONSTRUCTION_PM.map(pm => ({ value: pm.value, label: pm.label })), []); // 달력 이벤트 데이터 (달력 전용 필터 적용) const calendarEvents: ScheduleEvent[] = useMemo(() => { return allConstructions .filter((item) => { // 현장 필터 (달력용) if (calendarSiteFilters.length > 0) { const matchingSite = MOCK_CM_SITES.find((s) => s.label === item.siteName); if (!matchingSite || !calendarSiteFilters.includes(matchingSite.value)) { return false; } } // 작업반장 필터 (달력용) if (calendarWorkTeamFilters.length > 0) { const matchingLeader = MOCK_CM_WORK_TEAM_LEADERS.find((l) => l.label === item.workTeamLeader); if (!matchingLeader || !calendarWorkTeamFilters.includes(matchingLeader.value)) { return false; } } return true; }) .map((item) => ({ id: item.id, title: `${item.workTeamLeader} - ${item.siteName} / ${item.constructionNumber}`, startDate: item.periodStart, endDate: item.periodEnd, color: getConstructionScheduleColor(item.workTeamLeader), status: item.status, data: item, })); }, [allConstructions, calendarSiteFilters, calendarWorkTeamFilters]); // 달력 뱃지 (사용 안 함) const calendarBadges: DayBadge[] = []; // 날짜 포맷 const formatDate = useCallback((dateStr: string | null) => { if (!dateStr) return '-'; return dateStr.split('T')[0]; }, []); // ===== 핸들러 ===== const handleRowClick = useCallback( (item: ConstructionManagement) => { router.push(`/ko/construction/project/construction-management/${item.id}?mode=view`); }, [router] ); const handleEdit = useCallback( (item: ConstructionManagement) => { router.push(`/ko/construction/project/construction-management/${item.id}?mode=edit`); }, [router] ); // 달력 이벤트 핸들러 const handleCalendarDateClick = useCallback((date: Date) => { if (selectedCalendarDate && isSameDay(selectedCalendarDate, date)) { setSelectedCalendarDate(null); } else { setSelectedCalendarDate(date); } }, [selectedCalendarDate]); const handleCalendarEventClick = useCallback((event: ScheduleEvent) => { if (event.data) { router.push(`/ko/construction/project/construction-management/${event.id}?mode=view`); } }, [router]); const handleCalendarMonthChange = useCallback((date: Date) => { setCalendarDate(date); }, []); // 달력 필터 슬롯 const calendarFilterSlot = useMemo(() => (
), [siteOptions, workTeamOptions, calendarSiteFilters, calendarWorkTeamFilters]); // ===== UniversalListPage Config ===== const config: UniversalListConfig = useMemo( () => ({ // 페이지 기본 정보 title: '시공관리', description: '시공 스케줄 및 목록을 관리합니다', icon: HardHat, basePath: '/construction/project/construction-management', // ID 추출 idField: 'id', // API 액션 actions: { getList: async () => { const result = await getConstructionManagementList({ size: 1000, startDate: startDate || undefined, endDate: endDate || undefined, }); if (result.success && result.data) { return { success: true, data: result.data.items, totalCount: result.data.total, }; } return { success: false, error: result.error }; }, }, // 테이블 컬럼 columns: tableColumns, // 클라이언트 사이드 필터링 clientSideFiltering: true, itemsPerPage: 20, // 데이터 변경 콜백 (달력 이벤트용) onDataChange: (data) => setAllConstructions(data), // 검색 필터 searchPlaceholder: '시공번호, 거래처, 현장명, 작업반장, 작업자 검색', searchFilter: (item, searchValue) => { const search = searchValue.toLowerCase(); return ( item.constructionNumber.toLowerCase().includes(search) || item.partnerName.toLowerCase().includes(search) || item.siteName.toLowerCase().includes(search) || item.workTeamLeader.toLowerCase().includes(search) || item.worker.toLowerCase().includes(search) ); }, // 필터 설정 filterConfig: [ { key: 'partners', label: '거래처', type: 'multi', options: partnerOptions }, { key: 'sites', label: '현장명', type: 'multi', options: siteOptions }, { key: 'constructionPMs', label: '공사PM', type: 'multi', options: constructionPMOptions }, { key: 'workTeamLeaders', label: '작업반장', type: 'multi', options: workTeamOptions }, { key: 'status', label: '상태', type: 'single', options: CONSTRUCTION_MANAGEMENT_STATUS_OPTIONS.filter((o) => o.value !== 'all') }, { key: 'sortBy', label: '정렬', type: 'single', options: CONSTRUCTION_MANAGEMENT_SORT_OPTIONS }, ], initialFilters: { partners: [], sites: [], constructionPMs: [], workTeamLeaders: [], status: 'all', sortBy: 'latest', }, filterTitle: '시공관리 필터', // 커스텀 필터 함수 customFilterFn: (items, filterValues) => { if (!items || items.length === 0) return items; return items.filter((item) => { // Stats 탭 필터 if (activeStatTab === 'in_progress' && item.status !== 'in_progress') return false; if (activeStatTab === 'completed' && item.status !== 'completed') return false; // 거래처 필터 (다중선택) const partnerFilters = filterValues.partners as string[]; if (partnerFilters?.length > 0) { const matchingPartner = MOCK_CM_PARTNERS.find((p) => p.label === item.partnerName); if (!matchingPartner || !partnerFilters.includes(matchingPartner.value)) { return false; } } // 현장명 필터 (다중선택) const siteFilters = filterValues.sites as string[]; if (siteFilters?.length > 0) { const matchingSite = MOCK_CM_SITES.find((s) => s.label === item.siteName); if (!matchingSite || !siteFilters.includes(matchingSite.value)) { return false; } } // 공사PM 필터 (다중선택) const pmFilters = filterValues.constructionPMs as string[]; if (pmFilters?.length > 0) { const matchingPM = MOCK_CM_CONSTRUCTION_PM.find((p) => p.label === item.constructionPM); if (!matchingPM || !pmFilters.includes(matchingPM.value)) { return false; } } // 작업반장 필터 (다중선택) const teamLeaderFilters = filterValues.workTeamLeaders as string[]; if (teamLeaderFilters?.length > 0) { const matchingLeader = MOCK_CM_WORK_TEAM_LEADERS.find((l) => l.label === item.workTeamLeader); if (!matchingLeader || !teamLeaderFilters.includes(matchingLeader.value)) { return false; } } // 상태 필터 (단일선택) const statusFilter = filterValues.status as string; if (statusFilter && statusFilter !== 'all' && item.status !== statusFilter) { return false; } // 달력 날짜 필터 if (selectedCalendarDate) { const itemStart = startOfDay(parseISO(item.periodStart)); const itemEnd = startOfDay(parseISO(item.periodEnd)); const selected = startOfDay(selectedCalendarDate); if (selected < itemStart || selected > itemEnd) { return false; } } return true; }); }, // 커스텀 정렬 함수 customSortFn: (items, filterValues) => { const sorted = [...items]; const sortBy = (filterValues.sortBy as string) || 'latest'; switch (sortBy) { case 'register': sorted.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); break; case 'completionDateDesc': sorted.sort((a, b) => { if (!a.constructionEndDate) return 1; if (!b.constructionEndDate) return -1; return new Date(b.constructionEndDate).getTime() - new Date(a.constructionEndDate).getTime(); }); break; case 'partnerNameAsc': sorted.sort((a, b) => a.partnerName.localeCompare(b.partnerName, 'ko')); break; case 'partnerNameDesc': sorted.sort((a, b) => b.partnerName.localeCompare(a.partnerName, 'ko')); break; default: // latest sorted.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); break; } return sorted; }, // 검색창 (공통 컴포넌트에서 자동 생성) hideSearch: true, searchValue: searchQuery, onSearchChange: setSearchQuery, // 공통 헤더 옵션 dateRangeSelector: { enabled: true, startDate, endDate, onStartDateChange: setStartDate, onEndDateChange: setEndDate, }, // Stats 카드 computeStats: (): StatCard[] => [ { label: '시공진행', value: stats?.inProgress ?? 0, icon: Clock, iconColor: 'text-yellow-600', onClick: () => setActiveStatTab('in_progress'), isActive: activeStatTab === 'in_progress', }, { label: '시공완료', value: stats?.completed ?? 0, icon: CheckCircle, iconColor: 'text-green-600', onClick: () => setActiveStatTab('completed'), isActive: activeStatTab === 'completed', }, ], // 테이블 헤더 액션 (총건 + 달력 날짜 필터 해제) tableHeaderActions: ({ totalCount }) => (
총 {totalCount}건 {selectedCalendarDate && ( ({format(selectedCalendarDate, 'M/d')} 필터 적용중) )} {selectedCalendarDate && ( )}
), // 달력 섹션 (beforeTableContent) beforeTableContent: (
), // 테이블 행 렌더링 renderTableRow: ( item: ConstructionManagement, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => ( handleRowClick(item)} > e.stopPropagation()}> {globalIndex} {item.constructionNumber} {item.partnerName} {item.siteName} {item.constructionPM} {item.workTeamLeader} {item.worker} {formatDate(item.constructionStartDate)} {formatDate(item.constructionEndDate)} {CONSTRUCTION_MANAGEMENT_STATUS_LABELS[item.status]} {handlers.isSelected && (
)}
), // 모바일 카드 렌더링 renderMobileCard: ( item: ConstructionManagement, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => ( handleRowClick(item)} details={[ { label: '거래처', value: item.partnerName }, { label: '작업반장', value: item.workTeamLeader }, { label: '작업자', value: item.worker || '-' }, { label: '시공투입일', value: formatDate(item.constructionStartDate) }, { label: '시공완료일', value: formatDate(item.constructionEndDate) }, ]} /> ), }), [ startDate, endDate, searchQuery, activeStatTab, stats, selectedCalendarDate, calendarEvents, calendarBadges, calendarDate, calendarFilterSlot, partnerOptions, siteOptions, constructionPMOptions, workTeamOptions, handleRowClick, handleEdit, handleCalendarDateClick, handleCalendarEventClick, handleCalendarMonthChange, formatDate, ] ); return ; }