From c657ef972feeafff16ab0e07cb4771f7dd141ba0 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 10 Jul 2025 09:55:45 +0000 Subject: (대표님) 20250710 작업사항 - 평가 첨부, 로그인, SEDP 변경 요구사항 반영 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/evaluation-submit/evaluation-form.tsx | 453 ++++++++++++++++++++++++------ 1 file changed, 370 insertions(+), 83 deletions(-) (limited to 'lib/evaluation-submit/evaluation-form.tsx') diff --git a/lib/evaluation-submit/evaluation-form.tsx b/lib/evaluation-submit/evaluation-form.tsx index 65da72b6..fbdcee69 100644 --- a/lib/evaluation-submit/evaluation-form.tsx +++ b/lib/evaluation-submit/evaluation-form.tsx @@ -27,20 +27,28 @@ import { Send, ArrowLeft, AlertCircle, - FileText + FileText, + Upload, + File, + X, + Download, + Paperclip } from "lucide-react" import { useRouter } from "next/navigation" import { useToast } from "@/hooks/use-toast" import { updateEvaluationResponse, + updateVariableEvaluationResponse, completeEvaluation } from "./service" import { - type EvaluationFormData, type EvaluationQuestionItem, EVALUATION_CATEGORIES } from "./validation" import { DEPARTMENT_CODE_LABELS, divisionMap, vendortypeMap } from "@/types/evaluation" +import { EvaluationFormData } from "@/types/evaluation-form" +// 파일 다운로드 유틸리티 import +import { downloadFile, formatFileSize, getFileInfo } from "@/lib/file-download" interface EvaluationFormProps { formData: EvaluationFormData @@ -53,6 +61,15 @@ interface QuestionResponse { comment: string } +interface AttachmentInfo { + id: number + originalFileName: string + publicPath: string + fileSize: number + description?: string + createdAt: Date +} + /** * 평가 폼 메인 컴포넌트 (테이블 레이아웃) */ @@ -63,6 +80,7 @@ export function EvaluationForm({ formData, onSubmit }: EvaluationFormProps) { const [isSaving, setIsSaving] = React.useState(false) const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(false) const [showCompleteDialog, setShowCompleteDialog] = React.useState(false) + const [uploadingFiles, setUploadingFiles] = React.useState>(new Set()) const { evaluationInfo, questions } = formData @@ -85,6 +103,44 @@ export function EvaluationForm({ formData, onSubmit }: EvaluationFormProps) { return initial }) + // 첨부파일 상태 관리 (서버에서 받은 데이터로 초기화) + const [attachments, setAttachments] = React.useState>(() => { + console.log('Initializing attachments from server data...') + const initial: Record = {} + questions.forEach(question => { + const questionAttachments = question.attachments || [] + initial[question.criteriaId] = questionAttachments + if (questionAttachments.length > 0) { + console.log(`Question ${question.criteriaId} has ${questionAttachments.length} attachments:`, questionAttachments) + } + }) + console.log('Initial attachments state:', initial) + return initial + }) + + // 첨부파일 다운로드 핸들러 - downloadFile 사용 + const handleDownloadAttachment = async (attachment: AttachmentInfo) => { + try { + await downloadFile( + attachment.publicPath, + attachment.originalFileName, + { + action: 'download', + showToast: true, + showSuccessToast: true, + onError: (error) => { + console.error("파일 다운로드 실패:", error) + }, + onSuccess: (fileName, fileSize) => { + console.log("파일 다운로드 성공:", fileName, fileSize ? formatFileSize(fileSize) : '') + } + } + ) + } catch (error) { + console.error("다운로드 처리 중 오류:", error) + } + } + // 카테고리별 질문 그룹화 const questionsByCategory = React.useMemo(() => { const grouped = questions.reduce((acc, question) => { @@ -146,6 +202,130 @@ export function EvaluationForm({ formData, onSubmit }: EvaluationFormProps) { setHasUnsavedChanges(true) } + // 파일 업로드 핸들러 + const handleFileUpload = async (questionId: number, file: File, description?: string) => { + try { + console.log('Starting file upload for question:', questionId, 'file:', file.name) + setUploadingFiles(prev => new Set(prev).add(questionId)) + + // 질문 정보 확인 + const question = questions.find(q => q.criteriaId === questionId) + if (!question) { + throw new Error('질문을 찾을 수 없습니다.') + } + + // 답변이 있는지 확인 + const response = responses[questionId] + const isVariable = question.scoreType === 'variable' + const isAnswered = isVariable ? + (response.score !== null) : + (response.detailId !== null && response.detailId > 0) + + if (!isAnswered) { + throw new Error('먼저 답변을 선택해주세요.') + } + + // FormData 생성 + const formData = new FormData() + formData.append('file', file) + formData.append('questionId', questionId.toString()) + formData.append('evaluationId', evaluationInfo.id.toString()) + + if (description) { + formData.append('description', description) + } + + if (isVariable) { + formData.append('isVariable', 'true') + } + + console.log('Sending upload request...') + + // 파일 업로드 API 호출 + const response_api = await fetch('/api/evaluation/attachments', { + method: 'POST', + body: formData, + }) + + console.log('Upload response status:', response_api.status) + + if (!response_api.ok) { + const errorText = await response_api.text() + console.error('Upload failed with status:', response_api.status, 'error:', errorText) + throw new Error(`파일 업로드에 실패했습니다. (${response_api.status})`) + } + + const result = await response_api.json() + console.log('Upload result:', result) + + if (result.success && result.attachment) { + // 첨부파일 목록 업데이트 + setAttachments(prev => ({ + ...prev, + [questionId]: [...(prev[questionId] || []), { + id: result.attachment.id, + originalFileName: result.attachment.originalFileName, + publicPath: result.attachment.publicPath, + fileSize: result.attachment.fileSize, + description: result.attachment.description, + createdAt: new Date(result.attachment.createdAt), + }] + })) + + toast({ + title: "파일 업로드 완료", + description: `${file.name}이 성공적으로 업로드되었습니다.`, + }) + } else { + throw new Error(result.error || '파일 업로드에 실패했습니다.') + } + } catch (error) { + console.error('File upload failed:', error) + toast({ + title: "업로드 실패", + description: error instanceof Error ? error.message : "파일 업로드 중 오류가 발생했습니다.", + variant: "destructive", + }) + } finally { + setUploadingFiles(prev => { + const newSet = new Set(prev) + newSet.delete(questionId) + return newSet + }) + } + } + + // 첨부파일 삭제 핸들러 + const handleDeleteAttachment = async (questionId: number, attachmentId: number) => { + try { + const response = await fetch(`/api/evaluation/attachments/${attachmentId}`, { + method: 'DELETE', + }) + + if (!response.ok) { + throw new Error('파일 삭제에 실패했습니다.') + } + + // 첨부파일 목록에서 제거 + setAttachments(prev => ({ + ...prev, + [questionId]: prev[questionId]?.filter(att => att.id !== attachmentId) || [] + })) + + toast({ + title: "파일 삭제 완료", + description: "첨부파일이 삭제되었습니다.", + }) + } catch (error) { + console.error('File deletion failed:', error) + toast({ + title: "삭제 실패", + description: "파일 삭제 중 오류가 발생했습니다.", + variant: "destructive", + }) + } + } + // 임시저장 const handleSave = async () => { try { @@ -166,12 +346,23 @@ export function EvaluationForm({ formData, onSubmit }: EvaluationFormProps) { const question = questions.find(q => q.criteriaId === parseInt(questionId)) const isVariable = question?.scoreType === 'variable' - return updateEvaluationResponse( - evaluationInfo.id, - isVariable ? -1 : response.detailId!, - response.comment, - response.score || undefined - ) + if (isVariable) { + // Variable 타입은 별도 함수 사용 + return updateVariableEvaluationResponse( + evaluationInfo.id, + parseInt(questionId), // criteriaId + response.score!, + response.comment || undefined + ) + } else { + // 일반 타입 + return updateEvaluationResponse( + evaluationInfo.id, + response.detailId!, + response.comment || undefined, + response.score || undefined + ) + } }) await Promise.all(promises) @@ -285,35 +476,58 @@ export function EvaluationForm({ formData, onSubmit }: EvaluationFormProps) { -
-
- -
{evaluationInfo.vendorName} ({evaluationInfo.vendorCode})
-
-
- -
- - {divisionMap[evaluationInfo.division] || evaluationInfo.division} - -
-
-
- -
- - {vendortypeMap[evaluationInfo.materialType] || evaluationInfo.materialType} - -
-
-
- -
- {DEPARTMENT_CODE_LABELS[evaluationInfo.departmentCode] || evaluationInfo.departmentCode} -
-
-
-
+
+
+ +
{evaluationInfo.vendorName} ({evaluationInfo.vendorCode})
+
+
+ +
+ + {divisionMap[evaluationInfo.division] || evaluationInfo.division} + +
+
+
+ +
+ + {vendortypeMap[evaluationInfo.materialType] || evaluationInfo.materialType} + +
+
+
+ +
+ {DEPARTMENT_CODE_LABELS[evaluationInfo.departmentCode] || evaluationInfo.departmentCode} +
+
+
+ + {/* 📎 첨부파일 통계 정보 */} + {formData.attachmentStats && formData.attachmentStats.totalFiles > 0 && ( +
+
+
+ + 첨부파일 현황 +
+
+
+ 총 {formData.attachmentStats.totalFiles}개 파일 +
+
+ 크기: {formatFileSize(formData.attachmentStats.totalSize)} +
+
+ 첨부 질문: {formData.attachmentStats.questionsWithAttachments}개 +
+
+
+
+ )} + {/* 평가 테이블 - 카테고리별 */} @@ -370,12 +584,14 @@ export function EvaluationForm({ formData, onSubmit }: EvaluationFormProps) { 답변 선택 점수 추가 의견 + 첨부파일 상태 {categoryQuestions.map((question) => { const response = responses[question.criteriaId] + const questionAttachments = attachments[question.criteriaId] || [] const isVariable = question.scoreType === 'variable' const isAnswered = isVariable ? @@ -479,6 +695,80 @@ export function EvaluationForm({ formData, onSubmit }: EvaluationFormProps) { /> + +
+ {/* 파일 업로드 버튼 */} +
+ { + const file = e.target.files?.[0] + if (file && isAnswered) handleFileUpload(question.criteriaId, file) + }} + disabled={!isAnswered || isLoading || isSaving || uploadingFiles.has(question.criteriaId)} + /> + +
+ + {/* 첨부된 파일 목록 - 개선된 버전 */} + {questionAttachments.length > 0 && ( +
+ {questionAttachments.map((attachment) => { + const fileInfo = getFileInfo(attachment.originalFileName) + return ( +
+
+ {fileInfo.icon} +
+
+ {attachment.originalFileName} +
+
+ {formatFileSize(attachment.fileSize)} +
+
+
+
+ + +
+
+ ) + })} +
+ )} +
+
+ {isAnswered ? ( @@ -503,54 +793,51 @@ export function EvaluationForm({ formData, onSubmit }: EvaluationFormProps) { {/* 하단 액션 버튼 */}
- {!evaluationInfo.isCompleted && ( - <> -
- - {hasUnsavedChanges && ( -
- - 저장되지 않은 변경사항이 있습니다 + {!evaluationInfo.isCompleted && ( + <> +
+ {hasUnsavedChanges && ( +
+ + 저장되지 않은 변경사항이 있습니다 +
+ )} +
+ + 진행률: {completedCount}/{totalCount} +
- )} -
- - 진행률: {completedCount}/{totalCount} -
-
-
- - - - - - - -
- - )} +
+ + + + + +
+ + )}
-- cgit v1.2.3