-
- ① 자재 선택 (BOM 기준)
-
-
- {isLoading ? (
-
- ) : materials.length === 0 ? (
-
-
-
-
- 자재코드
- 자재명
- 단위
- 현재고
- 선택
-
-
-
-
-
- 이 공정에 배정된 자재가 없습니다.
+ ) : (
+
- ) : (
-
-
-
-
- 자재코드
- 자재명
- 단위
- 현재고
- 선택
-
-
-
- {materials.map((material) => (
-
-
- {material.materialCode}
-
- {material.materialName}
- {material.unit}
-
- {material.currentStock.toLocaleString()}
-
-
- handleToggleMaterial(String(material.id))}
- />
-
-
- ))}
-
-
-
- )}
-
+ ))}
+
+
+
+ )}
{/* 버튼 영역 */}
@@ -271,8 +277,8 @@ export function MaterialInputModal({
diff --git a/src/components/production/WorkerScreen/WorkItemCard.tsx b/src/components/production/WorkerScreen/WorkItemCard.tsx
new file mode 100644
index 00000000..1454c0fb
--- /dev/null
+++ b/src/components/production/WorkerScreen/WorkItemCard.tsx
@@ -0,0 +1,313 @@
+'use client';
+
+/**
+ * 작업 아이템 카드 컴포넌트 (기획서 기반)
+ *
+ * 공통: 번호 + 품목코드(품목명) + 층/부호, 제작사이즈, 진척률바, pills, 자재투입목록(토글)
+ * 스크린: 절단정보 (폭 X 장)
+ * 슬랫: 길이 / 슬랫매수 / 조인트바
+ * 절곡: 도면(IMG) + 공통사항 + 세부부품
+ */
+
+import { useState, useCallback } from 'react';
+import { ChevronDown, ChevronUp, Pencil, Trash2, ImageIcon } from 'lucide-react';
+import { Card, CardContent } from '@/components/ui/card';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import { Progress } from '@/components/ui/progress';
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table';
+import { cn } from '@/lib/utils';
+import type {
+ WorkItemData,
+ WorkStepData,
+ MaterialListItem,
+} from './types';
+
+interface WorkItemCardProps {
+ item: WorkItemData;
+ onStepClick: (itemId: string, step: WorkStepData) => void;
+ onEditMaterial: (itemId: string, material: MaterialListItem) => void;
+ onDeleteMaterial: (itemId: string, materialId: string) => void;
+}
+
+export function WorkItemCard({
+ item,
+ onStepClick,
+ onEditMaterial,
+ onDeleteMaterial,
+}: WorkItemCardProps) {
+ const [isMaterialListOpen, setIsMaterialListOpen] = useState(false);
+
+ // 진척률 계산
+ const completedSteps = item.steps.filter((s) => s.isCompleted).length;
+ const totalSteps = item.steps.length;
+ const progressPercent = totalSteps > 0 ? (completedSteps / totalSteps) * 100 : 0;
+
+ const handleStepClick = useCallback(
+ (step: WorkStepData) => {
+ onStepClick(item.id, step);
+ },
+ [item.id, onStepClick]
+ );
+
+ return (
+
+
+ {/* 헤더: 번호 + 품목코드(품목명) + 층/부호 */}
+
+
+
+ {item.itemNo}
+
+
+ {item.itemCode} ({item.itemName})
+
+
+
+ {item.floor} / {item.code}
+
+
+
+ {/* 제작 사이즈 */}
+
+ 제작 사이즈
+
+ {item.width.toLocaleString()} X {item.height.toLocaleString()} mm
+
+ {item.quantity}개
+
+
+ {/* 공정별 추가 정보 */}
+ {item.processType === 'screen' && item.cuttingInfo && (
+
+ )}
+
+ {item.processType === 'slat' && item.slatInfo && (
+
+ )}
+
+ {item.processType === 'bending' && item.bendingInfo && (
+
+ )}
+
+ {/* 진척률 프로그래스 바 */}
+
+
+
+ {completedSteps}/{totalSteps} 완료
+
+
+
+ {/* 공정 단계 pills */}
+
+ {item.steps.map((step) => (
+
+ ))}
+
+
+ {/* 자재 투입 목록 (토글) */}
+
+
+
+ {isMaterialListOpen && (
+
+ {(!item.materialInputs || item.materialInputs.length === 0) ? (
+
+ 투입된 자재가 없습니다.
+
+ ) : (
+
+
+
+ 로트번호
+ 품목명
+ 수량
+ 단위
+ 관리
+
+
+
+ {item.materialInputs.map((mat) => (
+
+ {mat.lotNo}
+ {mat.itemName}
+ {mat.quantity.toLocaleString()}
+ {mat.unit}
+
+
+
+
+
+
+
+ ))}
+
+
+ )}
+
+ )}
+
+
+
+ );
+}
+
+// ===== 스크린 전용: 절단정보 =====
+function ScreenCuttingInfo({ width, sheets }: { width: number; sheets: number }) {
+ return (
+
+
절단정보
+
+ 폭 {width.toLocaleString()}mm X {sheets}장
+
+
+ );
+}
+
+// ===== 슬랫 전용: 길이/매수/조인트바 =====
+function SlatExtraInfo({
+ length,
+ slatCount,
+ jointBar,
+}: {
+ length: number;
+ slatCount: number;
+ jointBar: number;
+}) {
+ return (
+
+
+ 길이 {length.toLocaleString()}mm
+
+
+ 슬랫 매수 {slatCount}장
+
+
+ 조인트바 {jointBar}개
+
+
+ );
+}
+
+// ===== 절곡 전용: 도면 + 공통사항 + 세부부품 =====
+import type { BendingInfo } from './types';
+
+function BendingExtraInfo({ info }: { info: BendingInfo }) {
+ return (
+
+ {/* 도면 + 공통사항 (가로 배치) */}
+
+ {/* 도면 이미지 */}
+
+ {info.drawingUrl ? (
+

+ ) : (
+
+
+ 도면
+
+ )}
+
+
+ {/* 공통사항 */}
+
+
공통사항
+
+
+ 종류
+ {info.common.kind}
+
+
+ 유형
+ {info.common.type}
+
+ {info.common.lengthQuantities.map((lq, i) => (
+
+ {i === 0 ? '길이별 수량' : ''}
+
+ {lq.length.toLocaleString()}mm X {lq.quantity}개
+
+
+ ))}
+
+
+
+
+ {/* 세부부품 */}
+ {info.detailParts.length > 0 && (
+
+
+ 세부부품 ({info.detailParts.length}개)
+
+
+ {info.detailParts.map((part, i) => (
+
+
+ {part.partName}
+ {part.material}
+
+
+ 바아시 정보 {part.barcyInfo}
+
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/src/components/production/WorkerScreen/WorkLogModal.tsx b/src/components/production/WorkerScreen/WorkLogModal.tsx
index ef8e1848..9e2e93e7 100644
--- a/src/components/production/WorkerScreen/WorkLogModal.tsx
+++ b/src/components/production/WorkerScreen/WorkLogModal.tsx
@@ -4,31 +4,85 @@
* 작업일지 모달
*
* document-system 통합 버전 (2026-01-22)
+ * 공정별 작업일지 지원 (2026-01-29)
* - DocumentViewer 사용
- * - WorkLogContent로 문서 본문 분리
+ * - 공정 타입에 따라 스크린/슬랫/절곡 작업일지 분기
+ * - processType 미지정 시 기존 WorkLogContent (범용) 사용
*/
import { useState, useEffect } from 'react';
import { Loader2 } from 'lucide-react';
import { DocumentViewer } from '@/components/document-system';
import { getWorkOrderById } from '../WorkOrders/actions';
-import type { WorkOrder } from '../WorkOrders/types';
+import type { WorkOrder, ProcessType } from '../WorkOrders/types';
import { WorkLogContent } from './WorkLogContent';
+import {
+ ScreenWorkLogContent,
+ SlatWorkLogContent,
+ BendingWorkLogContent,
+} from '../WorkOrders/documents';
interface WorkLogModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
workOrderId: string | null;
+ processType?: ProcessType;
}
-export function WorkLogModal({ open, onOpenChange, workOrderId }: WorkLogModalProps) {
+export function WorkLogModal({ open, onOpenChange, workOrderId, processType }: WorkLogModalProps) {
const [order, setOrder] = useState
(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
+ // 목업 WorkOrder 생성
+ const createMockOrder = (id: string, pType?: ProcessType): WorkOrder => ({
+ id,
+ workOrderNo: 'KD-WO-260129-01',
+ lotNo: 'KD-SA-260129-01',
+ processId: 1,
+ processName: pType === 'slat' ? '슬랫' : pType === 'bending' ? '절곡' : '스크린',
+ processCode: pType || 'screen',
+ processType: pType || 'screen',
+ status: 'in_progress',
+ client: '(주)경동',
+ projectName: '서울 강남 현장',
+ dueDate: '2026-02-05',
+ assignee: '홍길동',
+ assignees: [{ id: '1', name: '홍길동', isPrimary: true }],
+ orderDate: '2026-01-20',
+ scheduledDate: '2026-01-29',
+ shipmentDate: '2026-02-05',
+ salesOrderDate: '2026-01-15',
+ isAssigned: true,
+ isStarted: true,
+ priority: 3,
+ priorityLabel: '긴급',
+ shutterCount: 12,
+ department: '생산부',
+ items: [
+ { id: '1', no: 1, status: 'in_progress', productName: '와이어 스크린', floorCode: '1층/FSS-01', specification: '8,260 X 8,350', quantity: 2, unit: 'EA' },
+ { id: '2', no: 2, status: 'waiting', productName: '메쉬 스크린', floorCode: '2층/FSS-03', specification: '6,400 X 5,200', quantity: 4, unit: 'EA' },
+ { id: '3', no: 3, status: 'completed', productName: '광폭 와이어', floorCode: '3층/FSS-05', specification: '12,000 X 4,500', quantity: 1, unit: 'EA' },
+ ],
+ currentStep: { key: 'cutting', label: '절단', order: 2 },
+ completedSteps: ['material_input'],
+ totalProgress: 25,
+ issues: [],
+ memo: '',
+ createdAt: '2026-01-20T09:00:00',
+ updatedAt: '2026-01-29T14:00:00',
+ });
+
// 모달 열릴 때 데이터 fetch
useEffect(() => {
if (open && workOrderId) {
+ // 목업 ID인 경우 API 호출 생략
+ if (workOrderId.startsWith('mock-')) {
+ setOrder(createMockOrder(workOrderId, processType));
+ setError(null);
+ return;
+ }
+
setIsLoading(true);
setError(null);
@@ -51,13 +105,32 @@ export function WorkLogModal({ open, onOpenChange, workOrderId }: WorkLogModalPr
setOrder(null);
setError(null);
}
- }, [open, workOrderId]);
+ }, [open, workOrderId, processType]);
if (!workOrderId) return null;
// 로딩/에러 상태는 DocumentViewer 내부에서 처리
const subtitle = order ? `${order.processName} 생산부서` : undefined;
+ // 공정 타입에 따라 콘텐츠 분기
+ const renderContent = () => {
+ if (!order) return null;
+
+ // processType prop 또는 order의 processType 사용
+ const type = processType || order.processType;
+
+ switch (type) {
+ case 'screen':
+ return ;
+ case 'slat':
+ return ;
+ case 'bending':
+ return ;
+ default:
+ return ;
+ }
+ };
+
return (
{error || '데이터를 불러올 수 없습니다.'}
+ renderContent()
)}
);
diff --git a/src/components/production/WorkerScreen/actions.ts b/src/components/production/WorkerScreen/actions.ts
index 361e23e4..0062e4c9 100644
--- a/src/components/production/WorkerScreen/actions.ts
+++ b/src/components/production/WorkerScreen/actions.ts
@@ -59,11 +59,20 @@ function transformToWorkerScreenFormat(api: WorkOrderApiItem): WorkOrder {
? Math.ceil((today.getTime() - due.getTime()) / (1000 * 60 * 60 * 24))
: undefined;
+ // process_type → processCode/processName 매핑
+ const processTypeMap: Record