"use client"; import React, { useState, useEffect, useCallback, useRef } from 'react'; import { ScrollArea } from "@/components/ui/scroll-area"; import { Card, CardContent, CardHeader, CardTitle } 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 { toast } from "sonner"; import { FileText, MessageSquare, ChevronRight, ChevronDown, Search, AlertTriangle, CheckCircle2, Edit3, Save, X, Loader2, Hash, BookOpen, Minimize2, Maximize2, } from "lucide-react"; import { cn, formatDateTime } from "@/lib/utils"; import { getVendorGtcData, updateVendorClause, checkVendorClausesCommentStatus, type GtcVendorData } from "../service"; import { useSession } from "next-auth/react" interface GtcClausesComponentProps { contractId?: number; onCommentStatusChange?: ( hasComments: boolean, commentCount: number, reviewStatus?: string, isComplete?: boolean ) => void; t?: (key: string) => string; } // GTC 조항의 기본 타입 정의 type GtcVendorClause = { id: number; vendorClauseId: number | null; baseClauseId: number; vendorDocumentId: number | null; parentId: number | null; depth: number; sortOrder: string; fullPath: string | null; reviewStatus: string; negotiationNote: string | null; isExcluded: boolean; // 실제 표시될 값들 (기본 조항 값) effectiveItemNumber: string; effectiveCategory: string | null; effectiveSubtitle: string; effectiveContent: string | null; // 기본 조항 정보 (동일) baseItemNumber: string; baseCategory: string | null; baseSubtitle: string; baseContent: string | null; // 수정 여부 (코멘트만 있으면 false) hasModifications: boolean; isNumberModified: boolean; isCategoryModified: boolean; isSubtitleModified: boolean; isContentModified: boolean; // 코멘트 관련 hasComment: boolean; pendingComment: string | null; }; interface ClauseState extends GtcVendorClause { isExpanded?: boolean; isEditing?: boolean; tempComment?: string; isSaving?: boolean; uniqueId: number; commentHistory?: CommentHistory[]; // 추가 showHistory?: boolean; // 이력 표시 여부 } interface CommentHistory { vendorClauseId: number; comment: string; actorName?: string; actorEmail?: string; createdAt: Date; action: string; } export function GtcClausesComponent({ contractId, onCommentStatusChange, t = (key: string) => key }: GtcClausesComponentProps) { const [gtcData, setGtcData] = useState(null); const [clauses, setClauses] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [searchTerm, setSearchTerm] = useState(""); const [expandedItems, setExpandedItems] = useState>(new Set()); const [compactMode, setCompactMode] = useState(true); // 컴팩트 모드 상태 추가 const { data: session } = useSession(); const onCommentStatusChangeRef = useRef(onCommentStatusChange); onCommentStatusChangeRef.current = onCommentStatusChange; // 데이터 로드 const loadGtcData = useCallback(async () => { try { setLoading(true); setError(null); const data = await getVendorGtcData(contractId); if (!data) { setError("GTC 데이터를 찾을 수 없습니다."); return; } setGtcData(data); const initialClauses: ClauseState[] = data.clauses.map(clause => ({ ...clause, uniqueId: clause.id, isExpanded: false, isEditing: false, tempComment: clause.negotiationNote || "", isSaving: false, })); setClauses(initialClauses); } catch (err) { console.error('GTC 데이터 로드 실패:', err); setError(err instanceof Error ? err.message : 'GTC 데이터를 불러오는데 실패했습니다.'); } finally { setLoading(false); } }, [contractId]); const lastCommentStatusRef = useRef<{ hasComments: boolean; commentCount: number , reviewStatus:string} | null>(null); // 코멘트 상태 변경을 별도 useEffect로 처리 useEffect(() => { if (clauses.length > 0 && gtcData) { const commentCount = clauses.filter(c => c.hasComment).length; const hasComments = commentCount > 0; const reviewStatus = gtcData.vendorDocument?.reviewStatus || 'draft'; // reviewStatus가 complete이면 코멘트가 있어도 완료된 것으로 처리 const isComplete = reviewStatus === 'complete' || reviewStatus === 'approved'; const currentStatus = { hasComments, commentCount, reviewStatus, isComplete }; if (!lastCommentStatusRef.current || lastCommentStatusRef.current.hasComments !== hasComments || lastCommentStatusRef.current.commentCount !== commentCount || lastCommentStatusRef.current.reviewStatus !== reviewStatus) { lastCommentStatusRef.current = currentStatus; // isComplete 정보도 전달 onCommentStatusChangeRef.current?.(hasComments, commentCount, reviewStatus, isComplete); } } }, [clauses, gtcData]); useEffect(() => { loadGtcData(); }, [loadGtcData]); // 검색 필터링 const filteredClauses = React.useMemo(() => { if (!searchTerm.trim()) return clauses; const term = searchTerm.toLowerCase(); return clauses.filter(clause => clause.effectiveItemNumber.toLowerCase().includes(term) || clause.effectiveSubtitle.toLowerCase().includes(term) || clause.effectiveContent?.toLowerCase().includes(term) || clause.negotiationNote?.toLowerCase().includes(term) ); }, [clauses, searchTerm]); // 계층 구조로 조항 그룹화 const groupedClauses = React.useMemo(() => { const grouped: { [key: number]: ClauseState[] } = { 0: [] }; // 최상위는 0 filteredClauses.forEach(clause => { // parentId를 baseClauseId와 매핑 (parentId는 실제 baseClauseId를 가리킴) let parentKey = 0; // 기본값은 최상위 if (clause.parentId !== null) { // parentId에 해당하는 조항을 찾아서 그 조항의 uniqueId를 사용 const parentClause = filteredClauses.find(c => c.baseClauseId === clause.parentId); if (parentClause) { parentKey = parentClause.uniqueId; } } if (!grouped[parentKey]) { grouped[parentKey] = []; } grouped[parentKey].push(clause); }); // 정렬 Object.keys(grouped).forEach(key => { grouped[parseInt(key)].sort((a, b) => parseFloat(a.sortOrder) - parseFloat(b.sortOrder)); }); return grouped; }, [filteredClauses]); // 토글 확장/축소 const toggleExpand = useCallback((uniqueId: number) => { setExpandedItems(prev => { const next = new Set(prev); if (next.has(uniqueId)) { next.delete(uniqueId); } else { next.add(uniqueId); } return next; }); }, []); // 편집 모드 토글 const toggleEdit = useCallback((uniqueId: number) => { setClauses(prev => prev.map(clause => { if (clause.uniqueId === uniqueId) { return { ...clause, isEditing: !clause.isEditing, tempComment: "", }; } return clause; })); }, []); // 임시 코멘트 업데이트 const updateTempComment = useCallback((uniqueId: number, comment: string) => { setClauses(prev => prev.map(clause => { if (clause.uniqueId === uniqueId) { return { ...clause, tempComment: comment }; } return clause; })); }, []); // toggleCommentHistory 함수 추가 const toggleCommentHistory = useCallback((uniqueId: number) => { setClauses(prev => prev.map(clause => { if (clause.uniqueId === uniqueId) { return { ...clause, showHistory: !clause.showHistory }; } return clause; })); }, []); // 코멘트 저장 const saveComment = useCallback(async (uniqueId: number) => { const clause = clauses.find(c => c.uniqueId === uniqueId); if (!clause) return; // 빈 코멘트 체크 - 신규 입력 시에만 if (!clause.hasComment && (!clause.tempComment || clause.tempComment.trim() === "")) { toast.error("코멘트를 입력해주세요."); return; } setClauses(prev => prev.map(c => c.uniqueId === uniqueId ? { ...c, isSaving: true } : c )); try { const clauseData = { itemNumber: clause.effectiveItemNumber, category: clause.effectiveCategory, subtitle: clause.effectiveSubtitle, content: clause.effectiveContent, comment: clause.tempComment || "", }; const result = await updateVendorClause( clause.id, clause.vendorClauseId, clauseData, gtcData?.vendorDocument ); if (result.success) { if (!session?.user?.id) { toast.error("로그인이 필요합니다."); return; } // 새 코멘트를 이력에 추가 const newHistory = { vendorClauseId: result.vendorClauseId, comment: clause.tempComment || "", actorName: session.user.name ||"현재 사용자", // 실제로는 세션에서 가져와야 함 createdAt: new Date(), action: "commented" }; setClauses(prev => prev.map(c => { if (c.uniqueId === uniqueId) { const updatedHistory = [newHistory, ...(c.commentHistory || [])]; return { ...c, vendorClauseId: result.vendorClauseId || c.vendorClauseId, negotiationNote: clause.tempComment?.trim() || null, latestComment: clause.tempComment?.trim() || null, commentHistory: updatedHistory, hasComment: true, isEditing: false, isSaving: false, }; } return c; })); toast.success("코멘트가 저장되었습니다."); } else { toast.error(result.error || "코멘트 저장에 실패했습니다."); setClauses(prev => prev.map(c => c.uniqueId === uniqueId ? { ...c, isSaving: false } : c )); } } catch (error) { console.error('코멘트 저장 실패:', error); toast.error("코멘트 저장 중 오류가 발생했습니다."); setClauses(prev => prev.map(c => c.uniqueId === uniqueId ? { ...c, isSaving: false } : c )); } }, [clauses, gtcData]); // 편집 취소 const cancelEdit = useCallback((uniqueId: number) => { setClauses(prev => prev.map(clause => { if (clause.uniqueId === uniqueId) { return { ...clause, isEditing: false, tempComment: clause.negotiationNote || "", }; } return clause; })); }, []); // 컴팩트 모드 렌더링 const renderCompactClause = useCallback((clause: ClauseState, depth: number = 0): React.ReactNode => { const isExpanded = expandedItems.has(clause.uniqueId); const children = groupedClauses[clause.uniqueId] || []; const hasChildren = children.length > 0; return (
0 ? 'ml-4' : ''}`}>
{/* 확장/축소 버튼 */} {/* 조항 번호 */} {clause.effectiveItemNumber} {/* 제목 */} {clause.effectiveSubtitle} {/* 상태 표시 */}
{/* {clause.hasComment && ( )} */} {clause.isExcluded && ( 제외 )}
{/* 편집 버튼 */}
{clause.isEditing ? (
) : ( )}
{/* 확장된 내용 */} {isExpanded && (
{/* 카테고리 */} {clause.effectiveCategory && (
카테고리: {clause.effectiveCategory}
)} {/* 내용 */} {clause.effectiveContent && (

{clause.effectiveContent}

)} {/* 코멘트 편집 영역 */} {clause.isEditing && (
협의 코멘트