- QMS: InspectionModal/InspectionModalV2 개선, mockData 정리 - 전자결재: DocumentCreate 기능 수정 - 생산대시보드: ProductionDashboard 개선 - 템플릿: IntegratedDetailTemplate/UniversalListPage 기능 수정 - 문서: i18n 가이드 업데이트, 문서뷰어 아키텍처 계획 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
587 lines
24 KiB
TypeScript
587 lines
24 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog";
|
||
import { ZoomIn, ZoomOut, Download, Printer, AlertCircle, Maximize2 } from 'lucide-react';
|
||
import { Button } from "@/components/ui/button";
|
||
import { Document, DocumentItem } from '../types';
|
||
import { MOCK_WORK_ORDER, MOCK_SHIPMENT_DETAIL } from '../mockData';
|
||
|
||
// 기존 문서 컴포넌트 import
|
||
import { DeliveryConfirmation } from '@/components/outbound/ShipmentManagement/documents/DeliveryConfirmation';
|
||
import { ShippingSlip } from '@/components/outbound/ShipmentManagement/documents/ShippingSlip';
|
||
|
||
// 품질검사 문서 컴포넌트 import
|
||
import {
|
||
ImportInspectionDocument,
|
||
ProductInspectionDocument,
|
||
ScreenInspectionDocument,
|
||
BendingInspectionDocument,
|
||
SlatInspectionDocument,
|
||
JointbarInspectionDocument,
|
||
QualityDocumentUploader,
|
||
} from './documents';
|
||
|
||
interface InspectionModalProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
document: Document | null;
|
||
documentItem: DocumentItem | null;
|
||
}
|
||
|
||
// 문서 타입별 정보
|
||
const DOCUMENT_INFO: Record<string, { label: string; hasTemplate: boolean; color: string }> = {
|
||
import: { label: '수입검사 성적서', hasTemplate: true, color: 'text-green-600' },
|
||
order: { label: '수주서', hasTemplate: true, color: 'text-blue-600' },
|
||
log: { label: '작업일지', hasTemplate: true, color: 'text-orange-500' },
|
||
report: { label: '중간검사 성적서', hasTemplate: true, color: 'text-blue-500' },
|
||
confirmation: { label: '납품확인서', hasTemplate: true, color: 'text-red-500' },
|
||
shipping: { label: '출고증', hasTemplate: true, color: 'text-gray-600' },
|
||
product: { label: '제품검사 성적서', hasTemplate: true, color: 'text-green-500' },
|
||
quality: { label: '품질관리서', hasTemplate: false, color: 'text-purple-600' },
|
||
};
|
||
|
||
// Placeholder 컴포넌트 (양식 대기 문서용)
|
||
const PlaceholderDocument = ({ docType, docItem }: { docType: string; docItem: DocumentItem | null }) => {
|
||
const info = DOCUMENT_INFO[docType] || { label: '문서', hasTemplate: false, color: 'text-gray-600' };
|
||
|
||
return (
|
||
<div className="bg-white shadow-sm p-16 w-full h-full rounded flex flex-col items-center justify-center text-center">
|
||
<div className="w-16 h-16 text-amber-500 mb-4 mx-auto">
|
||
<AlertCircle className="w-full h-full" />
|
||
</div>
|
||
<h2 className="text-xl font-bold text-gray-800 mb-2">{info.label}</h2>
|
||
<p className="text-gray-500 text-sm mb-2">{docItem?.title || '문서'}</p>
|
||
{docItem?.date && (
|
||
<p className="text-gray-400 text-xs mb-2">{docItem.date}</p>
|
||
)}
|
||
{docItem?.code && (
|
||
<p className="text-xs text-green-600 font-mono bg-green-50 px-2 py-1 rounded mb-4">
|
||
로트 번호: {docItem.code}
|
||
</p>
|
||
)}
|
||
<div className="mt-4 p-4 bg-amber-50 rounded-lg border border-amber-200">
|
||
<p className="text-amber-700 text-sm font-medium">양식 준비 중</p>
|
||
<p className="text-amber-600 text-xs mt-1">디자인 파일이 필요합니다</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 수주서 문서 컴포넌트 (간소화 버전 - deprecated, V2 사용)
|
||
const OrderDocument = () => {
|
||
const data = { lotNumber: '', orderDate: '', client: '', siteName: '', manager: '', managerContact: '', deliveryRequestDate: '', expectedShipDate: '', deliveryMethod: '', address: '', items: [] as { id: string; name: string; specification: string; unit: string; quantity: number; unitPrice?: number; amount?: number }[], subtotal: 0, discountRate: 0, totalAmount: 0, remarks: '' };
|
||
|
||
return (
|
||
<div className="bg-white p-8 w-full text-sm shadow-sm">
|
||
{/* 헤더 */}
|
||
<div className="flex justify-between items-start mb-6">
|
||
<div className="flex items-center gap-4">
|
||
<div className="text-2xl font-bold">KD</div>
|
||
<div className="text-xs">경동기업</div>
|
||
</div>
|
||
<div className="text-2xl font-bold tracking-[0.5rem]">수 주 서</div>
|
||
<table className="text-xs border-collapse">
|
||
<tbody>
|
||
<tr>
|
||
<td className="border px-2 py-1 bg-gray-100" rowSpan={3}>
|
||
<div className="flex flex-col items-center">
|
||
<span>결</span><span>재</span>
|
||
</div>
|
||
</td>
|
||
<td className="border px-2 py-1 bg-gray-100 text-center w-16">작성</td>
|
||
<td className="border px-2 py-1 bg-gray-100 text-center w-16">검토</td>
|
||
<td className="border px-2 py-1 bg-gray-100 text-center w-16">승인</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="border px-2 py-1 h-10"></td>
|
||
<td className="border px-2 py-1 h-10"></td>
|
||
<td className="border px-2 py-1 h-10"></td>
|
||
</tr>
|
||
<tr>
|
||
<td className="border px-2 py-1 text-center bg-gray-50">판매</td>
|
||
<td className="border px-2 py-1 text-center bg-gray-50">생산</td>
|
||
<td className="border px-2 py-1 text-center bg-gray-50">품질</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* 기본 정보 */}
|
||
<table className="w-full border-collapse mb-6 text-xs">
|
||
<tbody>
|
||
<tr>
|
||
<td className="border px-3 py-2 bg-gray-100 w-24">LOT NO.</td>
|
||
<td className="border px-3 py-2">{data.lotNumber}</td>
|
||
<td className="border px-3 py-2 bg-gray-100 w-24">수주일</td>
|
||
<td className="border px-3 py-2">{data.orderDate}</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="border px-3 py-2 bg-gray-100">발주처</td>
|
||
<td className="border px-3 py-2">{data.client}</td>
|
||
<td className="border px-3 py-2 bg-gray-100">현장명</td>
|
||
<td className="border px-3 py-2">{data.siteName}</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="border px-3 py-2 bg-gray-100">담당자</td>
|
||
<td className="border px-3 py-2">{data.manager}</td>
|
||
<td className="border px-3 py-2 bg-gray-100">연락처</td>
|
||
<td className="border px-3 py-2">{data.managerContact}</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="border px-3 py-2 bg-gray-100">납기요청일</td>
|
||
<td className="border px-3 py-2">{data.deliveryRequestDate}</td>
|
||
<td className="border px-3 py-2 bg-gray-100">출고예정일</td>
|
||
<td className="border px-3 py-2">{data.expectedShipDate}</td>
|
||
</tr>
|
||
<tr>
|
||
<td className="border px-3 py-2 bg-gray-100">배송방법</td>
|
||
<td className="border px-3 py-2">{data.deliveryMethod}</td>
|
||
<td className="border px-3 py-2 bg-gray-100">배송지</td>
|
||
<td className="border px-3 py-2">{data.address}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
|
||
{/* 품목 테이블 */}
|
||
<table className="w-full border-collapse mb-6 text-xs">
|
||
<thead>
|
||
<tr className="bg-gray-100">
|
||
<th className="border px-2 py-2 w-10">No</th>
|
||
<th className="border px-2 py-2">품목명</th>
|
||
<th className="border px-2 py-2 w-24">규격</th>
|
||
<th className="border px-2 py-2 w-12">단위</th>
|
||
<th className="border px-2 py-2 w-12">수량</th>
|
||
<th className="border px-2 py-2 w-20">단가</th>
|
||
<th className="border px-2 py-2 w-24">금액</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{data.items.map((item, index) => (
|
||
<tr key={item.id}>
|
||
<td className="border px-2 py-2 text-center">{index + 1}</td>
|
||
<td className="border px-2 py-2">{item.name}</td>
|
||
<td className="border px-2 py-2 text-center">{item.specification}</td>
|
||
<td className="border px-2 py-2 text-center">{item.unit}</td>
|
||
<td className="border px-2 py-2 text-center">{item.quantity}</td>
|
||
<td className="border px-2 py-2 text-right">{item.unitPrice?.toLocaleString()}</td>
|
||
<td className="border px-2 py-2 text-right">{item.amount?.toLocaleString()}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
<tfoot>
|
||
<tr>
|
||
<td colSpan={5} className="border px-2 py-2 text-right bg-gray-50">소계</td>
|
||
<td colSpan={2} className="border px-2 py-2 text-right">{data.subtotal.toLocaleString()}원</td>
|
||
</tr>
|
||
<tr>
|
||
<td colSpan={5} className="border px-2 py-2 text-right bg-gray-50">할인 ({data.discountRate}%)</td>
|
||
<td colSpan={2} className="border px-2 py-2 text-right text-red-600">-{(data.subtotal * data.discountRate / 100).toLocaleString()}원</td>
|
||
</tr>
|
||
<tr>
|
||
<td colSpan={5} className="border px-2 py-2 text-right bg-gray-100 font-bold">총액</td>
|
||
<td colSpan={2} className="border px-2 py-2 text-right font-bold text-blue-600">{data.totalAmount.toLocaleString()}원</td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
|
||
{/* 비고 */}
|
||
{data.remarks && (
|
||
<div className="border p-4">
|
||
<h3 className="font-medium mb-2 text-xs">비고</h3>
|
||
<p className="text-xs text-gray-600">{data.remarks}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 작업일지 문서 컴포넌트 (간소화 버전)
|
||
const WorkLogDocument = () => {
|
||
const order = MOCK_WORK_ORDER;
|
||
const today = new Date().toLocaleDateString('ko-KR').replace(/\. /g, '-').replace('.', '');
|
||
const documentNo = `WL-${order.processCode.toUpperCase().slice(0, 3)}`;
|
||
const lotNo = `KD-TS-${new Date().toISOString().slice(2, 10).replace(/-/g, '')}-01`;
|
||
|
||
const items = [
|
||
{ no: 1, name: order.productName, location: '1층/A-01', spec: '3000×2500', qty: 1, status: '완료' },
|
||
{ no: 2, name: order.productName, location: '2층/A-02', spec: '3000×2500', qty: 1, status: '작업중' },
|
||
{ no: 3, name: order.productName, location: '3층/A-03', spec: '-', qty: 1, status: '대기' },
|
||
];
|
||
|
||
return (
|
||
<div className="bg-white p-8 w-full text-sm 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">스크린 생산부서</p>
|
||
</div>
|
||
<table className="text-xs shrink-0">
|
||
<tbody>
|
||
<tr>
|
||
<td rowSpan={3} className="w-8 text-center font-medium bg-gray-100 border-r border-b border-gray-300">
|
||
<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>
|
||
</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 className="border border-gray-300 mb-6">
|
||
<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">발주처</div>
|
||
<div className="flex-1 p-3 text-sm">{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">현장명</div>
|
||
<div className="flex-1 p-3 text-sm">{order.projectName}</div>
|
||
</div>
|
||
</div>
|
||
<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">작업일자</div>
|
||
<div className="flex-1 p-3 text-sm">{today}</div>
|
||
</div>
|
||
<div className="flex">
|
||
<div className="w-24 bg-gray-100 p-3 text-sm font-medium border-r border-gray-300">LOT NO.</div>
|
||
<div className="flex-1 p-3 text-sm">{lotNo}</div>
|
||
</div>
|
||
</div>
|
||
<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">납기일</div>
|
||
<div className="flex-1 p-3 text-sm">{order.dueDate}</div>
|
||
</div>
|
||
<div className="flex">
|
||
<div className="w-24 bg-gray-100 p-3 text-sm font-medium border-r border-gray-300">지시수량</div>
|
||
<div className="flex-1 p-3 text-sm">{order.quantity} EA</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">
|
||
<span className={`px-2 py-0.5 rounded text-xs ${
|
||
item.status === '완료' ? 'bg-green-100 text-green-700' :
|
||
item.status === '작업중' ? 'bg-blue-100 text-blue-700' :
|
||
'bg-gray-100 text-gray-700'
|
||
}`}>{item.status}</span>
|
||
</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>
|
||
);
|
||
};
|
||
|
||
// 줌 레벨 상수
|
||
const ZOOM_LEVELS = [50, 75, 100, 125, 150, 200];
|
||
const MIN_ZOOM = 50;
|
||
const MAX_ZOOM = 200;
|
||
|
||
export const InspectionModal = ({ isOpen, onClose, document: doc, documentItem }: InspectionModalProps) => {
|
||
// 줌 상태
|
||
const [zoom, setZoom] = useState(100);
|
||
|
||
// 드래그 상태
|
||
const [isDragging, setIsDragging] = useState(false);
|
||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
|
||
|
||
// refs
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
const contentRef = useRef<HTMLDivElement>(null);
|
||
|
||
// 모달 열릴 때 상태 초기화
|
||
useEffect(() => {
|
||
if (isOpen) {
|
||
setZoom(100);
|
||
setPosition({ x: 0, y: 0 });
|
||
}
|
||
}, [isOpen]);
|
||
|
||
// 줌 인
|
||
const handleZoomIn = useCallback(() => {
|
||
setZoom(prev => {
|
||
const nextIndex = ZOOM_LEVELS.findIndex(z => z > prev);
|
||
return nextIndex !== -1 ? ZOOM_LEVELS[nextIndex] : MAX_ZOOM;
|
||
});
|
||
}, []);
|
||
|
||
// 줌 아웃
|
||
const handleZoomOut = useCallback(() => {
|
||
setZoom(prev => {
|
||
const prevIndex = ZOOM_LEVELS.slice().reverse().findIndex(z => z < prev);
|
||
const index = prevIndex !== -1 ? ZOOM_LEVELS.length - 1 - prevIndex : 0;
|
||
return ZOOM_LEVELS[index] || MIN_ZOOM;
|
||
});
|
||
}, []);
|
||
|
||
// 줌 리셋
|
||
const handleZoomReset = useCallback(() => {
|
||
setZoom(100);
|
||
setPosition({ x: 0, y: 0 });
|
||
}, []);
|
||
|
||
// 마우스 드래그 시작
|
||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||
if (zoom > 100) {
|
||
setIsDragging(true);
|
||
setStartPos({ x: e.clientX - position.x, y: e.clientY - position.y });
|
||
}
|
||
}, [zoom, position]);
|
||
|
||
// 마우스 이동
|
||
const handleMouseMove = useCallback((e: React.MouseEvent) => {
|
||
if (!isDragging) return;
|
||
setPosition({
|
||
x: e.clientX - startPos.x,
|
||
y: e.clientY - startPos.y,
|
||
});
|
||
}, [isDragging, startPos]);
|
||
|
||
// 마우스 드래그 종료
|
||
const handleMouseUp = useCallback(() => {
|
||
setIsDragging(false);
|
||
}, []);
|
||
|
||
// 터치 드래그 시작
|
||
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
||
if (zoom > 100 && e.touches.length === 1) {
|
||
setIsDragging(true);
|
||
setStartPos({
|
||
x: e.touches[0].clientX - position.x,
|
||
y: e.touches[0].clientY - position.y,
|
||
});
|
||
}
|
||
}, [zoom, position]);
|
||
|
||
// 터치 이동
|
||
const handleTouchMove = useCallback((e: React.TouchEvent) => {
|
||
if (!isDragging || e.touches.length !== 1) return;
|
||
setPosition({
|
||
x: e.touches[0].clientX - startPos.x,
|
||
y: e.touches[0].clientY - startPos.y,
|
||
});
|
||
}, [isDragging, startPos]);
|
||
|
||
// 터치 종료
|
||
const handleTouchEnd = useCallback(() => {
|
||
setIsDragging(false);
|
||
}, []);
|
||
|
||
if (!doc) return null;
|
||
|
||
const docInfo = DOCUMENT_INFO[doc.type] || { label: doc.title, hasTemplate: false, color: 'text-gray-600' };
|
||
const subtitle = documentItem
|
||
? `${docInfo.label} - ${documentItem.date}${documentItem.code ? ` 로트: ${documentItem.code}` : ''}`
|
||
: docInfo.label;
|
||
|
||
const handlePrint = () => {
|
||
window.print();
|
||
};
|
||
|
||
// 중간검사 성적서 서브타입에 따른 렌더링
|
||
const renderReportDocument = () => {
|
||
const subType = documentItem?.subType;
|
||
switch (subType) {
|
||
case 'screen':
|
||
return <ScreenInspectionDocument />;
|
||
case 'bending':
|
||
return <BendingInspectionDocument />;
|
||
case 'slat':
|
||
return <SlatInspectionDocument />;
|
||
case 'jointbar':
|
||
return <JointbarInspectionDocument />;
|
||
default:
|
||
// 서브타입이 없으면 기본 스크린 문서
|
||
return <ScreenInspectionDocument />;
|
||
}
|
||
};
|
||
|
||
// 품질관리서 PDF 업로드 핸들러
|
||
const handleQualityFileUpload = (file: File) => {
|
||
console.log('[InspectionModal] 품질관리서 PDF 업로드:', file.name);
|
||
// TODO: 실제 API 연동 시 파일 업로드 로직 구현
|
||
};
|
||
|
||
const handleQualityFileDelete = () => {
|
||
console.log('[InspectionModal] 품질관리서 PDF 삭제');
|
||
// TODO: 실제 API 연동 시 파일 삭제 로직 구현
|
||
};
|
||
|
||
// 문서 타입에 따른 컨텐츠 렌더링
|
||
const renderDocumentContent = () => {
|
||
switch (doc.type) {
|
||
case 'order':
|
||
return <OrderDocument />;
|
||
case 'log':
|
||
return <WorkLogDocument />;
|
||
case 'confirmation':
|
||
return <DeliveryConfirmation data={MOCK_SHIPMENT_DETAIL} />;
|
||
case 'shipping':
|
||
return <ShippingSlip data={MOCK_SHIPMENT_DETAIL} />;
|
||
case 'import':
|
||
return <ImportInspectionDocument />;
|
||
case 'product':
|
||
return <ProductInspectionDocument />;
|
||
case 'report':
|
||
return renderReportDocument();
|
||
case 'quality':
|
||
// 품질관리서는 PDF 업로드/뷰어 사용
|
||
return (
|
||
<QualityDocumentUploader
|
||
onFileUpload={handleQualityFileUpload}
|
||
onFileDelete={handleQualityFileDelete}
|
||
/>
|
||
);
|
||
default:
|
||
// 양식 대기 중인 문서
|
||
return <PlaceholderDocument docType={doc.type} docItem={documentItem} />;
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Dialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||
<DialogContent className="w-[95vw] max-w-[1200px] sm:max-w-[1200px] h-[90vh] p-0 overflow-hidden bg-gray-50 flex flex-col">
|
||
<DialogHeader className="p-4 bg-white border-b border-gray-200 flex flex-row items-center justify-between space-y-0 shrink-0">
|
||
<div>
|
||
<DialogTitle className="text-lg font-bold text-gray-800">{doc.title}</DialogTitle>
|
||
<p className="text-xs text-gray-500 mt-1">{subtitle}</p>
|
||
</div>
|
||
</DialogHeader>
|
||
|
||
{/* Toolbar */}
|
||
<div className="flex items-center justify-between px-4 py-2 bg-white border-b border-gray-100 shrink-0 flex-wrap gap-2">
|
||
<div className="flex items-center gap-1 sm:gap-2">
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-8 gap-1 text-xs px-2 sm:px-3"
|
||
onClick={handleZoomOut}
|
||
disabled={zoom <= MIN_ZOOM}
|
||
>
|
||
<ZoomOut size={14} />
|
||
<span className="hidden sm:inline">축소</span>
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-8 gap-1 text-xs px-2 sm:px-3"
|
||
onClick={handleZoomIn}
|
||
disabled={zoom >= MAX_ZOOM}
|
||
>
|
||
<ZoomIn size={14} />
|
||
<span className="hidden sm:inline">확대</span>
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-8 gap-1 text-xs px-2 sm:px-3"
|
||
onClick={handleZoomReset}
|
||
>
|
||
<Maximize2 size={14} />
|
||
<span className="hidden sm:inline">맞춤</span>
|
||
</Button>
|
||
<span className="text-xs font-mono text-gray-600 bg-gray-100 px-2 py-1 rounded min-w-[48px] text-center">
|
||
{zoom}%
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center gap-1 sm:gap-2">
|
||
<Button variant="outline" size="sm" className="h-8 gap-1 text-xs px-2 sm:px-3" onClick={handlePrint}>
|
||
<Printer size={14} />
|
||
<span className="hidden sm:inline">인쇄</span>
|
||
</Button>
|
||
<Button size="sm" className="h-8 bg-green-600 hover:bg-green-700 text-white gap-1 text-xs px-2 sm:px-3">
|
||
<Download size={14} />
|
||
<span className="hidden sm:inline">다운로드</span>
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Content Area - 줌/드래그 가능한 영역 */}
|
||
<div
|
||
ref={containerRef}
|
||
className="flex-1 overflow-auto bg-gray-100 relative"
|
||
onMouseDown={handleMouseDown}
|
||
onMouseMove={handleMouseMove}
|
||
onMouseUp={handleMouseUp}
|
||
onMouseLeave={handleMouseUp}
|
||
onTouchStart={handleTouchStart}
|
||
onTouchMove={handleTouchMove}
|
||
onTouchEnd={handleTouchEnd}
|
||
style={{ cursor: zoom > 100 ? (isDragging ? 'grabbing' : 'grab') : 'default' }}
|
||
>
|
||
<div
|
||
ref={contentRef}
|
||
className="p-4 origin-top-left transition-transform duration-150 ease-out"
|
||
style={{
|
||
transform: `scale(${zoom / 100}) translate(${position.x / (zoom / 100)}px, ${position.y / (zoom / 100)}px)`,
|
||
minWidth: '800px',
|
||
}}
|
||
>
|
||
{renderDocumentContent()}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 모바일 줌 힌트 */}
|
||
{zoom === 100 && (
|
||
<div className="sm:hidden absolute bottom-20 left-1/2 -translate-x-1/2 bg-black/70 text-white text-xs px-3 py-1.5 rounded-full">
|
||
확대 후 드래그로 이동
|
||
</div>
|
||
)}
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
};
|