From c657ef972feeafff16ab0e07cb4771f7dd141ba0 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
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<Set<number>>(new Set())
 
   const { evaluationInfo, questions } = formData
 
@@ -85,6 +103,44 @@ export function EvaluationForm({ formData, onSubmit }: EvaluationFormProps) {
     return initial
   })
 
+  // 첨부파일 상태 관리 (서버에서 받은 데이터로 초기화)
+  const [attachments, setAttachments] = React.useState<Record<number, AttachmentInfo[]>>(() => {
+    console.log('Initializing attachments from server data...')
+    const initial: Record<number, AttachmentInfo[]> = {}
+    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) {
           </CardTitle>
         </CardHeader>
         <CardContent className="pt-4 pb-4">
-    <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
-      <div className="space-y-1">
-        <Label className="text-sm text-muted-foreground">협력업체</Label>
-        <div className="font-medium text-sm">{evaluationInfo.vendorName} ({evaluationInfo.vendorCode})</div>
-      </div>
-      <div className="space-y-1">
-        <Label className="text-sm text-muted-foreground">사업부</Label>
-        <div>
-        <Badge variant="outline">
-          {divisionMap[evaluationInfo.division] || evaluationInfo.division}
-        </Badge>
-        </div>
-      </div> 
-      <div className="space-y-1">
-        <Label className="text-sm text-muted-foreground">자재유형</Label>
-        <div>
-        <Badge variant="outline">
-          {vendortypeMap[evaluationInfo.materialType] || evaluationInfo.materialType}
-        </Badge>
-        </div>
-      </div>
-      <div className="space-y-1">
-        <Label className="text-sm text-muted-foreground">담당부서</Label>
-        <div className="font-medium text-sm">
-          {DEPARTMENT_CODE_LABELS[evaluationInfo.departmentCode] || evaluationInfo.departmentCode}
-        </div>
-      </div>
-    </div>
-  </CardContent>
+          <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
+            <div className="space-y-1">
+              <Label className="text-sm text-muted-foreground">협력업체</Label>
+              <div className="font-medium text-sm">{evaluationInfo.vendorName} ({evaluationInfo.vendorCode})</div>
+            </div>
+            <div className="space-y-1">
+              <Label className="text-sm text-muted-foreground">사업부</Label>
+              <div>
+                <Badge variant="outline">
+                  {divisionMap[evaluationInfo.division] || evaluationInfo.division}
+                </Badge>
+              </div>
+            </div>
+            <div className="space-y-1">
+              <Label className="text-sm text-muted-foreground">자재유형</Label>
+              <div>
+                <Badge variant="outline">
+                  {vendortypeMap[evaluationInfo.materialType] || evaluationInfo.materialType}
+                </Badge>
+              </div>
+            </div>
+            <div className="space-y-1">
+              <Label className="text-sm text-muted-foreground">담당부서</Label>
+              <div className="font-medium text-sm">
+                {DEPARTMENT_CODE_LABELS[evaluationInfo.departmentCode] || evaluationInfo.departmentCode}
+              </div>
+            </div>
+          </div>
+
+          {/* 📎 첨부파일 통계 정보 */}
+          {formData.attachmentStats && formData.attachmentStats.totalFiles > 0 && (
+            <div className="border-t pt-4">
+              <div className="flex items-center gap-4 text-sm">
+                <div className="flex items-center gap-2">
+                  <Paperclip className="h-4 w-4 text-muted-foreground" />
+                  <span className="font-medium">첨부파일 현황</span>
+                </div>
+                <div className="flex items-center gap-4">
+                  <div className="text-muted-foreground">
+                    총 <span className="font-medium text-foreground">{formData.attachmentStats.totalFiles}</span>개 파일
+                  </div>
+                  <div className="text-muted-foreground">
+                    크기: <span className="font-medium text-foreground">{formatFileSize(formData.attachmentStats.totalSize)}</span>
+                  </div>
+                  <div className="text-muted-foreground">
+                    첨부 질문: <span className="font-medium text-foreground">{formData.attachmentStats.questionsWithAttachments}</span>개
+                  </div>
+                </div>
+              </div>
+            </div>
+          )}
+        </CardContent>
       </Card>
 
       {/* 평가 테이블 - 카테고리별 */}
@@ -370,12 +584,14 @@ export function EvaluationForm({ formData, onSubmit }: EvaluationFormProps) {
                     <TableHead className="w-[200px]">답변 선택</TableHead>
                     <TableHead className="w-[80px]">점수</TableHead>
                     <TableHead className="w-[250px]">추가 의견</TableHead>
+                    <TableHead className="w-[200px]">첨부파일</TableHead>
                     <TableHead className="w-[80px]">상태</TableHead>
                   </TableRow>
                 </TableHeader>
                 <TableBody>
                   {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) {
                           />
                         </TableCell>
 
+                        <TableCell>
+                          <div className="space-y-2">
+                            {/* 파일 업로드 버튼 */}
+                            <div className="flex items-center gap-2">
+                              <input
+                                type="file"
+                                id={`file-${question.criteriaId}`}
+
+                                className="hidden"
+                                accept=".pdf,.doc,.docx,.hwp,.xls,.xlsx,.jpg,.jpeg,.png,.gif"
+                                onChange={(e) => {
+                                  const file = e.target.files?.[0]
+                                  if (file && isAnswered) handleFileUpload(question.criteriaId, file)
+                                }}
+                                disabled={!isAnswered || isLoading || isSaving || uploadingFiles.has(question.criteriaId)}
+                              />
+                              <Button
+                                asChild       /* shadcn/ui -> 내부에 <button> 대신 원하는 태그로 감싸 줌 */
+                                variant="outline"
+                                size="sm"
+                                disabled={!isAnswered || isLoading || isSaving || uploadingFiles.has(question.criteriaId)}
+                                className="flex items-center gap-1"
+                              >
+                                <label htmlFor={`file-${question.criteriaId}`} className="cursor-pointer">
+                                  <Upload className="h-3 w-3" />
+                                  {uploadingFiles.has(question.criteriaId) ? "업로드 중..." : "파일 첨부"}
+                                </label>
+                              </Button>
+                            </div>
+
+                            {/* 첨부된 파일 목록 - 개선된 버전 */}
+                            {questionAttachments.length > 0 && (
+                              <div className="space-y-1">
+                                {questionAttachments.map((attachment) => {
+                                  const fileInfo = getFileInfo(attachment.originalFileName)
+                                  return (
+                                    <div key={attachment.id} className="flex items-center justify-between p-2 bg-muted rounded-md">
+                                      <div className="flex items-center gap-2 flex-1">
+                                        <span className="text-sm">{fileInfo.icon}</span>
+                                        <div className="flex-1 min-w-0">
+                                          <div className="text-xs font-medium truncate">
+                                            {attachment.originalFileName}
+                                          </div>
+                                          <div className="text-xs text-muted-foreground">
+                                            {formatFileSize(attachment.fileSize)}
+                                          </div>
+                                        </div>
+                                      </div>
+                                      <div className="flex items-center gap-1">
+                                        <Button
+                                          variant="ghost"
+                                          size="sm"
+                                          onClick={() => handleDownloadAttachment(attachment)}
+                                          className="h-6 w-6 p-0"
+                                        >
+                                          <Download className="h-3 w-3" />
+                                        </Button>
+                                        <Button
+                                          variant="ghost"
+                                          size="sm"
+                                          onClick={() => handleDeleteAttachment(question.criteriaId, attachment.id)}
+                                          className="h-6 w-6 p-0 text-destructive hover:text-destructive"
+                                        >
+                                          <X className="h-3 w-3" />
+                                        </Button>
+                                      </div>
+                                    </div>
+                                  )
+                                })}
+                              </div>
+                            )}
+                          </div>
+                        </TableCell>
+
                         <TableCell>
                           {isAnswered ? (
                             <Badge variant="default" className="text-xs">
@@ -503,54 +793,51 @@ export function EvaluationForm({ formData, onSubmit }: EvaluationFormProps) {
       {/* 하단 액션 버튼 */}
       <div className="sticky bottom-0 bg-background border-t p-4">
         <div className="flex items-center justify-between max-w-7xl mx-auto">
-        {!evaluationInfo.isCompleted && (
-              <>
-          <div className="flex items-center gap-4 text-sm text-muted-foreground">
-            
-            {hasUnsavedChanges && (
-              <div className="flex items-center gap-1">
-                <AlertCircle className="h-4 w-4 text-amber-500" />
-                <span>저장되지 않은 변경사항이 있습니다</span>
+          {!evaluationInfo.isCompleted && (
+            <>
+              <div className="flex items-center gap-4 text-sm text-muted-foreground">
+                {hasUnsavedChanges && (
+                  <div className="flex items-center gap-1">
+                    <AlertCircle className="h-4 w-4 text-amber-500" />
+                    <span>저장되지 않은 변경사항이 있습니다</span>
+                  </div>
+                )}
+                <div className="flex items-center gap-1">
+                  <FileText className="h-4 w-4" />
+                  <span>진행률: {completedCount}/{totalCount}</span>
+                </div>
               </div>
-            )}
-            <div className="flex items-center gap-1">
-              <FileText className="h-4 w-4" />
-              <span>진행률: {completedCount}/{totalCount}</span>
-            </div>
-          </div>
 
-          <div className="flex items-center gap-2">
-            <Button
-              variant="outline"
-              onClick={() => router.back()}
-              disabled={isLoading || isSaving}
-            >
-              취소
-            </Button>
-
-            <Button
-              variant="secondary"
-              onClick={handleSave}
-              disabled={isLoading || isSaving || !hasUnsavedChanges}
-              className="flex items-center gap-2"
-            >
-              <Save className="h-4 w-4" />
-              {isSaving ? "저장 중..." : "임시저장"}
-            </Button>
-
-
-              <Button
-                onClick={handleCompleteClick}
-                disabled={isLoading || isSaving || !allCompleted}
-                className="flex items-center gap-2"
-              >
-                <Send className="h-4 w-4" />
-                평가 완료
-              </Button>
-   
-          </div>
-                     </>
-                    )}
+              <div className="flex items-center gap-2">
+                <Button
+                  variant="outline"
+                  onClick={() => router.back()}
+                  disabled={isLoading || isSaving}
+                >
+                  취소
+                </Button>
+
+                <Button
+                  variant="secondary"
+                  onClick={handleSave}
+                  disabled={isLoading || isSaving || !hasUnsavedChanges}
+                  className="flex items-center gap-2"
+                >
+                  <Save className="h-4 w-4" />
+                  {isSaving ? "저장 중..." : "임시저장"}
+                </Button>
+
+                <Button
+                  onClick={handleCompleteClick}
+                  disabled={isLoading || isSaving || !allCompleted}
+                  className="flex items-center gap-2"
+                >
+                  <Send className="h-4 w-4" />
+                  평가 완료
+                </Button>
+              </div>
+            </>
+          )}
         </div>
       </div>
 
-- 
cgit v1.2.3

