'use client'; /** * 악성채권 추심관리 상세 페이지 * IntegratedDetailTemplate 마이그레이션 완료 (2026-01-20) */ import { useState, useCallback, useMemo } from 'react'; import { useDaumPostcode } from '@/hooks/useDaumPostcode'; import { useRouter } from 'next/navigation'; import { format } from 'date-fns'; import { Plus, X, FileText, Receipt, CreditCard, Upload, Download, Trash2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; import { Label } from '@/components/ui/label'; import { PhoneInput } from '@/components/ui/phone-input'; import { BusinessNumberInput } from '@/components/ui/business-number-input'; import { Textarea } from '@/components/ui/textarea'; import { CurrencyInput } from '@/components/ui/currency-input'; import { NumberInput } from '@/components/ui/number-input'; import { Switch } from '@/components/ui/switch'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate'; import { badDebtConfig } from './badDebtConfig'; import { toast } from 'sonner'; import type { BadDebtRecord, BadDebtMemo, Manager, CollectionStatus, } from './types'; import { STATUS_SELECT_OPTIONS, VENDOR_TYPE_LABELS, } from './types'; import { createBadDebt, updateBadDebt, deleteBadDebt, addBadDebtMemo, deleteBadDebtMemo } from './actions'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; interface BadDebtDetailProps { mode: 'view' | 'edit' | 'new'; recordId?: string; initialData?: BadDebtRecord; } // 담당자 목록 (TODO: API에서 조회) const MANAGER_OPTIONS: Manager[] = [ { id: 'm1', departmentName: '경영지원팀', name: '홍길동', position: '과장', phone: '010-1234-1234' }, { id: 'm2', departmentName: '재무팀', name: '김철수', position: '대리', phone: '010-2345-2345' }, { id: 'm3', departmentName: '영업팀', name: '이영희', position: '차장', phone: '010-3456-3456' }, { id: 'm4', departmentName: '관리팀', name: '박민수', position: '사원', phone: '010-4567-4567' }, ]; // 빈 레코드 생성 (신규 등록용) const getEmptyRecord = (): Omit => ({ vendorId: '', vendorCode: '', vendorName: '', businessNumber: '', representativeName: '', vendorType: 'both', businessType: '', businessCategory: '', zipCode: '', address1: '', address2: '', phone: '', mobile: '', fax: '', email: '', contactName: '', contactPhone: '', systemManager: '', debtAmount: 0, status: 'collecting', overdueDays: 0, overdueToggle: false, occurrenceDate: format(new Date(), 'yyyy-MM-dd'), endDate: null, assignedManagerId: null, assignedManager: null, settingToggle: true, badDebtCount: 0, badDebts: [], files: [], memos: [], }); export function BadDebtDetail({ mode, recordId, initialData }: BadDebtDetailProps) { const router = useRouter(); const isViewMode = mode === 'view'; const isNewMode = mode === 'new'; // 폼 데이터: initialData가 있으면 사용, 없으면 빈 레코드 (신규 등록) const [formData, setFormData] = useState(initialData || getEmptyRecord() as BadDebtRecord); // Daum 우편번호 서비스 const { openPostcode } = useDaumPostcode({ onComplete: (result) => { setFormData(prev => ({ ...prev, zipCode: result.zonecode, address1: result.address, })); }, }); // 상태 const [isLoading, setIsLoading] = useState(false); // 새 메모 입력 const [newMemo, setNewMemo] = useState(''); // 파일 업로드 상태 const [newBusinessRegistrationFile, setNewBusinessRegistrationFile] = useState(null); const [newTaxInvoiceFile, setNewTaxInvoiceFile] = useState(null); const [newAdditionalFiles, setNewAdditionalFiles] = useState([]); // 필드 변경 핸들러 const handleChange = useCallback((field: string, value: string | number | boolean | null) => { setFormData(prev => ({ ...prev, [field]: value })); }, []); // 저장/등록 핸들러 (IntegratedDetailTemplate onSubmit용) const handleTemplateSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => { try { if (isNewMode) { const result = await createBadDebt(formData); if (result.success) { return { success: true }; } return { success: false, error: result.error || '등록에 실패했습니다.' }; } else { const result = await updateBadDebt(recordId!, formData); if (result.success) { return { success: true }; } return { success: false, error: result.error || '수정에 실패했습니다.' }; } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('저장 오류:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } }, [formData, recordId, isNewMode]); // 삭제 핸들러 (IntegratedDetailTemplate onDelete용) const handleTemplateDelete = useCallback(async (id: string | number): Promise<{ success: boolean; error?: string }> => { try { const result = await deleteBadDebt(String(id)); if (result.success) { return { success: true }; } return { success: false, error: result.error || '삭제에 실패했습니다.' }; } catch (error) { if (isNextRedirectError(error)) throw error; console.error('삭제 오류:', error); return { success: false, error: '서버 오류가 발생했습니다.' }; } }, []); // 메모 추가 핸들러 const handleAddMemo = useCallback(async () => { if (!newMemo.trim()) return; // 신규 등록 모드에서는 로컬 상태만 변경 if (isNewMode || !recordId) { const now = new Date(); const memo: BadDebtMemo = { id: String(Date.now()), content: newMemo, createdAt: now.toISOString(), createdBy: '사용자', }; setFormData(prev => ({ ...prev, memos: [...prev.memos, memo], })); setNewMemo(''); return; } // 기존 레코드 편집 시 API 호출 setIsLoading(true); try { const result = await addBadDebtMemo(recordId, newMemo); if (result.success && result.data) { setFormData(prev => ({ ...prev, memos: [...prev.memos, result.data!], })); setNewMemo(''); toast.success('메모가 추가되었습니다.'); } else { toast.error(result.error || '메모 추가에 실패했습니다.'); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('메모 추가 오류:', error); toast.error('서버 오류가 발생했습니다.'); } finally { setIsLoading(false); } }, [newMemo, isNewMode, recordId]); // 메모 삭제 핸들러 const handleDeleteMemo = useCallback(async (memoId: string) => { // 신규 등록 모드에서는 로컬 상태만 변경 if (isNewMode || !recordId) { setFormData(prev => ({ ...prev, memos: prev.memos.filter(m => m.id !== memoId), })); return; } // 기존 레코드 편집 시 API 호출 setIsLoading(true); try { const result = await deleteBadDebtMemo(recordId, memoId); if (result.success) { setFormData(prev => ({ ...prev, memos: prev.memos.filter(m => m.id !== memoId), })); toast.success('메모가 삭제되었습니다.'); } else { toast.error(result.error || '메모 삭제에 실패했습니다.'); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('메모 삭제 오류:', error); toast.error('서버 오류가 발생했습니다.'); } finally { setIsLoading(false); } }, [isNewMode, recordId]); // 담당자 변경 핸들러 const handleManagerChange = useCallback((managerId: string) => { const manager = MANAGER_OPTIONS.find(m => m.id === managerId) || null; setFormData(prev => ({ ...prev, assignedManagerId: managerId, assignedManager: manager, })); }, []); // 수취 어음 현황 버튼 const handleBillStatus = useCallback(() => { router.push(`/ko/accounting/bills?vendorId=${formData.vendorId}&type=received`); }, [router, formData.vendorId]); // 거래처 미수금 현황 버튼 const handleReceivablesStatus = useCallback(() => { router.push(`/ko/accounting/receivables-status?highlight=${formData.vendorId}`); }, [router, formData.vendorId]); // 파일 다운로드 핸들러 const handleFileDownload = useCallback((fileName: string) => { // TODO: 실제 다운로드 로직 }, []); // 기존 파일 삭제 핸들러 const handleDeleteExistingFile = useCallback((fileId: string) => { setFormData(prev => ({ ...prev, files: prev.files.filter(f => f.id !== fileId), })); }, []); // 추가 서류 추가 핸들러 const handleAddAdditionalFile = useCallback((file: File) => { setNewAdditionalFiles(prev => [...prev, file]); }, []); // 추가 서류 (새 파일) 삭제 핸들러 const handleRemoveNewAdditionalFile = useCallback((index: number) => { setNewAdditionalFiles(prev => prev.filter((_, i) => i !== index)); }, []); // 동적 config (mode에 따라 title 변경) const dynamicConfig = useMemo(() => { const titleMap: Record = { new: '악성채권 등록', edit: '악성채권 수정', view: '악성채권 추심관리 상세', }; return { ...badDebtConfig, title: titleMap[mode] || badDebtConfig.title, actions: { ...badDebtConfig.actions, deleteConfirmMessage: { title: '악성채권 삭제', description: '이 악성채권 기록을 삭제하시겠습니까? 확인 클릭 시 목록으로 이동합니다.', }, }, }; }, [mode]); // 입력 필드 렌더링 헬퍼 const renderField = ( label: string, field: string, value: string | number, options?: { required?: boolean; type?: 'text' | 'tel' | 'email' | 'number' | 'phone' | 'businessNumber'; placeholder?: string; disabled?: boolean; } ) => { const { required, type = 'text', placeholder, disabled } = options || {}; const isDisabled = isViewMode || disabled; const stringValue = value !== null && value !== undefined ? String(value) : ''; const renderInput = () => { switch (type) { case 'phone': return ( handleChange(field, v)} placeholder={placeholder} disabled={isDisabled} className="bg-white" /> ); case 'businessNumber': return ( handleChange(field, v)} placeholder={placeholder} disabled={isDisabled} showValidation className="bg-white" /> ); default: return ( handleChange(field, type === 'number' ? Number(e.target.value) : e.target.value)} placeholder={placeholder} disabled={isDisabled} className="bg-white" /> ); } }; return (
{renderInput()}
); }; // 폼 콘텐츠 렌더링 const renderFormContent = useCallback(() => (
{/* 기본 정보 */} 기본 정보 {renderField('사업자등록번호', 'businessNumber', formData.businessNumber, { required: true, type: 'businessNumber', placeholder: '000-00-00000', disabled: true })} {renderField('거래처 코드', 'vendorCode', formData.vendorCode, { disabled: true })} {renderField('거래처명', 'vendorName', formData.vendorName, { required: true })} {renderField('대표자명', 'representativeName', formData.representativeName)} {/* 거래처 유형 - 읽기 전용 */}
{/* 악성채권 등록 토글 + 업태/업종 */}
handleChange('settingToggle', checked)} disabled={isViewMode} className="data-[state=checked]:bg-orange-500" />
handleChange('businessType', e.target.value)} placeholder="업태" disabled={isViewMode} className="bg-white" /> handleChange('businessCategory', e.target.value)} placeholder="업종" disabled={isViewMode} className="bg-white" />
{/* 연락처 정보 */} 연락처 정보 {/* 주소 */}
handleChange('zipCode', e.target.value)} placeholder="우편번호" disabled={isViewMode} className="w-[120px] bg-white" />
handleChange('address1', e.target.value)} placeholder="기본주소" disabled={isViewMode} className="bg-white" /> handleChange('address2', e.target.value)} placeholder="상세주소" disabled={isViewMode} className="bg-white" />
{renderField('전화번호', 'phone', formData.phone, { type: 'phone', placeholder: '02-0000-0000' })} {renderField('모바일', 'mobile', formData.mobile, { type: 'phone', placeholder: '010-0000-0000' })} {renderField('팩스', 'fax', formData.fax, { type: 'phone', placeholder: '02-0000-0000' })} {renderField('이메일', 'email', formData.email, { type: 'email' })}
{/* 담당자 정보 */} 담당자 정보 {renderField('담당자명', 'contactName', formData.contactName)} {renderField('담당자 전화', 'contactPhone', formData.contactPhone, { type: 'phone' })} {renderField('시스템 관리자', 'systemManager', formData.systemManager, { disabled: true })} {/* 필요 서류 */} 필요 서류 {/* 사업자등록증 */}
{/* 기존 파일 있는 경우 */} {formData.files.find(f => f.type === 'businessRegistration') && !newBusinessRegistrationFile ? (
{formData.files.find(f => f.type === 'businessRegistration')?.name}
{!isViewMode && ( <> )}
) : newBusinessRegistrationFile ? ( /* 새 파일 선택된 경우 */
{newBusinessRegistrationFile.name} (새 파일)
) : ( /* 파일 없는 경우 */
{!isViewMode && ( setNewBusinessRegistrationFile(e.target.files?.[0] || null)} className="hidden" /> )}
)}
{/* 세금계산서 */}
{/* 기존 파일 있는 경우 */} {formData.files.find(f => f.type === 'taxInvoice') && !newTaxInvoiceFile ? (
{formData.files.find(f => f.type === 'taxInvoice')?.name}
{!isViewMode && ( <> )}
) : newTaxInvoiceFile ? ( /* 새 파일 선택된 경우 */
{newTaxInvoiceFile.name} (새 파일)
) : ( /* 파일 없는 경우 */
{!isViewMode && ( setNewTaxInvoiceFile(e.target.files?.[0] || null)} className="hidden" /> )}
)}
{/* 추가 서류 */}
{!isViewMode && ( )}
{/* 기존 추가 서류 */} {formData.files.filter(f => f.type === 'additional').map((file) => (
{file.name}
{!isViewMode && ( )}
))} {/* 새로 추가된 파일 */} {newAdditionalFiles.map((file, index) => (
{file.name} (새 파일)
))} {/* 파일 없는 경우 안내 */} {formData.files.filter(f => f.type === 'additional').length === 0 && newAdditionalFiles.length === 0 && (
추가 서류가 없습니다
)}
{/* 악성 채권 정보 */} 악성 채권 정보
{/* 미수금 */}
handleChange('debtAmount', value ?? 0)} disabled={isViewMode} className="bg-white" />
{/* 상태 */}
{/* 연체일수 */}
handleChange('overdueDays', value ?? 0)} disabled={isViewMode} className="bg-white w-[100px]" min={0} />
{/* 본사 담당자 */}
{/* 악성채권 발생일 */}
handleChange('occurrenceDate', date)} disabled={isViewMode} />
{/* 악성채권 종료일 */}
handleChange('endDate', date || null)} disabled={isViewMode} placeholder="-" />
{/* 연동 버튼 */}
{/* 메모 */} 메모 {/* 메모 입력 */} {!isViewMode && (