From 1a6774d195b5fb9e3547f3268bf3527a8718c9bf Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 17 Nov 2025 08:43:00 +0000 Subject: (임수민) GTC 기본계약 코멘트 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agreement-comments/agreement-comment-list.tsx | 517 +++++++++++++++++++++ 1 file changed, 517 insertions(+) create mode 100644 lib/basic-contract/agreement-comments/agreement-comment-list.tsx (limited to 'lib/basic-contract/agreement-comments/agreement-comment-list.tsx') diff --git a/lib/basic-contract/agreement-comments/agreement-comment-list.tsx b/lib/basic-contract/agreement-comments/agreement-comment-list.tsx new file mode 100644 index 00000000..8b9cdbea --- /dev/null +++ b/lib/basic-contract/agreement-comments/agreement-comment-list.tsx @@ -0,0 +1,517 @@ +"use client"; + +import React, { useState, useCallback, useEffect } from 'react'; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Card, CardContent } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { toast } from "sonner"; +import { + MessageSquare, + Plus, + Trash2, + Paperclip, + X, + Save, + Upload, + FileText, + User, + Building2, + Loader2, + Download, +} from "lucide-react"; +import { cn, formatDateTime } from "@/lib/utils"; +import { + getAgreementComments, + addAgreementComment, + deleteAgreementComment, + uploadCommentAttachment, + deleteCommentAttachment, + type AgreementCommentData, + type AgreementCommentAuthorType, +} from "./actions"; + +export interface AgreementCommentListProps { + basicContractId: number; + currentUserType?: AgreementCommentAuthorType; + readOnly?: boolean; + className?: string; + onCommentCountChange?: (count: number) => void; +} + +export function AgreementCommentList({ + basicContractId, + currentUserType = 'Vendor', + readOnly = false, + className, + onCommentCountChange, +}: AgreementCommentListProps) { + const [comments, setComments] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isAdding, setIsAdding] = useState(false); + const [newComment, setNewComment] = useState(''); + const [newAuthorName, setNewAuthorName] = useState(''); + const [uploadingFiles, setUploadingFiles] = useState>(new Set()); + const [isSaving, setIsSaving] = useState(false); + + // 코멘트 로드 + const loadComments = useCallback(async () => { + try { + setIsLoading(true); + const data = await getAgreementComments(basicContractId); + setComments(data); + onCommentCountChange?.(data.length); + } catch (error) { + console.error('코멘트 로드 실패:', error); + toast.error("코멘트를 불러오는데 실패했습니다."); + } finally { + setIsLoading(false); + } + }, [basicContractId]); // onCommentCountChange를 dependency에서 제거 + + // 초기 로드 + useEffect(() => { + loadComments(); + }, [basicContractId]); // loadComments 대신 basicContractId만 의존 + + // 코멘트 추가 핸들러 + const handleAddComment = useCallback(async () => { + if (!newComment.trim()) { + toast.error("코멘트를 입력해주세요."); + return; + } + + setIsSaving(true); + try { + const result = await addAgreementComment({ + basicContractId, + comment: newComment.trim(), + authorName: newAuthorName.trim() || undefined, + }); + + if (result.success) { + setNewComment(''); + setNewAuthorName(''); + setIsAdding(false); + toast.success("코멘트가 추가되었습니다."); + await loadComments(); // 목록 새로고침 + } else { + toast.error(result.error || "코멘트 추가에 실패했습니다."); + } + } catch (error) { + console.error('코멘트 추가 실패:', error); + toast.error("코멘트 추가에 실패했습니다."); + } finally { + setIsSaving(false); + } + }, [newComment, newAuthorName, basicContractId]); // loadComments 제거 + + // 코멘트 삭제 핸들러 + const handleDeleteComment = useCallback(async (commentId: number) => { + if (!confirm("이 코멘트를 삭제하시겠습니까?")) { + return; + } + + try { + const result = await deleteAgreementComment(commentId); + if (result.success) { + toast.success("코멘트가 삭제되었습니다."); + await loadComments(); // 목록 새로고침 + } else { + toast.error(result.error || "코멘트 삭제에 실패했습니다."); + } + } catch (error) { + console.error('코멘트 삭제 실패:', error); + toast.error("코멘트 삭제에 실패했습니다."); + } + }, []); // loadComments 제거 + + // 첨부파일 업로드 핸들러 + const handleUploadAttachment = useCallback(async (commentId: number, file: File) => { + setUploadingFiles(prev => new Set(prev).add(commentId)); + try { + const result = await uploadCommentAttachment(commentId, file); + if (result.success) { + toast.success(`${file.name}이(가) 업로드되었습니다.`); + await loadComments(); // 목록 새로고침 + } else { + toast.error(result.error || "파일 업로드에 실패했습니다."); + } + } catch (error) { + console.error('파일 업로드 실패:', error); + toast.error("파일 업로드에 실패했습니다."); + } finally { + setUploadingFiles(prev => { + const next = new Set(prev); + next.delete(commentId); + return next; + }); + } + }, []); // loadComments 제거 + + // 첨부파일 삭제 핸들러 + const handleDeleteAttachment = useCallback(async (commentId: number, attachmentId: string) => { + if (!confirm("이 첨부파일을 삭제하시겠습니까?")) { + return; + } + + try { + const result = await deleteCommentAttachment(commentId, attachmentId); + if (result.success) { + toast.success("첨부파일이 삭제되었습니다."); + await loadComments(); // 목록 새로고침 + } else { + toast.error(result.error || "첨부파일 삭제에 실패했습니다."); + } + } catch (error) { + console.error('첨부파일 삭제 실패:', error); + toast.error("첨부파일 삭제에 실패했습니다."); + } + }, []); // loadComments 제거 + + // 파일 크기 포맷팅 + const formatFileSize = (bytes: number): string => { + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* 헤더 */} +
+
+
+ +

협의 코멘트

+
+
+ + 총 {comments.length}개 + + {!readOnly && ( + + )} +
+
+ +

+ SHI와 협력업체 간 기본계약서 협의 내용을 작성하고 공유합니다. +

+
+ + {/* 코멘트 리스트 */} + +
+ {/* 새 코멘트 입력 폼 */} + {isAdding && !readOnly && ( + + +
+
+ +
+ + {currentUserType === 'SHI' ? ( + <> + + SHI + + ) : ( + <> + + Vendor + + )} + +
+
+ +
+ + setNewAuthorName(e.target.value)} + placeholder="작성자 이름을 입력하세요..." + className="mt-1.5" + /> +
+ +
+ +