"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, Send, CheckCircle2, } from "lucide-react"; import { cn, formatDateTime } from "@/lib/utils"; import { getAgreementComments, addAgreementComment, deleteAgreementComment, submitAgreementComment, uploadCommentAttachment, deleteCommentAttachment, completeNegotiation, type AgreementCommentData, type AgreementCommentAuthorType, } from "./actions"; export interface AgreementCommentListProps { basicContractId: number; currentUserType?: AgreementCommentAuthorType; readOnly?: boolean; className?: string; onCommentCountChange?: (count: number) => void; isNegotiationCompleted?: boolean; // 협의 완료 여부 onNegotiationComplete?: () => void; // 협의 완료 콜백 } export function AgreementCommentList({ basicContractId, currentUserType = 'Vendor', readOnly = false, className, onCommentCountChange, isNegotiationCompleted = false, onNegotiationComplete, }: 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 [pendingFiles, setPendingFiles] = useState([]); // 첨부 대기 중인 파일들 const [isCompletingNegotiation, setIsCompletingNegotiation] = useState(false); const [submittingComments, setSubmittingComments] = useState>(new Set()); // 제출 중인 코멘트 ID // 코멘트 로드 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 (shouldSendEmail: boolean = false) => { if (!newComment.trim()) { toast.error("코멘트를 입력해주세요."); return; } setIsSaving(true); try { const result = await addAgreementComment({ basicContractId, comment: newComment.trim(), authorName: newAuthorName.trim() || undefined, files: pendingFiles, shouldSendEmail, // 이메일 발송 여부 전달 }); if (result.success) { setNewComment(''); setNewAuthorName(''); setPendingFiles([]); setIsAdding(false); if (shouldSendEmail) { toast.success("코멘트가 제출되었으며 상대방에게 이메일이 발송되었습니다."); } else { toast.success("코멘트가 저장되었습니다."); } await loadComments(); // 목록 새로고침 } else { toast.error(result.error || "코멘트 추가에 실패했습니다."); } } catch (error) { console.error('코멘트 추가 실패:', error); toast.error("코멘트 추가에 실패했습니다."); } finally { setIsSaving(false); } }, [newComment, newAuthorName, basicContractId, pendingFiles]); // pendingFiles 추가 // 파일 선택 핸들러 const handleFileSelect = useCallback((e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); if (files.length > 0) { setPendingFiles(prev => [...prev, ...files]); toast.success(`${files.length}개의 파일이 추가되었습니다.`); } e.target.value = ''; // 파일 입력 초기화 }, []); // 대기 중인 파일 삭제 핸들러 const handleRemovePendingFile = useCallback((index: number) => { setPendingFiles(prev => prev.filter((_, i) => i !== index)); }, []); // 코멘트 삭제 핸들러 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 handleSubmitComment = useCallback(async (commentId: number) => { if (!confirm("이 코멘트를 제출하시겠습니까?\n제출 시 상대방에게 이메일이 발송됩니다.")) { return; } setSubmittingComments(prev => new Set(prev).add(commentId)); try { const result = await submitAgreementComment(commentId); if (result.success) { toast.success("코멘트가 제출되었으며 상대방에게 이메일이 발송되었습니다."); await loadComments(); // 목록 새로고침 } else { toast.error(result.error || "코멘트 제출에 실패했습니다."); } } catch (error) { console.error('코멘트 제출 실패:', error); toast.error("코멘트 제출에 실패했습니다."); } finally { setSubmittingComments(prev => { const next = new Set(prev); next.delete(commentId); return next; }); } }, []); // loadComments 제거 // 협의 완료 핸들러 const handleCompleteNegotiation = useCallback(async () => { if (!confirm("협의를 완료하시겠습니까?\n협의 완료 후에는 법무검토 요청이 가능합니다.")) { return; } setIsCompletingNegotiation(true); try { const result = await completeNegotiation(basicContractId); if (result.success) { toast.success("협의가 완료되었습니다. 이제 법무검토 요청이 가능합니다."); if (onNegotiationComplete) { onNegotiationComplete(); } } else { toast.error(result.error || "협의 완료 처리에 실패했습니다."); } } catch (error) { console.error('협의 완료 실패:', error); toast.error("협의 완료 처리 중 오류가 발생했습니다."); } finally { setIsCompletingNegotiation(false); } }, [basicContractId, onNegotiationComplete]); // 파일 크기 포맷팅 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 (
{/* 헤더 */}

협의 코멘트

{isNegotiationCompleted && ( 협의 완료 )}
총 {comments.length}개 {!readOnly && !isNegotiationCompleted && ( <> {comments.length > 0 && currentUserType === 'SHI' && ( )} )}

{isNegotiationCompleted ? "협의가 완료되었습니다. 법무검토 요청이 가능합니다." : "SHI와 협력업체 간 기본계약서 협의 내용을 작성하고 공유합니다."}

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