- 입찰관리: 목록/상세/수정 페이지 및 목업 데이터 - 계약관리: 목록/상세/수정 페이지 구현 - 주문관리: 수주/발주 목록 및 상세 페이지 구현 - 견적 상세 폼: 섹션별 분리 및 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>
288 lines
14 KiB
TypeScript
288 lines
14 KiB
TypeScript
'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>
|
||
);
|
||
} |