fix: 품목관리 수정 기능 버그 수정 및 Sales 페이지 추가

## 품목관리 수정 버그 수정
- FG(제품) 수정 시 품목명 반영 안되는 문제 해결
  - productName → name 필드 매핑 추가
  - FG 품목코드 = 품목명 동기화 로직 추가
- Materials(SM, RM, CS) 수정페이지 진입 오류 해결
- UNIQUE 제약조건 위반 오류 해결

## Sales 페이지
- 거래처관리 (client-management-sales-admin) 페이지 구현
- 견적관리 (quote-management) 페이지 구현
- 관련 컴포넌트 및 훅 추가

## 기타
- 회원가입 페이지 차단 처리
- 디버깅용 콘솔 로그 추가 (PUT 요청/응답 확인용)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-12-04 20:52:42 +09:00
parent 42f80e2b16
commit 751e65f59b
52 changed files with 8869 additions and 1088 deletions

View File

@@ -0,0 +1,461 @@
/**
* 견적서 (Quote Document)
*
* - 수요자/공급자 정보
* - 총 견적금액
* - 제품구성 정보
* - 품목 내역 테이블
* - 비용 산출
* - 비고사항
* - 서명란
*/
import { QuoteFormData } from "./QuoteRegistration";
interface QuoteDocumentProps {
quote: QuoteFormData;
}
export function QuoteDocument({ quote }: 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: '개소',
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;
}
.doc-header {
text-align: center;
border-bottom: 3px double #000;
padding-bottom: 20px;
margin-bottom: 30px;
}
.doc-title {
font-size: 26px;
font-weight: 700;
letter-spacing: 2px;
margin-bottom: 12px;
}
.doc-number {
font-size: 14px;
color: #333;
}
.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;
}
.stamp-area {
border: 2px solid #000;
width: 80px;
height: 80px;
display: inline-block;
position: relative;
margin-left: 20px;
}
.stamp-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 10px;
color: #999;
text-align: center;
line-height: 1.3;
}
.footer-note {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #ccc;
font-size: 11px;
color: #666;
line-height: 1.6;
}
.signature-section {
margin-top: 30px;
text-align: right;
}
`}</style>
{/* 견적서 내용 */}
<div id="quote-document-content" className="official-doc p-12 print:p-8">
{/* 문서 헤더 */}
<div className="doc-header">
<div className="doc-title"> </div>
<div className="doc-number">
: {quote.id || 'Q-XXXXXX'} | : {formatDate(quote.registrationDate || '')}
</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 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></td>
<th></th>
<td>139-87-00333</td>
</tr>
<tr>
<th></th>
<td> </td>
<th></th>
<td></td>
</tr>
<tr>
<th></th>
<td colSpan={3}>, , </td>
</tr>
<tr>
<th></th>
<td colSpan={3}> 45-22</td>
</tr>
<tr>
<th></th>
<td>031-983-5130</td>
<th></th>
<td>02-6911-6315</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.reduce((sum, item) => sum + (item.quantity || 0), 0)}</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>
</>
)}
{/* 서명란 */}
<div className="signature-section">
<div style={{ display: 'inline-block', textAlign: 'left' }}>
<div style={{ marginBottom: '15px', fontSize: '14px' }}>
.
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '20px' }}>
<div>
<div style={{ fontSize: '13px', marginBottom: '5px' }}>{formatDate(quote.registrationDate || '')}</div>
<div style={{ fontSize: '15px', fontWeight: '600' }}>
공급자: 동호기업 ()
</div>
</div>
<div className="stamp-area">
<div className="stamp-text">
(<br/>)
</div>
</div>
</div>
</div>
</div>
{/* 하단 안내사항 */}
<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' }}>
: {quote.writer || '담당자'} | {quote.contact || '031-983-5130'}
</p>
</div>
</div>
</>
);
}