From ee77f36b1ceece1236d45fba102c3ea410acebc1 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Thu, 11 Sep 2025 11:20:42 +0000
Subject: (최겸) 구매 계약 메인 및 상세 기능 개발(템플릿 연동 및 계약 전달 개발
 필요)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../detail/general-contract-documents.tsx          | 372 +++++++++++++++++++++
 1 file changed, 372 insertions(+)
 create mode 100644 lib/general-contracts/detail/general-contract-documents.tsx

(limited to 'lib/general-contracts/detail/general-contract-documents.tsx')

diff --git a/lib/general-contracts/detail/general-contract-documents.tsx b/lib/general-contracts/detail/general-contract-documents.tsx
new file mode 100644
index 00000000..11d2de68
--- /dev/null
+++ b/lib/general-contracts/detail/general-contract-documents.tsx
@@ -0,0 +1,372 @@
+'use client'
+
+import React, { useState, useEffect } from '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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Badge } from '@/components/ui/badge'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
+import { 
+  Download, 
+  Trash2, 
+  FileText, 
+  LoaderIcon,
+  Paperclip,
+  MessageSquare
+} from 'lucide-react'
+import { toast } from 'sonner'
+import { useTransition } from 'react'
+import { 
+  uploadContractAttachment, 
+  getContractAttachments, 
+  getContractAttachmentForDownload,
+  deleteContractAttachment 
+} from '../service'
+import { downloadFile } from '@/lib/file-download'
+
+interface ContractDocument {
+  id: number
+  contractId: number
+  documentName: string
+  fileName: string
+  filePath: string
+  shiComment?: string | null
+  vendorComment?: string | null
+  uploadedAt: Date
+  uploadedById: number
+}
+
+interface ContractDocumentsProps {
+  contractId: number
+  userId: string
+  readOnly?: boolean
+}
+
+export function ContractDocuments({ contractId, userId, readOnly = false }: ContractDocumentsProps) {
+  const [documents, setDocuments] = useState<ContractDocument[]>([])
+  const [isLoading, setIsLoading] = useState(false)
+  const [isPending, startTransition] = useTransition()
+  const [editingComment, setEditingComment] = useState<{ id: number; type: 'shi' | 'vendor' } | null>(null)
+  const [commentText, setCommentText] = useState('')
+  const [selectedDocumentType, setSelectedDocumentType] = useState('')
+
+  const loadDocuments = React.useCallback(async () => {
+    setIsLoading(true)
+    try {
+      const documentList = await getContractAttachments(contractId)
+      setDocuments(documentList as ContractDocument[])
+    } catch (error) {
+      console.error('Error loading documents:', error)
+      toast.error('문서 목록을 불러오는 중 오류가 발생했습니다.')
+    } finally {
+      setIsLoading(false)
+    }
+  }, [contractId])
+
+  useEffect(() => {
+    loadDocuments()
+  }, [loadDocuments])
+
+  const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
+    const file = event.target.files?.[0]
+    if (!file) return
+
+    if (!selectedDocumentType) {
+      toast.error('문서 유형을 선택해주세요.')
+      return
+    }
+
+    startTransition(async () => {
+      try {
+        await uploadContractAttachment(contractId, file, userId, selectedDocumentType)
+        toast.success('문서가 업로드되었습니다.')
+        loadDocuments()
+        // 파일 입력 초기화
+        event.target.value = ''
+      } catch (error) {
+        console.error('Error uploading document:', error)
+        toast.error('문서 업로드 중 오류가 발생했습니다.')
+      }
+    })
+  }
+
+  const handleDownload = async (document: ContractDocument) => {
+    try {
+      const fileData = await getContractAttachmentForDownload(document.id, contractId)
+      downloadFile(fileData.attachment?.filePath || '', fileData.attachment?.fileName || '', {
+        showToast: true
+      })
+    } catch (error) {
+      console.error('Error downloading document:', error)
+      toast.error('문서 다운로드 중 오류가 발생했습니다.')
+    }
+  }
+
+  const handleDelete = async (documentId: number) => {
+
+    startTransition(async () => {
+      try {
+        await deleteContractAttachment(documentId, contractId, userId)
+        toast.success('문서가 삭제되었습니다.')
+        loadDocuments()
+      } catch (error) {
+        console.error('Error deleting document:', error)
+        toast.error('문서 삭제 중 오류가 발생했습니다.')
+      }
+    })
+  }
+
+  const handleEditComment = (documentId: number, type: 'shi' | 'vendor', currentComment?: string) => {
+    setEditingComment({ id: documentId, type })
+    setCommentText(currentComment || '')
+  }
+
+  const handleSaveComment = async () => {
+    if (!editingComment) return
+
+    try {
+      // TODO: API 호출로 댓글 저장
+      toast.success('댓글이 저장되었습니다.')
+      setEditingComment(null)
+      setCommentText('')
+      loadDocuments()
+    } catch (error) {
+      console.error('Error saving comment:', error)
+      toast.error('댓글 저장 중 오류가 발생했습니다.')
+    }
+  }
+
+  const getDocumentTypeLabel = (documentName: string) => {
+    switch (documentName) {
+      case 'specification': return '사양'
+      case 'pricing': return '단가종류'
+      case 'other': return '기타'
+      default: return documentName
+    }
+  }
+
+  const getDocumentTypeColor = (documentName: string) => {
+    switch (documentName) {
+      case 'specification': return 'bg-blue-100 text-blue-800'
+      case 'pricing': return 'bg-green-100 text-green-800'
+      case 'other': return 'bg-gray-100 text-gray-800'
+      default: return 'bg-gray-100 text-gray-800'
+    }
+  }
+
+  const groupedDocuments = documents.reduce((acc, doc) => {
+    const type = doc.documentName
+    if (!acc[type]) {
+      acc[type] = []
+    }
+    acc[type].push(doc)
+    return acc
+  }, {} as Record<string, ContractDocument[]>)
+
+  const documentTypes = [
+    { value: 'specification', label: '사양' },
+    { value: 'pricing', label: '단가종류' },
+    { value: 'other', label: '기타' }
+  ]
+
+  return (
+    <Card>
+      <CardHeader>
+        <CardTitle className="flex items-center gap-2">
+          <Paperclip className="h-5 w-5" />
+          계약 첨부문서
+        </CardTitle>
+      </CardHeader>
+      <CardContent className="space-y-6">
+        {/* 파일 업로드 */}
+        {!readOnly && (
+          <div className="space-y-4">
+            <div className="flex items-center gap-4">
+              <Select value={selectedDocumentType} onValueChange={setSelectedDocumentType}>
+                <SelectTrigger className="w-40">
+                  <SelectValue placeholder="문서 유형" />
+                </SelectTrigger>
+                <SelectContent>
+                  {documentTypes.map((type) => (
+                    <SelectItem key={type.value} value={type.value}>
+                      {type.label}
+                    </SelectItem>
+                  ))}
+                </SelectContent>
+              </Select>
+              <Input
+                type="file"
+                onChange={handleFileUpload}
+                disabled={isPending}
+                className="flex-1"
+              />
+            </div>
+          </div>
+        )}
+
+        {/* 문서 목록 */}
+        {isLoading ? (
+          <div className="flex items-center justify-center py-8">
+            <LoaderIcon className="h-6 w-6 animate-spin" />
+            <span className="ml-2">문서를 불러오는 중...</span>
+          </div>
+        ) : documents.length === 0 ? (
+          <div className="text-center py-8 text-muted-foreground">
+            <FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
+            <p>업로드된 문서가 없습니다.</p>
+          </div>
+        ) : (
+          <div className="space-y-6">
+            {Object.entries(groupedDocuments).map(([type, docs]) => (
+              <div key={type} className="space-y-3">
+                <div className="flex items-center gap-2">
+                  <Badge className={getDocumentTypeColor(type)}>
+                    {getDocumentTypeLabel(type)}
+                  </Badge>
+                  <span className="text-sm text-muted-foreground">
+                    {docs.length}개 문서
+                  </span>
+                </div>
+                
+                <div className="space-y-3">
+                  {docs.map((doc) => (
+                    <div key={doc.id} className="border rounded-lg p-4 space-y-3">
+                      <div className="flex items-center justify-between">
+                        <div className="flex items-center gap-3">
+                          <FileText className="h-5 w-5 text-muted-foreground" />
+                          <div>
+                            <p className="font-medium">{doc.fileName}</p>
+                            <p className="text-sm text-muted-foreground">
+                              업로드: {new Date(doc.uploadedAt).toLocaleDateString()}
+                            </p>
+                          </div>
+                        </div>
+                        
+                        {!readOnly && (
+                          <div className="flex items-center gap-2">
+                            <Button
+                              variant="outline"
+                              size="sm"
+                              onClick={() => handleDownload(doc)}
+                            >
+                              <Download className="h-4 w-4" />
+                            </Button>
+                            <Button
+                              variant="outline"
+                              size="sm"
+                              onClick={() => handleDelete(doc.id)}
+                              className="text-red-600 hover:text-red-700"
+                            >
+                              <Trash2 className="h-4 w-4" />
+                            </Button>
+                          </div>
+                        )}
+                      </div>
+
+                      {/* 댓글 섹션 */}
+                      <div className="grid grid-cols-2 gap-4">
+                        {/* SHI 댓글 */}
+                        <div className="space-y-2">
+                          <div className="flex items-center justify-between">
+                            <Label className="text-sm font-medium">SHI 댓글</Label>
+                            {!readOnly && (
+                              <Button
+                                variant="ghost"
+                                size="sm"
+                                onClick={() => handleEditComment(doc.id, 'shi', doc.shiComment)}
+                              >
+                                <MessageSquare className="h-4 w-4" />
+                              </Button>
+                            )}
+                          </div>
+                          {editingComment?.id === doc.id && editingComment.type === 'shi' ? (
+                            <div className="space-y-2">
+                              <Textarea
+                                value={commentText}
+                                onChange={(e) => setCommentText(e.target.value)}
+                                placeholder="SHI 댓글을 입력하세요"
+                                rows={3}
+                              />
+                              <div className="flex gap-2">
+                                <Button size="sm" onClick={handleSaveComment}>
+                                  저장
+                                </Button>
+                                <Button 
+                                  size="sm" 
+                                  variant="outline"
+                                  onClick={() => setEditingComment(null)}
+                                >
+                                  취소
+                                </Button>
+                              </div>
+                            </div>
+                          ) : (
+                            <div className="min-h-[60px] p-3 bg-gray-50 rounded border">
+                              {doc.shiComment ? (
+                                <p className="text-sm">{doc.shiComment}</p>
+                              ) : (
+                                <p className="text-sm text-muted-foreground">댓글이 없습니다.</p>
+                              )}
+                            </div>
+                          )}
+                        </div>
+
+                        {/* Vendor 댓글 */}
+                        <div className="space-y-2">
+                          <div className="flex items-center justify-between">
+                            <Label className="text-sm font-medium">Vendor 댓글</Label>
+                            {!readOnly && (
+                              <Button
+                                variant="ghost"
+                                size="sm"
+                                onClick={() => handleEditComment(doc.id, 'vendor', doc.vendorComment)}
+                              >
+                                <MessageSquare className="h-4 w-4" />
+                              </Button>
+                            )}
+                          </div>
+                          {editingComment?.id === doc.id && editingComment.type === 'vendor' ? (
+                            <div className="space-y-2">
+                              <Textarea
+                                value={commentText}
+                                onChange={(e) => setCommentText(e.target.value)}
+                                placeholder="Vendor 댓글을 입력하세요"
+                                rows={3}
+                              />
+                              <div className="flex gap-2">
+                                <Button size="sm" onClick={handleSaveComment}>
+                                  저장
+                                </Button>
+                                <Button 
+                                  size="sm" 
+                                  variant="outline"
+                                  onClick={() => setEditingComment(null)}
+                                >
+                                  취소
+                                </Button>
+                              </div>
+                            </div>
+                          ) : (
+                            <div className="min-h-[60px] p-3 bg-gray-50 rounded border">
+                              {doc.vendorComment ? (
+                                <p className="text-sm">{doc.vendorComment}</p>
+                              ) : (
+                                <p className="text-sm text-muted-foreground">댓글이 없습니다.</p>
+                              )}
+                            </div>
+                          )}
+                        </div>
+                      </div>
+                    </div>
+                  ))}
+                </div>
+              </div>
+            ))}
+          </div>
+        )}
+      </CardContent>
+    </Card>
+  )
+}
-- 
cgit v1.2.3

