refactor: UI 컴포넌트 추상화 및 입금/출금 등록 버튼 추가
- 입금관리, 출금관리 리스트에 등록 버튼 추가 - skeleton, confirm-dialog, empty-state, status-badge UI 컴포넌트 추가 - document-system 컴포넌트 추상화 (ApprovalLine, DocumentHeader 등) - 여러 페이지 컴포넌트 리팩토링 및 코드 정리 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,16 +31,7 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../ui/table";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "../ui/alert-dialog";
|
||||
import { DeleteConfirmDialog } from "../ui/confirm-dialog";
|
||||
|
||||
import type { LocationItem } from "./QuoteRegistrationV2";
|
||||
import type { FinishedGoods } from "./actions";
|
||||
@@ -518,30 +509,18 @@ export function LocationListPanel({
|
||||
)}
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={!!deleteTarget} onOpenChange={() => setDeleteTarget(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>개소 삭제</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
선택한 개소를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>취소</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
if (deleteTarget) {
|
||||
onDeleteLocation(deleteTarget);
|
||||
setDeleteTarget(null);
|
||||
}
|
||||
}}
|
||||
className="bg-red-500 hover:bg-red-600"
|
||||
>
|
||||
삭제
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={!!deleteTarget}
|
||||
onOpenChange={() => setDeleteTarget(null)}
|
||||
onConfirm={() => {
|
||||
if (deleteTarget) {
|
||||
onDeleteLocation(deleteTarget);
|
||||
setDeleteTarget(null);
|
||||
}
|
||||
}}
|
||||
title="개소 삭제"
|
||||
description="선택한 개소를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
/**
|
||||
* 발주서 (Purchase Order Document)
|
||||
*
|
||||
* - 로트번호 및 결재란
|
||||
* - 신청업체 정보
|
||||
* - 신청내용
|
||||
* - 부자재 목록
|
||||
* 공통 컴포넌트 사용:
|
||||
* - DocumentHeader: quote 레이아웃 + LotApprovalTable
|
||||
*/
|
||||
|
||||
import { QuoteFormData } from "./QuoteRegistration";
|
||||
import type { CompanyFormData } from "@/components/settings/CompanyInfoManagement/types";
|
||||
import { DocumentHeader, LotApprovalTable } from "@/components/document-system";
|
||||
|
||||
interface PurchaseOrderDocumentProps {
|
||||
quote: QuoteFormData;
|
||||
@@ -64,138 +63,7 @@ export function PurchaseOrderDocument({ quote, companyInfo }: PurchaseOrderDocum
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.po-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #000;
|
||||
}
|
||||
|
||||
.po-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.po-title h1 {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.po-approval-section {
|
||||
border: 2px solid #000;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.po-lot-number-row {
|
||||
display: grid;
|
||||
grid-template-columns: 100px 1fr;
|
||||
border-bottom: 2px solid #000;
|
||||
}
|
||||
|
||||
.po-lot-label {
|
||||
background: #e8e8e8;
|
||||
border-right: 2px solid #000;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.po-lot-value {
|
||||
background: white;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.po-approval-box {
|
||||
width: 100%;
|
||||
border: none;
|
||||
display: grid;
|
||||
grid-template-columns: 60px 1fr;
|
||||
grid-template-rows: auto auto auto;
|
||||
}
|
||||
|
||||
.po-approval-merged-vertical-cell {
|
||||
border-right: 1px solid #000;
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
grid-row: 1 / 4;
|
||||
}
|
||||
|
||||
.po-approval-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
.po-approval-header-cell {
|
||||
border-right: 1px solid #000;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.po-approval-header-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.po-approval-content-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
|
||||
.po-approval-name-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
.po-approval-signature-cell {
|
||||
border-right: 1px solid #000;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
height: 50px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.po-approval-signature-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.po-approval-name-cell {
|
||||
border-right: 1px solid #000;
|
||||
padding: 6px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.po-approval-name-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
/* 헤더 스타일은 공통 컴포넌트 사용 */
|
||||
|
||||
.po-section-table {
|
||||
width: 100%;
|
||||
@@ -255,49 +123,21 @@ export function PurchaseOrderDocument({ quote, companyInfo }: PurchaseOrderDocum
|
||||
{/* 발주서 내용 */}
|
||||
<div id="purchase-order-content" className="purchase-order p-12 print:p-8">
|
||||
|
||||
{/* 헤더: 제목 + 결재란 */}
|
||||
<div className="po-header">
|
||||
{/* 제목 */}
|
||||
<div className="po-title">
|
||||
<h1>발 주 서</h1>
|
||||
</div>
|
||||
|
||||
{/* 로트번호 + 결재란 */}
|
||||
<div className="po-approval-section">
|
||||
{/* 로트번호 */}
|
||||
<div className="po-lot-number-row">
|
||||
<div className="po-lot-label">
|
||||
로트번호
|
||||
</div>
|
||||
<div className="po-lot-value">
|
||||
{purchaseOrderNumber}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 결재란 */}
|
||||
<div className="po-approval-box">
|
||||
<div className="po-approval-merged-vertical-cell">결<br/>재</div>
|
||||
{/* 결재란 헤더 */}
|
||||
<div className="po-approval-header">
|
||||
<div className="po-approval-header-cell">작성</div>
|
||||
<div className="po-approval-header-cell">검토</div>
|
||||
<div className="po-approval-header-cell">승인</div>
|
||||
</div>
|
||||
{/* 결재+서명란 */}
|
||||
<div className="po-approval-content-row">
|
||||
<div className="po-approval-signature-cell">전진</div>
|
||||
<div className="po-approval-signature-cell"></div>
|
||||
<div className="po-approval-signature-cell"></div>
|
||||
</div>
|
||||
{/* 이름란 */}
|
||||
<div className="po-approval-name-row">
|
||||
<div className="po-approval-name-cell">판매/전진</div>
|
||||
<div className="po-approval-name-cell">회계</div>
|
||||
<div className="po-approval-name-cell">생산</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 헤더: 제목 + 로트번호/결재란 (공통 컴포넌트) */}
|
||||
<DocumentHeader
|
||||
title="발 주 서"
|
||||
layout="quote"
|
||||
customApproval={
|
||||
<LotApprovalTable
|
||||
lotNumber={purchaseOrderNumber}
|
||||
approvers={{
|
||||
writer: { name: '전진', department: '판매/전진' },
|
||||
reviewer: { name: '', department: '회계' },
|
||||
approver: { name: '', department: '생산' },
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 신청업체 */}
|
||||
<table className="po-section-table">
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
/**
|
||||
* 견적서 (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;
|
||||
@@ -81,24 +78,7 @@ export function QuoteDocument({ quote, companyInfo }: QuoteDocumentProps) {
|
||||
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;
|
||||
@@ -210,25 +190,7 @@ export function QuoteDocument({ quote, companyInfo }: QuoteDocumentProps) {
|
||||
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;
|
||||
@@ -238,22 +200,18 @@ export function QuoteDocument({ quote, companyInfo }: QuoteDocumentProps) {
|
||||
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>
|
||||
{/* 문서 헤더 (공통 컴포넌트) */}
|
||||
<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">
|
||||
@@ -424,27 +382,14 @@ export function QuoteDocument({ quote, companyInfo }: QuoteDocumentProps) {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 서명란 */}
|
||||
<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' }}>
|
||||
공급자: {companyInfo?.companyName || '-'} (인)
|
||||
</div>
|
||||
</div>
|
||||
<div className="stamp-area">
|
||||
<div className="stamp-text">
|
||||
(인감<br/>날인)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 서명란 (공통 컴포넌트) */}
|
||||
<SignatureSection
|
||||
label="상기와 같이 견적합니다."
|
||||
date={formatDate(quote.registrationDate || '')}
|
||||
companyName={companyInfo?.companyName || '-'}
|
||||
role="공급자"
|
||||
showStamp={true}
|
||||
/>
|
||||
|
||||
{/* 하단 안내사항 */}
|
||||
<div className="footer-note">
|
||||
|
||||
@@ -43,16 +43,7 @@ import {
|
||||
} from '@/components/templates/UniversalListPage';
|
||||
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
|
||||
import { StandardDialog } from '@/components/molecules/StandardDialog';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { toast } from 'sonner';
|
||||
import { formatAmount, formatAmountManwon } from '@/utils/formatAmount';
|
||||
import type { Quote, QuoteFilterType } from './types';
|
||||
@@ -696,49 +687,36 @@ export function QuoteManagementClient({
|
||||
</StandardDialog>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>견적 삭제 확인</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{deleteTargetId
|
||||
? `견적번호: ${allQuotes.find((q) => q.id === deleteTargetId)?.quoteNumber || deleteTargetId}`
|
||||
: ''}
|
||||
<br />
|
||||
이 견적을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isPending}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmDelete} disabled={isPending}>
|
||||
{isPending ? '삭제 중...' : '삭제'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<DeleteConfirmDialog
|
||||
open={isDeleteDialogOpen}
|
||||
onOpenChange={setIsDeleteDialogOpen}
|
||||
description={
|
||||
<>
|
||||
{deleteTargetId
|
||||
? `견적번호: ${allQuotes.find((q) => q.id === deleteTargetId)?.quoteNumber || deleteTargetId}`
|
||||
: ''}
|
||||
<br />
|
||||
이 견적을 삭제하시겠습니까? 삭제된 데이터는 복구할 수 없습니다.
|
||||
</>
|
||||
}
|
||||
loading={isPending}
|
||||
onConfirm={handleConfirmDelete}
|
||||
/>
|
||||
|
||||
{/* 일괄 삭제 확인 다이얼로그 */}
|
||||
<AlertDialog
|
||||
<DeleteConfirmDialog
|
||||
open={isBulkDeleteDialogOpen}
|
||||
onOpenChange={setIsBulkDeleteDialogOpen}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>일괄 삭제 확인</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
선택한 {bulkDeleteIds.length}개의 견적을 삭제하시겠습니까?
|
||||
<br />
|
||||
삭제된 데이터는 복구할 수 없습니다.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={isPending}>취소</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleConfirmBulkDelete} disabled={isPending}>
|
||||
{isPending ? '삭제 중...' : '삭제'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
description={
|
||||
<>
|
||||
선택한 {bulkDeleteIds.length}개의 견적을 삭제하시겠습니까?
|
||||
<br />
|
||||
삭제된 데이터는 복구할 수 없습니다.
|
||||
</>
|
||||
}
|
||||
loading={isPending}
|
||||
onConfirm={handleConfirmBulkDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user