feat: [전표] 일반전표 복사 기능 추가

- 전표 수정 모달에 복사 버튼 추가
- 복사 시 일자 선택 다이얼로그 표시
- 선택한 일자 기준 신규 전표번호 자동 생성
- 분개 내역(계정과목, 금액, 거래처, 적요) 그대로 복사
This commit is contained in:
김보곤
2026-03-12 16:00:04 +09:00
parent 1bd5ba817a
commit 388cf174bb

View File

@@ -59,6 +59,7 @@
const Calendar = createIcon('calendar');
const CreditCard = createIcon('credit-card');
const Lock = createIcon('lock');
const Copy = createIcon('copy');
const CSRF_TOKEN = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
@@ -1463,6 +1464,9 @@ className="p-1 text-stone-300 group-hover:text-emerald-500 hover:bg-emerald-50 r
const [existingEntryNo, setExistingEntryNo] = useState('');
const [showAddPartnerModal, setShowAddPartnerModal] = useState(false);
const [addPartnerLineIndex, setAddPartnerLineIndex] = useState(null);
const [isCopyMode, setIsCopyMode] = useState(false);
const [showCopyDialog, setShowCopyDialog] = useState(false);
const [copyDate, setCopyDate] = useState(getKoreanDate());
const getDefaultLines = () => [
{ key: Date.now(), dc_type: 'debit', account_code: '', account_name: '', trading_partner_id: null, trading_partner_name: '', debit_amount: 0, credit_amount: 0, description: '' },
@@ -1481,8 +1485,8 @@ className="p-1 text-stone-300 group-hover:text-emerald-500 hover:bg-emerald-50 r
};
useEffect(() => {
if (!isEditMode) fetchPreviewEntryNo(entryDate);
}, [entryDate]);
if (!isEditMode || isCopyMode) fetchPreviewEntryNo(entryDate);
}, [entryDate, isCopyMode]);
// 수정 모드: 기존 전표 로드
useEffect(() => {
@@ -1558,8 +1562,8 @@ className="p-1 text-stone-300 group-hover:text-emerald-500 hover:bg-emerald-50 r
})),
};
const url = isEditMode ? `/finance/journal-entries/${entryId}` : '/finance/journal-entries/store';
const method = isEditMode ? 'PUT' : 'POST';
const url = (isEditMode && !isCopyMode) ? `/finance/journal-entries/${entryId}` : '/finance/journal-entries/store';
const method = (isEditMode && !isCopyMode) ? 'PUT' : 'POST';
const res = await fetch(url, {
method, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': CSRF_TOKEN },
@@ -1579,7 +1583,7 @@ className="p-1 text-stone-300 group-hover:text-emerald-500 hover:bg-emerald-50 r
const data = await res.json();
if (data.success) {
notify(data.message || (isEditMode ? '전표가 수정되었습니다.' : '전표가 저장되었습니다.'), 'success');
notify(data.message || (isCopyMode ? '전표가 복사되었습니다.' : isEditMode ? '전표가 수정되었습니다.' : '전표가 저장되었습니다.'), 'success');
onSaved();
} else {
notify(data.message || '저장 실패', 'error');
@@ -1609,13 +1613,25 @@ className="p-1 text-stone-300 group-hover:text-emerald-500 hover:bg-emerald-50 r
}
};
const handleCopyClick = () => {
setShowCopyDialog(true);
setCopyDate(getKoreanDate());
};
const confirmCopy = () => {
setIsCopyMode(true);
setShowCopyDialog(false);
setEntryDate(copyDate);
fetchPreviewEntryNo(copyDate);
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 min-h-[80vh] max-h-[95vh] flex flex-col overflow-hidden">
{/* 헤더 */}
<div className="px-6 py-4 border-b border-stone-100 flex items-center justify-between flex-shrink-0">
<h3 className="text-lg font-bold text-stone-900">
{isEditMode ? `전표 수정 (${existingEntryNo})` : '수동 전표 생성'}
{isCopyMode ? `전표 복사 (원본: ${existingEntryNo})` : isEditMode ? `전표 수정 (${existingEntryNo})` : '수동 전표 생성'}
</h3>
<button onClick={onClose} className="p-2 hover:bg-stone-100 rounded-lg transition-colors">
<X className="w-5 h-5 text-stone-500" />
@@ -1642,7 +1658,7 @@ className="w-full px-3 py-2 text-sm border border-stone-200 rounded-lg focus:rin
<div>
<label className="block text-xs font-medium text-stone-500 mb-1">전표번호</label>
<div className="w-full px-3 py-2 text-sm bg-white border border-stone-200 rounded-lg text-stone-500 font-mono">
{isEditMode ? existingEntryNo : (previewEntryNo || '자동 생성')}
{isCopyMode ? (previewEntryNo || '자동 생성') : isEditMode ? existingEntryNo : (previewEntryNo || '자동 생성')}
</div>
</div>
<div>
@@ -1773,19 +1789,25 @@ className={`p-1 rounded ${lines.length <= 2 ? 'text-stone-200 cursor-not-allowed
{/* 하단 버튼 */}
<div className="px-6 py-4 border-t border-stone-100 flex justify-between flex-shrink-0">
<div>
{isEditMode && (
<div className="flex gap-2">
{isEditMode && !isCopyMode && (
<button onClick={handleDelete} disabled={saving}
className="px-4 py-2 bg-red-50 text-red-600 rounded-lg text-sm font-medium hover:bg-red-100 transition-colors disabled:opacity-50">
<Trash2 className="w-4 h-4 inline mr-1" /> 삭제
</button>
)}
{isEditMode && !isCopyMode && (
<button onClick={handleCopyClick} disabled={saving}
className="px-4 py-2 bg-blue-50 text-blue-600 rounded-lg text-sm font-medium hover:bg-blue-100 transition-colors disabled:opacity-50">
<Copy className="w-4 h-4 inline mr-1" /> 복사
</button>
)}
</div>
<div className="flex gap-2">
<button onClick={onClose} className="px-4 py-2 text-sm text-stone-600 bg-stone-100 rounded-lg hover:bg-stone-200 transition-colors">취소</button>
<button onClick={handleSave} disabled={!isBalanced || saving}
className={`px-6 py-2 text-sm font-medium rounded-lg flex items-center gap-1 transition-colors ${isBalanced && !saving ? 'bg-emerald-600 text-white hover:bg-emerald-700' : 'bg-stone-200 text-stone-400 cursor-not-allowed'}`}>
<Save className="w-4 h-4" /> {saving ? '저장 중...' : isEditMode ? '수정' : '저장'}
<Save className="w-4 h-4" /> {saving ? '저장 중...' : isCopyMode ? '복사 저장' : isEditMode ? '수정' : '저장'}
</button>
</div>
</div>
@@ -1809,6 +1831,25 @@ className={`px-6 py-2 text-sm font-medium rounded-lg flex items-center gap-1 tra
setAddPartnerLineIndex(null);
}}
/>
{/* 전표 복사 일자 선택 다이얼로그 */}
{showCopyDialog && (
<div className="fixed inset-0 bg-black/30 flex items-center justify-center" style={{ zIndex: 60 }}>
<div className="bg-white rounded-xl shadow-lg p-6" style={{ width: '320px' }}>
<h4 className="text-sm font-bold text-stone-800 mb-1">전표 복사</h4>
<p className="text-xs text-stone-500 mb-4">복사할 전표의 일자를 선택하세요. 전표번호는 해당 일자 기준으로 자동 생성됩니다.</p>
<label className="block text-xs font-medium text-stone-500 mb-1">복사 일자</label>
<input type="date" value={copyDate} onChange={(e) => setCopyDate(e.target.value)}
className="w-full px-3 py-2 text-sm border border-stone-200 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none mb-4" />
<div className="flex justify-end gap-2">
<button onClick={() => setShowCopyDialog(false)}
className="px-4 py-2 text-sm text-stone-600 bg-stone-100 rounded-lg hover:bg-stone-200 transition-colors">취소</button>
<button onClick={confirmCopy} disabled={!copyDate}
className="px-4 py-2 text-sm font-medium bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50">복사하기</button>
</div>
</div>
</div>
)}
</div>
);
};