diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-19 06:15:43 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-11-19 06:15:43 +0000 |
| commit | c92bd1b8caa6ddabe6acee42018262febd5d91fb (patch) | |
| tree | 833a62c9577894b0f77d3677d4d0274e1cb99385 /lib/basic-contract/agreement-comments/agreement-comment-list.tsx | |
| parent | 9bf5b15734cdf87a02c68b2d2a25046a0678a037 (diff) | |
(임수민) 기본계약 코멘트, 법무검토 수정
Diffstat (limited to 'lib/basic-contract/agreement-comments/agreement-comment-list.tsx')
| -rw-r--r-- | lib/basic-contract/agreement-comments/agreement-comment-list.tsx | 231 |
1 files changed, 204 insertions, 27 deletions
diff --git a/lib/basic-contract/agreement-comments/agreement-comment-list.tsx b/lib/basic-contract/agreement-comments/agreement-comment-list.tsx index 8b9cdbea..bad5aee5 100644 --- a/lib/basic-contract/agreement-comments/agreement-comment-list.tsx +++ b/lib/basic-contract/agreement-comments/agreement-comment-list.tsx @@ -22,6 +22,8 @@ import { Building2, Loader2, Download, + Send, + CheckCircle2, } from "lucide-react"; import { cn, formatDateTime } from "@/lib/utils"; import { @@ -30,6 +32,7 @@ import { deleteAgreementComment, uploadCommentAttachment, deleteCommentAttachment, + completeNegotiation, type AgreementCommentData, type AgreementCommentAuthorType, } from "./actions"; @@ -40,6 +43,8 @@ export interface AgreementCommentListProps { readOnly?: boolean; className?: string; onCommentCountChange?: (count: number) => void; + isNegotiationCompleted?: boolean; // 협의 완료 여부 + onNegotiationComplete?: () => void; // 협의 완료 콜백 } export function AgreementCommentList({ @@ -48,6 +53,8 @@ export function AgreementCommentList({ readOnly = false, className, onCommentCountChange, + isNegotiationCompleted = false, + onNegotiationComplete, }: AgreementCommentListProps) { const [comments, setComments] = useState<AgreementCommentData[]>([]); const [isLoading, setIsLoading] = useState(true); @@ -56,6 +63,8 @@ export function AgreementCommentList({ const [newAuthorName, setNewAuthorName] = useState(''); const [uploadingFiles, setUploadingFiles] = useState<Set<number>>(new Set()); const [isSaving, setIsSaving] = useState(false); + const [pendingFiles, setPendingFiles] = useState<File[]>([]); // 첨부 대기 중인 파일들 + const [isCompletingNegotiation, setIsCompletingNegotiation] = useState(false); // 코멘트 로드 const loadComments = useCallback(async () => { @@ -77,8 +86,8 @@ export function AgreementCommentList({ loadComments(); }, [basicContractId]); // loadComments 대신 basicContractId만 의존 - // 코멘트 추가 핸들러 - const handleAddComment = useCallback(async () => { + // 코멘트 추가 핸들러 (저장만 - 이메일 발송 없음) + const handleAddComment = useCallback(async (shouldSendEmail: boolean = false) => { if (!newComment.trim()) { toast.error("코멘트를 입력해주세요."); return; @@ -90,13 +99,22 @@ export function AgreementCommentList({ basicContractId, comment: newComment.trim(), authorName: newAuthorName.trim() || undefined, + files: pendingFiles, + shouldSendEmail, // 이메일 발송 여부 전달 }); if (result.success) { setNewComment(''); setNewAuthorName(''); + setPendingFiles([]); setIsAdding(false); - toast.success("코멘트가 추가되었습니다."); + + if (shouldSendEmail) { + toast.success("코멘트가 제출되었으며 상대방에게 이메일이 발송되었습니다."); + } else { + toast.success("코멘트가 저장되었습니다."); + } + await loadComments(); // 목록 새로고침 } else { toast.error(result.error || "코멘트 추가에 실패했습니다."); @@ -107,7 +125,22 @@ export function AgreementCommentList({ } finally { setIsSaving(false); } - }, [newComment, newAuthorName, basicContractId]); // loadComments 제거 + }, [newComment, newAuthorName, basicContractId, pendingFiles]); // pendingFiles 추가 + + // 파일 선택 핸들러 + const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { + 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) => { @@ -172,6 +205,31 @@ export function AgreementCommentList({ } }, []); // 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'; @@ -195,27 +253,51 @@ export function AgreementCommentList({ <div className="flex items-center space-x-2"> <MessageSquare className="h-5 w-5 text-blue-500" /> <h3 className="font-semibold text-gray-800">협의 코멘트</h3> + {isNegotiationCompleted && ( + <Badge className="bg-green-500 text-white border-green-600"> + 협의 완료 + </Badge> + )} </div> <div className="flex items-center space-x-2"> <Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200 h-8 px-3 text-sm flex items-center"> 총 {comments.length}개 </Badge> - {!readOnly && ( - <Button - size="sm" - onClick={() => setIsAdding(true)} - disabled={isAdding} - className="h-8" - > - <Plus className="h-4 w-4 mr-1" /> - 코멘트 추가 - </Button> + {!readOnly && !isNegotiationCompleted && ( + <> + <Button + size="sm" + onClick={() => setIsAdding(true)} + disabled={isAdding} + className="h-8" + > + <Plus className="h-4 w-4 mr-1" /> + 코멘트 추가 + </Button> + {comments.length > 0 && currentUserType === 'SHI' && ( + <Button + size="sm" + onClick={handleCompleteNegotiation} + disabled={isCompletingNegotiation} + className="h-8 bg-green-600 hover:bg-green-700" + > + {isCompletingNegotiation ? ( + <Loader2 className="h-4 w-4 mr-1 animate-spin" /> + ) : ( + <CheckCircle2 className="h-4 w-4 mr-1" /> + )} + 협의 완료 + </Button> + )} + </> )} </div> </div> <p className="text-sm text-gray-600"> - SHI와 협력업체 간 기본계약서 협의 내용을 작성하고 공유합니다. + {isNegotiationCompleted + ? "협의가 완료되었습니다. 법무검토 요청이 가능합니다." + : "SHI와 협력업체 간 기본계약서 협의 내용을 작성하고 공유합니다."} </p> </div> @@ -223,7 +305,7 @@ export function AgreementCommentList({ <ScrollArea className="flex-1"> <div className="p-3 space-y-3"> {/* 새 코멘트 입력 폼 */} - {isAdding && !readOnly && ( + {isAdding && !readOnly && !isNegotiationCompleted && ( <Card className={cn( "border-l-4", @@ -289,7 +371,74 @@ export function AgreementCommentList({ /> </div> - <div className="flex items-center justify-end space-x-2"> + {/* 파일 첨부 영역 */} + <div className="space-y-2"> + <div className="flex items-center justify-between"> + <Label className="text-sm font-medium text-gray-700 flex items-center"> + <Paperclip className="h-4 w-4 mr-1" /> + 첨부파일 {pendingFiles.length > 0 ? `(${pendingFiles.length})` : ''} + </Label> + <label htmlFor="new-comment-files"> + <Button + type="button" + variant="outline" + size="sm" + className="h-8" + onClick={() => document.getElementById('new-comment-files')?.click()} + disabled={isSaving} + > + <Upload className="h-3 w-3 mr-1" /> + 파일 추가 + </Button> + <input + id="new-comment-files" + type="file" + multiple + className="hidden" + onChange={handleFileSelect} + accept="*/*" + /> + </label> + </div> + + {/* 대기 중인 파일 목록 */} + {pendingFiles.length > 0 && ( + <div className="space-y-1.5 max-h-32 overflow-y-auto"> + {pendingFiles.map((file, index) => ( + <div + key={`${file.name}-${index}`} + className="flex items-center justify-between bg-white rounded p-2 border border-gray-200" + > + <div className="flex items-center space-x-2 flex-1 min-w-0"> + <FileText className="h-4 w-4 text-blue-500 flex-shrink-0" /> + <div className="flex-1 min-w-0"> + <p className="text-sm text-gray-700 truncate"> + {file.name} + </p> + <p className="text-xs text-gray-500"> + {formatFileSize(file.size)} + </p> + </div> + </div> + <Button + variant="ghost" + size="sm" + onClick={() => handleRemovePendingFile(index)} + className="h-6 w-6 p-0 text-red-500 hover:text-red-700 hover:bg-red-50" + > + <X className="h-3 w-3" /> + </Button> + </div> + ))} + </div> + )} + + <p className="text-xs text-gray-500"> + 💡 파일을 먼저 추가한 후 저장 또는 제출 버튼을 눌러주세요. + </p> + </div> + + <div className="flex items-center justify-end gap-2 pt-2 border-t"> <Button variant="outline" size="sm" @@ -297,6 +446,7 @@ export function AgreementCommentList({ setIsAdding(false); setNewComment(''); setNewAuthorName(''); + setPendingFiles([]); }} disabled={isSaving} > @@ -304,8 +454,9 @@ export function AgreementCommentList({ 취소 </Button> <Button + variant="outline" size="sm" - onClick={handleAddComment} + onClick={() => handleAddComment(false)} disabled={isSaving || !newComment.trim()} > {isSaving ? ( @@ -315,6 +466,19 @@ export function AgreementCommentList({ )} 저장 </Button> + <Button + size="sm" + onClick={() => handleAddComment(true)} + disabled={isSaving || !newComment.trim()} + className="bg-blue-600 hover:bg-blue-700" + > + {isSaving ? ( + <Loader2 className="h-4 w-4 mr-1 animate-spin" /> + ) : ( + <Send className="h-4 w-4 mr-1" /> + )} + 제출 (메일발송) + </Button> </div> </div> </CardContent> @@ -326,9 +490,11 @@ export function AgreementCommentList({ <div className="text-center py-8"> <MessageSquare className="h-12 w-12 text-gray-300 mx-auto mb-3" /> <p className="text-sm text-gray-500"> - 아직 코멘트가 없습니다. + {isNegotiationCompleted + ? "협의가 완료되어 더 이상 코멘트를 추가할 수 없습니다." + : "아직 코멘트가 없습니다."} </p> - {!readOnly && ( + {!readOnly && !isNegotiationCompleted && ( <Button variant="outline" size="sm" @@ -341,7 +507,11 @@ export function AgreementCommentList({ )} </div> ) : ( - comments.map((comment) => ( + comments.map((comment) => { + // 현재 사용자가 이 코멘트의 작성자인지 확인 + const isCommentOwner = comment.authorType === currentUserType; + + return ( <Card key={comment.id} className={cn( @@ -384,7 +554,7 @@ export function AgreementCommentList({ )} </div> - {!readOnly && ( + {!readOnly && isCommentOwner && ( <Button variant="ghost" size="sm" @@ -404,14 +574,14 @@ export function AgreementCommentList({ </div> {/* 첨부파일 */} - {(comment.attachments.length > 0 || !readOnly) && ( + {(comment.attachments.length > 0 || (!readOnly && isCommentOwner)) && ( <div className="space-y-2"> <div className="flex items-center justify-between"> <Label className="text-xs font-medium text-gray-600 flex items-center"> <Paperclip className="h-3 w-3 mr-1" /> 첨부파일 {comment.attachments.length > 0 ? `(${comment.attachments.length})` : ''} </Label> - {!readOnly && ( + {!readOnly && isCommentOwner && ( <label htmlFor={`file-${comment.id}`}> <Button type="button" @@ -469,11 +639,17 @@ export function AgreementCommentList({ asChild className="h-6 w-6 p-0 text-blue-500 hover:text-blue-700 hover:bg-blue-50" > - <a href={attachment.filePath} download target="_blank" rel="noopener noreferrer"> + <a + href={attachment.filePath} + download={attachment.fileName} + target="_blank" + rel="noopener noreferrer" + title={`${attachment.fileName} 다운로드`} + > <Download className="h-3 w-3" /> </a> </Button> - {!readOnly && ( + {!readOnly && isCommentOwner && ( <Button variant="ghost" size="sm" @@ -505,7 +681,8 @@ export function AgreementCommentList({ </div> </CardContent> </Card> - )) + ); + }) )} </div> </ScrollArea> |
