From e0b2ab63e796daea337095736813c74433b255c2 Mon Sep 17 00:00:00 2001 From: byeongcheolryu Date: Tue, 23 Dec 2025 22:23:40 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20WorkerScreen=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B8=B0=ED=9A=8D=20=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WorkCard: 헤더 박스(품목명+수량), 뱃지 영역, 담당자 정보, 버튼 레이아웃 개선 - ProcessDetailSection: 자재 투입 섹션, 공정 단계 뱃지, 검사 요청 AlertDialog 추가 - MaterialInputModal: FIFO 순위 설명, 테이블 형태 자재 목록, 중복 닫기 버튼 제거 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../WorkerScreen/MaterialInputModal.tsx | 241 +++++----- .../WorkerScreen/ProcessDetailSection.tsx | 419 ++++++++++++------ .../production/WorkerScreen/WorkCard.tsx | 249 ++++++----- 3 files changed, 531 insertions(+), 378 deletions(-) diff --git a/src/components/production/WorkerScreen/MaterialInputModal.tsx b/src/components/production/WorkerScreen/MaterialInputModal.tsx index be5d82ba..5e3ca7cd 100644 --- a/src/components/production/WorkerScreen/MaterialInputModal.tsx +++ b/src/components/production/WorkerScreen/MaterialInputModal.tsx @@ -3,18 +3,16 @@ /** * 자재투입 모달 * - * - FIFO 순위 표시 - * - 자재 테이블 (BOM 기준) - * - 투입 등록 기능 + * 기획 화면에 맞춘 레이아웃: + * - FIFO 순위 설명 (1 최우선, 2 차선, 3+ 대기) + * - ① 자재 선택 (BOM 기준) 테이블 + * - 취소 / 투입 등록 버튼 (전체 너비) */ import { useState } from 'react'; -import { Package } from 'lucide-react'; import { Dialog, DialogContent, - DialogDescription, - DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; @@ -64,13 +62,9 @@ interface MaterialInputModalProps { open: boolean; onOpenChange: (open: boolean) => void; order: WorkOrder | null; - /** 전량완료 흐름에서 사용 - 투입 등록/취소 후 완료 처리 */ onComplete?: () => void; - /** 전량완료 흐름 여부 (취소 시에도 완료 처리) */ isCompletionFlow?: boolean; - /** 자재 투입 저장 콜백 */ onSaveMaterials?: (orderId: string, materials: MaterialInput[]) => void; - /** 이미 투입된 자재 목록 */ savedMaterials?: MaterialInput[]; } @@ -81,14 +75,10 @@ export function MaterialInputModal({ onComplete, isCompletionFlow = false, onSaveMaterials, - savedMaterials = [], }: MaterialInputModalProps) { const [selectedMaterials, setSelectedMaterials] = useState>(new Set()); const [materials] = useState(MOCK_MATERIALS); - // 이미 투입된 자재가 있으면 선택 상태로 초기화 - const hasSavedMaterials = savedMaterials.length > 0; - const handleToggleMaterial = (materialId: string) => { setSelectedMaterials((prev) => { const next = new Set(prev); @@ -101,144 +91,153 @@ export function MaterialInputModal({ }); }; - // 자재 투입 등록 + // 투입 등록 const handleSubmit = () => { if (!order) return; - // 선택된 자재 정보 추출 const selectedMaterialList = materials.filter((m) => selectedMaterials.has(m.id)); console.log('[자재투입] 저장:', order.id, selectedMaterialList); - // 자재 저장 콜백 if (onSaveMaterials) { onSaveMaterials(order.id, selectedMaterialList); } - setSelectedMaterials(new Set()); - onOpenChange(false); + resetAndClose(); - // 전량완료 흐름이면 완료 처리 if (isCompletionFlow && onComplete) { onComplete(); } }; - // 건너뛰기 (자재 없이 완료) - 전량완료 흐름에서만 사용 - const handleSkip = () => { - setSelectedMaterials(new Set()); - onOpenChange(false); - // 전량완료 흐름이면 완료 처리 - if (onComplete) { - onComplete(); - } - }; - - // 취소 (모달만 닫기) + // 취소 const handleCancel = () => { - setSelectedMaterials(new Set()); - onOpenChange(false); + resetAndClose(); }; - const getFifoRankBadge = (rank: number) => { - const colors = { - 1: 'bg-red-100 text-red-800', - 2: 'bg-orange-100 text-orange-800', - 3: 'bg-gray-100 text-gray-800', - }; - const labels = { - 1: '최우선', - 2: '차선', - 3: '대기', - }; - return ( - - {rank}위 ({labels[rank as 1 | 2 | 3] || labels[3]}) - - ); + const resetAndClose = () => { + setSelectedMaterials(new Set()); + onOpenChange(false); }; if (!order) return null; return ( - - - - - 투입자재 등록 - - - 작업지시 {order.orderNo}에 투입할 자재를 선택하세요. - + + {/* 헤더 */} + + 투입자재 등록 -
- {/* FIFO 순위 안내 */} -
- FIFO 순위: - - 1 최우선 - - - 2 차선 - - - 3+ 대기 - +
+ {/* FIFO 순위 설명 */} +
+ FIFO 순위: +
+ + + 1 + + 최우선 + + + + 2 + + 차선 + + + + 3+ + + 대기 + +
- {/* 자재 테이블 */} - {materials.length === 0 ? ( -
- 이 공정에 배정된 자재가 없습니다. -
- ) : ( - - - - 선택 - 자재코드 - 자재명 - 단위 - 현재고 - FIFO - - - - {materials.map((material) => ( - - - handleToggleMaterial(material.id)} - /> - - {material.materialCode} - {material.materialName} - {material.unit} - {material.currentStock.toLocaleString()} - {getFifoRankBadge(material.fifoRank)} - - ))} - -
- )} -
+ {/* 자재 선택 섹션 */} +
+

+ ① 자재 선택 (BOM 기준) +

- - - {isCompletionFlow && ( -
+ + {/* 버튼 영역 */} +
+ - )} - - + +
+
); -} \ No newline at end of file +} diff --git a/src/components/production/WorkerScreen/ProcessDetailSection.tsx b/src/components/production/WorkerScreen/ProcessDetailSection.tsx index 3612a8e1..9c1341ea 100644 --- a/src/components/production/WorkerScreen/ProcessDetailSection.tsx +++ b/src/components/production/WorkerScreen/ProcessDetailSection.tsx @@ -3,28 +3,38 @@ /** * 공정상세 섹션 컴포넌트 * - * WorkCard 내부에서 토글 확장되는 공정 상세 정보 - * - 자재 투입 필요 섹션 - * - 공정 단계 (5단계) - * - 각 단계별 세부 항목 + * 기획 화면에 맞춘 레이아웃: + * - 자재 투입 필요 섹션 (흰색 박스, 검은색 전체너비 버튼) + * - 공정 단계 (N단계) + N/N 완료 + * - 숫자 뱃지 + 공정명 + 검사 뱃지 + 진행률 + * - 검사 항목: 검사 요청 버튼 + * - 상세 정보: 위치, 규격, LOT, 자재 */ import { useState } from 'react'; -import { Package, CheckCircle2, Circle, AlertTriangle, MapPin, Ruler } from 'lucide-react'; +import { ChevronDown } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; import { cn } from '@/lib/utils'; import type { ProcessStep, ProcessStepItem } from './types'; -// Mock 공정 단계 데이터 +// Mock 공정 단계 데이터 (기획서 기준 8단계) const MOCK_PROCESS_STEPS: ProcessStep[] = [ { id: 'step-1', stepNo: 1, - name: '절곡판/코일 절단', + name: '자재투입', completed: 0, - total: 2, + total: 3, items: [ { id: 'item-1-1', @@ -32,71 +42,113 @@ const MOCK_PROCESS_STEPS: ProcessStep[] = [ location: '1층 1호-A', isPriority: true, spec: 'W2500 × H3000', - material: '절곡판', - lot: 'LOT-절곡-2025-001', + material: '스크린 원단', + lot: 'LOT-스크-2025-001', }, { id: 'item-1-2', itemNo: '#2', - location: '1층 1호-B', + location: '1층 2호-B', isPriority: false, - spec: 'W2000 × H2500', - material: '절곡판', - lot: 'LOT-절곡-2025-002', + spec: 'W2600 × H3120', + material: '스크린 원단', + lot: 'LOT-스크-2025-002', + }, + { + id: 'item-1-3', + itemNo: '#3', + location: '2층 3호-C', + isPriority: false, + spec: 'W2700 × H3240', + material: '스크린 원단', + lot: 'LOT-스크-2025-003', }, ], }, { id: 'step-2', stepNo: 2, - name: 'V컷팅', + name: '절단매수확인', completed: 0, - total: 2, - items: [ - { - id: 'item-2-1', - itemNo: '#1', - location: '1층 2호-A', - isPriority: false, - spec: 'V10 × L2500', - material: 'V컷팅재', - lot: 'LOT-V컷-2025-001', - }, - ], + total: 3, + items: [], }, { id: 'step-3', stepNo: 3, - name: '절곡', - completed: 0, + name: '원단 절단', + completed: 3, total: 3, - items: [ - { - id: 'item-3-1', - itemNo: '#1', - location: '2층 1호', - isPriority: true, - spec: '90° × 2회', - material: '절곡판', - lot: 'LOT-절곡-2025-001', - }, - ], + items: [], }, { id: 'step-4', stepNo: 4, - name: '중간검사', + name: '절단 Check', isInspection: true, completed: 0, - total: 1, + total: 3, items: [], }, { id: 'step-5', stepNo: 5, + name: '미싱', + completed: 1, + total: 3, + items: [ + { + id: 'item-5-1', + itemNo: '#1', + location: '1층 1호-A', + isPriority: true, + spec: 'W2500 × H3000', + material: '스크린 원단', + lot: 'LOT-스크-2025-001', + }, + { + id: 'item-5-2', + itemNo: '#2', + location: '1층 2호-B', + isPriority: false, + spec: 'W2600 × H3120', + material: '스크린 원단', + lot: 'LOT-스크-2025-002', + }, + { + id: 'item-5-3', + itemNo: '#3', + location: '2층 3호-C', + isPriority: false, + spec: 'W2700 × H3240', + material: '스크린 원단', + lot: 'LOT-스크-2025-003', + }, + ], + }, + { + id: 'step-6', + stepNo: 6, + name: '앤드락 작업', + completed: 0, + total: 3, + items: [], + }, + { + id: 'step-7', + stepNo: 7, + name: '중간검사', + isInspection: true, + completed: 0, + total: 3, + items: [], + }, + { + id: 'step-8', + stepNo: 8, name: '포장', completed: 0, - total: 1, + total: 3, items: [], }, ]; @@ -112,8 +164,11 @@ export function ProcessDetailSection({ materialRequired, onMaterialInput, }: ProcessDetailSectionProps) { - const [steps] = useState(MOCK_PROCESS_STEPS); - const [expandedSteps, setExpandedSteps] = useState>(new Set()); + const [steps, setSteps] = useState(MOCK_PROCESS_STEPS); + const [expandedSteps, setExpandedSteps] = useState>(new Set(['step-1'])); + const [inspectionDialogOpen, setInspectionDialogOpen] = useState(false); + const [inspectionStepName, setInspectionStepName] = useState(''); + const [pendingInspectionStepId, setPendingInspectionStepId] = useState(null); const totalSteps = steps.length; const completedSteps = steps.filter((s) => s.completed === s.total).length; @@ -130,39 +185,58 @@ export function ProcessDetailSection({ }); }; + // 검사 요청 핸들러 + const handleInspectionRequest = (step: ProcessStep) => { + setInspectionStepName(step.name); + setPendingInspectionStepId(step.id); + setInspectionDialogOpen(true); + }; + + // 검사 요청 확인 후 완료 처리 + const handleInspectionConfirm = () => { + if (pendingInspectionStepId) { + setSteps((prev) => + prev.map((step) => + step.id === pendingInspectionStepId + ? { ...step, completed: step.total } + : step + ) + ); + // 다음 단계 펼치기 + const stepIndex = steps.findIndex((s) => s.id === pendingInspectionStepId); + if (stepIndex < steps.length - 1) { + setExpandedSteps((prev) => new Set([...prev, steps[stepIndex + 1].id])); + } + } + setInspectionDialogOpen(false); + setPendingInspectionStepId(null); + }; + if (!isExpanded) return null; return ( -
+
{/* 자재 투입 필요 섹션 */} {materialRequired && ( -
-
-
- - - 자재 투입이 필요합니다 - -
- -
+
+

자재 투입 필요

+
)} {/* 공정 단계 헤더 */}
-

공정 단계

- - {completedSteps}/{totalSteps} 완료 - +

+ 공정 단계 ({totalSteps}단계) +

+ + {completedSteps} / {totalSteps} 완료 +
{/* 공정 단계 목록 */} @@ -173,9 +247,30 @@ export function ProcessDetailSection({ step={step} isExpanded={expandedSteps.has(step.id)} onToggle={() => toggleStep(step.id)} + onInspectionRequest={() => handleInspectionRequest(step)} /> ))}
+ + {/* 검사 요청 완료 다이얼로그 */} + + + + 검사 요청 완료 + + {inspectionStepName} 검사 요청이 품질팀에 전송되었습니다. + + + + + 확인 + + + +
); } @@ -186,99 +281,153 @@ interface ProcessStepCardProps { step: ProcessStep; isExpanded: boolean; onToggle: () => void; + onInspectionRequest: () => void; } -function ProcessStepCard({ step, isExpanded, onToggle }: ProcessStepCardProps) { +function ProcessStepCard({ step, isExpanded, onToggle, onInspectionRequest }: ProcessStepCardProps) { const isCompleted = step.completed === step.total; const hasItems = step.items.length > 0; return ( - - -
-
- {isCompleted ? ( - - ) : ( - +
+ {/* 헤더 */} +
+
+ {/* 숫자 뱃지 */} +
-
- - {step.stepNo}. {step.name} - - {step.isInspection && ( - - 검사 - - )} -
- - {step.completed}/{step.total} 완료 - -
+ > + {step.stepNo}
- {hasItems && ( - - {isExpanded ? '접기' : '펼치기'} - + {/* 공정명 + 검사 뱃지 */} +
+ {step.name} + {step.isInspection && ( + + 검사 + + )} +
+
+ {/* 진행률 + 완료 표시 */} +
+ {isCompleted && ( + 완료 + )} + + {step.completed}/{step.total} + + {(hasItems || step.isInspection) && ( + )}
- +
- {hasItems && ( - -
- {step.items.map((item) => ( - - ))} -
-
+ {/* 검사 요청 버튼 (검사 항목일 때만) */} + {step.isInspection && !isCompleted && isExpanded && ( +
+ +
)} - + + {/* 상세 항목 리스트 */} + {hasItems && isExpanded && ( +
+ {step.items.map((item, index) => ( + + ))} +
+ )} +
); } interface ProcessStepItemCardProps { item: ProcessStepItem; + index: number; + isCompleted: boolean; } -function ProcessStepItemCard({ item }: ProcessStepItemCardProps) { +function ProcessStepItemCard({ item, index, isCompleted }: ProcessStepItemCardProps) { return ( -
-
-
- {item.itemNo} +
+ {/* 인덱스 뱃지 */} +
+ {index} +
+ + {/* 상세 정보 */} +
+ {/* 첫 번째 줄: #N + 위치 + 선행생산 + 완료 */} +
+ {item.itemNo} + + {item.location} + {item.isPriority && ( - + 선행 생산 )} + {isCompleted && ( + 완료 + )}
- - {item.lot} - -
-
-
- - {item.location} -
-
- - {item.spec} -
-
- + + {/* 두 번째 줄: 규격 + 자재 */} +
+ 규격: {item.spec} 자재: {item.material}
+ + {/* 세 번째 줄: LOT */} +
+ LOT: {item.lot} +
); diff --git a/src/components/production/WorkerScreen/WorkCard.tsx b/src/components/production/WorkerScreen/WorkCard.tsx index 821b0307..531d323a 100644 --- a/src/components/production/WorkerScreen/WorkCard.tsx +++ b/src/components/production/WorkerScreen/WorkCard.tsx @@ -3,13 +3,19 @@ /** * 작업 카드 컴포넌트 * - * 각 작업 항목을 카드 형태로 표시 - * 버튼: 전량완료, 공정상세, 자재투입, 작업일지, 이슈보고 - * 공정상세 토글 시 ProcessDetailSection 표시 + * 기획 화면에 맞춘 레이아웃: + * - 헤더 박스: 품목명 + 수량 + * - 뱃지 영역: 순위/긴급/상태(좌측), 납기(우측) + * - 공정 + 작업번호 + * - 업체/현장 + * - 담당자 + * - 지시 박스 (회색 배경) + * - 전량완료 버튼 (검은색, 전체너비) + * - 4버튼 2x2 그리드 */ import { useState } from 'react'; -import { CheckCircle, Layers, Package, FileText, AlertTriangle, ChevronDown } from 'lucide-react'; +import { ChevronDown } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -36,146 +42,145 @@ export function WorkCard({ }: WorkCardProps) { const [isExpanded, setIsExpanded] = useState(false); - // 상태별 배지 스타일 - const statusBadgeStyle = { - waiting: 'bg-gray-100 text-gray-700 border-gray-200', - inProgress: 'bg-blue-100 text-blue-700 border-blue-200', - completed: 'bg-green-100 text-green-700 border-green-200', - }; - - // 납기일 포맷 (YYYY. M. D.) + // 납기일 포맷 (YYYY-MM-DD) const formatDueDate = (dateStr: string) => { const date = new Date(dateStr); - return `${date.getFullYear()}. ${date.getMonth() + 1}. ${date.getDate()}.`; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; }; return ( - - - {/* 상단: 작업번호 + 상태 + 순위 */} -
+ + + {/* 헤더 박스: 품목명 + 수량 */} +
+

+ {order.productName} +

+
+ {order.quantity} + EA +
+
+ + {/* 본문 영역 */} +
+ {/* 뱃지 영역: 순위/긴급/상태(좌측), 납기(우측) */} +
+
+ {/* 순위 뱃지 */} + {order.priority <= 3 && ( + + {order.priority}순위 + + )} + {/* 긴급 뱃지 */} + {order.isUrgent && ( + + 긴급 + + )} + {/* 상태 뱃지 */} + + {STATUS_LABELS[order.status]} + +
+ {/* 납기 */} +
+

납기

+

{formatDueDate(order.dueDate)}

+
+
+ + {/* 공정 뱃지 + 작업번호 */}
- - {order.orderNo} - - {STATUS_LABELS[order.status]} + {PROCESS_LABELS[order.process]} + {order.orderNo}
- {order.priority <= 3 && ( - - 순위 {order.priority} - + + {/* 업체/현장 */} +

+ {order.client} · {order.projectName} +

+ + {/* 담당자 */} + {order.assignees && order.assignees.length > 0 && ( +

+ 담당: {order.assignees.join(', ')} +

)} -
- {/* 제품명 + 수량 */} -
-
-

{order.productName}

-

{order.client}

-

{order.projectName}

-
-
-

{order.quantity}

-

EA

-
-
- - {/* 공정 + 납기 */} -
- - {PROCESS_LABELS[order.process]} - - - 납기: {formatDueDate(order.dueDate)} - - {order.isDelayed && order.delayDays && ( - - +{order.delayDays}일 지연 - + {/* 지시 박스 (회색 배경) */} + {order.instruction && ( +
+

+ 지시: {order.instruction} +

+
)} -
- {/* 지시사항 */} - {order.instruction && ( -
- {order.instruction} -
- )} - - {/* 버튼 영역 - 첫 번째 줄 */} -
+ {/* 전량완료 버튼 - 검은색, 전체 너비 */} - - - -
- {/* 버튼 영역 - 두 번째 줄 (이슈보고) */} -
- -
+ {/* 4버튼 2x2 그리드 */} +
+ + + + +
- {/* 공정상세 섹션 (토글) */} - onMaterialInput(order)} - /> + {/* 공정상세 섹션 (토글) */} + onMaterialInput(order)} + /> +
); -} +} \ No newline at end of file