Files
sam-react-prod/src/components/accounting/BillManagement/BillDetail.tsx
권혁성 a4f99ae339 feat: [출하/배차/회계] 배차 다중행 + 어음 리팩토링 + 출고관리
- 배차차량관리 목업→API 연동, 배차정보 다중 행
- ShipmentManagement 출고관리 API 매핑
- BillManagement 리팩토링 (섹션 분리, hooks, constants)
- 상품권 actions/types 확장
- 출하관리 캘린더 기본 뷰 week-time
2026-03-07 03:03:27 +09:00

261 lines
7.9 KiB
TypeScript

'use client';
import { useState, useCallback, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { billConfig } from './billConfig';
import { apiDataToFormData, transformFormDataToApi } from './types';
import type { BillApiData } from './types';
import { getBillRaw, createBillRaw, updateBillRaw, deleteBill, getClients } from './actions';
import { useBillForm } from './hooks/useBillForm';
import { useBillConditions } from './hooks/useBillConditions';
import {
BasicInfoSection,
ElectronicBillSection,
ExchangeBillSection,
DiscountInfoSection,
EndorsementSection,
CollectionSection,
HistorySection,
RenewalSection,
RecourseSection,
BuybackSection,
DishonoredSection,
} from './sections';
import { useDetailData } from '@/hooks';
interface BillDetailProps {
billId: string;
mode: 'view' | 'edit' | 'new';
}
interface ClientOption {
id: string;
name: string;
}
export function BillDetail({ billId, mode }: BillDetailProps) {
const router = useRouter();
const isViewMode = mode === 'view';
const isNewMode = mode === 'new';
// 거래처 목록
const [clients, setClients] = useState<ClientOption[]>([]);
// V8 폼 훅
const {
formData,
updateField,
handleInstrumentTypeChange,
handleDirectionChange,
addInstallment,
removeInstallment,
updateInstallment,
setFormDataFull,
} = useBillForm();
// 조건부 표시 플래그
const conditions = useBillConditions(formData);
// 거래처 목록 로드
useEffect(() => {
async function loadClients() {
const result = await getClients();
if (result.success && result.data) {
setClients(result.data.map(c => ({ id: String(c.id), name: c.name })));
}
}
loadClients();
}, []);
// API 데이터 로딩 (BillApiData 그대로)
const fetchBillWrapper = useCallback(
(id: string | number) => getBillRaw(String(id)),
[]
);
const {
data: billApiData,
isLoading,
error: loadError,
} = useDetailData<BillApiData>(
billId !== 'new' ? billId : null,
fetchBillWrapper,
{ skip: isNewMode }
);
// API 데이터 → V8 폼 데이터로 변환
useEffect(() => {
if (billApiData) {
setFormDataFull(apiDataToFormData(billApiData));
}
}, [billApiData, setFormDataFull]);
// 로드 에러
useEffect(() => {
if (loadError) {
toast.error(loadError);
router.push('/ko/accounting/bills');
}
}, [loadError, router]);
// 유효성 검사
const validateForm = useCallback((): { valid: boolean; error?: string } => {
if (!formData.billNumber.trim()) return { valid: false, error: '어음번호를 입력해주세요.' };
const vendorId = conditions.isReceived ? formData.vendor : formData.payee;
if (!vendorId) return { valid: false, error: '거래처를 선택해주세요.' };
if (formData.amount <= 0) return { valid: false, error: '금액을 입력해주세요.' };
if (!formData.issueDate) return { valid: false, error: '발행일을 입력해주세요.' };
if (conditions.isBill && !formData.maturityDate) return { valid: false, error: '만기일을 입력해주세요.' };
return { valid: true };
}, [formData, conditions.isReceived, conditions.isBill]);
// 제출
const [isSubmitting, setIsSubmitting] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
const validation = validateForm();
if (!validation.valid) {
toast.error(validation.error!);
return { success: false, error: validation.error };
}
setIsSubmitting(true);
try {
const vendorName = clients.find(c => c.id === (conditions.isReceived ? formData.vendor : formData.payee))?.name || '';
const apiPayload = transformFormDataToApi(formData, vendorName);
if (isNewMode) {
const result = await createBillRaw(apiPayload);
if (result.success) {
toast.success('등록되었습니다.');
router.push('/ko/accounting/bills');
return { success: false, error: '' };
}
return result;
} else {
const result = await updateBillRaw(String(billId), apiPayload);
return result;
}
} finally {
setIsSubmitting(false);
}
}, [formData, clients, conditions.isReceived, isNewMode, billId, validateForm, router]);
const handleDelete = useCallback(async (): Promise<{ success: boolean; error?: string }> => {
setIsDeleting(true);
try {
return await deleteBill(String(billId));
} finally {
setIsDeleting(false);
}
}, [billId]);
// 폼 콘텐츠 렌더링
const renderFormContent = () => (
<>
{/* 1. 기본 정보 */}
<BasicInfoSection
formData={formData}
updateField={updateField}
isViewMode={isViewMode}
clients={clients}
conditions={conditions}
onInstrumentTypeChange={handleInstrumentTypeChange}
onDirectionChange={handleDirectionChange}
/>
{/* 2. 전자어음 정보 */}
{conditions.showElectronic && (
<ElectronicBillSection formData={formData} updateField={updateField} isViewMode={isViewMode} />
)}
{/* 3. 환어음 정보 */}
{conditions.showExchangeBill && (
<ExchangeBillSection
formData={formData}
updateField={updateField}
isViewMode={isViewMode}
showAcceptanceRefusal={conditions.showAcceptanceRefusal}
/>
)}
{/* 4. 할인 정보 */}
{conditions.showDiscount && (
<DiscountInfoSection formData={formData} updateField={updateField} isViewMode={isViewMode} />
)}
{/* 5. 배서양도 정보 */}
{conditions.showEndorsement && (
<EndorsementSection formData={formData} updateField={updateField} isViewMode={isViewMode} />
)}
{/* 6. 추심 정보 */}
{conditions.showCollection && (
<CollectionSection formData={formData} updateField={updateField} isViewMode={isViewMode} />
)}
{/* 7. 이력 관리 (받을어음만) */}
{conditions.isReceived && (
<HistorySection
formData={formData}
updateField={updateField}
isViewMode={isViewMode}
isElectronic={conditions.isElectronic}
maxSplitCount={conditions.maxSplitCount}
onAddInstallment={addInstallment}
onRemoveInstallment={removeInstallment}
onUpdateInstallment={updateInstallment}
/>
)}
{/* 8. 개서 정보 */}
{conditions.showRenewal && (
<RenewalSection formData={formData} updateField={updateField} isViewMode={isViewMode} />
)}
{/* 9. 소구 정보 */}
{conditions.showRecourse && (
<RecourseSection formData={formData} updateField={updateField} isViewMode={isViewMode} />
)}
{/* 10. 환매 정보 */}
{conditions.showBuyback && (
<BuybackSection formData={formData} updateField={updateField} isViewMode={isViewMode} />
)}
{/* 11. 부도 정보 */}
{conditions.showDishonored && (
<DishonoredSection formData={formData} updateField={updateField} isViewMode={isViewMode} />
)}
</>
);
// 템플릿 설정
const templateMode = isNewMode ? 'create' : mode;
const dynamicConfig = {
...billConfig,
title: isViewMode ? '어음/수표 상세' : '어음/수표',
actions: {
...billConfig.actions,
submitLabel: isNewMode ? '등록' : '저장',
},
};
return (
<IntegratedDetailTemplate
config={dynamicConfig}
mode={templateMode}
initialData={{}}
itemId={billId}
isLoading={isLoading || isSubmitting || isDeleting}
onSubmit={handleSubmit}
onDelete={billId && billId !== 'new' ? handleDelete : undefined}
renderView={() => renderFormContent()}
renderForm={() => renderFormContent()}
/>
);
}