From 25749225689c3934bc10ad1e8285e13020b61282 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Thu, 4 Dec 2025 09:04:09 +0000
Subject: (최겸)구매 입찰, 계약 수정
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 lib/bidding/selection/selection-result-form.tsx | 213 +++++++++++++++++++-----
 1 file changed, 169 insertions(+), 44 deletions(-)

(limited to 'lib/bidding/selection/selection-result-form.tsx')

diff --git a/lib/bidding/selection/selection-result-form.tsx b/lib/bidding/selection/selection-result-form.tsx
index 54687cc9..af6b8d43 100644
--- a/lib/bidding/selection/selection-result-form.tsx
+++ b/lib/bidding/selection/selection-result-form.tsx
@@ -9,8 +9,8 @@ import { Button } from '@/components/ui/button'
 import { Textarea } from '@/components/ui/textarea'
 import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
 import { useToast } from '@/hooks/use-toast'
-import { saveSelectionResult } from './actions'
-import { Loader2, Save, FileText } from 'lucide-react'
+import { saveSelectionResult, getSelectionResult } from './actions'
+import { Loader2, Save, FileText, Download, X } from 'lucide-react'
 import { Dropzone, DropzoneZone, DropzoneUploadIcon, DropzoneTitle, DropzoneDescription, DropzoneInput } from '@/components/ui/dropzone'
 
 const selectionResultSchema = z.object({
@@ -22,12 +22,25 @@ type SelectionResultFormData = z.infer<typeof selectionResultSchema>
 interface SelectionResultFormProps {
   biddingId: number
   onSuccess: () => void
+  readOnly?: boolean
 }
 
-export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFormProps) {
+interface AttachmentInfo {
+  id: number
+  fileName: string
+  originalFileName: string
+  fileSize: number
+  mimeType: string
+  filePath: string
+  uploadedAt: Date | null
+}
+
+export function SelectionResultForm({ biddingId, onSuccess, readOnly = false }: SelectionResultFormProps) {
   const { toast } = useToast()
   const [isSubmitting, setIsSubmitting] = React.useState(false)
+  const [isLoading, setIsLoading] = React.useState(true)
   const [attachmentFiles, setAttachmentFiles] = React.useState<File[]>([])
+  const [existingAttachments, setExistingAttachments] = React.useState<AttachmentInfo[]>([])
 
   const form = useForm<SelectionResultFormData>({
     resolver: zodResolver(selectionResultSchema),
@@ -36,10 +49,53 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor
     },
   })
 
+  // 기존 선정결과 로드
+  React.useEffect(() => {
+    const loadSelectionResult = async () => {
+      setIsLoading(true)
+      try {
+        const result = await getSelectionResult(biddingId)
+        if (result.success && result.data) {
+          form.reset({
+            summary: result.data.summary || '',
+          })
+          if (result.data.attachments) {
+            setExistingAttachments(result.data.attachments)
+          }
+        }
+      } catch (error) {
+        console.error('Failed to load selection result:', error)
+        toast({
+          title: '로드 실패',
+          description: '선정결과를 불러오는데 실패했습니다.',
+          variant: 'destructive',
+        })
+      } finally {
+        setIsLoading(false)
+      }
+    }
+
+    loadSelectionResult()
+  }, [biddingId, form, toast])
+
   const removeAttachmentFile = (index: number) => {
     setAttachmentFiles(prev => prev.filter((_, i) => i !== index))
   }
 
+  const removeExistingAttachment = (id: number) => {
+    setExistingAttachments(prev => prev.filter(att => att.id !== id))
+  }
+
+  const downloadAttachment = (filePath: string, fileName: string) => {
+    // 파일 다운로드 (filePath가 절대 경로인 경우)
+    if (filePath.startsWith('http') || filePath.startsWith('/')) {
+      window.open(filePath, '_blank')
+    } else {
+      // 상대 경로인 경우
+      window.open(`/api/files/${filePath}`, '_blank')
+    }
+  }
+
   const onSubmit = async (data: SelectionResultFormData) => {
     setIsSubmitting(true)
     try {
@@ -74,6 +130,22 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor
     }
   }
 
+  if (isLoading) {
+    return (
+      <Card>
+        <CardHeader>
+          <CardTitle>선정결과</CardTitle>
+        </CardHeader>
+        <CardContent>
+          <div className="flex items-center justify-center py-8">
+            <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
+            <span className="ml-2 text-sm text-muted-foreground">로딩 중...</span>
+          </div>
+        </CardContent>
+      </Card>
+    )
+  }
+
   return (
     <Card>
       <CardHeader>
@@ -94,6 +166,7 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor
                       placeholder="선정결과에 대한 요약을 입력해주세요..."
                       className="min-h-[120px]"
                       {...field}
+                      disabled={readOnly}
                     />
                   </FormControl>
                   <FormMessage />
@@ -104,35 +177,83 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor
             {/* 첨부파일 */}
             <div className="space-y-4">
               <FormLabel>첨부파일</FormLabel>
-              <Dropzone
-                maxSize={10 * 1024 * 1024} // 10MB
-                onDropAccepted={(files) => {
-                  const newFiles = Array.from(files)
-                  setAttachmentFiles(prev => [...prev, ...newFiles])
-                }}
-                onDropRejected={() => {
-                  toast({
-                    title: "파일 업로드 거부",
-                    description: "파일 크기 및 형식을 확인해주세요.",
-                    variant: "destructive",
-                  })
-                }}
-              >
-                <DropzoneZone>
-                  <DropzoneUploadIcon className="mx-auto h-12 w-12 text-muted-foreground" />
-                  <DropzoneTitle className="text-lg font-medium">
-                    파일을 드래그하거나 클릭하여 업로드
-                  </DropzoneTitle>
-                  <DropzoneDescription className="text-sm text-muted-foreground">
-                    PDF, Word, Excel, 이미지 파일 (최대 10MB)
-                  </DropzoneDescription>
-                </DropzoneZone>
-                <DropzoneInput />
-              </Dropzone>
+              
+              {/* 기존 첨부파일 */}
+              {existingAttachments.length > 0 && (
+                <div className="space-y-2">
+                  <h4 className="text-sm font-medium">기존 첨부파일</h4>
+                  <div className="space-y-2">
+                    {existingAttachments.map((attachment) => (
+                      <div
+                        key={attachment.id}
+                        className="flex items-center justify-between p-3 bg-muted rounded-lg"
+                      >
+                        <div className="flex items-center gap-3">
+                          <FileText className="h-4 w-4 text-muted-foreground" />
+                          <div>
+                            <p className="text-sm font-medium">{attachment.originalFileName || attachment.fileName}</p>
+                            <p className="text-xs text-muted-foreground">
+                              {(attachment.fileSize / 1024 / 1024).toFixed(2)} MB
+                            </p>
+                          </div>
+                        </div>
+                        <div className="flex items-center gap-2">
+                          <Button
+                            type="button"
+                            variant="ghost"
+                            size="sm"
+                            onClick={() => downloadAttachment(attachment.filePath, attachment.originalFileName || attachment.fileName)}
+                          >
+                            <Download className="h-4 w-4" />
+                          </Button>
+                          {!readOnly && (
+                            <Button
+                              type="button"
+                              variant="ghost"
+                              size="sm"
+                              onClick={() => removeExistingAttachment(attachment.id)}
+                            >
+                              <X className="h-4 w-4" />
+                            </Button>
+                          )}
+                        </div>
+                      </div>
+                    ))}
+                  </div>
+                </div>
+              )}
+
+              {!readOnly && (
+                <Dropzone
+                  maxSize={10 * 1024 * 1024} // 10MB
+                  onDropAccepted={(files) => {
+                    const newFiles = Array.from(files)
+                    setAttachmentFiles(prev => [...prev, ...newFiles])
+                  }}
+                  onDropRejected={() => {
+                    toast({
+                      title: "파일 업로드 거부",
+                      description: "파일 크기 및 형식을 확인해주세요.",
+                      variant: "destructive",
+                    })
+                  }}
+                >
+                  <DropzoneZone>
+                    <DropzoneUploadIcon className="mx-auto h-12 w-12 text-muted-foreground" />
+                    <DropzoneTitle className="text-lg font-medium">
+                      파일을 드래그하거나 클릭하여 업로드
+                    </DropzoneTitle>
+                    <DropzoneDescription className="text-sm text-muted-foreground">
+                      PDF, Word, Excel, 이미지 파일 (최대 10MB)
+                    </DropzoneDescription>
+                  </DropzoneZone>
+                  <DropzoneInput />
+                </Dropzone>
+              )}
 
               {attachmentFiles.length > 0 && (
                 <div className="space-y-2">
-                  <h4 className="text-sm font-medium">업로드된 파일</h4>
+                  <h4 className="text-sm font-medium">새로 추가할 파일</h4>
                   <div className="space-y-2">
                     {attachmentFiles.map((file, index) => (
                       <div
@@ -148,14 +269,16 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor
                             </p>
                           </div>
                         </div>
-                        <Button
-                          type="button"
-                          variant="ghost"
-                          size="sm"
-                          onClick={() => removeAttachmentFile(index)}
-                        >
-                          제거
-                        </Button>
+                        {!readOnly && (
+                          <Button
+                            type="button"
+                            variant="ghost"
+                            size="sm"
+                            onClick={() => removeAttachmentFile(index)}
+                          >
+                            제거
+                          </Button>
+                        )}
                       </div>
                     ))}
                   </div>
@@ -164,13 +287,15 @@ export function SelectionResultForm({ biddingId, onSuccess }: SelectionResultFor
             </div>
 
             {/* 저장 버튼 */}
-            <div className="flex justify-end">
-              <Button type="submit" disabled={isSubmitting}>
-                {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
-                <Save className="mr-2 h-4 w-4" />
-                저장
-              </Button>
-            </div>
+            {!readOnly && (
+              <div className="flex justify-end">
+                <Button type="submit" disabled={isSubmitting}>
+                  {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+                  <Save className="mr-2 h-4 w-4" />
+                  저장
+                </Button>
+              </div>
+            )}
           </form>
         </Form>
       </CardContent>
-- 
cgit v1.2.3

