Files
sam-react-prod/src/components/accounting/TaxInvoiceIssuance/TaxInvoiceForm.tsx
유병철 7f39f3066f feat(WEB): 회계/설정/카드 관리 페이지 대규모 기능 추가 및 리팩토링
- 일반전표입력, 상품권관리, 세금계산서 발행/조회 신규 페이지 추가
- 바로빌 연동 설정 페이지 추가
- 카드관리/계좌관리 리스트 UniversalListPage 공통 구조로 전환
- 카드거래조회/은행거래조회 리팩토링 (모달 분리, 액션 확장)
- 계좌 상세 폼(AccountDetailForm) 신규 구현
- 카드 상세(CardDetail) 신규 구현 + CardNumberInput 적용
- DateRangeSelector, StatCards, IntegratedListTemplateV2 공통 컴포넌트 개선
- 레거시 파일 정리 (CardManagementUnified, cardConfig, _legacy 등)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:18:45 +09:00

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