Files
sam-react-prod/src/components/production/WorkerScreen/WorkLogModal.tsx
byeongcheolryu 386cd30bc0 feat(WEB): 입찰/계약/주문관리 기능 추가 및 견적 상세 리팩토링
- 입찰관리: 목록/상세/수정 페이지 및 목업 데이터
- 계약관리: 목록/상세/수정 페이지 구현
- 주문관리: 수주/발주 목록 및 상세 페이지 구현
- 견적 상세 폼: 섹션별 분리 및 hooks/utils 리팩토링
- 품목관리, 카테고리관리, 단가관리 기능 추가
- 현장설명회/협력업체 폼 개선
- 프린트 유틸리티 공통화 (print-utils.ts)
- 문서 모달 공통 컴포넌트 정리
- IntegratedListTemplateV2, StatCards 개선

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 18:59:04 +09:00

288 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
/**
* 작업일지 모달
*
* - 헤더: sam-design 작업일지 스타일
* - 내부 문서: 스크린샷 기준 작업일지 양식
*/
import { Printer, X } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogTitle,
} from '@/components/ui/dialog';
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
import { Button } from '@/components/ui/button';
import { printArea } from '@/lib/print-utils';
import type { WorkOrder } from '../ProductionDashboard/types';
import { PROCESS_LABELS } from '../ProductionDashboard/types';
interface WorkLogModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
order: WorkOrder | null;
}
export function WorkLogModal({ open, onOpenChange, order }: WorkLogModalProps) {
const handlePrint = () => {
printArea({ title: '작업일지 인쇄' });
};
if (!order) return null;
const today = new Date().toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).replace(/\. /g, '-').replace('.', '');
const documentNo = `WL-${order.process.toUpperCase().slice(0, 3)}`;
const lotNo = `KD-TS-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01`;
// 샘플 품목 데이터 (스크린샷 기준)
const items = [
{ no: 1, name: '스크린 사타 (표준형)', location: '1층/A-01', spec: '3000×2500', qty: 1, status: '대기' },
{ no: 2, name: '스크린 사타 (표준형)', location: '2층/A-02', spec: '3000×2500', qty: 1, status: '대기' },
{ no: 3, name: '스크린 사타 (표준형)', location: '3층/A-03', spec: '-', qty: '-', status: '대기' },
];
// 작업내역 데이터 (스크린샷 기준)
const workStats = {
workType: '필름 스크린',
workWidth: '1016mm',
general: 3,
ironing: 3,
sandblast: 3,
packing: 1,
orderQty: 3,
completedQty: 1,
progress: 33,
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-5xl max-h-[90vh] overflow-y-auto p-0 gap-0 bg-gray-100">
{/* 접근성을 위한 숨겨진 타이틀 */}
<VisuallyHidden>
<DialogTitle> - {order.orderNo}</DialogTitle>
</VisuallyHidden>
{/* 모달 헤더 - sam-design 스타일 (인쇄 시 숨김) */}
<div className="print-hidden flex items-center justify-between px-6 py-4 border-b bg-white sticky top-0 z-10">
<div className="flex items-center gap-3">
<span className="font-semibold text-lg"></span>
<span className="text-sm text-muted-foreground">
{PROCESS_LABELS[order.process]}
</span>
<span className="text-sm text-muted-foreground">
({documentNo})
</span>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handlePrint}>
<Printer className="w-4 h-4 mr-1.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => onOpenChange(false)}
>
<X className="w-4 h-4" />
</Button>
</div>
</div>
{/* 문서 본문 (인쇄 시 이 영역만 출력) */}
<div className="print-area m-6 p-6 bg-white rounded-lg shadow-sm">
{/* 문서 헤더: 로고 + 제목 + 결재라인 */}
<div className="flex justify-between items-start mb-6 border border-gray-300">
{/* 좌측: 로고 영역 */}
<div className="w-24 border-r border-gray-300 flex flex-col items-center justify-center p-3">
<span className="text-2xl font-bold">KD</span>
<span className="text-xs text-gray-500"></span>
</div>
{/* 중앙: 문서 제목 */}
<div className="flex-1 flex flex-col items-center justify-center p-3 border-r border-gray-300">
<h1 className="text-xl font-bold tracking-widest mb-1"> </h1>
<p className="text-xs text-gray-500">{documentNo}</p>
<p className="text-sm font-medium mt-1">{PROCESS_LABELS[order.process]} </p>
</div>
{/* 우측: 결재라인 */}
<div className="shrink-0 text-xs">
<table className="border-collapse">
<tbody>
{/* 첫 번째 행: 결재 + 작성/검토/승인 */}
<tr>
<td rowSpan={3} className="w-8 text-center font-medium bg-gray-100 border-r border-b border-gray-300 align-middle">
<div className="flex flex-col items-center">
<span></span>
<span></span>
</div>
</td>
<td className="w-16 p-2 text-center font-medium bg-gray-100 border-r border-b border-gray-300"></td>
<td className="w-16 p-2 text-center font-medium bg-gray-100 border-r border-b border-gray-300"></td>
<td className="w-16 p-2 text-center font-medium bg-gray-100 border-b border-gray-300"></td>
</tr>
{/* 두 번째 행: 이름 + 날짜 */}
<tr>
<td className="w-16 p-2 text-center border-r border-b border-gray-300">
<div>{order.assignees[0] || '-'}</div>
<div className="text-[10px] text-gray-500">
{new Date().toLocaleDateString('ko-KR', { month: '2-digit', day: '2-digit' }).replace('. ', '/').replace('.', '')}
</div>
</td>
<td className="w-16 p-2 text-center border-r border-b border-gray-300"></td>
<td className="w-16 p-2 text-center border-b border-gray-300"></td>
</tr>
{/* 세 번째 행: 부서 */}
<tr>
<td className="w-16 p-2 text-center bg-gray-50 border-r border-gray-300">/</td>
<td className="w-16 p-2 text-center bg-gray-50 border-r border-gray-300"></td>
<td className="w-16 p-2 text-center bg-gray-50"></td>
</tr>
</tbody>
</table>
</div>
</div>
{/* 기본 정보 테이블 */}
<div className="border border-gray-300 mb-6">
{/* Row 1 */}
<div className="grid grid-cols-2 border-b border-gray-300">
<div className="flex border-r border-gray-300">
<div className="w-24 bg-gray-100 p-3 text-sm font-medium border-r border-gray-300 flex items-center">
</div>
<div className="flex-1 p-3 text-sm flex items-center">{order.client}</div>
</div>
<div className="flex">
<div className="w-24 bg-gray-100 p-3 text-sm font-medium border-r border-gray-300 flex items-center">
</div>
<div className="flex-1 p-3 text-sm flex items-center">{order.projectName}</div>
</div>
</div>
{/* Row 2 */}
<div className="grid grid-cols-2 border-b border-gray-300">
<div className="flex border-r border-gray-300">
<div className="w-24 bg-gray-100 p-3 text-sm font-medium border-r border-gray-300 flex items-center">
</div>
<div className="flex-1 p-3 text-sm flex items-center">{today}</div>
</div>
<div className="flex">
<div className="w-24 bg-gray-100 p-3 text-sm font-medium border-r border-gray-300 flex items-center">
LOT NO.
</div>
<div className="flex-1 p-3 text-sm flex items-center">{lotNo}</div>
</div>
</div>
{/* Row 3 */}
<div className="grid grid-cols-2">
<div className="flex border-r border-gray-300">
<div className="w-24 bg-gray-100 p-3 text-sm font-medium border-r border-gray-300 flex items-center">
</div>
<div className="flex-1 p-3 text-sm flex items-center">
{new Date(order.dueDate).toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).replace(/\. /g, '-').replace('.', '')}
</div>
</div>
<div className="flex">
<div className="w-24 bg-gray-100 p-3 text-sm font-medium border-r border-gray-300 flex items-center">
</div>
<div className="flex-1 p-3 text-sm flex items-center">W- x H-</div>
</div>
</div>
</div>
{/* 품목 테이블 */}
<div className="border border-gray-300 mb-6">
{/* 테이블 헤더 */}
<div className="grid grid-cols-12 border-b border-gray-300 bg-gray-100">
<div className="col-span-1 p-2 text-sm font-medium text-center border-r border-gray-300">No</div>
<div className="col-span-4 p-2 text-sm font-medium text-center border-r border-gray-300"></div>
<div className="col-span-2 p-2 text-sm font-medium text-center border-r border-gray-300">/</div>
<div className="col-span-2 p-2 text-sm font-medium text-center border-r border-gray-300"></div>
<div className="col-span-1 p-2 text-sm font-medium text-center border-r border-gray-300"></div>
<div className="col-span-2 p-2 text-sm font-medium text-center"></div>
</div>
{/* 테이블 데이터 */}
{items.map((item, index) => (
<div
key={item.no}
className={`grid grid-cols-12 ${index < items.length - 1 ? 'border-b border-gray-300' : ''}`}
>
<div className="col-span-1 p-2 text-sm text-center border-r border-gray-300">{item.no}</div>
<div className="col-span-4 p-2 text-sm border-r border-gray-300">{item.name}</div>
<div className="col-span-2 p-2 text-sm text-center border-r border-gray-300">{item.location}</div>
<div className="col-span-2 p-2 text-sm text-center border-r border-gray-300">{item.spec}</div>
<div className="col-span-1 p-2 text-sm text-center border-r border-gray-300">{item.qty}</div>
<div className="col-span-2 p-2 text-sm text-center">{item.status}</div>
</div>
))}
</div>
{/* 작업내역 */}
<div className="border border-gray-300 mb-6">
{/* 검정 헤더 */}
<div className="bg-gray-800 text-white p-2.5 text-sm font-medium text-center">
{PROCESS_LABELS[order.process]}
</div>
{/* 작업내역 그리드 */}
<div className="grid grid-cols-4 border-b border-gray-300">
<div className="p-2 text-sm bg-gray-100 border-r border-gray-300 text-center font-medium"> </div>
<div className="p-2 text-sm border-r border-gray-300 text-center">{workStats.workType}</div>
<div className="p-2 text-sm bg-gray-100 border-r border-gray-300 text-center font-medium"> </div>
<div className="p-2 text-sm text-center">{workStats.workWidth}</div>
</div>
<div className="grid grid-cols-4 border-b border-gray-300">
<div className="p-2 text-sm bg-gray-100 border-r border-gray-300 text-center font-medium"></div>
<div className="p-2 text-sm border-r border-gray-300 text-center">{workStats.general} EA</div>
<div className="p-2 text-sm bg-gray-100 border-r border-gray-300 text-center font-medium"></div>
<div className="p-2 text-sm text-center">{workStats.ironing} EA</div>
</div>
<div className="grid grid-cols-4 border-b border-gray-300">
<div className="p-2 text-sm bg-gray-100 border-r border-gray-300 text-center font-medium"> </div>
<div className="p-2 text-sm border-r border-gray-300 text-center">{workStats.sandblast} EA</div>
<div className="p-2 text-sm bg-gray-100 border-r border-gray-300 text-center font-medium"></div>
<div className="p-2 text-sm text-center">{workStats.packing} EA</div>
</div>
{/* 수량 및 진행률 */}
<div className="grid grid-cols-6">
<div className="p-2 text-sm bg-gray-100 border-r border-gray-300 text-center font-medium"></div>
<div className="p-2 text-sm border-r border-gray-300 text-center">{workStats.orderQty} EA</div>
<div className="p-2 text-sm bg-gray-100 border-r border-gray-300 text-center font-medium"></div>
<div className="p-2 text-sm border-r border-gray-300 text-center">{workStats.completedQty} EA</div>
<div className="p-2 text-sm bg-gray-100 border-r border-gray-300 text-center font-medium"></div>
<div className="p-2 text-sm text-center font-medium text-blue-600">{workStats.progress}%</div>
</div>
</div>
{/* 특이 사항 */}
<div className="border border-gray-300">
<div className="bg-gray-800 text-white p-2.5 text-sm font-medium text-center">
</div>
<div className="p-4 min-h-[60px] text-sm">
{order.instruction || '-'}
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}