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:
유병철
2026-01-22 17:21:42 +09:00
parent 777dccc7bd
commit 269b901e64
86 changed files with 3761 additions and 2614 deletions

View File

@@ -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>
);
}

View File

@@ -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">

View File

@@ -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">

View File

@@ -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}
/>
</>
);
}