From 1a6774d195b5fb9e3547f3268bf3527a8718c9bf Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
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

---
 lib/basic-contract/viewer/clause-review-list.tsx | 475 +++++++++++++++++++++++
 1 file changed, 475 insertions(+)
 create mode 100644 lib/basic-contract/viewer/clause-review-list.tsx

(limited to 'lib/basic-contract/viewer/clause-review-list.tsx')

diff --git a/lib/basic-contract/viewer/clause-review-list.tsx b/lib/basic-contract/viewer/clause-review-list.tsx
new file mode 100644
index 00000000..9b0c6271
--- /dev/null
+++ b/lib/basic-contract/viewer/clause-review-list.tsx
@@ -0,0 +1,475 @@
+"use client";
+
+import React, { useState, useCallback } 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,
+} from "lucide-react";
+import { cn, formatDateTime } from "@/lib/utils";
+
+export type ReviewCommentAuthorType = 'SHI' | 'Vendor';
+
+export interface ReviewComment {
+  id: string;
+  authorType: ReviewCommentAuthorType;
+  authorName?: string;
+  comment: string;
+  attachments?: ReviewAttachment[];
+  createdAt: Date;
+  updatedAt?: Date;
+}
+
+export interface ReviewAttachment {
+  id: string;
+  fileName: string;
+  filePath: string;
+  fileSize: number;
+  uploadedAt: Date;
+}
+
+export interface ClauseReviewListProps {
+  documentId?: number;
+  comments: ReviewComment[];
+  onAddComment?: (comment: Omit<ReviewComment, 'id' | 'createdAt'>) => Promise<void>;
+  onDeleteComment?: (commentId: string) => Promise<void>;
+  onUploadAttachment?: (commentId: string, file: File) => Promise<ReviewAttachment>;
+  onDeleteAttachment?: (commentId: string, attachmentId: string) => Promise<void>;
+  filterAuthorType?: ReviewCommentAuthorType | 'ALL';
+  readOnly?: boolean;
+  currentUserType?: ReviewCommentAuthorType;
+  className?: string;
+}
+
+export function ClauseReviewList({
+  documentId,
+  comments,
+  onAddComment,
+  onDeleteComment,
+  onUploadAttachment,
+  onDeleteAttachment,
+  filterAuthorType = 'ALL',
+  readOnly = false,
+  currentUserType = 'Vendor',
+  className,
+}: ClauseReviewListProps) {
+  const [isAdding, setIsAdding] = useState(false);
+  const [newComment, setNewComment] = useState('');
+  const [newAuthorName, setNewAuthorName] = useState('');
+  const [uploadingFiles, setUploadingFiles] = useState<Set<string>>(new Set());
+  const [isSaving, setIsSaving] = useState(false);
+
+  // 필터링된 코멘트
+  const filteredComments = React.useMemo(() => {
+    if (filterAuthorType === 'ALL') return comments;
+    return comments.filter(c => c.authorType === filterAuthorType);
+  }, [comments, filterAuthorType]);
+
+  // 코멘트 추가 핸들러
+  const handleAddComment = useCallback(async () => {
+    if (!newComment.trim()) {
+      toast.error("코멘트를 입력해주세요.");
+      return;
+    }
+
+    if (!onAddComment) {
+      toast.error("코멘트 추가 기능이 활성화되지 않았습니다.");
+      return;
+    }
+
+    setIsSaving(true);
+    try {
+      await onAddComment({
+        authorType: currentUserType,
+        authorName: newAuthorName.trim() || undefined,
+        comment: newComment.trim(),
+        attachments: [],
+      });
+
+      setNewComment('');
+      setNewAuthorName('');
+      setIsAdding(false);
+      toast.success("코멘트가 추가되었습니다.");
+    } catch (error) {
+      console.error('코멘트 추가 실패:', error);
+      toast.error("코멘트 추가에 실패했습니다.");
+    } finally {
+      setIsSaving(false);
+    }
+  }, [newComment, newAuthorName, currentUserType, onAddComment]);
+
+  // 코멘트 삭제 핸들러
+  const handleDeleteComment = useCallback(async (commentId: string) => {
+    if (!onDeleteComment) return;
+
+    try {
+      await onDeleteComment(commentId);
+      toast.success("코멘트가 삭제되었습니다.");
+    } catch (error) {
+      console.error('코멘트 삭제 실패:', error);
+      toast.error("코멘트 삭제에 실패했습니다.");
+    }
+  }, [onDeleteComment]);
+
+  // 첨부파일 업로드 핸들러
+  const handleUploadAttachment = useCallback(async (commentId: string, file: File) => {
+    if (!onUploadAttachment) {
+      toast.error("첨부파일 업로드 기능이 활성화되지 않았습니다.");
+      return;
+    }
+
+    setUploadingFiles(prev => new Set(prev).add(commentId));
+    try {
+      await onUploadAttachment(commentId, file);
+      toast.success(`${file.name}이(가) 업로드되었습니다.`);
+    } catch (error) {
+      console.error('파일 업로드 실패:', error);
+      toast.error("파일 업로드에 실패했습니다.");
+    } finally {
+      setUploadingFiles(prev => {
+        const next = new Set(prev);
+        next.delete(commentId);
+        return next;
+      });
+    }
+  }, [onUploadAttachment]);
+
+  // 첨부파일 삭제 핸들러
+  const handleDeleteAttachment = useCallback(async (commentId: string, attachmentId: string) => {
+    if (!onDeleteAttachment) return;
+
+    try {
+      await onDeleteAttachment(commentId, attachmentId);
+      toast.success("첨부파일이 삭제되었습니다.");
+    } catch (error) {
+      console.error('첨부파일 삭제 실패:', error);
+      toast.error("첨부파일 삭제에 실패했습니다.");
+    }
+  }, [onDeleteAttachment]);
+
+  // 파일 크기 포맷팅
+  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';
+  };
+
+  return (
+    <div className={cn("h-full flex flex-col", className)}>
+      {/* 헤더 */}
+      <div className="flex-shrink-0 border-b bg-gray-50 p-3">
+        <div className="flex items-center justify-between mb-2">
+          <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>
+          </div>
+          <div className="flex items-center space-x-2">
+            <Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">
+              총 {filteredComments.length}개
+            </Badge>
+            {!readOnly && (
+              <Button
+                size="sm"
+                onClick={() => setIsAdding(true)}
+                disabled={isAdding}
+                className="h-8"
+              >
+                <Plus className="h-4 w-4 mr-1" />
+                코멘트 추가
+              </Button>
+            )}
+          </div>
+        </div>
+
+        {/* 안내 메시지 */}
+        <p className="text-sm text-gray-600">
+          {filterAuthorType === 'SHI' 
+            ? 'SHI(삼성중공업) 코멘트만 표시됩니다.'
+            : filterAuthorType === 'Vendor'
+            ? '협력업체 코멘트만 표시됩니다.'
+            : '모든 코멘트를 표시합니다.'}
+        </p>
+      </div>
+
+      {/* 코멘트 리스트 */}
+      <ScrollArea className="flex-1">
+        <div className="p-3 space-y-3">
+          {/* 새 코멘트 입력 폼 */}
+          {isAdding && !readOnly && (
+            <Card className="border-blue-200 bg-blue-50">
+              <CardContent className="pt-4">
+                <div className="space-y-3">
+                  <div>
+                    <Label className="text-sm font-medium text-gray-700">
+                      작성자 유형
+                    </Label>
+                    <div className="mt-1.5 flex items-center space-x-2">
+                      <Badge 
+                        variant="outline" 
+                        className={cn(
+                          "px-3 py-1",
+                          currentUserType === 'SHI' 
+                            ? "bg-blue-100 text-blue-700 border-blue-300"
+                            : "bg-green-100 text-green-700 border-green-300"
+                        )}
+                      >
+                        {currentUserType === 'SHI' ? (
+                          <>
+                            <Building2 className="h-3 w-3 mr-1" />
+                            SHI
+                          </>
+                        ) : (
+                          <>
+                            <User className="h-3 w-3 mr-1" />
+                            협력업체
+                          </>
+                        )}
+                      </Badge>
+                    </div>
+                  </div>
+
+                  <div>
+                    <Label htmlFor="authorName" className="text-sm font-medium text-gray-700">
+                      작성자 이름 (선택사항)
+                    </Label>
+                    <Input
+                      id="authorName"
+                      value={newAuthorName}
+                      onChange={(e) => setNewAuthorName(e.target.value)}
+                      placeholder="작성자 이름을 입력하세요..."
+                      className="mt-1.5"
+                    />
+                  </div>
+
+                  <div>
+                    <Label htmlFor="comment" className="text-sm font-medium text-gray-700">
+                      코멘트 *
+                    </Label>
+                    <Textarea
+                      id="comment"
+                      value={newComment}
+                      onChange={(e) => setNewComment(e.target.value)}
+                      placeholder="코멘트를 입력하세요..."
+                      className="mt-1.5 min-h-[100px]"
+                    />
+                  </div>
+
+                  <div className="flex items-center justify-end space-x-2">
+                    <Button
+                      variant="outline"
+                      size="sm"
+                      onClick={() => {
+                        setIsAdding(false);
+                        setNewComment('');
+                        setNewAuthorName('');
+                      }}
+                      disabled={isSaving}
+                    >
+                      <X className="h-4 w-4 mr-1" />
+                      취소
+                    </Button>
+                    <Button
+                      size="sm"
+                      onClick={handleAddComment}
+                      disabled={isSaving || !newComment.trim()}
+                    >
+                      {isSaving ? (
+                        <Loader2 className="h-4 w-4 mr-1 animate-spin" />
+                      ) : (
+                        <Save className="h-4 w-4 mr-1" />
+                      )}
+                      저장
+                    </Button>
+                  </div>
+                </div>
+              </CardContent>
+            </Card>
+          )}
+
+          {/* 기존 코멘트 목록 */}
+          {filteredComments.length === 0 ? (
+            <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">
+                {isAdding 
+                  ? "첫 번째 코멘트를 작성해보세요."
+                  : "아직 코멘트가 없습니다."}
+              </p>
+            </div>
+          ) : (
+            filteredComments.map((comment) => (
+              <Card 
+                key={comment.id} 
+                className={cn(
+                  "transition-all duration-200 hover:shadow-md",
+                  comment.authorType === 'SHI' 
+                    ? "border-blue-200 bg-blue-50/50"
+                    : "border-green-200 bg-green-50/50"
+                )}
+              >
+                <CardContent className="pt-4">
+                  <div className="space-y-3">
+                    {/* 헤더: 작성자 정보 */}
+                    <div className="flex items-start justify-between">
+                      <div className="flex items-center space-x-2">
+                        <Badge 
+                          variant="outline" 
+                          className={cn(
+                            "px-2 py-0.5",
+                            comment.authorType === 'SHI' 
+                              ? "bg-blue-100 text-blue-700 border-blue-300"
+                              : "bg-green-100 text-green-700 border-green-300"
+                          )}
+                        >
+                          {comment.authorType === 'SHI' ? (
+                            <>
+                              <Building2 className="h-3 w-3 mr-1" />
+                              SHI
+                            </>
+                          ) : (
+                            <>
+                              <User className="h-3 w-3 mr-1" />
+                              협력업체
+                            </>
+                          )}
+                        </Badge>
+                        {comment.authorName && (
+                          <span className="text-sm font-medium text-gray-700">
+                            {comment.authorName}
+                          </span>
+                        )}
+                      </div>
+
+                      {!readOnly && (
+                        <Button
+                          variant="ghost"
+                          size="sm"
+                          onClick={() => handleDeleteComment(comment.id)}
+                          className="h-7 w-7 p-0 text-red-500 hover:text-red-700 hover:bg-red-50"
+                        >
+                          <Trash2 className="h-4 w-4" />
+                        </Button>
+                      )}
+                    </div>
+
+                    {/* 코멘트 내용 */}
+                    <div className="bg-white rounded-md p-3 border border-gray-200">
+                      <p className="text-sm text-gray-800 whitespace-pre-wrap leading-relaxed">
+                        {comment.comment}
+                      </p>
+                    </div>
+
+                    {/* 첨부파일 */}
+                    {(comment.attachments && comment.attachments.length > 0) || (!readOnly && onUploadAttachment) ? (
+                      <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 ? `(${comment.attachments.length})` : ''}
+                          </Label>
+                          {!readOnly && onUploadAttachment && (
+                            <label htmlFor={`file-${comment.id}`}>
+                              <Button
+                                type="button"
+                                variant="ghost"
+                                size="sm"
+                                className="h-6 text-xs"
+                                disabled={uploadingFiles.has(comment.id)}
+                                onClick={() => document.getElementById(`file-${comment.id}`)?.click()}
+                              >
+                                {uploadingFiles.has(comment.id) ? (
+                                  <Loader2 className="h-3 w-3 mr-1 animate-spin" />
+                                ) : (
+                                  <Upload className="h-3 w-3 mr-1" />
+                                )}
+                                업로드
+                              </Button>
+                              <input
+                                id={`file-${comment.id}`}
+                                type="file"
+                                className="hidden"
+                                onChange={(e) => {
+                                  const file = e.target.files?.[0];
+                                  if (file) {
+                                    handleUploadAttachment(comment.id, file);
+                                  }
+                                  e.target.value = '';
+                                }}
+                              />
+                            </label>
+                          )}
+                        </div>
+
+                        {comment.attachments && comment.attachments.length > 0 && (
+                          <div className="space-y-1.5">
+                            {comment.attachments.map((attachment) => (
+                              <div
+                                key={attachment.id}
+                                className="flex items-center justify-between bg-white rounded p-2 border border-gray-200 hover:border-gray-300 transition-colors"
+                              >
+                                <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">
+                                      {attachment.fileName}
+                                    </p>
+                                    <p className="text-xs text-gray-500">
+                                      {formatFileSize(attachment.fileSize)}
+                                    </p>
+                                  </div>
+                                </div>
+                                {!readOnly && onDeleteAttachment && (
+                                  <Button
+                                    variant="ghost"
+                                    size="sm"
+                                    onClick={() => handleDeleteAttachment(comment.id, attachment.id)}
+                                    className="h-6 w-6 p-0 text-red-500 hover:text-red-700 hover:bg-red-50 flex-shrink-0"
+                                  >
+                                    <X className="h-3 w-3" />
+                                  </Button>
+                                )}
+                              </div>
+                            ))}
+                          </div>
+                        )}
+                      </div>
+                    ) : null}
+
+                    {/* 푸터: 작성일시 */}
+                    <div className="flex items-center justify-between text-xs text-gray-500 pt-2 border-t border-gray-200">
+                      <span>
+                        작성일: {formatDateTime(comment.createdAt, "KR")}
+                      </span>
+                      {comment.updatedAt && comment.updatedAt.getTime() !== comment.createdAt.getTime() && (
+                        <span>
+                          수정일: {formatDateTime(comment.updatedAt, "KR")}
+                        </span>
+                      )}
+                    </div>
+                  </div>
+                </CardContent>
+              </Card>
+            ))
+          )}
+        </div>
+      </ScrollArea>
+    </div>
+  );
+}
+
-- 
cgit v1.2.3

