diff options
Diffstat (limited to 'lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx')
| -rw-r--r-- | lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx | 335 |
1 files changed, 201 insertions, 134 deletions
diff --git a/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx b/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx index c146e42b..34259d37 100644 --- a/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx +++ b/lib/rfq-last/vendor-response/editor/vendor-response-editor.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { useState,useEffect } from "react" import { useForm, FormProvider } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import * as z from "zod" @@ -163,18 +163,74 @@ export default function VendorResponseEditor({ const methods = useForm<VendorResponseFormData>({ resolver: zodResolver(vendorResponseSchema), - defaultValues + defaultValues, + mode: 'onChange' // 추가: 실시간 validation }) + const { formState: { errors, isValid } } = methods + + useEffect(() => { + if (Object.keys(errors).length > 0) { + console.log('Validation errors:', errors) + } + }, [errors]) + + + + const handleFormSubmit = (isSubmit: boolean = false) => { + // 임시저장일 경우 validation 없이 바로 저장 + if (!isSubmit) { + const formData = methods.getValues() + onSubmit(formData, false) + return + } + + // 제출일 경우에만 validation 수행 + methods.handleSubmit( + (data) => onSubmit(data, isSubmit), + (errors) => { + console.error('Form validation errors:', errors) + + // 첫 번째 에러 필드로 포커스 이동 + const firstErrorField = Object.keys(errors)[0] + if (firstErrorField) { + // 어느 탭에 에러가 있는지 확인 + if (firstErrorField.startsWith('vendor') && + !firstErrorField.startsWith('vendorFirst') && + !firstErrorField.startsWith('vendorSparepart')) { + setActiveTab('terms') + } else if (firstErrorField === 'quotationItems') { + setActiveTab('items') + } + + // 구체적인 에러 메시지 표시 + if (errors.quotationItems) { + toast.error("견적 품목 정보를 확인해주세요. 모든 품목의 단가와 총액을 입력해야 합니다.") + } else { + toast.error("입력 정보를 확인해주세요.") + } + } + } + )() + } + const onSubmit = async (data: VendorResponseFormData, isSubmit: boolean = false) => { + console.log('onSubmit called with:', { data, isSubmit }) // 디버깅용 + setLoading(true) setUploadProgress(0) try { const formData = new FormData() + const fileMetadata = attachments.map((file: any) => ({ + attachmentType: file.attachmentType || "기타", + description: file.description || "" + })) + + // 기본 데이터 추가 - formData.append('data', JSON.stringify({ + const submitData = { ...data, rfqsLastId: rfq.id, rfqLastDetailsId: rfqDetail.id, @@ -183,69 +239,76 @@ export default function VendorResponseEditor({ submittedAt: isSubmit ? new Date().toISOString() : null, submittedBy: isSubmit ? userId : null, totalAmount: data.quotationItems.reduce((sum, item) => sum + item.totalPrice, 0), - updatedBy: userId - })) + updatedBy: userId, + fileMetadata + } + + console.log('Submitting data:', submitData) // 디버깅용 + + formData.append('data', JSON.stringify(submitData)) // 첨부파일 추가 attachments.forEach((file, index) => { formData.append(`attachments`, file) }) - // const response = await fetch(`/api/partners/rfq-last/${rfq.id}/response`, { - // method: existingResponse ? 'PUT' : 'POST', - // body: formData - // }) - - // if (!response.ok) { - // throw new Error('응답 저장에 실패했습니다.') - // } - - // XMLHttpRequest 사용하여 업로드 진행률 추적 - const xhr = new XMLHttpRequest() - - // Promise로 감싸서 async/await 사용 가능하게 - const uploadPromise = new Promise((resolve, reject) => { - // 업로드 진행률 이벤트 - xhr.upload.addEventListener('progress', (event) => { - if (event.lengthComputable) { - const percentComplete = Math.round((event.loaded / event.total) * 100) - setUploadProgress(percentComplete) - } - }) - - // 완료 이벤트 - xhr.addEventListener('load', () => { - if (xhr.status >= 200 && xhr.status < 300) { - setUploadProgress(100) - resolve(JSON.parse(xhr.responseText)) - } else { - reject(new Error('응답 저장에 실패했습니다.')) + // XMLHttpRequest 사용하여 업로드 진행률 추적 + const xhr = new XMLHttpRequest() + + const uploadPromise = new Promise((resolve, reject) => { + xhr.upload.addEventListener('progress', (event) => { + if (event.lengthComputable) { + const percentComplete = Math.round((event.loaded / event.total) * 100) + setUploadProgress(percentComplete) + } + }) + + xhr.addEventListener('load', () => { + if (xhr.status >= 200 && xhr.status < 300) { + setUploadProgress(100) + try { + const response = JSON.parse(xhr.responseText) + resolve(response) + } catch (e) { + console.error('Response parsing error:', e) + reject(new Error('응답 파싱 실패')) } - }) - - // 에러 이벤트 - xhr.addEventListener('error', () => { - reject(new Error('네트워크 오류가 발생했습니다.')) - }) - - // 요청 전송 - xhr.open(existingResponse ? 'PUT' : 'POST', `/api/partners/rfq-last/${rfq.id}/response`) - xhr.send(formData) + } else { + console.error('Server error:', xhr.status, xhr.responseText) + reject(new Error(`서버 오류: ${xhr.status}`)) + } + }) + + xhr.addEventListener('error', () => { + console.error('Network error') + reject(new Error('네트워크 오류가 발생했습니다.')) }) + + // 요청 전송 + const method = existingResponse ? 'PUT' : 'POST' + const url = `/api/partners/rfq-last/${rfq.id}/response` + + console.log(`Sending ${method} request to ${url}`) // 디버깅용 - await uploadPromise + xhr.open(method, url) + xhr.send(formData) + }) + + await uploadPromise toast.success(isSubmit ? "견적서가 제출되었습니다." : "견적서가 저장되었습니다.") router.push('/partners/rfq-last') router.refresh() } catch (error) { - console.error('Error:', error) - toast.error("오류가 발생했습니다.") + console.error('Submit error:', error) // 더 상세한 에러 로깅 + toast.error(error instanceof Error ? error.message : "오류가 발생했습니다.") } finally { setLoading(false) + setUploadProgress(0) } } + const totalAmount = methods.watch('quotationItems')?.reduce( (sum, item) => sum + (item.totalPrice || 0), 0 ) || 0 @@ -256,7 +319,10 @@ export default function VendorResponseEditor({ return ( <FormProvider {...methods}> - <form onSubmit={methods.handleSubmit((data) => onSubmit(data, false))}> + <form onSubmit={(e) => { + e.preventDefault() // 기본 submit 동작 방지 + handleFormSubmit(false) + }}> <div className="space-y-6"> {/* 헤더 정보 */} <RfqInfoHeader rfq={rfq} rfqDetail={rfqDetail} vendor={vendor} /> @@ -293,92 +359,92 @@ export default function VendorResponseEditor({ </CardDescription> </CardHeader> <CardContent> - {basicContracts.length > 0 ? ( - <div className="space-y-4"> - {/* 계약 목록 - 그리드 레이아웃 */} - <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> - {basicContracts.map((contract) => ( - <div - key={contract.id} - className="p-3 border rounded-lg bg-card hover:bg-muted/50 transition-colors" - > - <div className="flex items-start gap-2"> - <div className="p-1.5 bg-primary/10 rounded"> - <Shield className="h-3.5 w-3.5 text-primary" /> - </div> - <div className="flex-1 min-w-0"> - <h4 className="font-medium text-sm truncate" title={contract.templateName}> - {contract.templateName} - </h4> - <Badge - variant={contract.signedAt ? "success" : "warning"} - className="text-xs mt-1.5" - > - {contract.signedAt ? ( - <> - <CheckCircle className="h-3 w-3 mr-1" /> - 서명완료 - </> + {basicContracts.length > 0 ? ( + <div className="space-y-4"> + {/* 계약 목록 - 그리드 레이아웃 */} + <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> + {basicContracts.map((contract) => ( + <div + key={contract.id} + className="p-3 border rounded-lg bg-card hover:bg-muted/50 transition-colors" + > + <div className="flex items-start gap-2"> + <div className="p-1.5 bg-primary/10 rounded"> + <Shield className="h-3.5 w-3.5 text-primary" /> + </div> + <div className="flex-1 min-w-0"> + <h4 className="font-medium text-sm truncate" title={contract.templateName}> + {contract.templateName} + </h4> + <Badge + variant={contract.signedAt ? "success" : "warning"} + className="text-xs mt-1.5" + > + {contract.signedAt ? ( + <> + <CheckCircle className="h-3 w-3 mr-1" /> + 서명완료 + </> + ) : ( + <> + <Clock className="h-3 w-3 mr-1" /> + 서명대기 + </> + )} + </Badge> + <p className="text-xs text-muted-foreground mt-1"> + {contract.signedAt + ? `${formatDate(new Date(contract.signedAt))}` + : contract.deadline + ? `~${formatDate(new Date(contract.deadline))}` + : '마감일 없음'} + </p> + </div> + </div> + </div> + ))} + </div> + + {/* 서명 상태 요약 및 액션 */} + {basicContracts.some(contract => !contract.signedAt) ? ( + <div className="flex items-center justify-between p-3 bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-900 rounded-lg"> + <div className="flex items-center gap-2"> + <AlertCircle className="h-4 w-4 text-amber-600" /> + <div> + <p className="text-sm font-medium"> + 서명 대기: {basicContracts.filter(c => !c.signedAt).length}/{basicContracts.length}개 + </p> + <p className="text-xs text-muted-foreground"> + 견적서 제출 전 모든 계약서 서명 필요 + </p> + </div> + </div> + <Button + type="button" + size="sm" + onClick={() => router.push(`/partners/basic-contract`)} + > + 서명하기 + </Button> + </div> + ) : ( + <Alert className="border-green-200 bg-green-50 dark:bg-green-950/20"> + <CheckCircle className="h-4 w-4 text-green-600" /> + <AlertDescription className="text-sm"> + 모든 기본계약 서명 완료 + </AlertDescription> + </Alert> + )} + </div> ) : ( - <> - <Clock className="h-3 w-3 mr-1" /> - 서명대기 - </> + <div className="text-center py-8"> + <FileText className="h-12 w-12 text-muted-foreground mx-auto mb-4" /> + <p className="text-muted-foreground"> + 이 RFQ에 요청된 기본계약이 없습니다 + </p> + </div> )} - </Badge> - <p className="text-xs text-muted-foreground mt-1"> - {contract.signedAt - ? `${formatDate(new Date(contract.signedAt))}` - : contract.deadline - ? `~${formatDate(new Date(contract.deadline))}` - : '마감일 없음'} - </p> - </div> - </div> - </div> - ))} - </div> - - {/* 서명 상태 요약 및 액션 */} - {basicContracts.some(contract => !contract.signedAt) ? ( - <div className="flex items-center justify-between p-3 bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-900 rounded-lg"> - <div className="flex items-center gap-2"> - <AlertCircle className="h-4 w-4 text-amber-600" /> - <div> - <p className="text-sm font-medium"> - 서명 대기: {basicContracts.filter(c => !c.signedAt).length}/{basicContracts.length}개 - </p> - <p className="text-xs text-muted-foreground"> - 견적서 제출 전 모든 계약서 서명 필요 - </p> - </div> - </div> - <Button - type="button" - size="sm" - onClick={() => router.push(`/partners/basic-contract`)} - > - 서명하기 - </Button> - </div> - ) : ( - <Alert className="border-green-200 bg-green-50 dark:bg-green-950/20"> - <CheckCircle className="h-4 w-4 text-green-600" /> - <AlertDescription className="text-sm"> - 모든 기본계약 서명 완료 - </AlertDescription> - </Alert> - )} - </div> - ) : ( - <div className="text-center py-8"> - <FileText className="h-12 w-12 text-muted-foreground mx-auto mb-4" /> - <p className="text-muted-foreground"> - 이 RFQ에 요청된 기본계약이 없습니다 - </p> - </div> - )} -</CardContent> + </CardContent> </Card> </TabsContent> @@ -429,8 +495,9 @@ export default function VendorResponseEditor({ 취소 </Button> <Button - type="submit" + type="button" // submit에서 button으로 변경 variant="secondary" + onClick={() => handleFormSubmit(false)} // 직접 핸들러 호출 disabled={loading} > {loading ? ( @@ -448,7 +515,7 @@ export default function VendorResponseEditor({ <Button type="button" variant="default" - onClick={methods.handleSubmit((data) => onSubmit(data, true))} + onClick={() => handleFormSubmit(true)} // 직접 핸들러 호출 disabled={loading || !allContractsSigned} > {!allContractsSigned ? ( |
