- 일반전표입력, 상품권관리, 세금계산서 발행/조회 신규 페이지 추가 - 바로빌 연동 설정 페이지 추가 - 카드관리/계좌관리 리스트 UniversalListPage 공통 구조로 전환 - 카드거래조회/은행거래조회 리팩토링 (모달 분리, 액션 확장) - 계좌 상세 폼(AccountDetailForm) 신규 구현 - 카드 상세(CardDetail) 신규 구현 + CardNumberInput 적용 - DateRangeSelector, StatCards, IntegratedListTemplateV2 공통 컴포넌트 개선 - 레거시 파일 정리 (CardManagementUnified, cardConfig, _legacy 등) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
216 lines
8.1 KiB
TypeScript
216 lines
8.1 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useCallback } from 'react';
|
|
import { format } from 'date-fns';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
import { Label } from '@/components/ui/label';
|
|
import { DatePicker } from '@/components/ui/date-picker';
|
|
import { Search } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { TaxInvoiceItemTable } from './TaxInvoiceItemTable';
|
|
import { createEmptyItem, createEmptyBusinessEntity } from './types';
|
|
import type { BusinessEntity, TaxInvoiceItem, TaxInvoiceFormData } from './types';
|
|
|
|
interface TaxInvoiceFormProps {
|
|
supplier: BusinessEntity;
|
|
onSubmit: (data: TaxInvoiceFormData) => Promise<void>;
|
|
onCancel: () => void;
|
|
onVendorSearch: () => void;
|
|
selectedVendor: BusinessEntity | null;
|
|
}
|
|
|
|
export function TaxInvoiceForm({
|
|
supplier,
|
|
onSubmit,
|
|
onCancel,
|
|
onVendorSearch,
|
|
selectedVendor,
|
|
}: TaxInvoiceFormProps) {
|
|
const [writeDate, setWriteDate] = useState(format(new Date(), 'yyyy-MM-dd'));
|
|
const [items, setItems] = useState<TaxInvoiceItem[]>([createEmptyItem()]);
|
|
const [memo, setMemo] = useState('');
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
const receiver = selectedVendor ?? createEmptyBusinessEntity();
|
|
|
|
const handleSubmit = useCallback(async () => {
|
|
if (!selectedVendor) {
|
|
toast.error('공급받는자를 선택하세요.');
|
|
return;
|
|
}
|
|
if (items.length === 0) {
|
|
toast.error('품목을 하나 이상 추가하세요.');
|
|
return;
|
|
}
|
|
const hasEmptyItem = items.some((item) => !item.itemName.trim());
|
|
if (hasEmptyItem) {
|
|
toast.error('품목명을 입력하세요.');
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
try {
|
|
await onSubmit({
|
|
supplier,
|
|
receiver,
|
|
writeDate,
|
|
items,
|
|
memo,
|
|
});
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}, [supplier, receiver, writeDate, items, memo, selectedVendor, onSubmit]);
|
|
|
|
const thClass = 'border border-gray-300 bg-gray-50 px-2 py-1.5 text-left font-medium whitespace-nowrap w-[70px] text-xs';
|
|
const tdClass = 'border border-gray-300 px-2 py-1.5 text-sm';
|
|
|
|
return (
|
|
<Card>
|
|
<CardContent className="space-y-6 pt-6">
|
|
{/* 공급자 / 공급받는자 테이블 */}
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full border-collapse table-fixed text-sm min-w-[800px]">
|
|
<colgroup>
|
|
{/* 공급자 */}
|
|
<col style={{ width: '30px' }} />
|
|
<col style={{ width: '70px' }} />
|
|
<col />
|
|
<col style={{ width: '70px' }} />
|
|
<col />
|
|
{/* 공급받는자 */}
|
|
<col style={{ width: '30px' }} />
|
|
<col style={{ width: '70px' }} />
|
|
<col />
|
|
<col style={{ width: '70px' }} />
|
|
<col />
|
|
</colgroup>
|
|
<tbody>
|
|
{/* Row 1: 등록번호 / 종사업장 */}
|
|
<tr>
|
|
<td rowSpan={6} className="border border-gray-300 bg-gray-100 text-center font-semibold w-8 align-middle">
|
|
<div className="[writing-mode:vertical-rl] mx-auto tracking-widest">공급자</div>
|
|
</td>
|
|
<th className={thClass}>등록번호</th>
|
|
<td className={tdClass}>{supplier.businessNumber || '-'}</td>
|
|
<th className={thClass}>종사업장</th>
|
|
<td className={tdClass}></td>
|
|
|
|
<td rowSpan={6} className="border border-gray-300 bg-gray-100 text-center font-semibold w-8 align-middle">
|
|
<div className="[writing-mode:vertical-rl] mx-auto tracking-widest">공급받는자</div>
|
|
</td>
|
|
<th className={thClass}>등록번호</th>
|
|
<td className={tdClass}>
|
|
<div className="flex items-center gap-2">
|
|
<span className="flex-1">{receiver.businessNumber || ''}</span>
|
|
<Button type="button" variant="outline" size="sm" onClick={onVendorSearch} className="h-6 text-xs px-2 shrink-0">
|
|
<Search className="h-3 w-3 mr-1" />
|
|
검색
|
|
</Button>
|
|
</div>
|
|
</td>
|
|
<th className={thClass}>종사업장</th>
|
|
<td className={tdClass}></td>
|
|
</tr>
|
|
|
|
{/* Row 2: 상호 / 대표자 */}
|
|
<tr>
|
|
<th className={thClass}>상호</th>
|
|
<td className={tdClass}>{supplier.companyName || ''}</td>
|
|
<th className={thClass}>대표자</th>
|
|
<td className={tdClass}>{supplier.representativeName || ''}</td>
|
|
|
|
<th className={thClass}>상호</th>
|
|
<td className={tdClass}>{receiver.companyName || ''}</td>
|
|
<th className={thClass}>대표자</th>
|
|
<td className={tdClass}>{receiver.representativeName || ''}</td>
|
|
</tr>
|
|
|
|
{/* Row 3: 사업장주소 */}
|
|
<tr>
|
|
<th className={thClass}>사업장주소</th>
|
|
<td className={tdClass} colSpan={3}>{supplier.address || ''}</td>
|
|
|
|
<th className={thClass}>사업장주소</th>
|
|
<td className={tdClass} colSpan={3}>{receiver.address || ''}</td>
|
|
</tr>
|
|
|
|
{/* Row 4: 업태 / 종목 */}
|
|
<tr>
|
|
<th className={thClass}>업태</th>
|
|
<td className={tdClass}>{supplier.businessType || ''}</td>
|
|
<th className={thClass}>종목</th>
|
|
<td className={tdClass}>{supplier.businessItem || ''}</td>
|
|
|
|
<th className={thClass}>업태</th>
|
|
<td className={tdClass}>{receiver.businessType || ''}</td>
|
|
<th className={thClass}>종목</th>
|
|
<td className={tdClass}>{receiver.businessItem || ''}</td>
|
|
</tr>
|
|
|
|
{/* Row 5: 담당자 / 연락처 */}
|
|
<tr>
|
|
<th className={thClass}>담당자</th>
|
|
<td className={tdClass}>{supplier.contactName || ''}</td>
|
|
<th className={thClass}>연락처</th>
|
|
<td className={tdClass}>{supplier.contactPhone || ''}</td>
|
|
|
|
<th className={thClass}>담당자</th>
|
|
<td className={tdClass}>{receiver.contactName || ''}</td>
|
|
<th className={thClass}>연락처</th>
|
|
<td className={tdClass}>{receiver.contactPhone || ''}</td>
|
|
</tr>
|
|
|
|
{/* Row 6: 이메일 */}
|
|
<tr>
|
|
<th className={thClass}>이메일</th>
|
|
<td className={tdClass} colSpan={3}>{supplier.contactEmail || ''}</td>
|
|
|
|
<th className={thClass}>이메일</th>
|
|
<td className={tdClass} colSpan={3}>
|
|
{receiver.contactEmail || <span className="text-muted-foreground text-xs">세금계산서 수신 이메일</span>}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{/* 작성일자 */}
|
|
<div className="max-w-xs">
|
|
<Label className="mb-1 block">작성일자</Label>
|
|
<DatePicker
|
|
value={writeDate}
|
|
onChange={setWriteDate}
|
|
/>
|
|
</div>
|
|
|
|
{/* 품목 테이블 */}
|
|
<TaxInvoiceItemTable items={items} onItemsChange={setItems} />
|
|
|
|
{/* 비고 */}
|
|
<div>
|
|
<Label className="mb-1 block">비고</Label>
|
|
<Textarea
|
|
value={memo}
|
|
onChange={(e) => setMemo(e.target.value)}
|
|
placeholder="비고 사항을 입력하세요"
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
|
|
{/* 하단 버튼 */}
|
|
<div className="flex justify-end gap-2 pt-2 border-t">
|
|
<Button variant="outline" onClick={onCancel} disabled={isSubmitting}>
|
|
취소
|
|
</Button>
|
|
<Button onClick={handleSubmit} disabled={isSubmitting}>
|
|
{isSubmitting ? '발행 중...' : '세금계산서 발행'}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|