Files
sam-react-prod/src/components/accounting/TaxInvoiceManagement/ManualEntryModal.tsx
권혁성 81373281ea feat: 회계 모듈 전면 개선 — 계정과목 공통화·전표·세금계산서·어음·상품권
- AccountSubjectSelect 공통 컴포넌트 신규 (계정과목 선택 통합)
- 일반전표 수동입력/수정 모달 계정과목 연동
- 세금계산서 관리 타입 시스템 재정의 + 전표 연동 모달
- 어음관리 리팩토링 + 상품권 접대비 연동
- 카드거래 조회 전표 연동 모달 개선
- 악성채권/입출금/매입매출/거래처 상세 뷰 보강
2026-03-10 11:35:26 +09:00

296 lines
9.3 KiB
TypeScript

'use client';
/**
* 세금계산서 수기 입력 팝업
*
* - 구분 Select (매출/매입)
* - 작성일자 DatePicker
* - 공급자 정보: "카드 내역 불러오기" 버튼 + 공급자명/사업자번호
* - 공급가액/세액/합계 (합계 자동계산)
* - 품목/과세유형/비고
* - 취소/저장 버튼
*/
import { useState, useCallback, useEffect } from 'react';
import { toast } from 'sonner';
import { CreditCard } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { DatePicker } from '@/components/ui/date-picker';
import { FormField } from '@/components/molecules/FormField';
import { BusinessNumberInput } from '@/components/ui/business-number-input';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { getTodayString } from '@/lib/utils/date';
import { createTaxInvoice } from './actions';
import { CardHistoryModal } from './CardHistoryModal';
import type { InvoiceTab, TaxType, ManualEntryFormData, CardHistoryRecord } from './types';
import { DIVISION_OPTIONS, TAX_TYPE_OPTIONS } from './types';
interface ManualEntryModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
defaultDivision?: InvoiceTab;
}
const initialFormData: ManualEntryFormData = {
division: 'sales',
writeDate: getTodayString(),
vendorName: '',
vendorBusinessNumber: '',
supplyAmount: 0,
taxAmount: 0,
totalAmount: 0,
itemName: '',
taxType: 'taxable',
memo: '',
};
export function ManualEntryModal({
open,
onOpenChange,
onSuccess,
defaultDivision = 'sales',
}: ManualEntryModalProps) {
const [formData, setFormData] = useState<ManualEntryFormData>({
...initialFormData,
division: defaultDivision,
});
const [isSaving, setIsSaving] = useState(false);
const [showCardHistory, setShowCardHistory] = useState(false);
// 모달 열릴 때 폼 초기화
useEffect(() => {
if (open) {
setFormData({
...initialFormData,
division: defaultDivision,
});
}
}, [open, defaultDivision]);
// 공급가액/세액 변경 시 합계 자동 계산
const handleAmountChange = useCallback(
(field: 'supplyAmount' | 'taxAmount', value: string) => {
const numValue = Number(value) || 0;
setFormData((prev) => {
const updated = { ...prev, [field]: numValue };
updated.totalAmount = updated.supplyAmount + updated.taxAmount;
return updated;
});
},
[]
);
// 필드 변경
const handleChange = useCallback(
(field: keyof ManualEntryFormData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
},
[]
);
// 카드 내역 선택 시
const handleCardSelect = useCallback((record: CardHistoryRecord) => {
setFormData((prev) => ({
...prev,
vendorName: record.merchantName,
vendorBusinessNumber: record.businessNumber,
supplyAmount: Math.round(record.amount / 1.1),
taxAmount: record.amount - Math.round(record.amount / 1.1),
totalAmount: record.amount,
}));
setShowCardHistory(false);
}, []);
// 저장
const handleSave = useCallback(async () => {
if (!formData.vendorName.trim()) {
toast.warning('공급자명을 입력해주세요.');
return;
}
if (formData.supplyAmount <= 0) {
toast.warning('공급가액을 입력해주세요.');
return;
}
setIsSaving(true);
try {
const result = await createTaxInvoice(formData);
if (result.success) {
onSuccess();
} else {
toast.error(result.error || '등록에 실패했습니다.');
}
} catch {
toast.error('서버 오류가 발생했습니다.');
} finally {
setIsSaving(false);
}
}, [formData, onSuccess]);
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[520px]">
<DialogHeader>
<DialogTitle> </DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
{/* 구분 + 작성일자 */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div className="space-y-1.5">
<Label className="text-sm"></Label>
<Select
value={formData.division}
onValueChange={(v) => handleChange('division', v)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{DIVISION_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label className="text-sm"></Label>
<DatePicker
value={formData.writeDate}
onChange={(date) => handleChange('writeDate', date)}
/>
</div>
</div>
{/* 공급자 정보 */}
<div className="space-y-2 p-3 border rounded-lg bg-muted/30">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<Label className="text-sm font-semibold"> </Label>
<Button
variant="outline"
size="sm"
onClick={() => setShowCardHistory(true)}
>
<CreditCard className="h-4 w-4 mr-1" />
</Button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<FormField
label="공급자명"
value={formData.vendorName}
onChange={(value) => handleChange('vendorName', value)}
placeholder="공급자명"
/>
<div className="space-y-1.5">
<Label className="text-sm"></Label>
<BusinessNumberInput
value={formData.vendorBusinessNumber}
onChange={(value) => handleChange('vendorBusinessNumber', value)}
placeholder="000-00-00000"
/>
</div>
</div>
</div>
{/* 금액 */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<FormField
label="공급가액"
type="number"
value={formData.supplyAmount || ''}
onChange={(value) => handleAmountChange('supplyAmount', value)}
placeholder="0"
/>
<FormField
label="세액"
type="number"
value={formData.taxAmount || ''}
onChange={(value) => handleAmountChange('taxAmount', value)}
placeholder="0"
/>
<FormField
label="합계"
type="number"
value={formData.totalAmount || ''}
disabled
inputClassName="bg-muted"
/>
</div>
{/* 품목 + 과세유형 */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<FormField
label="품목"
value={formData.itemName}
onChange={(value) => handleChange('itemName', value)}
placeholder="품목"
/>
<div className="space-y-1.5">
<Label className="text-sm"></Label>
<Select
value={formData.taxType}
onValueChange={(v) => handleChange('taxType', v as TaxType)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{TAX_TYPE_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* 비고 */}
<FormField
label="비고"
value={formData.memo}
onChange={(value) => handleChange('memo', value)}
placeholder="비고"
/>
</div>
<DialogFooter className="gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button onClick={handleSave} disabled={isSaving}>
{isSaving ? '저장 중...' : '저장'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 카드 내역 불러오기 팝업 (팝업 in 팝업) */}
<CardHistoryModal
open={showCardHistory}
onOpenChange={setShowCardHistory}
onSelect={handleCardSelect}
/>
</>
);
}