feat(WEB): 입력 컴포넌트 공통화 및 UI 개선

- 숫자/통화/전화번호/사업자번호 등 특수 입력 컴포넌트 추가
- MobileCard 컴포넌트 통합 (ListMobileCard 제거)
- IntegratedListTemplateV2 페이지네이션 버그 수정 (NaN 이슈)
- IntegratedDetailTemplate 타이틀 중복 수정
- 문서 시스템 컴포넌트 추가
- 헤더 벨 아이콘 포커스 스타일 개선

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-21 20:56:17 +09:00
parent cfa72fe19b
commit 835c06ce94
190 changed files with 8575 additions and 2354 deletions

View File

@@ -59,4 +59,4 @@ export default function OrderDetailPage({ params }: OrderDetailPageProps) {
}
return <OrderDetailForm mode={mode} orderId={id} initialData={data} />;
}
}

View File

@@ -60,4 +60,4 @@ export default function ContractDetailPage({ params }: ContractDetailPageProps)
initialData={data}
/>
);
}
}

View File

@@ -62,4 +62,4 @@ export default function HandoverReportDetailPage({ params }: HandoverReportDetai
initialData={data}
/>
);
}
}

View File

@@ -0,0 +1,411 @@
"use client";
import React from 'react';
import { AlertCircle } from 'lucide-react';
import { DocumentViewer } from '@/components/document-system';
import { Document, DocumentItem } from '../types';
import { MOCK_ORDER_DATA, 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 InspectionModalV2Props {
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>
);
};
// 수주서 문서 컴포넌트 (간소화 버전)
const OrderDocument = () => {
const data = MOCK_ORDER_DATA;
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.process.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>
);
};
/**
* InspectionModal V2
* - DocumentViewer 시스템 사용
* - 기존 문서 렌더링 로직 유지
*/
export const InspectionModalV2 = ({
isOpen,
onClose,
document: doc,
documentItem,
}: InspectionModalV2Props) => {
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;
// 품질관리서 PDF 업로드 핸들러
const handleQualityFileUpload = (file: File) => {
console.log('[InspectionModalV2] 품질관리서 PDF 업로드:', file.name);
};
const handleQualityFileDelete = () => {
console.log('[InspectionModalV2] 품질관리서 PDF 삭제');
};
// 중간검사 성적서 서브타입에 따른 렌더링
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 />;
}
};
// 문서 타입에 따른 컨텐츠 렌더링
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':
return (
<QualityDocumentUploader
onFileUpload={handleQualityFileUpload}
onFileDelete={handleQualityFileDelete}
/>
);
default:
return <PlaceholderDocument docType={doc.type} docItem={documentItem} />;
}
};
// 다운로드 핸들러 (TODO: 실제 구현)
const handleDownload = () => {
console.log('[InspectionModalV2] 다운로드 요청:', doc.type);
};
return (
<DocumentViewer
title={doc.title}
subtitle={subtitle}
preset="inspection"
open={isOpen}
onOpenChange={(open) => !open && onClose()}
onDownload={handleDownload}
>
{renderDocumentContent()}
</DocumentViewer>
);
};

View File

@@ -6,7 +6,8 @@ import { Filters } from './components/Filters';
import { ReportList } from './components/ReportList';
import { RouteList } from './components/RouteList';
import { DocumentList } from './components/DocumentList';
import { InspectionModal } from './components/InspectionModal';
// import { InspectionModal } from './components/InspectionModal';
import { InspectionModalV2 as InspectionModal } from './components/InspectionModalV2';
import { DayTabs } from './components/DayTabs';
import { Day1ChecklistPanel } from './components/Day1ChecklistPanel';
import { Day1DocumentSection } from './components/Day1DocumentSection';

View File

@@ -45,7 +45,7 @@ import {
TableCell,
} from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import { ListMobileCard, InfoField } from "@/components/organisms/ListMobileCard";
import { ListMobileCard, InfoField } from "@/components/organisms/MobileCard";
import {
AlertDialog,
AlertDialogAction,

View File

@@ -13,6 +13,7 @@ import { useState, useEffect, useCallback } from "react";
import { useRouter, useParams } from "next/navigation";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { PhoneInput } from "@/components/ui/phone-input";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -403,10 +404,10 @@ export default function OrderEditPage() {
<Label>
<span className="text-red-500">*</span>
</Label>
<Input
<PhoneInput
value={form.receiverContact}
onChange={(e) =>
setForm({ ...form, receiverContact: e.target.value })
onChange={(value) =>
setForm({ ...form, receiverContact: value })
}
/>
</div>

View File

@@ -40,7 +40,7 @@ import {
} from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import { formatAmount, formatAmountManwon } from "@/utils/formatAmount";
import { ListMobileCard, InfoField } from "@/components/organisms/ListMobileCard";
import { ListMobileCard, InfoField } from "@/components/organisms/MobileCard";
import {
AlertDialog,
AlertDialogAction,

View File

@@ -47,7 +47,7 @@ import {
type TableColumn,
} from "@/components/templates/UniversalListPage";
import { BadgeSm } from "@/components/atoms/BadgeSm";
import { ListMobileCard, InfoField } from "@/components/organisms/ListMobileCard";
import { ListMobileCard, InfoField } from "@/components/organisms/MobileCard";
// 생산지시 상태 타입
type ProductionOrderStatus =

View File

@@ -37,22 +37,13 @@ import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { DocumentViewer } from "@/components/document-system";
import {
FileText,
Edit,
List,
Printer,
FileOutput,
Download,
Mail,
MessageCircle,
X,
FileCheck,
Package,
ChevronDown,
@@ -596,267 +587,83 @@ export default function QuoteDetailPage() {
)}
{/* 견적서 다이얼로그 */}
<Dialog open={isQuoteDocumentOpen} onOpenChange={setIsQuoteDocumentOpen}>
<DialogContent className="max-w-[95vw] md:max-w-[800px] lg:max-w-[900px] h-[95vh] flex flex-col p-0">
<DialogHeader className="flex-shrink-0 px-6 py-4 border-b">
<DialogTitle></DialogTitle>
</DialogHeader>
{/* 버튼 영역 */}
<div className="flex-shrink-0 px-6 py-3 border-b bg-muted/30 flex flex-wrap gap-2 justify-between items-center">
<div className="flex gap-2 flex-wrap">
<Button
size="sm"
variant="default"
className="bg-red-600 hover:bg-red-700"
onClick={() => {
toast.info('인쇄 대화상자에서 "PDF로 저장" 옵션을 선택하세요.');
window.print();
}}
>
<Download className="w-4 h-4 mr-2" />
PDF
</Button>
<Button
size="sm"
variant="default"
className="bg-blue-600 hover:bg-blue-700"
onClick={handleSendEmail}
disabled={isProcessing}
>
<Mail className="w-4 h-4 mr-2" />
</Button>
<Button
size="sm"
variant="default"
className="bg-purple-600 hover:bg-purple-700"
onClick={() => toast.info("팩스 전송 기능은 준비 중입니다.")}
>
<FileOutput className="w-4 h-4 mr-2" />
</Button>
<Button
size="sm"
variant="default"
className="bg-yellow-500 hover:bg-yellow-600"
onClick={handleSendKakao}
disabled={isProcessing}
>
<MessageCircle className="w-4 h-4 mr-2" />
</Button>
<Button size="sm" variant="outline" onClick={() => window.print()}>
<Printer className="w-4 h-4 mr-2" />
</Button>
</div>
<Button
size="sm"
variant="outline"
onClick={() => setIsQuoteDocumentOpen(false)}
>
<X className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 문서 영역 */}
<div className="flex-1 overflow-y-auto bg-gray-100 p-4">
<div className="max-w-[210mm] mx-auto bg-white shadow-lg rounded-lg p-8">
<QuoteDocument quote={quote} companyInfo={companyInfo} />
</div>
</div>
</DialogContent>
</Dialog>
<DocumentViewer
title="견적서"
preset="quote"
open={isQuoteDocumentOpen}
onOpenChange={setIsQuoteDocumentOpen}
onPdf={() => {
toast.info('인쇄 대화상자에서 "PDF로 저장" 옵션을 선택하세요.');
window.print();
}}
onEmail={handleSendEmail}
onFax={() => toast.info("팩스 전송 기능은 준비 중입니다.")}
onKakao={handleSendKakao}
>
<QuoteDocument quote={quote} companyInfo={companyInfo} />
</DocumentViewer>
{/* 산출내역서 다이얼로그 */}
<Dialog
<DocumentViewer
title="산출내역서"
preset="quote"
open={isCalculationReportOpen}
onOpenChange={setIsCalculationReportOpen}
>
<DialogContent className="max-w-[95vw] md:max-w-[800px] lg:max-w-[900px] h-[95vh] flex flex-col p-0 overflow-hidden">
<DialogHeader className="flex-shrink-0 px-6 py-4 border-b">
<DialogTitle></DialogTitle>
</DialogHeader>
{/* 버튼 영역 */}
<div className="flex-shrink-0 px-6 py-3 border-b bg-muted/30 flex flex-wrap gap-2 justify-between items-center">
<div className="flex gap-4 flex-wrap items-center">
<div className="flex gap-2 flex-wrap">
<Button
size="sm"
variant="default"
className="bg-red-600 hover:bg-red-700"
onClick={() => {
toast.info('인쇄 대화상자에서 "PDF로 저장" 옵션을 선택하세요.');
window.print();
}}
>
<Download className="w-4 h-4 mr-2" />
PDF
</Button>
<Button
size="sm"
variant="default"
className="bg-blue-600 hover:bg-blue-700"
onClick={handleSendEmail}
disabled={isProcessing}
>
<Mail className="w-4 h-4 mr-2" />
</Button>
<Button
size="sm"
variant="default"
className="bg-purple-600 hover:bg-purple-700"
onClick={() => toast.info("팩스 전송 기능은 준비 중입니다.")}
>
<FileOutput className="w-4 h-4 mr-2" />
</Button>
<Button
size="sm"
variant="default"
className="bg-yellow-500 hover:bg-yellow-600"
onClick={handleSendKakao}
disabled={isProcessing}
>
<MessageCircle className="w-4 h-4 mr-2" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => window.print()}
>
<Printer className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 표시 옵션 체크박스 */}
<div className="flex gap-4 pl-4 border-l">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={showDetailedBreakdown}
onChange={(e) => setShowDetailedBreakdown(e.target.checked)}
className="w-4 h-4 rounded border-gray-300"
/>
<span className="text-sm"></span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={showMaterialList}
onChange={(e) => setShowMaterialList(e.target.checked)}
className="w-4 h-4 rounded border-gray-300"
/>
<span className="text-sm"> </span>
</label>
</div>
</div>
<Button
size="sm"
variant="outline"
onClick={() => setIsCalculationReportOpen(false)}
>
<X className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 문서 영역 */}
<div className="flex-1 overflow-y-auto bg-gray-100 p-4">
<div className="max-w-[210mm] mx-auto bg-white shadow-lg rounded-lg p-8">
<QuoteCalculationReport
quote={quote}
companyInfo={companyInfo}
documentType="견적산출내역서"
showDetailedBreakdown={showDetailedBreakdown}
showMaterialList={showMaterialList}
onPdf={() => {
toast.info('인쇄 대화상자에서 "PDF로 저장" 옵션을 선택하세요.');
window.print();
}}
onEmail={handleSendEmail}
onFax={() => toast.info("팩스 전송 기능은 준비 중입니다.")}
onKakao={handleSendKakao}
toolbarExtra={
<>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={showDetailedBreakdown}
onChange={(e) => setShowDetailedBreakdown(e.target.checked)}
className="w-4 h-4 rounded border-gray-300"
/>
</div>
</div>
</DialogContent>
</Dialog>
<span className="text-sm"></span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={showMaterialList}
onChange={(e) => setShowMaterialList(e.target.checked)}
className="w-4 h-4 rounded border-gray-300"
/>
<span className="text-sm"> </span>
</label>
</>
}
>
<QuoteCalculationReport
quote={quote}
companyInfo={companyInfo}
documentType="견적산출내역서"
showDetailedBreakdown={showDetailedBreakdown}
showMaterialList={showMaterialList}
/>
</DocumentViewer>
{/* 발주서 다이얼로그 */}
<Dialog open={isPurchaseOrderOpen} onOpenChange={setIsPurchaseOrderOpen}>
<DialogContent className="max-w-[95vw] md:max-w-[800px] lg:max-w-[900px] h-[95vh] flex flex-col p-0 overflow-hidden">
<DialogHeader className="flex-shrink-0 px-6 py-4 border-b">
<DialogTitle></DialogTitle>
</DialogHeader>
{/* 버튼 영역 */}
<div className="flex-shrink-0 px-6 py-3 border-b bg-muted/30 flex flex-wrap gap-2 justify-between items-center">
<div className="flex gap-2 flex-wrap">
<Button
size="sm"
variant="default"
className="bg-red-600 hover:bg-red-700"
onClick={() => {
toast.info('인쇄 대화상자에서 "PDF로 저장" 옵션을 선택하세요.');
window.print();
}}
>
<Download className="w-4 h-4 mr-2" />
PDF
</Button>
<Button
size="sm"
variant="default"
className="bg-blue-600 hover:bg-blue-700"
onClick={handleSendEmail}
disabled={isProcessing}
>
<Mail className="w-4 h-4 mr-2" />
</Button>
<Button
size="sm"
variant="default"
className="bg-purple-600 hover:bg-purple-700"
onClick={() => toast.info("팩스 전송 기능은 준비 중입니다.")}
>
<FileOutput className="w-4 h-4 mr-2" />
</Button>
<Button
size="sm"
variant="default"
className="bg-yellow-500 hover:bg-yellow-600"
onClick={handleSendKakao}
disabled={isProcessing}
>
<MessageCircle className="w-4 h-4 mr-2" />
</Button>
<Button size="sm" variant="outline" onClick={() => window.print()}>
<Printer className="w-4 h-4 mr-2" />
</Button>
</div>
<Button
size="sm"
variant="outline"
onClick={() => setIsPurchaseOrderOpen(false)}
>
<X className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 문서 영역 */}
<div className="flex-1 overflow-y-auto bg-gray-100 p-4">
<div className="max-w-[210mm] mx-auto bg-white shadow-lg rounded-lg p-8">
<PurchaseOrderDocument quote={quote} companyInfo={companyInfo} />
</div>
</div>
</DialogContent>
</Dialog>
<DocumentViewer
title="발주서"
preset="quote"
open={isPurchaseOrderOpen}
onOpenChange={setIsPurchaseOrderOpen}
onPdf={() => {
toast.info('인쇄 대화상자에서 "PDF로 저장" 옵션을 선택하세요.');
window.print();
}}
onEmail={handleSendEmail}
onFax={() => toast.info("팩스 전송 기능은 준비 중입니다.")}
onKakao={handleSendKakao}
>
<PurchaseOrderDocument quote={quote} companyInfo={companyInfo} />
</DocumentViewer>
</div>
);
}