diff options
Diffstat (limited to 'lib/general-contracts_old/detail/general-contract-documents.tsx')
| -rw-r--r-- | lib/general-contracts_old/detail/general-contract-documents.tsx | 383 |
1 files changed, 383 insertions, 0 deletions
diff --git a/lib/general-contracts_old/detail/general-contract-documents.tsx b/lib/general-contracts_old/detail/general-contract-documents.tsx new file mode 100644 index 00000000..b0f20e7f --- /dev/null +++ b/lib/general-contracts_old/detail/general-contract-documents.tsx @@ -0,0 +1,383 @@ +'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
+ documentType?: 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 {
+ // 본 계약문서 타입인 경우 기존 문서 확인
+ if (selectedDocumentType === 'main') {
+ const existingMainDoc = documents.find(doc => doc.documentType === 'main')
+ if (existingMainDoc) {
+ toast.info('기존 계약문서가 새롭게 업로드한 문서로 대체됩니다.')
+ // 기존 본 계약문서 삭제
+ await deleteContractAttachment(existingMainDoc.id, contractId)
+ }
+ }
+
+ 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)
+ 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>
+ )
+}
|
