From ba8cd44a0ed2c613a5f2cee06bfc9bd0f61f21c7 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 7 Nov 2025 08:39:04 +0000 Subject: (최겸) 입찰/견적 수정사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../detail/general-contract-documents.tsx | 383 +++++++++++++++++++++ 1 file changed, 383 insertions(+) create mode 100644 lib/general-contracts_old/detail/general-contract-documents.tsx (limited to 'lib/general-contracts_old/detail/general-contract-documents.tsx') 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([]) + 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) => { + 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) + + const documentTypes = [ + { value: 'specification', label: '사양' }, + { value: 'pricing', label: '단가종류' }, + { value: 'other', label: '기타' } + ] + + return ( + + + + + 계약 첨부문서 + + + + {/* 파일 업로드 */} + {!readOnly && ( +
+
+ + +
+
+ )} + + {/* 문서 목록 */} + {isLoading ? ( +
+ + 문서를 불러오는 중... +
+ ) : documents.length === 0 ? ( +
+ +

업로드된 문서가 없습니다.

+
+ ) : ( +
+ {Object.entries(groupedDocuments).map(([type, docs]) => ( +
+
+ + {getDocumentTypeLabel(type)} + + + {docs.length}개 문서 + +
+ +
+ {docs.map((doc) => ( +
+
+
+ +
+

{doc.fileName}

+

+ 업로드: {new Date(doc.uploadedAt).toLocaleDateString()} +

+
+
+ + {!readOnly && ( +
+ + +
+ )} +
+ + {/* 댓글 섹션 */} +
+ {/* SHI 댓글 */} +
+
+ + {!readOnly && ( + + )} +
+ {editingComment?.id === doc.id && editingComment.type === 'shi' ? ( +
+