'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('screen'); 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('order-1'); 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(); }, []); // activeTab 변경 시 해당 공정의 중간검사 설정 조회 useEffect(() => { if (processListCache.length === 0) return; // activeTab에 해당하는 공정 찾기 const tabToProcessName: Record = { screen: ['스크린', 'screen'], slat: ['슬랫', 'slat'], bending: ['절곡', 'bending'], }; const matchNames = tabToProcessName[activeTab] || []; const matchedProcess = processListCache.find((p) => matchNames.some((name) => p.processName.toLowerCase().includes(name.toLowerCase())) ); if (matchedProcess?.steps) { // 검사 단계에서 inspectionSetting 찾기 const inspectionStep = matchedProcess.steps.find( (step) => step.needsInspection && step.inspectionSetting ); setCurrentInspectionSetting(inspectionStep?.inspectionSetting); } else { setCurrentInspectionSetting(undefined); } }, [activeTab, processListCache]); // ===== 탭별 필터링된 작업 ===== const filteredWorkOrders = useMemo(() => { // process_type 기반 필터링 return workOrders.filter((order) => { // WorkOrder의 processCode/processName으로 매칭 const processName = (order.processName || '').toLowerCase(); switch (activeTab) { case 'screen': return processName.includes('스크린') || processName === 'screen'; case 'slat': return processName.includes('슬랫') || processName === 'slat'; case 'bending': return processName.includes('절곡') || processName === 'bending'; default: return true; } }); }, [workOrders, activeTab]); // ===== 통계 계산 (탭별) ===== 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]); // ===== WorkOrder → WorkItemData 변환 + 목업 ===== const workItems: WorkItemData[] = useMemo(() => { const apiItems: WorkItemData[] = filteredWorkOrders.map((order, index) => { const stepsKey = (activeTab === 'bending' && bendingSubMode === 'wip') ? 'bending_wip' : activeTab; const stepsTemplate = PROCESS_STEPS[stepsKey]; const steps: WorkStepData[] = stepsTemplate.map((st, si) => { const stepKey = `${order.id}-${st.name}`; return { id: `${order.id}-step-${si}`, name: st.name, isMaterialInput: st.isMaterialInput, isInspection: st.isInspection, isCompleted: stepCompletionMap[stepKey] || false, }; }); return { id: order.id, itemNo: index + 1, itemCode: order.orderNo || '-', itemName: order.productName || '-', floor: '-', code: '-', width: 0, height: 0, quantity: order.quantity || 0, processType: activeTab, steps, materialInputs: [], }; }); // 목업 데이터 합치기 (API 데이터 뒤에 번호 이어서) // 절곡 탭에서 재공품 서브모드면 WIP 전용 목업 사용 // 슬랫 탭에서 조인트바 서브모드면 조인트바 전용 목업 사용 const baseMockItems = (activeTab === 'bending' && bendingSubMode === 'wip') ? MOCK_ITEMS_BENDING_WIP : (activeTab === 'slat' && slatSubMode === 'jointbar') ? MOCK_ITEMS_SLAT_JOINTBAR : MOCK_ITEMS[activeTab]; 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, activeTab, stepCompletionMap, bendingSubMode, slatSubMode]); // ===== 수주 정보 (첫 번째 작업 기반 표시) ===== const orderInfo = useMemo(() => { const first = filteredWorkOrders[0]; if (!first) return null; return { orderDate: first.createdAt ? new Date(first.createdAt).toLocaleDateString('ko-KR') : '-', lotNo: '-', siteName: first.projectName || '-', client: first.client || '-', salesManager: first.assignees?.[0] || '-', managerPhone: '-', shippingDate: first.dueDate ? new Date(first.dueDate).toLocaleDateString('ko-KR') : '-', }; }, [filteredWorkOrders]); // ===== 핸들러 ===== // pill 클릭 핸들러 const handleStepClick = useCallback( (itemId: string, step: WorkStepData) => { if (step.isMaterialInput) { // 자재투입 → 자재 투입 모달 열기 const order = workOrders.find((o) => o.id === itemId); 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 (activeTab === 'bending' && bendingSubMode === 'wip') { return 'bending_wip'; } if (activeTab === 'slat' && slatSubMode === 'jointbar') { return 'slat_jointbar'; } return activeTab as InspectionProcessType; }, [activeTab, 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 activeTab === 'bending' && workItems.some(item => item.isWip); }, [activeTab, workItems]); // ===== 조인트바 감지 ===== const hasJointBarItems = useMemo(() => { return activeTab === 'slat' && slatSubMode === 'jointbar'; }, [activeTab, slatSubMode]); // 재공품 통합 문서 (작업일지 + 중간검사) 핸들러 const handleWipInspection = useCallback(() => { const target = getTargetOrder(); if (target) { setSelectedOrder(target); setIsInspectionModalOpen(true); } else { toast.error('표시할 작업이 없습니다.'); } }, [getTargetOrder]); return (
{/* 완료 토스트 */} {toastInfo && } {/* 헤더 */}

작업자 화면

작업을 관리합니다

{/* 공정별 탭 */} setActiveTab(v as ProcessTab)} > {(['screen', 'slat', 'bending'] as ProcessTab[]).map((tab) => ( {PROCESS_TAB_LABELS[tab]} ))} {(['screen', 'slat', 'bending'] as ProcessTab[]).map((tab) => ( {/* 모바일: 사이드바 토글 */}
{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 ? ( 해당 공정에 배정된 작업이 없습니다. ) : (
{workItems.map((item) => ( ))}
)}
))}
{/* 하단 고정 버튼 */}
{hasWipItems ? ( ) : ( <> )}
{/* 모달/다이얼로그 */}
); } // ===== 사이드바 컨텐츠 ===== interface SidebarContentProps { tab: ProcessTab; selectedOrderId: string; onSelectOrder: (id: string, subType?: SidebarOrder['subType']) => void; } function SidebarContent({ tab, selectedOrderId, onSelectOrder, }: SidebarContentProps) { const orders = MOCK_SIDEBAR_ORDERS[tab]; return (

작업 목록

{/* 우선순위별 작업지시 카드 + 태그 */} {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 ( ); })}
); })}
); } // ===== 하위 컴포넌트 ===== 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 || '-'}

); }