refactor: 모달 Content 컴포넌트 분리 및 파일 입력 UI 공통화

- 모달 컴포넌트에서 Content 분리하여 재사용성 향상
  - EstimateDocumentContent, DirectConstructionContent 등
  - WorkLogContent, QuotePreviewContent, ReceivingReceiptContent
- 파일 입력 공통 UI 컴포넌트 추가
  - file-dropzone, file-input, file-list, image-upload
- 폼 컴포넌트 코드 정리 및 중복 제거 (-4,056줄)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-22 15:07:17 +09:00
parent 6fa69d81f4
commit 9464a368ba
48 changed files with 3900 additions and 4063 deletions

View File

@@ -1,31 +1,15 @@
'use client';
/**
* 공정 상세 - 작업일지 양식 미리보기 모달
* 공정 작업일지 미리보기 모달
*
* 기획서 기준 양식:
* - 신청업체/신청내용 테이블
* - 순번/작업항목/규격/수량/단위/작업자/비고 테이블
* document-system 통합 버전 (2026-01-22)
*/
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 { DocumentViewer } from '@/components/document-system';
import type { Process } from '@/types/process';
import { ProcessWorkLogContent } from './ProcessWorkLogContent';
interface ProcessWorkLogPreviewModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
process: Process;
}
// 공정명을 문서 코드로 매핑
const getDocumentCode = (processName: string): string => {
if (processName.includes('스크린')) return 'WL-SCR';
if (processName.includes('슬랫')) return 'WL-SLT';
@@ -33,188 +17,28 @@ const getDocumentCode = (processName: string): string => {
return 'WL-STK';
};
// 공정명을 부서명으로 매핑
const getDepartmentName = (processName: string, department?: string): string => {
if (processName.includes('스크린')) return '스크린 생산부서';
if (processName.includes('슬랫')) return '슬랫 생산부서';
if (processName.includes('절곡')) return '절곡 생산부서';
if (processName.includes('포밍')) return '포밍 생산부서';
return department || '생산부서';
};
interface ProcessWorkLogPreviewModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
process: Process;
}
export function ProcessWorkLogPreviewModal({
open,
onOpenChange,
process
}: ProcessWorkLogPreviewModalProps) {
const handlePrint = () => {
printArea({ title: `${process.workLogTemplate} 인쇄` });
};
const documentCode = getDocumentCode(process.processName);
const departmentName = getDepartmentName(process.processName, process.department);
const today = new Date().toLocaleDateString('ko-KR', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).replace(/\. /g, '-').replace('.', '');
// 샘플 작업항목 데이터 (기획서 기준)
const workItems = [
{ no: 1, name: '원단 재단', spec: 'W2900 × H3900', qty: 9, unit: 'EA', worker: '이작업', note: '' },
{ no: 2, name: '미싱 작업', spec: 'W2800 × H3800', qty: 8, unit: 'EA', worker: '김작업', note: '' },
{ no: 3, name: '앤드락 조립', spec: 'W2700 × H3700', qty: 7, unit: 'EA', worker: '이작업', note: '' },
{ no: 4, name: '검수', spec: 'W2600 × H3600', qty: 6, unit: 'EA', worker: '김작업', note: '' },
{ no: 5, name: '포장', spec: 'W2500 × H3500', qty: 5, unit: 'EA', worker: '이작업', note: '' },
];
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>{process.workLogTemplate} </DialogTitle>
</VisuallyHidden>
{/* 모달 헤더 (인쇄 시 숨김) */}
<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">{process.workLogTemplate} </span>
<span className="text-sm text-muted-foreground">({documentCode})</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 border border-gray-300 mb-6">
{/* 좌측: 로고 영역 */}
<div className="w-24 border-r border-gray-300 flex flex-col items-center justify-center p-3 shrink-0">
<div className="text-2xl font-bold">KD</div>
<div className="text-xs text-gray-500"></div>
</div>
{/* 중앙: 문서 제목 */}
<div className="flex-1 border-r border-gray-300 flex flex-col items-center justify-center p-3">
<h1 className="text-xl font-bold tracking-widest mb-1"> </h1>
<p className="text-xs text-gray-500">{documentCode}</p>
<p className="text-sm font-medium mt-1">{departmentName}</p>
</div>
{/* 우측: 결재라인 */}
<div className="shrink-0">
<table className="border-collapse text-xs h-full">
<tbody>
<tr>
<td rowSpan={3} className="w-8 border-r border-gray-300 text-center align-middle bg-gray-100 font-medium">
<div></div>
<div></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></div>
<div className="text-[10px] text-gray-500">12/17</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>
{/* 신청업체 / 신청내용 테이블 */}
<table className="w-full border-collapse border border-gray-300 mb-6 text-sm">
<thead>
<tr>
<th colSpan={2} className="bg-gray-800 text-white p-2.5 font-medium border-r border-gray-300">
</th>
<th colSpan={2} className="bg-gray-800 text-white p-2.5 font-medium">
</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-gray-300">
<td className="w-24 bg-gray-100 p-3 font-medium border-r border-gray-300"> </td>
<td className="p-3 border-r border-gray-300">{today}</td>
<td className="w-24 bg-gray-100 p-3 font-medium border-r border-gray-300"> </td>
<td className="p-3"> A동</td>
</tr>
<tr className="border-b border-gray-300">
<td className="bg-gray-100 p-3 font-medium border-r border-gray-300"> </td>
<td className="p-3 border-r border-gray-300">()</td>
<td className="bg-gray-100 p-3 font-medium border-r border-gray-300"></td>
<td className="p-3">{today}</td>
</tr>
<tr className="border-b border-gray-300">
<td className="bg-gray-100 p-3 font-medium border-r border-gray-300"> </td>
<td className="p-3 border-r border-gray-300"></td>
<td className="bg-gray-100 p-3 font-medium border-r border-gray-300"> LOT NO.</td>
<td className="p-3">KD-TS-251217-01-01</td>
</tr>
<tr>
<td className="bg-gray-100 p-3 font-medium border-r border-gray-300"></td>
<td className="p-3 border-r border-gray-300">SH3040 &nbsp; W3000×H4000</td>
<td className="bg-gray-100 p-3 font-medium border-r border-gray-300"></td>
<td className="p-3"> &nbsp; </td>
</tr>
</tbody>
</table>
{/* 작업항목 테이블 */}
<table className="w-full border-collapse border border-gray-300 text-sm">
<thead>
<tr className="bg-gray-800 text-white">
<th className="p-2.5 font-medium border-r border-gray-600 w-16"></th>
<th className="p-2.5 font-medium border-r border-gray-600"></th>
<th className="p-2.5 font-medium border-r border-gray-600 w-40"></th>
<th className="p-2.5 font-medium border-r border-gray-600 w-20"></th>
<th className="p-2.5 font-medium border-r border-gray-600 w-16"></th>
<th className="p-2.5 font-medium border-r border-gray-600 w-20"></th>
<th className="p-2.5 font-medium w-24"></th>
</tr>
</thead>
<tbody>
{workItems.map((item, index) => (
<tr key={item.no} className={index < workItems.length - 1 ? 'border-b border-gray-300' : ''}>
<td className="p-3 text-center border-r border-gray-300">{item.no}</td>
<td className="p-3 border-r border-gray-300">{item.name}</td>
<td className="p-3 text-center border-r border-gray-300">{item.spec}</td>
<td className="p-3 text-center border-r border-gray-300">{item.qty}</td>
<td className="p-3 text-center border-r border-gray-300">{item.unit}</td>
<td className="p-3 text-center border-r border-gray-300">{item.worker}</td>
<td className="p-3 text-center">{item.note}</td>
</tr>
))}
</tbody>
</table>
</div>
</DialogContent>
</Dialog>
<DocumentViewer
title={`${process.workLogTemplate} 미리보기`}
subtitle={`(${documentCode})`}
preset="inspection"
open={open}
onOpenChange={onOpenChange}
>
<ProcessWorkLogContent data={process} />
</DocumentViewer>
);
}
}