2025-12-04 20:52:42 +09:00
|
|
|
|
/**
|
|
|
|
|
|
* 견적서 (Quote Document)
|
|
|
|
|
|
*
|
2026-01-22 17:21:42 +09:00
|
|
|
|
* 공통 컴포넌트 사용:
|
|
|
|
|
|
* - DocumentHeader: simple 레이아웃
|
|
|
|
|
|
* - SignatureSection: 서명/도장 영역
|
2025-12-04 20:52:42 +09:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { QuoteFormData } from "./QuoteRegistration";
|
2026-01-06 21:20:49 +09:00
|
|
|
|
import type { CompanyFormData } from "@/components/settings/CompanyInfoManagement/types";
|
2026-01-22 17:21:42 +09:00
|
|
|
|
import { DocumentHeader, SignatureSection } from "@/components/document-system";
|
2025-12-04 20:52:42 +09:00
|
|
|
|
|
|
|
|
|
|
interface QuoteDocumentProps {
|
|
|
|
|
|
quote: QuoteFormData;
|
2026-01-06 21:20:49 +09:00
|
|
|
|
companyInfo?: CompanyFormData | null;
|
2025-12-04 20:52:42 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-06 21:20:49 +09:00
|
|
|
|
export function QuoteDocument({ quote, companyInfo }: QuoteDocumentProps) {
|
2025-12-04 20:52:42 +09:00
|
|
|
|
const formatAmount = (amount: number | undefined) => {
|
|
|
|
|
|
if (amount === undefined || amount === null) return '0';
|
|
|
|
|
|
return amount.toLocaleString('ko-KR');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const formatDate = (dateStr: string) => {
|
|
|
|
|
|
if (!dateStr) return '';
|
|
|
|
|
|
const date = new Date(dateStr);
|
|
|
|
|
|
return `${date.getFullYear()}년 ${String(date.getMonth() + 1).padStart(2, '0')}월 ${String(date.getDate()).padStart(2, '0')}일`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 품목 내역 생성
|
|
|
|
|
|
const quoteItems = quote.items?.map((item, index) => ({
|
|
|
|
|
|
no: index + 1,
|
|
|
|
|
|
itemName: item.productName || '스크린셔터',
|
|
|
|
|
|
spec: `${item.openWidth}×${item.openHeight}`,
|
|
|
|
|
|
quantity: item.quantity || 1,
|
2026-01-06 21:20:49 +09:00
|
|
|
|
unit: item.unit || '', // 각 품목의 단위 사용, 없으면 빈 문자열
|
2025-12-04 20:52:42 +09:00
|
|
|
|
unitPrice: item.unitPrice || 0,
|
|
|
|
|
|
totalPrice: item.totalAmount || 0,
|
|
|
|
|
|
})) || [];
|
|
|
|
|
|
|
|
|
|
|
|
// 합계 계산
|
|
|
|
|
|
const subtotal = quoteItems.reduce((sum, item) => sum + item.totalPrice, 0);
|
|
|
|
|
|
const vat = Math.round(subtotal * 0.1);
|
|
|
|
|
|
const totalAmount = subtotal + vat;
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<style>{`
|
|
|
|
|
|
@media print {
|
|
|
|
|
|
@page {
|
|
|
|
|
|
size: A4 portrait;
|
|
|
|
|
|
margin: 15mm;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
body {
|
|
|
|
|
|
background: white !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.print\\:hidden {
|
|
|
|
|
|
display: none !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#quote-document-content {
|
|
|
|
|
|
background: white !important;
|
|
|
|
|
|
padding: 0 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
table {
|
|
|
|
|
|
page-break-inside: avoid;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 공문서 스타일 */
|
|
|
|
|
|
.official-doc {
|
|
|
|
|
|
font-family: 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
color: #000;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 17:21:42 +09:00
|
|
|
|
/* 헤더 스타일은 공통 컴포넌트 사용 */
|
2025-12-04 20:52:42 +09:00
|
|
|
|
|
|
|
|
|
|
.info-box {
|
|
|
|
|
|
border: 2px solid #000;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-box-header {
|
|
|
|
|
|
background: #f0f0f0;
|
|
|
|
|
|
border-bottom: 2px solid #000;
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-box-content {
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-table {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-table th {
|
|
|
|
|
|
background: #f8f8f8;
|
|
|
|
|
|
border: 1px solid #999;
|
|
|
|
|
|
padding: 8px 10px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
width: 100px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.info-table td {
|
|
|
|
|
|
border: 1px solid #999;
|
|
|
|
|
|
padding: 8px 10px;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.amount-box {
|
|
|
|
|
|
border: 3px double #000;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin: 30px 0;
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.amount-label {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.amount-value {
|
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: #000;
|
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.amount-note {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
|
background: #000;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 10px 15px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
margin: 30px 0 15px 0;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-table {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
border: 2px solid #000;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-table thead th {
|
|
|
|
|
|
background: #e8e8e8;
|
|
|
|
|
|
border: 1px solid #666;
|
|
|
|
|
|
padding: 10px 6px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-table tbody td {
|
|
|
|
|
|
border: 1px solid #999;
|
|
|
|
|
|
padding: 8px 6px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-table tbody tr:hover {
|
|
|
|
|
|
background: #f9f9f9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.detail-table tfoot td {
|
|
|
|
|
|
background: #f0f0f0;
|
|
|
|
|
|
border: 1px solid #666;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-22 17:21:42 +09:00
|
|
|
|
/* 서명/도장 스타일은 공통 컴포넌트 사용 */
|
2025-12-04 20:52:42 +09:00
|
|
|
|
|
|
|
|
|
|
.footer-note {
|
|
|
|
|
|
margin-top: 40px;
|
|
|
|
|
|
padding-top: 20px;
|
|
|
|
|
|
border-top: 1px solid #ccc;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
`}</style>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 견적서 내용 */}
|
|
|
|
|
|
<div id="quote-document-content" className="official-doc p-12 print:p-8">
|
2026-01-22 17:21:42 +09:00
|
|
|
|
{/* 문서 헤더 (공통 컴포넌트) */}
|
|
|
|
|
|
<DocumentHeader
|
|
|
|
|
|
title="견 적 서"
|
|
|
|
|
|
documentCode={quote.id || 'Q-XXXXXX'}
|
|
|
|
|
|
subtitle={`작성일자: ${formatDate(quote.registrationDate || '')}`}
|
|
|
|
|
|
layout="simple"
|
|
|
|
|
|
className="border-b-[3px] border-double border-black pb-5 mb-8"
|
|
|
|
|
|
/>
|
2025-12-04 20:52:42 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 수요자 정보 */}
|
|
|
|
|
|
<div className="info-box">
|
|
|
|
|
|
<div className="info-box-header">수 요 자</div>
|
|
|
|
|
|
<div className="info-box-content">
|
|
|
|
|
|
<table className="info-table">
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>업체명</th>
|
|
|
|
|
|
<td colSpan={3}>{quote.clientName || '-'}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>프로젝트명</th>
|
|
|
|
|
|
<td>{quote.siteName || '-'}</td>
|
|
|
|
|
|
<th>담당자</th>
|
|
|
|
|
|
<td>{quote.manager || '-'}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>견적일자</th>
|
|
|
|
|
|
<td>{formatDate(quote.registrationDate || '')}</td>
|
|
|
|
|
|
<th>연락처</th>
|
|
|
|
|
|
<td>{quote.contact || '-'}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>유효기간</th>
|
|
|
|
|
|
<td colSpan={3}>{formatDate(quote.dueDate || '')}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 공급자 정보 */}
|
|
|
|
|
|
<div className="info-box">
|
|
|
|
|
|
<div className="info-box-header">공 급 자</div>
|
|
|
|
|
|
<div className="info-box-content">
|
|
|
|
|
|
<table className="info-table">
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>상호</th>
|
2026-01-06 21:20:49 +09:00
|
|
|
|
<td>{companyInfo?.companyName || '-'}</td>
|
2025-12-04 20:52:42 +09:00
|
|
|
|
<th>사업자등록번호</th>
|
2026-01-06 21:20:49 +09:00
|
|
|
|
<td>{companyInfo?.businessNumber || '-'}</td>
|
2025-12-04 20:52:42 +09:00
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>대표자</th>
|
2026-01-06 21:20:49 +09:00
|
|
|
|
<td>{companyInfo?.representativeName || '-'}</td>
|
2025-12-04 20:52:42 +09:00
|
|
|
|
<th>업태</th>
|
2026-01-06 21:20:49 +09:00
|
|
|
|
<td>{companyInfo?.businessType || '-'}</td>
|
2025-12-04 20:52:42 +09:00
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>종목</th>
|
2026-01-06 21:20:49 +09:00
|
|
|
|
<td colSpan={3}>{companyInfo?.businessCategory || '-'}</td>
|
2025-12-04 20:52:42 +09:00
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>사업장주소</th>
|
2026-01-06 21:20:49 +09:00
|
|
|
|
<td colSpan={3}>{companyInfo?.address || '-'}</td>
|
2025-12-04 20:52:42 +09:00
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>전화</th>
|
2026-01-06 21:20:49 +09:00
|
|
|
|
<td>{companyInfo?.managerPhone || '-'}</td>
|
|
|
|
|
|
<th>이메일</th>
|
|
|
|
|
|
<td>{companyInfo?.email || '-'}</td>
|
2025-12-04 20:52:42 +09:00
|
|
|
|
</tr>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 총 견적금액 */}
|
|
|
|
|
|
<div className="amount-box">
|
|
|
|
|
|
<div className="amount-label">총 견적금액</div>
|
|
|
|
|
|
<div className="amount-value">₩ {formatAmount(totalAmount)}</div>
|
|
|
|
|
|
<div className="amount-note">※ 부가가치세 포함</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 제품구성 정보 */}
|
|
|
|
|
|
{quote.items && quote.items.length > 0 && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="section-title">제 품 구 성 정 보</div>
|
|
|
|
|
|
<div className="info-box" style={{ marginTop: '15px' }}>
|
|
|
|
|
|
<div className="info-box-content">
|
|
|
|
|
|
<table className="info-table">
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>모델</th>
|
|
|
|
|
|
<td>{quote.items[0]?.productName || '스크린셔터'}</td>
|
|
|
|
|
|
<th>총 수량</th>
|
2026-01-06 21:20:49 +09:00
|
|
|
|
<td>{quote.items[0]?.quantity || ''}{quote.unitSymbol ? ` ${quote.unitSymbol}` : ''}</td>
|
2025-12-04 20:52:42 +09:00
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>오픈사이즈</th>
|
|
|
|
|
|
<td>{quote.items[0]?.openWidth}×{quote.items[0]?.openHeight}</td>
|
|
|
|
|
|
<th>설치유형</th>
|
|
|
|
|
|
<td>{quote.items[0]?.installType || '-'}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 품목 내역 */}
|
|
|
|
|
|
{quoteItems.length > 0 && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="section-title">품 목 내 역</div>
|
|
|
|
|
|
<table className="detail-table">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th style={{ width: '40px' }}>No.</th>
|
|
|
|
|
|
<th style={{ width: '200px' }}>품목명</th>
|
|
|
|
|
|
<th style={{ width: '150px' }}>규격</th>
|
|
|
|
|
|
<th style={{ width: '70px' }}>수량</th>
|
|
|
|
|
|
<th style={{ width: '50px' }}>단위</th>
|
|
|
|
|
|
<th style={{ width: '110px' }}>단가</th>
|
|
|
|
|
|
<th style={{ width: '130px' }}>금액</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
{quoteItems.map((item) => (
|
|
|
|
|
|
<tr key={item.no}>
|
|
|
|
|
|
<td style={{ textAlign: 'center' }}>{item.no}</td>
|
|
|
|
|
|
<td>{item.itemName}</td>
|
|
|
|
|
|
<td style={{ textAlign: 'center' }}>{item.spec || '-'}</td>
|
|
|
|
|
|
<td style={{ textAlign: 'right' }}>{item.quantity}</td>
|
|
|
|
|
|
<td style={{ textAlign: 'center' }}>{item.unit}</td>
|
|
|
|
|
|
<td style={{ textAlign: 'right' }}>{formatAmount(item.unitPrice)}</td>
|
|
|
|
|
|
<td style={{ textAlign: 'right', fontWeight: '600' }}>{formatAmount(item.totalPrice)}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
<tfoot>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td colSpan={6} style={{ textAlign: 'right', padding: '12px' }}>공급가액 합계</td>
|
|
|
|
|
|
<td style={{ textAlign: 'right', padding: '12px' }}>{formatAmount(subtotal)}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td colSpan={6} style={{ textAlign: 'right', padding: '12px' }}>부가가치세 (10%)</td>
|
|
|
|
|
|
<td style={{ textAlign: 'right', padding: '12px' }}>{formatAmount(vat)}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td colSpan={6} style={{ textAlign: 'right', padding: '12px', background: '#e0e0e0' }}>총 견적금액</td>
|
|
|
|
|
|
<td style={{ textAlign: 'right', padding: '12px', background: '#e0e0e0', fontSize: '15px', fontWeight: '700' }}>
|
|
|
|
|
|
{formatAmount(totalAmount)}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</tfoot>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 비고사항 */}
|
|
|
|
|
|
{quote.remarks && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="section-title">비 고 사 항</div>
|
|
|
|
|
|
<div style={{
|
|
|
|
|
|
border: '2px solid #000',
|
|
|
|
|
|
padding: '15px',
|
|
|
|
|
|
minHeight: '100px',
|
|
|
|
|
|
whiteSpace: 'pre-wrap',
|
|
|
|
|
|
fontSize: '13px',
|
|
|
|
|
|
lineHeight: '1.8',
|
|
|
|
|
|
marginTop: '15px'
|
|
|
|
|
|
}}>
|
|
|
|
|
|
{quote.remarks}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-01-22 17:21:42 +09:00
|
|
|
|
{/* 서명란 (공통 컴포넌트) */}
|
|
|
|
|
|
<SignatureSection
|
|
|
|
|
|
label="상기와 같이 견적합니다."
|
|
|
|
|
|
date={formatDate(quote.registrationDate || '')}
|
|
|
|
|
|
companyName={companyInfo?.companyName || '-'}
|
|
|
|
|
|
role="공급자"
|
|
|
|
|
|
showStamp={true}
|
|
|
|
|
|
/>
|
2025-12-04 20:52:42 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 하단 안내사항 */}
|
|
|
|
|
|
<div className="footer-note">
|
|
|
|
|
|
<p style={{ fontWeight: '600', marginBottom: '8px' }}>【 유의사항 】</p>
|
|
|
|
|
|
<p>1. 본 견적서는 {formatDate(quote.registrationDate || '')} 기준으로 작성되었으며, 자재 가격 변동 시 조정될 수 있습니다.</p>
|
|
|
|
|
|
<p>2. 견적 유효기간은 {formatDate(quote.dueDate || '')}까지이며, 기간 경과 시 재견적이 필요합니다.</p>
|
|
|
|
|
|
<p>3. 제작 사양 및 수량 변경 시 견적 금액이 변동될 수 있습니다.</p>
|
|
|
|
|
|
<p>4. 현장 여건에 따라 추가 비용이 발생할 수 있습니다.</p>
|
|
|
|
|
|
<p style={{ marginTop: '12px', textAlign: 'center', fontWeight: '600' }}>
|
2026-01-06 21:20:49 +09:00
|
|
|
|
문의: {companyInfo?.managerName || quote.writer || '담당자'} | {companyInfo?.managerPhone || '-'}
|
2025-12-04 20:52:42 +09:00
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|