Files
sam-react-prod/src/components/quotes/QuoteDocument.tsx
유병철 f344dc7d00 refactor(WEB): 회계/견적/설정/생산 등 전반적 코드 개선 및 공통화 2차
- 회계 모듈 전면 개선: 청구/입금/출금/매입/매출/세금계산서/일반전표/거래처원장 등
- 견적 모듈 금액 포맷/할인/수식/미리보기 등 코드 정리
- 설정 모듈: 계정관리/직급/직책/권한 상세 간소화
- 생산 모듈: 작업지시서/작업자화면/검수 문서 코드 정리
- UniversalListPage 엑셀 다운로드 및 필터 기능 확장
- 대시보드/게시판/수주 등 날짜 유틸 공통화 적용
- claudedocs 문서 인덱스 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 10:45:47 +09:00

410 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 "./types";
import type { CompanyFormData } from "@/components/settings/CompanyInfoManagement/types";
import { DocumentHeader, SignatureSection } from "@/components/document-system";
import { formatNumber } from "@/lib/utils/amount";
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 formatNumber(amount);
};
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>
</>
);
}