'use client'; /** * 작업자 화면 메인 컴포넌트 (기획서 기반 전면 개편) * * 구조: * - 상단: 페이지 제목 * - 탭: 스크린/슬랫/절곡 (디폴트: 스크린) * - 상태 카드 4개 (할일/작업중/완료/긴급) * - 수주 정보 섹션 (읽기 전용) * - 작업 정보 섹션 (생산담당자 셀렉트 + 생산일자) * - 작업 목록 (WorkItemCard 나열) * - 하단 고정 버튼 (작업일지보기 / 중간검사하기) */ import { useState, useMemo, useCallback, useEffect } from 'react'; import { useMenuStore } from '@/store/menuStore'; import { ClipboardList, PlayCircle, CheckCircle2, AlertTriangle, ChevronDown, ChevronUp, List } from 'lucide-react'; import { ContentSkeleton } from '@/components/ui/skeleton'; import { Card, CardContent } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; import { PageLayout } from '@/components/organisms/PageLayout'; import { cn } from '@/lib/utils'; import { toast } from 'sonner'; import { getMyWorkOrders, completeWorkOrder } from './actions'; import { getProcessList } from '@/components/process-management/actions'; import type { InspectionSetting, Process } from '@/types/process'; import type { WorkOrder } from '../ProductionDashboard/types'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import type { WorkerStats, CompletionToastInfo, MaterialInput, ProcessTab, WorkItemData, WorkStepData, MaterialListItem, } from './types'; import { PROCESS_TAB_LABELS } from './types'; import { WorkItemCard } from './WorkItemCard'; import { CompletionConfirmDialog } from './CompletionConfirmDialog'; import { CompletionToast } from './CompletionToast'; import { MaterialInputModal } from './MaterialInputModal'; import { WorkLogModal } from './WorkLogModal'; import { IssueReportModal } from './IssueReportModal'; import { WorkCompletionResultDialog } from './WorkCompletionResultDialog'; import { InspectionReportModal } from '../WorkOrders/documents'; import { InspectionInputModal, type InspectionProcessType, type InspectionData } from './InspectionInputModal'; // ===== 목업 데이터 ===== const MOCK_ITEMS: Record = { screen: [ { id: 'mock-s1', itemNo: 1, itemCode: 'KWWS03', itemName: '와이어', floor: '1층', code: 'FSS-01', width: 8260, height: 8350, quantity: 2, processType: 'screen', cuttingInfo: { width: 1210, sheets: 8 }, steps: [ { id: 's1-1', name: '자재투입', isMaterialInput: true, isCompleted: true }, { id: 's1-2', name: '절단', isMaterialInput: false, isCompleted: true }, { id: 's1-3', name: '미싱', isMaterialInput: false, isCompleted: false }, { id: 's1-4', name: '중간검사', isMaterialInput: false, isInspection: true, isCompleted: false }, { id: 's1-5', name: '포장완료', isMaterialInput: false, isCompleted: false }, ], materialInputs: [ { id: 'm1', lotNo: 'LOT-2026-001', itemName: '스크린 원단 A', quantity: 500, unit: 'm' }, { id: 'm2', lotNo: 'LOT-2026-002', itemName: '와이어 B', quantity: 120, unit: 'EA' }, ], }, { id: 'mock-s2', itemNo: 2, itemCode: 'KWWS05', itemName: '메쉬', floor: '2층', code: 'FSS-03', width: 6400, height: 5200, quantity: 4, processType: 'screen', cuttingInfo: { width: 1600, sheets: 4 }, steps: [ { id: 's2-1', name: '자재투입', isMaterialInput: true, isCompleted: false }, { id: 's2-2', name: '절단', isMaterialInput: false, isCompleted: false }, { id: 's2-3', name: '미싱', isMaterialInput: false, isCompleted: false }, { id: 's2-4', name: '중간검사', isMaterialInput: false, isInspection: true, isCompleted: false }, { id: 's2-5', name: '포장완료', isMaterialInput: false, isCompleted: false }, ], }, { id: 'mock-s3', itemNo: 3, itemCode: 'KWWS08', itemName: '와이어(광폭)', floor: '3층', code: 'FSS-05', width: 12000, height: 4500, quantity: 1, processType: 'screen', cuttingInfo: { width: 2400, sheets: 5 }, steps: [ { id: 's3-1', name: '자재투입', isMaterialInput: true, isCompleted: true }, { id: 's3-2', name: '절단', isMaterialInput: false, isCompleted: true }, { id: 's3-3', name: '미싱', isMaterialInput: false, isCompleted: true }, { id: 's3-4', name: '중간검사', isMaterialInput: false, isInspection: true, isCompleted: false }, { id: 's3-5', name: '포장완료', isMaterialInput: false, isCompleted: false }, ], materialInputs: [ { id: 'm3', lotNo: 'LOT-2026-005', itemName: '광폭 원단', quantity: 300, unit: 'm' }, ], }, ], slat: [ { id: 'mock-l1', itemNo: 1, itemCode: 'KQTS01', itemName: '슬랫코일', floor: '1층', code: 'FSS-01', width: 8260, height: 8350, quantity: 2, processType: 'slat', slatInfo: { length: 3910, slatCount: 40, jointBar: 4 }, steps: [ { id: 'l1-1', name: '자재투입', isMaterialInput: true, isCompleted: true }, { id: 'l1-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false }, { id: 'l1-3', name: '중간검사', isMaterialInput: false, isInspection: true, isCompleted: false }, { id: 'l1-4', name: '포장완료', isMaterialInput: false, isCompleted: false }, ], materialInputs: [ { id: 'm4', lotNo: 'LOT-2026-010', itemName: '슬랫 코일 A', quantity: 200, unit: 'kg' }, ], }, { id: 'mock-l2', itemNo: 2, itemCode: 'KQTS03', itemName: '슬랫코일(광폭)', floor: '2층', code: 'FSS-02', width: 10500, height: 6200, quantity: 3, processType: 'slat', slatInfo: { length: 5200, slatCount: 55, jointBar: 6 }, steps: [ { id: 'l2-1', name: '자재투입', isMaterialInput: true, isCompleted: false }, { id: 'l2-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false }, { id: 'l2-3', name: '중간검사', isMaterialInput: false, isInspection: true, isCompleted: false }, { id: 'l2-4', name: '포장완료', isMaterialInput: false, isCompleted: false }, ], }, ], bending: [ { id: 'mock-b1', itemNo: 1, itemCode: 'KWWS03', itemName: '가이드레일', floor: '1층', code: 'FSS-01', width: 0, height: 0, quantity: 6, processType: 'bending', bendingInfo: { common: { kind: '혼합형 120X70', type: '혼합형', lengthQuantities: [{ length: 4000, quantity: 6 }, { length: 3000, quantity: 6 }], }, detailParts: [ { partName: '엘바', material: 'EGI 1.6T', barcyInfo: '16 I 75' }, { partName: '하장바', material: 'EGI 1.6T', barcyInfo: '16|75|16|75|16(A각)' }, ], }, steps: [ { id: 'b1-1', name: '자재투입', isMaterialInput: true, isCompleted: true }, { id: 'b1-2', name: '절단', isMaterialInput: false, isCompleted: true }, { id: 'b1-3', name: '절곡', isMaterialInput: false, isCompleted: false }, { id: 'b1-4', name: '중간검사', isMaterialInput: false, isInspection: true, isCompleted: false }, ], }, ], }; // 절곡 재공품 전용 목업 데이터 (토글로 전환) const MOCK_ITEMS_BENDING_WIP: WorkItemData[] = [ { id: 'mock-bw1', itemNo: 1, itemCode: 'KWWS03', itemName: '케이스 - 전면부', floor: '-', code: '-', width: 0, height: 0, quantity: 6, processType: 'bending', isWip: true, wipInfo: { specification: 'EGI 1.55T (W576)', lengthQuantity: '4,000mm X 6개' }, steps: [ { id: 'bw1-1', name: '자재투입', isMaterialInput: true, isCompleted: false }, { id: 'bw1-2', name: '절단', isMaterialInput: false, isCompleted: false }, { id: 'bw1-3', name: '중간검사', isMaterialInput: false, isInspection: true, isCompleted: false }, ], }, { id: 'mock-bw2', itemNo: 2, itemCode: 'KWWS04', itemName: '케이스 - 후면부', floor: '-', code: '-', width: 0, height: 0, quantity: 4, processType: 'bending', isWip: true, wipInfo: { specification: 'EGI 1.55T (W576)', lengthQuantity: '3,500mm X 4개' }, steps: [ { id: 'bw2-1', name: '자재투입', isMaterialInput: true, isCompleted: false }, { id: 'bw2-2', name: '절단', isMaterialInput: false, isCompleted: false }, { id: 'bw2-3', name: '중간검사', isMaterialInput: false, isInspection: true, isCompleted: false }, ], }, { id: 'mock-bw3', itemNo: 3, itemCode: 'KWWS05', itemName: '하단마감재', floor: '-', code: '-', width: 0, height: 0, quantity: 10, processType: 'bending', isWip: true, wipInfo: { specification: 'EGI 1.2T (W400)', lengthQuantity: '2,800mm X 10개' }, steps: [ { id: 'bw3-1', name: '자재투입', isMaterialInput: true, isCompleted: false }, { id: 'bw3-2', name: '절단', isMaterialInput: false, isCompleted: false }, { id: 'bw3-3', name: '중간검사', isMaterialInput: false, isInspection: true, isCompleted: false }, ], }, ]; // 슬랫 조인트바 전용 목업 데이터 (토글로 전환) const MOCK_ITEMS_SLAT_JOINTBAR: WorkItemData[] = [ { id: 'mock-jb1', itemNo: 1, itemCode: 'KQJB01', itemName: '조인트바 A', floor: '1층', code: 'FSS-01', width: 0, height: 0, quantity: 8, processType: 'slat', isJointBar: true, slatJointBarInfo: { specification: 'EGI 1.6T', length: 3910, quantity: 8 }, steps: [ { id: 'jb1-1', name: '자재투입', isMaterialInput: true, isCompleted: true }, { id: 'jb1-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false }, { id: 'jb1-3', name: '중간검사', isMaterialInput: false, isInspection: true, isCompleted: false }, { id: 'jb1-4', name: '포장완료', isMaterialInput: false, isCompleted: false }, ], materialInputs: [ { id: 'mjb1', lotNo: 'LOT-2026-020', itemName: '조인트바 코일', quantity: 100, unit: 'kg' }, ], }, { id: 'mock-jb2', itemNo: 2, itemCode: 'KQJB02', itemName: '조인트바 B', floor: '2층', code: 'FSS-02', width: 0, height: 0, quantity: 12, processType: 'slat', isJointBar: true, slatJointBarInfo: { specification: 'EGI 1.6T', length: 5200, quantity: 12 }, steps: [ { id: 'jb2-1', name: '자재투입', isMaterialInput: true, isCompleted: false }, { id: 'jb2-2', name: '포밍/절단', isMaterialInput: false, isCompleted: false }, { id: 'jb2-3', name: '중간검사', isMaterialInput: false, isInspection: true, isCompleted: false }, { id: 'jb2-4', name: '포장완료', isMaterialInput: false, isCompleted: false }, ], }, ]; // 사이드바 작업지시 목업 데이터 interface SidebarOrder { id: string; siteName: string; date: string; quantity: number; shutterCount: number; priority: 'urgent' | 'priority' | 'normal'; subType?: 'slat' | 'jointbar' | 'bending' | 'wip'; } // 스크린: subType 없음 / 슬랫: slat|jointbar / 절곡: bending|wip const MOCK_SIDEBAR_ORDERS: Record = { screen: [ { id: 'order-s1', siteName: '현장명', date: '2024-09-24', quantity: 7, shutterCount: 5, priority: 'urgent' }, { id: 'order-s2', siteName: '현장명', date: '2024-09-24', quantity: 7, shutterCount: 5, priority: 'priority' }, { id: 'order-s3', siteName: '현장명', date: '2024-09-24', quantity: 7, shutterCount: 5, priority: 'normal' }, { id: 'order-s4', siteName: '현장명', date: '2024-09-24', quantity: 7, shutterCount: 5, priority: 'normal' }, ], slat: [ { id: 'order-l1', siteName: '현장명A', date: '2024-09-24', quantity: 7, shutterCount: 5, priority: 'urgent', subType: 'slat' }, { id: 'order-l2', siteName: '현장명B', date: '2024-09-24', quantity: 3, shutterCount: 2, priority: 'priority', subType: 'jointbar' }, { id: 'order-l3', siteName: '현장명C', date: '2024-09-24', quantity: 5, shutterCount: 4, priority: 'normal', subType: 'slat' }, { id: 'order-l4', siteName: '현장명D', date: '2024-09-24', quantity: 4, shutterCount: 3, priority: 'normal', subType: 'jointbar' }, ], bending: [ { id: 'order-b1', siteName: '현장명A', date: '2024-09-24', quantity: 7, shutterCount: 5, priority: 'urgent', subType: 'bending' }, { id: 'order-b2', siteName: '현장명B', date: '2024-09-24', quantity: 3, shutterCount: 2, priority: 'priority', subType: 'wip' }, { id: 'order-b3', siteName: '현장명C', date: '2024-09-24', quantity: 5, shutterCount: 4, priority: 'normal', subType: 'bending' }, { id: 'order-b4', siteName: '현장명D', date: '2024-09-24', quantity: 4, shutterCount: 3, priority: 'normal', subType: 'wip' }, ], }; const SUB_TYPE_TAGS: Record = { slat: { label: '슬랫', className: 'bg-blue-100 text-blue-700' }, jointbar: { label: '조인트바', className: 'bg-purple-100 text-purple-700' }, bending: { label: '절곡', className: 'bg-amber-100 text-amber-700' }, wip: { label: '재공품', className: 'bg-orange-100 text-orange-700' }, }; const PRIORITY_GROUPS = [ { key: 'urgent' as const, label: '긴급', color: 'text-red-600' }, { key: 'priority' as const, label: '우선', color: 'text-orange-600' }, { key: 'normal' as const, label: '일반', color: 'text-gray-600' }, ]; // 하드코딩된 공정별 단계 폴백 const PROCESS_STEPS: Record = { screen: [ { name: '자재투입', isMaterialInput: true }, { name: '절단', isMaterialInput: false }, { name: '미싱', isMaterialInput: false }, { name: '중간검사', isMaterialInput: false, isInspection: true }, { name: '포장완료', isMaterialInput: false }, ], slat: [ { name: '자재투입', isMaterialInput: true }, { name: '포밍/절단', isMaterialInput: false }, { name: '중간검사', isMaterialInput: false, isInspection: true }, { name: '포장완료', isMaterialInput: false }, ], bending: [ { name: '자재투입', isMaterialInput: true }, { name: '절단', isMaterialInput: false }, { name: '절곡', isMaterialInput: false }, { name: '중간검사', isMaterialInput: false, isInspection: true }, ], bending_wip: [ { name: '자재투입', isMaterialInput: true }, { name: '절단', isMaterialInput: false }, { name: '중간검사', isMaterialInput: false, isInspection: true }, ], }; export default function WorkerScreen() { // ===== 상태 관리 ===== const { sidebarCollapsed } = useMenuStore(); const [workOrders, setWorkOrders] = useState([]); const [isLoading, setIsLoading] = useState(true); const [activeTab, setActiveTab] = useState(''); const [bendingSubMode, setBendingSubMode] = useState<'normal' | 'wip'>('normal'); const [slatSubMode, setSlatSubMode] = useState<'normal' | 'jointbar'>('normal'); // 작업 정보 const [departmentId, setDepartmentId] = useState(''); const [productionManagerId, setProductionManagerId] = useState(''); const [productionDate, setProductionDate] = useState(''); // 좌측 사이드바 const [selectedSidebarOrderId, setSelectedSidebarOrderId] = useState(''); const [isSidebarOpen, setIsSidebarOpen] = useState(false); // 공정별 step 완료 상태: { [itemId-stepName]: boolean } const [stepCompletionMap, setStepCompletionMap] = useState>({}); // 데이터 로드 const loadData = useCallback(async () => { setIsLoading(true); try { const result = await getMyWorkOrders(); if (result.success) { setWorkOrders(result.data); } else { toast.error(result.error || '작업 목록 조회에 실패했습니다.'); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[WorkerScreen] loadData error:', error); toast.error('데이터 로드 중 오류가 발생했습니다.'); } finally { setIsLoading(false); } }, []); useEffect(() => { loadData(); }, [loadData]); // PC에서 사이드바 sticky 동작을 위해 main의 overflow 임시 해제 useEffect(() => { const mainEl = document.querySelector('main'); if (!mainEl) return; const original = mainEl.style.overflow; mainEl.style.overflow = 'visible'; return () => { mainEl.style.overflow = original; }; }, []); // 모달/다이얼로그 상태 const [selectedOrder, setSelectedOrder] = useState(null); const [isCompletionDialogOpen, setIsCompletionDialogOpen] = useState(false); const [isMaterialModalOpen, setIsMaterialModalOpen] = useState(false); const [isWorkLogModalOpen, setIsWorkLogModalOpen] = useState(false); const [isInspectionModalOpen, setIsInspectionModalOpen] = useState(false); const [isIssueReportModalOpen, setIsIssueReportModalOpen] = useState(false); const [isInspectionInputModalOpen, setIsInspectionInputModalOpen] = useState(false); // 공정의 중간검사 설정 const [currentInspectionSetting, setCurrentInspectionSetting] = useState(); // 중간검사 체크 상태 관리: { [itemId]: boolean } const [inspectionCheckedMap, setInspectionCheckedMap] = useState>({}); // 체크된 검사 항목 수 계산 const checkedInspectionCount = useMemo(() => { return Object.values(inspectionCheckedMap).filter(Boolean).length; }, [inspectionCheckedMap]); // 중간검사 완료 데이터 관리 const [inspectionDataMap, setInspectionDataMap] = useState>(new Map()); // 전량완료 흐름 상태 const [isCompletionFlow, setIsCompletionFlow] = useState(false); const [isCompletionResultOpen, setIsCompletionResultOpen] = useState(false); const [completionLotNo, setCompletionLotNo] = useState(''); // 투입된 자재 관리 (orderId -> MaterialInput[]) const [inputMaterialsMap, setInputMaterialsMap] = useState>( new Map() ); // 완료 토스트 상태 const [toastInfo, setToastInfo] = useState(null); // 공정 목록 캐시 const [processListCache, setProcessListCache] = useState([]); // 공정 목록 조회 (최초 1회) useEffect(() => { const fetchProcessList = async () => { try { const result = await getProcessList({ size: 100 }); if (result.success && result.data?.items) { setProcessListCache(result.data.items); } } catch (error) { console.error('Failed to fetch process list:', error); } }; fetchProcessList(); }, []); // 활성 공정 목록 (탭용) - 공정관리에서 등록된 활성 공정만 const processTabs = useMemo(() => { return processListCache.filter((p) => p.status === '사용중'); }, [processListCache]); // 공정 목록 로드 후 첫 번째 공정을 기본 선택 useEffect(() => { if (processTabs.length > 0 && !activeTab) { setActiveTab(processTabs[0].id); } }, [processTabs, activeTab]); // 선택된 공정의 ProcessTab 키 (mock 데이터 및 기존 로직 호환용) const activeProcessTabKey: ProcessTab = useMemo(() => { const process = processListCache.find((p) => p.id === activeTab); if (!process) return 'screen'; const name = process.processName.toLowerCase(); if (name.includes('스크린')) return 'screen'; if (name.includes('슬랫')) return 'slat'; if (name.includes('절곡')) return 'bending'; return 'screen'; }, [activeTab, processListCache]); // activeTab 변경 시 해당 공정의 중간검사 설정 조회 useEffect(() => { if (processListCache.length === 0 || !activeTab) return; const matchedProcess = processListCache.find((p) => p.id === activeTab); if (matchedProcess?.steps) { const inspectionStep = matchedProcess.steps.find( (step) => step.needsInspection && step.inspectionSetting ); setCurrentInspectionSetting(inspectionStep?.inspectionSetting); } else { setCurrentInspectionSetting(undefined); } }, [activeTab, processListCache]); // ===== 탭별 필터링된 작업 ===== const filteredWorkOrders = useMemo(() => { const selectedProcess = processListCache.find((p) => p.id === activeTab); if (!selectedProcess) return workOrders; const selectedName = selectedProcess.processName.toLowerCase(); return workOrders.filter((order) => { const orderProcessName = (order.processName || '').toLowerCase(); return orderProcessName.includes(selectedName) || selectedName.includes(orderProcessName); }); }, [workOrders, activeTab, processListCache]); // ===== API WorkOrders → SidebarOrder 변환 ===== const apiSidebarOrders: SidebarOrder[] = useMemo(() => { return filteredWorkOrders.map((wo) => ({ id: wo.id, siteName: wo.projectName || wo.client || '-', date: wo.dueDate || (wo.createdAt ? wo.createdAt.slice(0, 10) : '-'), quantity: wo.quantity, shutterCount: wo.shutterCount || 0, priority: (wo.isUrgent ? 'urgent' : (wo.priority <= 3 ? 'priority' : 'normal')) as SidebarOrder['priority'], })); }, [filteredWorkOrders]); // ===== 탭 변경/데이터 로드 시 최상위 우선순위 작업 자동 선택 ===== useEffect(() => { if (isLoading) return; const allOrders: SidebarOrder[] = [...apiSidebarOrders, ...MOCK_SIDEBAR_ORDERS[activeProcessTabKey]]; // 우선순위 순서: urgent → priority → normal for (const group of PRIORITY_GROUPS) { const first = allOrders.find((o) => o.priority === group.key); if (first) { setSelectedSidebarOrderId(first.id); // subType에 따라 서브모드도 설정 if (activeProcessTabKey === 'slat') { setSlatSubMode(first.subType === 'jointbar' ? 'jointbar' : 'normal'); } if (activeProcessTabKey === 'bending') { setBendingSubMode(first.subType === 'wip' ? 'wip' : 'normal'); } return; } } }, [isLoading, apiSidebarOrders, activeProcessTabKey]); // ===== 통계 계산 (탭별) ===== const stats: WorkerStats = useMemo(() => { return { assigned: filteredWorkOrders.length, inProgress: filteredWorkOrders.filter((o) => o.status === 'inProgress').length, completed: 0, urgent: filteredWorkOrders.filter((o) => o.isUrgent).length, }; }, [filteredWorkOrders]); // ===== 선택된 작업지시의 개소별 WorkItemData 변환 + 목업 ===== const workItems: WorkItemData[] = useMemo(() => { const selectedOrder = filteredWorkOrders.find((wo) => wo.id === selectedSidebarOrderId); const stepsKey = (activeProcessTabKey === 'bending' && bendingSubMode === 'wip') ? 'bending_wip' : activeProcessTabKey; const stepsTemplate = PROCESS_STEPS[stepsKey]; const apiItems: WorkItemData[] = []; if (selectedOrder && selectedOrder.nodeGroups && selectedOrder.nodeGroups.length > 0) { // 개소별로 WorkItemData 생성 selectedOrder.nodeGroups.forEach((group, index) => { const nodeKey = group.nodeId != null ? String(group.nodeId) : `unassigned-${index}`; const steps: WorkStepData[] = stepsTemplate.map((st, si) => { const stepKey = `${selectedOrder.id}-${nodeKey}-${st.name}`; return { id: `${selectedOrder.id}-${nodeKey}-step-${si}`, name: st.name, isMaterialInput: st.isMaterialInput, isInspection: st.isInspection, isCompleted: stepCompletionMap[stepKey] || false, }; }); // 개소 내 아이템 이름 요약 const itemNames = group.items.map((it) => it.itemName).filter(Boolean); const itemSummary = itemNames.length > 0 ? itemNames.join(', ') : '-'; apiItems.push({ id: `${selectedOrder.id}-node-${nodeKey}`, itemNo: index + 1, itemCode: selectedOrder.orderNo || '-', itemName: `${group.nodeName} : ${itemSummary}`, floor: '-', code: '-', width: 0, height: 0, quantity: group.totalQuantity, processType: activeProcessTabKey, steps, materialInputs: [], }); }); } else if (selectedOrder) { // nodeGroups가 없는 경우 폴백 (단일 항목) const steps: WorkStepData[] = stepsTemplate.map((st, si) => { const stepKey = `${selectedOrder.id}-${st.name}`; return { id: `${selectedOrder.id}-step-${si}`, name: st.name, isMaterialInput: st.isMaterialInput, isInspection: st.isInspection, isCompleted: stepCompletionMap[stepKey] || false, }; }); apiItems.push({ id: selectedOrder.id, itemNo: 1, itemCode: selectedOrder.orderNo || '-', itemName: selectedOrder.productName || '-', floor: '-', code: '-', width: 0, height: 0, quantity: selectedOrder.quantity || 0, processType: activeProcessTabKey, steps, materialInputs: [], }); } // 목업 데이터 합치기 (API 데이터 뒤에 번호 이어서) // 절곡 탭에서 재공품 서브모드면 WIP 전용 목업 사용 // 슬랫 탭에서 조인트바 서브모드면 조인트바 전용 목업 사용 const baseMockItems = (activeProcessTabKey === 'bending' && bendingSubMode === 'wip') ? MOCK_ITEMS_BENDING_WIP : (activeProcessTabKey === 'slat' && slatSubMode === 'jointbar') ? MOCK_ITEMS_SLAT_JOINTBAR : MOCK_ITEMS[activeProcessTabKey]; const mockItems = baseMockItems.map((item, i) => ({ ...item, itemNo: apiItems.length + i + 1, steps: item.steps.map((step) => { const stepKey = `${item.id}-${step.name}`; return { ...step, isCompleted: stepCompletionMap[stepKey] ?? step.isCompleted, }; }), })); return [...apiItems, ...mockItems]; }, [filteredWorkOrders, selectedSidebarOrderId, activeProcessTabKey, stepCompletionMap, bendingSubMode, slatSubMode]); // ===== 수주 정보 (사이드바 선택 항목 기반) ===== const orderInfo = useMemo(() => { // 1. 선택된 API 작업지시에서 찾기 const apiOrder = filteredWorkOrders.find((wo) => wo.id === selectedSidebarOrderId); if (apiOrder) { return { orderDate: apiOrder.createdAt ? new Date(apiOrder.createdAt).toLocaleDateString('ko-KR') : '-', salesOrderNo: apiOrder.salesOrderNo || '-', siteName: apiOrder.projectName || '-', client: apiOrder.client || '-', salesManager: apiOrder.assignees?.[0] || '-', managerPhone: '-', shippingDate: apiOrder.dueDate ? new Date(apiOrder.dueDate).toLocaleDateString('ko-KR') : '-', }; } // 2. 목업 사이드바에서 찾기 const mockOrder = MOCK_SIDEBAR_ORDERS[activeProcessTabKey].find((o) => o.id === selectedSidebarOrderId); if (mockOrder) { return { orderDate: mockOrder.date, salesOrderNo: 'SO-2024-0001', siteName: mockOrder.siteName, client: '-', salesManager: '-', managerPhone: '-', shippingDate: '-', }; } // 3. 폴백: 첫 번째 작업 const first = filteredWorkOrders[0]; if (!first) return null; return { orderDate: first.createdAt ? new Date(first.createdAt).toLocaleDateString('ko-KR') : '-', salesOrderNo: first.salesOrderNo || '-', siteName: first.projectName || '-', client: first.client || '-', salesManager: first.assignees?.[0] || '-', managerPhone: '-', shippingDate: first.dueDate ? new Date(first.dueDate).toLocaleDateString('ko-KR') : '-', }; }, [filteredWorkOrders, selectedSidebarOrderId, activeProcessTabKey]); // ===== 핸들러 ===== // pill 클릭 핸들러 const handleStepClick = useCallback( (itemId: string, step: WorkStepData) => { if (step.isMaterialInput) { // 자재투입 → 자재 투입 모달 열기 const order = workOrders.find((o) => o.id === itemId || itemId.startsWith(`${o.id}-node-`)); if (order) { setSelectedOrder(order); setIsMaterialModalOpen(true); } else { // 목업 아이템인 경우 합성 WorkOrder 생성 const mockItem = workItems.find((item) => item.id === itemId); if (mockItem) { const syntheticOrder: WorkOrder = { id: mockItem.id, orderNo: mockItem.itemCode, productName: mockItem.itemName, processCode: mockItem.processType, processName: PROCESS_TAB_LABELS[mockItem.processType], client: '-', projectName: '-', assignees: [], quantity: mockItem.quantity, dueDate: '', priority: 5, status: 'waiting', isUrgent: false, isDelayed: false, createdAt: '', }; setSelectedOrder(syntheticOrder); setIsMaterialModalOpen(true); } } } else { // 기타 → 완료/미완료 토글 const stepKey = `${itemId}-${step.name}`; setStepCompletionMap((prev) => ({ ...prev, [stepKey]: !prev[stepKey], })); } }, [workOrders, workItems] ); // 자재 수정 핸들러 const handleEditMaterial = useCallback( (itemId: string, material: MaterialListItem) => { console.log('[WorkerScreen] editMaterial:', itemId, material); // 추후 구현 }, [] ); // 자재 삭제 핸들러 const handleDeleteMaterial = useCallback( (itemId: string, materialId: string) => { console.log('[WorkerScreen] deleteMaterial:', itemId, materialId); // 추후 구현 }, [] ); // 자재 저장 핸들러 const handleSaveMaterials = useCallback((orderId: string, materials: MaterialInput[]) => { setInputMaterialsMap((prev) => { const next = new Map(prev); next.set(orderId, materials); return next; }); // 자재투입 step 완료로 마킹 const stepKey = `${orderId}-자재투입`; setStepCompletionMap((prev) => ({ ...prev, [stepKey]: true, })); }, []); // 완료 확인 → MaterialInputModal 열기 const handleCompletionConfirm = useCallback(() => { setIsCompletionFlow(true); setIsMaterialModalOpen(true); }, []); // MaterialInputModal 완료 후 → 작업 완료 결과 팝업 const handleWorkCompletion = useCallback(() => { if (!selectedOrder) return; const lotNo = `KD-SA-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01`; setCompletionLotNo(lotNo); setIsCompletionResultOpen(true); setIsCompletionFlow(false); }, [selectedOrder]); // 완료 결과 팝업 확인 → API 완료 처리 const handleCompletionResultConfirm = useCallback(async () => { if (!selectedOrder) return; try { const materials = inputMaterialsMap.get(selectedOrder.id); const result = await completeWorkOrder( selectedOrder.id, materials?.map((m) => ({ materialId: parseInt(m.id), quantity: m.inputQuantity, lotNo: m.lotNo, })) ); if (result.success) { toast.success('작업이 완료되었습니다.'); setInputMaterialsMap((prev) => { const next = new Map(prev); next.delete(selectedOrder.id); return next; }); setWorkOrders((prev) => prev.filter((o) => o.id !== selectedOrder.id)); } else { toast.error(result.error || '작업 완료 처리에 실패했습니다.'); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[WorkerScreen] handleCompletionResultConfirm error:', error); toast.error('작업 완료 중 오류가 발생했습니다.'); } finally { setSelectedOrder(null); setCompletionLotNo(''); } }, [selectedOrder, inputMaterialsMap]); // 하단 버튼용 합성 WorkOrder (API 데이터 없을 때 목업 폴백) const getTargetOrder = useCallback((): WorkOrder | null => { const apiTarget = filteredWorkOrders[0]; if (apiTarget) return apiTarget; // 목업 아이템으로 폴백 const mockItem = workItems[0]; if (!mockItem) return null; return { id: mockItem.id, orderNo: mockItem.itemCode, productName: mockItem.itemName, processCode: mockItem.processType, processName: PROCESS_TAB_LABELS[mockItem.processType], client: '-', projectName: '-', assignees: [], quantity: mockItem.quantity, dueDate: '', priority: 5, status: 'waiting', isUrgent: false, isDelayed: false, createdAt: '', }; }, [filteredWorkOrders, workItems]); // 중간검사 버튼 클릭 핸들러 - 바로 모달 열기 const handleInspectionClick = useCallback((itemId: string) => { // 해당 아이템 찾기 const item = workItems.find((w) => w.id === itemId); if (item) { // 합성 WorkOrder 생성 const syntheticOrder: WorkOrder = { id: item.id, orderNo: item.itemCode, productName: item.itemName, processCode: item.processType, processName: PROCESS_TAB_LABELS[item.processType], client: '-', projectName: '-', assignees: [], quantity: item.quantity, dueDate: '', priority: 5, status: 'waiting', isUrgent: false, isDelayed: false, createdAt: '', }; setSelectedOrder(syntheticOrder); setIsInspectionInputModalOpen(true); } }, [workItems]); // 현재 공정에 맞는 중간검사 타입 결정 const getInspectionProcessType = useCallback((): InspectionProcessType => { if (activeProcessTabKey === 'bending' && bendingSubMode === 'wip') { return 'bending_wip'; } if (activeProcessTabKey === 'slat' && slatSubMode === 'jointbar') { return 'slat_jointbar'; } return activeProcessTabKey as InspectionProcessType; }, [activeProcessTabKey, bendingSubMode, slatSubMode]); // 하단 버튼 핸들러 const handleWorkLog = useCallback(() => { const target = getTargetOrder(); if (target) { setSelectedOrder(target); setIsWorkLogModalOpen(true); } else { toast.error('표시할 작업이 없습니다.'); } }, [getTargetOrder]); const handleInspection = useCallback(() => { const target = getTargetOrder(); if (target) { setSelectedOrder(target); setIsInspectionModalOpen(true); } else { toast.error('표시할 작업이 없습니다.'); } }, [getTargetOrder]); // 중간검사 완료 핸들러 const handleInspectionComplete = useCallback((data: InspectionData) => { if (selectedOrder) { setInspectionDataMap((prev) => { const next = new Map(prev); next.set(selectedOrder.id, data); return next; }); toast.success('중간검사가 완료되었습니다.'); } }, [selectedOrder]); // ===== 재공품 감지 ===== const hasWipItems = useMemo(() => { return activeProcessTabKey === 'bending' && workItems.some(item => item.isWip); }, [activeProcessTabKey, workItems]); // ===== 조인트바 감지 ===== const hasJointBarItems = useMemo(() => { return activeProcessTabKey === 'slat' && slatSubMode === 'jointbar'; }, [activeProcessTabKey, slatSubMode]); // 재공품 통합 문서 (작업일지 + 중간검사) 핸들러 const handleWipInspection = useCallback(() => { const target = getTargetOrder(); if (target) { setSelectedOrder(target); setIsInspectionModalOpen(true); } else { toast.error('표시할 작업이 없습니다.'); } }, [getTargetOrder]); return (
{/* 완료 토스트 */} {toastInfo && } {/* 헤더 */}

작업자 화면

작업을 관리합니다

{/* 공정별 탭 (공정관리 API 기반 동적 생성) */} setActiveTab(v)} > {processTabs.length > 0 ? ( processTabs.map((proc) => ( {proc.processName} )) ) : ( (['screen', 'slat', 'bending'] as ProcessTab[]).map((tab) => ( {PROCESS_TAB_LABELS[tab]} )) )} {(processTabs.length > 0 ? processTabs.map((p) => p.id) : ['screen', 'slat', 'bending'] ).map((tabValue) => ( {/* 모바일: 사이드바 토글 */}
{isSidebarOpen && (
{ setSelectedSidebarOrderId(id); if (subType === 'slat' || subType === 'jointbar') setSlatSubMode(subType === 'jointbar' ? 'jointbar' : 'normal'); if (subType === 'bending' || subType === 'wip') setBendingSubMode(subType === 'wip' ? 'wip' : 'normal'); setIsSidebarOpen(false); }} />
)}
{/* 2-패널 레이아웃 */}
{/* 좌측 사이드바 - 데스크탑만 (스크롤 따라다님) */} {/* 우측 메인 패널 */}
{/* 상태 카드 */}
} variant="default" /> } variant="blue" /> } variant="green" /> } variant="red" />
{/* 수주 정보 */}

수주 정보

{/* 작업 정보 - 부서 필드 추가 */}

작업 정보

setProductionDate(date)} />
{/* 작업 목록 */}

작업 목록

{isLoading ? ( ) : workItems.length === 0 ? ( 해당 공정에 배정된 작업이 없습니다. ) : (
{(() => { const apiCount = workItems.filter((i) => !i.id.startsWith('mock-')).length; return apiCount > 0 ? ( 실제 데이터 ({apiCount}건) ) : null; })()} {workItems.map((item, index) => { const isFirstMock = item.id.startsWith('mock-') && (index === 0 || !workItems[index - 1]?.id.startsWith('mock-')); return (
{isFirstMock && (
{workItems.some((i) => !i.id.startsWith('mock-')) &&
} 목업 데이터
)}
); })}
)}
))}
{/* 하단 고정 버튼 */}
{hasWipItems ? ( ) : ( <> )}
{/* 모달/다이얼로그 */} ); } // ===== 사이드바 컨텐츠 ===== interface SidebarContentProps { tab: ProcessTab; selectedOrderId: string; apiOrders: SidebarOrder[]; onSelectOrder: (id: string, subType?: SidebarOrder['subType']) => void; } function SidebarContent({ tab, selectedOrderId, onSelectOrder, apiOrders, }: SidebarContentProps) { const mockOrders = MOCK_SIDEBAR_ORDERS[tab]; const renderOrders = (orders: SidebarOrder[]) => ( <> {PRIORITY_GROUPS.map((group) => { const groupOrders = orders.filter((o) => o.priority === group.key); if (groupOrders.length === 0) return null; return (

{group.label}

{groupOrders.map((order) => { const tag = order.subType ? SUB_TYPE_TAGS[order.subType] : null; return ( ); })}
); })} ); return (

수주 목록

{/* API 실제 데이터 */} {apiOrders.length > 0 && (
실제 데이터 ({apiOrders.length}건) {renderOrders(apiOrders)}
)} {/* 구분선 */} {apiOrders.length > 0 && mockOrders.length > 0 && (
)} {/* 목업 데이터 */} {mockOrders.length > 0 && (
목업 데이터 {renderOrders(mockOrders)}
)}
); } // ===== 하위 컴포넌트 ===== interface StatCardProps { title: string; value: number; icon: React.ReactNode; variant: 'default' | 'blue' | 'green' | 'red'; } function StatCard({ title, value, icon, variant }: StatCardProps) { const variantClasses = { default: 'bg-gray-50 text-gray-700', blue: 'bg-blue-50 text-blue-700', green: 'bg-green-50 text-green-700', red: 'bg-red-50 text-red-700', }; return (
{title} {icon}

{value}

); } interface InfoFieldProps { label: string; value?: string; } function InfoField({ label, value }: InfoFieldProps) { return (

{label}

{value || '-'}

); }