'use client'; /** * 발주관리 리스트 - UniversalListPage 버전 * * 특이 케이스: * - ScheduleCalendar 컴포넌트 (beforeTableContent) * - 9개의 다중선택 필터 * - 달력 날짜 클릭 시 테이블 필터링 * - 클라이언트 사이드 필터링/페이지네이션 */ import { useState, useMemo, useCallback, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Package, Pencil, Trash2, Plus } 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 { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard'; import { Badge } from '@/components/ui/badge'; import { UniversalListPage, type UniversalListConfig, type SelectionHandlers, type RowClickHandlers, type FilterFieldConfig, } from '@/components/templates/UniversalListPage'; import { DateRangeSelector } from '@/components/molecules/DateRangeSelector'; import { ScheduleCalendar, ScheduleEvent, DayBadge } from '@/components/common/ScheduleCalendar'; import { format, parseISO, isSameDay, startOfDay } from 'date-fns'; import type { Order } from './types'; import { ORDER_STATUS_OPTIONS, ORDER_SORT_OPTIONS, ORDER_STATUS_STYLES, ORDER_STATUS_LABELS, ORDER_TYPE_OPTIONS, ORDER_TYPE_LABELS, MOCK_PARTNERS, MOCK_SITES, MOCK_CONSTRUCTION_PM, MOCK_ORDER_MANAGERS, MOCK_ORDER_COMPANIES, MOCK_WORK_TEAM_LEADERS, getScheduleColorByManager, } from './types'; import { getOrderList, deleteOrder, deleteOrders, } from './actions'; interface OrderManagementUnifiedProps { initialData?: Order[]; } export function OrderManagementUnified({ initialData }: OrderManagementUnifiedProps) { const router = useRouter(); // 달력 관련 상태 (beforeTableContent에서 사용하므로 config 외부에서 관리) const [selectedCalendarDate, setSelectedCalendarDate] = useState(null); const [calendarDate, setCalendarDate] = useState(new Date()); const [siteFilters, setSiteFilters] = useState([]); const [workTeamFilters, setWorkTeamFilters] = useState([]); // 날짜 범위 필터 상태 const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); // 전체 데이터 (달력 이벤트용) const [allOrders, setAllOrders] = useState(initialData || []); const [isLoading, setIsLoading] = useState(false); // 필터 옵션들 const siteOptions: MultiSelectOption[] = useMemo(() => MOCK_SITES, []); const workTeamOptions: MultiSelectOption[] = useMemo(() => MOCK_WORK_TEAM_LEADERS, []); const partnerOptions: MultiSelectOption[] = useMemo(() => MOCK_PARTNERS, []); const constructionPMOptions: MultiSelectOption[] = useMemo(() => MOCK_CONSTRUCTION_PM, []); const orderManagerOptions: MultiSelectOption[] = useMemo(() => MOCK_ORDER_MANAGERS, []); const orderCompanyOptions: MultiSelectOption[] = useMemo(() => MOCK_ORDER_COMPANIES, []); const orderTypeOptions: MultiSelectOption[] = useMemo(() => ORDER_TYPE_OPTIONS, []); // 데이터 로드 const loadData = useCallback(async () => { setIsLoading(true); try { const result = await getOrderList({ size: 1000, startDate: startDate || undefined, endDate: endDate || undefined, }); if (result.success && result.data) { setAllOrders(result.data.items); } } catch { console.error('데이터 로드 실패'); } finally { setIsLoading(false); } }, [startDate, endDate]); // 초기 데이터가 없으면 로드 useEffect(() => { if (!initialData || initialData.length === 0) { loadData(); } }, [initialData, loadData]); // 달력용 이벤트 데이터 변환 (필터 적용) const calendarEvents: ScheduleEvent[] = useMemo(() => { return allOrders .filter((order) => { // 현장 필터 if (siteFilters.length > 0) { const matchingSite = MOCK_SITES.find((s) => order.siteName.includes(s.label.split(' ')[0])); if (!matchingSite || !siteFilters.includes(matchingSite.value)) { return false; } } // 작업반장 필터 if (workTeamFilters.length > 0) { const matchingLeader = MOCK_WORK_TEAM_LEADERS.find((l) => order.orderManager.includes(l.label.replace('반장', ''))); if (!matchingLeader || !workTeamFilters.includes(matchingLeader.value)) { return false; } } return true; }) .map((order) => ({ id: order.id, title: `${order.orderManager} - ${order.siteName} / ${order.orderNumber}`, startDate: order.periodStart, endDate: order.periodEnd, color: getScheduleColorByManager(order.orderManager), status: order.status, data: order, })); }, [allOrders, siteFilters, workTeamFilters]); // 달력용 뱃지 데이터 - 사용하지 않음 const calendarBadges: DayBadge[] = []; // 달력 이벤트 핸들러 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/order/order-management/${event.id}`); } }, [router]); const handleCalendarMonthChange = useCallback((date: Date) => { setCalendarDate(date); }, []); // 날짜 포맷 const formatDate = (dateStr: string | null) => { if (!dateStr) return '-'; return dateStr.split('T')[0]; }; // 달력 필터 슬롯 const calendarFilterSlot = (
); // UniversalListPage Config 정의 const config: UniversalListConfig = useMemo(() => ({ // ===== 페이지 기본 정보 ===== title: '발주관리', description: '발주 스케줄 및 목록을 관리합니다', icon: Package, basePath: '/construction/order/order-management', // ===== ID 추출 ===== idField: 'id', // ===== API 액션 ===== actions: { getList: async () => { const result = await getOrderList({ size: 1000, startDate: startDate || undefined, endDate: endDate || undefined, }); return { success: result.success, data: result.data?.items || [], totalCount: result.data?.items?.length || 0, error: result.error, }; }, deleteItem: async (id: string) => { return await deleteOrder(id); }, deleteBulk: async (ids: string[]) => { return await deleteOrders(ids); }, }, // ===== 테이블 컬럼 ===== columns: [ { key: 'no', label: '번호', className: 'w-[50px] text-center' }, { key: 'contractNumber', label: '계약번호', className: 'w-[100px]' }, { key: 'partnerName', label: '거래처', className: 'w-[80px]' }, { key: 'siteName', label: '현장명', className: 'min-w-[100px]' }, { key: 'name', label: '명칭', className: 'w-[80px]' }, { key: 'constructionPM', label: '공사PM', className: 'w-[70px]' }, { key: 'orderManager', label: '발주담당자', className: 'w-[80px]' }, { key: 'orderNumber', label: '발주번호', className: 'w-[100px]' }, { key: 'orderCompany', label: '발주처명', className: 'w-[80px]' }, { key: 'workTeamLeader', label: '작업반장', className: 'w-[70px]' }, { key: 'constructionStartDate', label: '시공투입일', className: 'w-[90px]' }, { key: 'orderType', label: '구분', className: 'w-[80px] text-center' }, { key: 'item', label: '품목', className: 'w-[80px]' }, { key: 'quantity', label: '수량', className: 'w-[60px] text-right' }, { key: 'orderDate', label: '발주일', className: 'w-[90px]' }, { key: 'plannedDeliveryDate', label: '계획인수일', className: 'w-[90px]' }, { key: 'actualDeliveryDate', label: '실제인수일', className: 'w-[90px]' }, { key: 'status', label: '상태', className: 'w-[80px] text-center' }, { key: 'actions', label: '작업', className: 'w-[80px] text-center' }, ], // ===== 클라이언트 사이드 필터링 ===== clientSideFiltering: true, // 검색 필터 함수 searchFilter: (item: Order, searchValue: string) => { const search = searchValue.toLowerCase(); return ( item.orderNumber.toLowerCase().includes(search) || item.partnerName.toLowerCase().includes(search) || item.siteName.toLowerCase().includes(search) || item.orderManager.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: 'orderManagers', label: '발주담당자', type: 'multi', options: orderManagerOptions }, { key: 'orderCompanies', label: '발주처', type: 'multi', options: orderCompanyOptions }, { key: 'workTeamLeaders', label: '작업반장', type: 'multi', options: workTeamOptions }, { key: 'orderTypes', label: '구분', type: 'multi', options: orderTypeOptions }, { key: 'status', label: '상태', type: 'single', options: ORDER_STATUS_OPTIONS.filter(opt => opt.value !== 'all') }, { key: 'sortBy', label: '정렬', type: 'single', options: ORDER_SORT_OPTIONS, allOptionLabel: '최신순' }, ] as FilterFieldConfig[], // 커스텀 필터 적용 함수 customFilterFn: (items: Order[], filterValues: Record) => { return items.filter((order) => { // 거래처 필터 const partners = filterValues.partners as string[] || []; if (partners.length > 0) { const matchingPartner = MOCK_PARTNERS.find((p) => p.label === order.partnerName); if (!matchingPartner || !partners.includes(matchingPartner.value)) { return false; } } // 현장명 필터 const sites = filterValues.sites as string[] || []; if (sites.length > 0) { const matchingSite = MOCK_SITES.find((s) => s.label === order.siteName); if (!matchingSite || !sites.includes(matchingSite.value)) { return false; } } // 공사PM 필터 const constructionPMs = filterValues.constructionPMs as string[] || []; if (constructionPMs.length > 0) { const matchingPM = MOCK_CONSTRUCTION_PM.find((p) => p.label === order.constructionPM); if (!matchingPM || !constructionPMs.includes(matchingPM.value)) { return false; } } // 발주담당자 필터 const orderManagers = filterValues.orderManagers as string[] || []; if (orderManagers.length > 0) { const matchingManager = MOCK_ORDER_MANAGERS.find((m) => m.label === order.orderManager); if (!matchingManager || !orderManagers.includes(matchingManager.value)) { return false; } } // 발주처 필터 const orderCompanies = filterValues.orderCompanies as string[] || []; if (orderCompanies.length > 0) { const matchingCompany = MOCK_ORDER_COMPANIES.find((c) => c.label === order.orderCompany); if (!matchingCompany || !orderCompanies.includes(matchingCompany.value)) { return false; } } // 작업반장 필터 const workTeamLeaders = filterValues.workTeamLeaders as string[] || []; if (workTeamLeaders.length > 0) { const matchingLeader = MOCK_WORK_TEAM_LEADERS.find((l) => l.label === order.workTeamLeader); if (!matchingLeader || !workTeamLeaders.includes(matchingLeader.value)) { return false; } } // 구분 필터 const orderTypes = filterValues.orderTypes as string[] || []; if (orderTypes.length > 0 && !orderTypes.includes(order.orderType)) { return false; } // 상태 필터 const status = filterValues.status as string || 'all'; if (status !== 'all' && order.status !== status) { return false; } // 달력 날짜 필터 (selectedCalendarDate) if (selectedCalendarDate) { const orderStart = startOfDay(parseISO(order.periodStart)); const orderEnd = startOfDay(parseISO(order.periodEnd)); const selected = startOfDay(selectedCalendarDate); if (selected < orderStart || selected > orderEnd) { return false; } } return true; }); }, // 커스텀 정렬 함수 customSortFn: (items: Order[], filterValues: Record) => { const sortBy = filterValues.sortBy as string || 'latest'; const sorted = [...items]; 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 '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; case 'siteNameAsc': sorted.sort((a, b) => a.siteName.localeCompare(b.siteName, 'ko')); break; case 'siteNameDesc': sorted.sort((a, b) => b.siteName.localeCompare(a.siteName, 'ko')); break; case 'deliveryDateAsc': sorted.sort((a, b) => a.plannedDeliveryDate.localeCompare(b.plannedDeliveryDate)); break; case 'deliveryDateDesc': sorted.sort((a, b) => b.plannedDeliveryDate.localeCompare(a.plannedDeliveryDate)); break; } return sorted; }, // ===== 검색 설정 ===== searchPlaceholder: '발주번호, 거래처, 현장명, 발주담당 검색', // ===== 상세 보기 모드 ===== detailMode: 'page', // ===== 헤더 액션 ===== headerActions: ({ onCreate }) => ( 발주 등록 } /> ), // ===== 테이블 헤더 추가 액션 ===== tableHeaderActions: (
{selectedCalendarDate && ( <> ({format(selectedCalendarDate, 'M/d')} 필터 적용중) )}
), // ===== 삭제 확인 메시지 ===== deleteConfirmMessage: { title: '발주 삭제', description: '선택한 발주를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.', }, // ===== 테이블 행 렌더링 ===== renderTableRow: ( item: Order, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => { const { isSelected, onToggle, onRowClick, onEdit, onDelete } = handlers; return ( onRowClick(item)} > e.stopPropagation()}> {globalIndex} {item.contractNumber} {item.partnerName} {item.siteName} {item.name} {item.constructionPM} {item.orderManager} {item.orderNumber} {item.orderCompany} {item.workTeamLeader} {formatDate(item.constructionStartDate)} {ORDER_TYPE_LABELS[item.orderType]} {item.item} {item.quantity} {formatDate(item.orderDate)} {formatDate(item.plannedDeliveryDate)} {formatDate(item.actualDeliveryDate)} {ORDER_STATUS_LABELS[item.status]} e.stopPropagation()}> {isSelected && (
)}
); }, // ===== 모바일 카드 렌더링 ===== renderMobileCard: ( item: Order, index: number, globalIndex: number, handlers: SelectionHandlers & RowClickHandlers ) => { const { isSelected, onToggle, onRowClick, onEdit, onDelete } = handlers; return ( #{globalIndex} {item.orderNumber} } statusBadge={ {ORDER_STATUS_LABELS[item.status]} } isSelected={isSelected} onToggleSelection={onToggle} onCardClick={() => onRowClick(item)} infoGrid={
} actions={ isSelected ? (
) : undefined } /> ); }, // ===== 테이블 전 콘텐츠 (달력) ===== beforeTableContent: (
), // ===== 추가 옵션 ===== showCheckbox: true, showRowNumber: true, itemsPerPage: 20, }), [ startDate, endDate, selectedCalendarDate, calendarDate, calendarEvents, calendarBadges, calendarFilterSlot, isLoading, handleCalendarDateClick, handleCalendarEventClick, handleCalendarMonthChange, partnerOptions, siteOptions, constructionPMOptions, orderManagerOptions, orderCompanyOptions, workTeamOptions, orderTypeOptions, router, ]); return ( config={config} initialData={initialData} /> ); } export default OrderManagementUnified;