Files
sam-react-prod/src/components/quotes/QuoteDocument.tsx
유병철 269b901e64 refactor: UI 컴포넌트 추상화 및 입금/출금 등록 버튼 추가
- 입금관리, 출금관리 리스트에 등록 버튼 추가
- skeleton, confirm-dialog, empty-state, status-badge UI 컴포넌트 추가
- document-system 컴포넌트 추상화 (ApprovalLine, DocumentHeader 등)
- 여러 페이지 컴포넌트 리팩토링 및 코드 정리

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 17:21:42 +09:00

409 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 견적서 (Quote Document)
*
* 공통 컴포넌트 사용:
* - DocumentHeader: simple 레이아웃
* - SignatureSection: 서명/도장 영역
*/
import { QuoteFormData } from "./QuoteRegistration";
import type { CompanyFormData } from "@/components/settings/CompanyInfoManagement/types";
import { DocumentHeader, SignatureSection } from "@/components/document-system";
interface QuoteDocumentProps {
quote: QuoteFormData;
companyInfo?: CompanyFormData | null;
}
export function QuoteDocument({ quote, companyInfo }: QuoteDocumentProps) {
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,
unit: item.unit || '', // 각 품목의 단위 사용, 없으면 빈 문자열
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;
}
/* 헤더 스타일은 공통 컴포넌트 사용 */
.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;
}
/* 서명/도장 스타일은 공통 컴포넌트 사용 */
.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">
{/* 문서 헤더 (공통 컴포넌트) */}
<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"
/>
{/* 수요자 정보 */}
<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>
<td>{companyInfo?.companyName || '-'}</td>
<th></th>
<td>{companyInfo?.businessNumber || '-'}</td>
</tr>
<tr>
<th></th>
<td>{companyInfo?.representativeName || '-'}</td>
<th></th>
<td>{companyInfo?.businessType || '-'}</td>
</tr>
<tr>
<th></th>
<td colSpan={3}>{companyInfo?.businessCategory || '-'}</td>
</tr>
<tr>
<th></th>
<td colSpan={3}>{companyInfo?.address || '-'}</td>
</tr>
<tr>
<th></th>
<td>{companyInfo?.managerPhone || '-'}</td>
<th></th>
<td>{companyInfo?.email || '-'}</td>
</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>
<td>{quote.items[0]?.quantity || ''}{quote.unitSymbol ? ` ${quote.unitSymbol}` : ''}</td>
</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>
</>
)}
{/* 서명란 (공통 컴포넌트) */}
<SignatureSection
label="상기와 같이 견적합니다."
date={formatDate(quote.registrationDate || '')}
companyName={companyInfo?.companyName || '-'}
role="공급자"
showStamp={true}
/>
{/* 하단 안내사항 */}
<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' }}>
: {companyInfo?.managerName || quote.writer || '담당자'} | {companyInfo?.managerPhone || '-'}
</p>
</div>
</div>
</>
);
}