'use client'; import { useState, useCallback, useMemo, useRef, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Plus, X, Upload, FileText, Image as ImageIcon, Download } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; 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 { partnerConfig } from './partnerConfig'; import { useDaumPostcode } from '@/hooks/useDaumPostcode'; import { toast } from 'sonner'; import type { Partner, PartnerFormData, PartnerMemo, PartnerDocument } from './types'; import { PARTNER_TYPE_OPTIONS, CREDIT_RATING_OPTIONS, TRANSACTION_GRADE_OPTIONS, PAYMENT_DAY_OPTIONS, getEmptyPartnerFormData, partnerToFormData, } from './types'; import { createPartner, updatePartner, deletePartner } from './actions'; // 목업 문서 목록 (상세 모드에서 다운로드 버튼 테스트용) const MOCK_DOCUMENTS: PartnerDocument[] = [ { id: '1', fileName: '사업자등록증.pdf', fileUrl: '#', fileSize: 1024000, // 1MB uploadedAt: '2024-12-15T10:00:00', }, { id: '2', fileName: '통장사본.jpg', fileUrl: '#', fileSize: 512000, // 500KB uploadedAt: '2024-12-16T14:30:00', }, { id: '3', fileName: '인감증명서.pdf', fileUrl: '#', fileSize: 768000, // 750KB uploadedAt: '2024-12-18T09:00:00', }, ]; interface PartnerFormProps { mode: 'view' | 'edit' | 'new'; partnerId?: string; initialData?: Partner; } export default function PartnerForm({ mode, partnerId, initialData }: PartnerFormProps) { const router = useRouter(); const isViewMode = mode === 'view'; const isNewMode = mode === 'new'; const isEditMode = mode === 'edit'; // 폼 데이터 const [formData, setFormData] = useState( initialData ? partnerToFormData(initialData) : getEmptyPartnerFormData() ); // 새 메모 입력 const [newMemo, setNewMemo] = useState(''); // 파일 업로드 ref const logoInputRef = useRef(null); const documentInputRef = useRef(null); // 드래그 상태 const [isDragging, setIsDragging] = useState(false); // 상세/수정 모드에서 목데이터 초기화 useEffect(() => { if (initialData) { setFormData((prev) => ({ ...prev, // 문서 목데이터 documents: prev.documents.length === 0 ? MOCK_DOCUMENTS : prev.documents, // 회사 로고 목데이터 logoUrl: prev.logoUrl || prev.logoBlob ? prev.logoUrl : 'https://placehold.co/750x250/f97316/white?text=Company+Logo', })); } }, [initialData]); // Daum 우편번호 서비스 const { openPostcode } = useDaumPostcode({ onComplete: (result) => { setFormData((prev) => ({ ...prev, zipCode: result.zonecode, address1: result.address, })); }, }); // 필드 변경 핸들러 const handleChange = useCallback((field: keyof PartnerFormData, value: unknown) => { setFormData((prev) => ({ ...prev, [field]: value })); }, []); // 저장 핸들러 (IntegratedDetailTemplate용) const handleSubmit = useCallback(async (): Promise<{ success: boolean; error?: string }> => { if (!formData.partnerName.trim()) { return { success: false, error: '거래처명을 입력해주세요.' }; } try { let result; if (isNewMode) { result = await createPartner(formData); } else if (partnerId) { result = await updatePartner(partnerId, formData); } else { return { success: false, error: '거래처 ID가 없습니다.' }; } if (!result.success) { return { success: false, error: result.error || '저장에 실패했습니다.' }; } toast.success(isNewMode ? '거래처가 등록되었습니다.' : '수정이 완료되었습니다.'); router.push('/ko/construction/project/bidding/partners'); router.refresh(); return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : '저장에 실패했습니다.' }; } }, [isNewMode, partnerId, formData, router]); // 삭제 핸들러 (IntegratedDetailTemplate용) const handleDelete = useCallback(async (): Promise<{ success: boolean; error?: string }> => { if (!partnerId) { return { success: false, error: '거래처 ID가 없습니다.' }; } try { const result = await deletePartner(partnerId); if (!result.success) { return { success: false, error: result.error || '삭제에 실패했습니다.' }; } toast.success('거래처가 삭제되었습니다.'); router.push('/ko/construction/project/bidding/partners'); router.refresh(); return { success: true }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : '삭제에 실패했습니다.' }; } }, [partnerId, router]); // 메모 추가 핸들러 const handleAddMemo = useCallback(() => { if (!newMemo.trim()) return; const now = new Date(); const dateStr = now.toISOString().slice(0, 10); const timeStr = now.toTimeString().slice(0, 5); const memo: PartnerMemo = { id: String(Date.now()), content: `${dateStr} ${timeStr} [사용자] ${newMemo}`, createdAt: now.toISOString(), }; setFormData((prev) => ({ ...prev, memos: [...prev.memos, memo], })); setNewMemo(''); }, [newMemo]); // 메모 삭제 핸들러 const handleDeleteMemo = useCallback((memoId: string) => { setFormData((prev) => ({ ...prev, memos: prev.memos.filter((m) => m.id !== memoId), })); }, []); // 로고 업로드 핸들러 const handleLogoUpload = useCallback((e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; // 파일 크기 검증 (10MB) if (file.size > 10 * 1024 * 1024) { toast.error('파일 크기는 10MB 이하여야 합니다.'); return; } // 파일 타입 검증 if (!['image/png', 'image/jpeg', 'image/gif'].includes(file.type)) { toast.error('PNG, JPEG, GIF 파일만 업로드 가능합니다.'); return; } // BLOB으로 변환 const reader = new FileReader(); reader.onload = () => { setFormData((prev) => ({ ...prev, logoBlob: reader.result as string, logoUrl: null, })); }; reader.readAsDataURL(file); }, []); // 로고 삭제 핸들러 const handleLogoRemove = useCallback(() => { setFormData((prev) => ({ ...prev, logoBlob: null, logoUrl: null, })); if (logoInputRef.current) { logoInputRef.current.value = ''; } }, []); // 문서 업로드 핸들러 const handleDocumentUpload = useCallback((e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; // 파일 크기 검증 (10MB) if (file.size > 10 * 1024 * 1024) { toast.error('파일 크기는 10MB 이하여야 합니다.'); return; } const doc: PartnerDocument = { id: String(Date.now()), fileName: file.name, fileUrl: URL.createObjectURL(file), fileSize: file.size, uploadedAt: new Date().toISOString(), }; setFormData((prev) => ({ ...prev, documents: [...prev.documents, doc], })); if (documentInputRef.current) { documentInputRef.current.value = ''; } }, []); // 문서 삭제 핸들러 const handleDocumentRemove = useCallback((docId: string) => { setFormData((prev) => ({ ...prev, documents: prev.documents.filter((d) => d.id !== docId), })); }, []); // 드래그앤드롭 핸들러 const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (!isViewMode) { setIsDragging(true); } }, [isViewMode]); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); }, []); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); if (isViewMode) return; const files = Array.from(e.dataTransfer.files); files.forEach((file) => { // 파일 크기 검증 (10MB) if (file.size > 10 * 1024 * 1024) { toast.error(`${file.name}: 파일 크기는 10MB 이하여야 합니다.`); return; } const doc: PartnerDocument = { id: String(Date.now() + Math.random()), fileName: file.name, fileUrl: URL.createObjectURL(file), fileSize: file.size, uploadedAt: new Date().toISOString(), }; setFormData((prev) => ({ ...prev, documents: [...prev.documents, doc], })); }); }, [isViewMode]); // 동적 Config (모드별 타이틀/설명) const dynamicConfig = useMemo(() => { if (isNewMode) { return { ...partnerConfig, title: '거래처 등록', description: '새로운 거래처를 등록합니다', }; } if (isEditMode) { return { ...partnerConfig, title: '거래처 수정', description: '거래처 정보를 수정합니다', }; } return partnerConfig; }, [isNewMode, isEditMode]); // 입력 필드 렌더링 헬퍼 const renderField = ( label: string, field: keyof PartnerFormData, value: string | number, options?: { required?: boolean; type?: 'text' | 'tel' | 'email' | 'number'; placeholder?: string; disabled?: boolean; } ) => { const { required, type = 'text', placeholder, disabled } = options || {}; return (
handleChange(field, type === 'number' ? Number(e.target.value) : e.target.value) } placeholder={placeholder} disabled={isViewMode || disabled} className="bg-white" />
); }; // 셀렉트 필드 렌더링 헬퍼 const renderSelectField = ( label: string, field: keyof PartnerFormData, value: string, options: { value: string; label: string }[], required?: boolean ) => { return (
); }; // 폼 내용 렌더링 함수 (IntegratedDetailTemplate용) const renderFormContent = () => (
{/* 기본 정보 */} 기본 정보 * {renderField('사업자등록번호', 'businessNumber', formData.businessNumber, { placeholder: '000-00-00000', })} {renderField('거래처 코드', 'partnerCode', formData.partnerCode || '', { placeholder: '자동생성', disabled: true, })} {renderField('거래처명', 'partnerName', formData.partnerName, { required: true })} {renderField('대표자명', 'representative', formData.representative)} {renderSelectField('거래처 유형', 'partnerType', formData.partnerType, PARTNER_TYPE_OPTIONS)} {renderField('업태', 'businessType', formData.businessType)} {renderField('업종', 'businessCategory', formData.businessCategory)} {/* 연락처 정보 */} 연락처 정보 {/* 주소 */}
handleChange('zipCode', e.target.value)} placeholder="우편번호" disabled className="w-[120px] bg-gray-50" /> handleChange('address1', e.target.value)} placeholder="기본주소" disabled className="flex-1 bg-gray-50" />
handleChange('address2', e.target.value)} placeholder="상세주소" disabled={isViewMode} className="bg-white" />
{renderField('전화번호', 'phone', formData.phone, { type: 'tel', placeholder: '02-0000-0000', })} {renderField('모바일', 'mobile', formData.mobile, { type: 'tel', placeholder: '010-0000-0000', })} {renderField('팩스', 'fax', formData.fax, { type: 'tel', placeholder: '02-0000-0000', })} {renderField('이메일', 'email', formData.email, { type: 'email' })}
{/* 담당자 정보 */} 담당자 정보 {renderField('담당자명', 'manager', formData.manager)} {renderField('담당자 전화', 'managerPhone', formData.managerPhone, { type: 'tel' })}
{renderField('시스템 관리자', 'systemManager', formData.systemManager)}
{/* 회사 정보 */} 회사 정보 {/* 회사 로고 */}
!isViewMode && logoInputRef.current?.click()} > {formData.logoBlob || formData.logoUrl ? (
회사 로고 {!isViewMode && ( )}
) : ( <>

750 X 250px, 10MB 이하의 PNG, JPEG, GIF

{!isViewMode && ( )} )}
{renderSelectField( '매출 결제일', 'salesPaymentDay', String(formData.salesPaymentDay || 15), PAYMENT_DAY_OPTIONS )} {renderSelectField('신용등급', 'creditRating', formData.creditRating, CREDIT_RATING_OPTIONS)} {renderSelectField( '거래등급', 'transactionGrade', formData.transactionGrade, TRANSACTION_GRADE_OPTIONS )} {renderField('세금계산서 이메일', 'taxInvoiceEmail', formData.taxInvoiceEmail, { type: 'email', })}
{/* 추가 정보 */} 추가 정보
{/* 미수금 */}
{/* 연체 */}
handleChange('overdueToggle', checked)} disabled={isViewMode} /> {formData.overdueToggle ? 'ON' : 'OFF'}
{/* 악성채권 */}
{formData.badDebtToggle ? '악성채권' : '-'}
handleChange('badDebtToggle', checked)} disabled={isViewMode} /> {formData.badDebtToggle ? 'ON' : 'OFF'}
{/* 메모 */}
{/* 메모 입력 */} {!isViewMode && (