diff options
Diffstat (limited to 'lib/basic-contract/viewer/GtcClausesComponent.tsx')
| -rw-r--r-- | lib/basic-contract/viewer/GtcClausesComponent.tsx | 837 |
1 files changed, 837 insertions, 0 deletions
diff --git a/lib/basic-contract/viewer/GtcClausesComponent.tsx b/lib/basic-contract/viewer/GtcClausesComponent.tsx new file mode 100644 index 00000000..8f565971 --- /dev/null +++ b/lib/basic-contract/viewer/GtcClausesComponent.tsx @@ -0,0 +1,837 @@ +"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 } from "@/lib/utils"; +import { + getVendorGtcData, + updateVendorClause, + checkVendorClausesCommentStatus, + type GtcVendorData +} from "../service"; + +interface GtcClausesComponentProps { + contractId?: number; + onCommentStatusChange?: (hasComments: boolean, commentCount: number) => 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; +} + +export function GtcClausesComponent({ + contractId, + onCommentStatusChange, + t = (key: string) => key +}: GtcClausesComponentProps) { + const [gtcData, setGtcData] = useState<GtcVendorData | null>(null); + const [clauses, setClauses] = useState<ClauseState[]>([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<string | null>(null); + const [searchTerm, setSearchTerm] = useState(""); + const [expandedItems, setExpandedItems] = useState<Set<number>>(new Set()); + const [compactMode, setCompactMode] = useState(true); // 컴팩트 모드 상태 추가 + + 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 } | null>(null); + + // 코멘트 상태 변경을 별도 useEffect로 처리 + useEffect(() => { + if (clauses.length > 0) { + const commentCount = clauses.filter(c => c.hasComment).length; + const hasComments = commentCount > 0; + + // Only call callback if status actually changed + const currentStatus = { hasComments, commentCount }; + if (!lastCommentStatusRef.current || + lastCommentStatusRef.current.hasComments !== hasComments || + lastCommentStatusRef.current.commentCount !== commentCount) { + + lastCommentStatusRef.current = currentStatus; + onCommentStatusChangeRef.current?.(hasComments, commentCount); + } + } + }, [clauses]); + + 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: clause.negotiationNote || "", + }; + } + return clause; + })); + }, []); + + // 임시 코멘트 업데이트 + const updateTempComment = useCallback((uniqueId: number, comment: string) => { + setClauses(prev => prev.map(clause => { + if (clause.uniqueId === uniqueId) { + return { ...clause, tempComment: comment }; + } + return clause; + })); + }, []); + + // 코멘트 저장 + const saveComment = useCallback(async (uniqueId: number) => { + const clause = clauses.find(c => c.uniqueId === uniqueId); + if (!clause) 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) { + const hasComment = !!(clause.tempComment?.trim()); + + setClauses(prev => prev.map(c => { + if (c.uniqueId === uniqueId) { + return { + ...c, + vendorClauseId: result.vendorClauseId || c.vendorClauseId, + negotiationNote: clause.tempComment?.trim() || null, + hasComment, + 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 ( + <div key={clause.uniqueId} className={`${depth > 0 ? 'ml-4' : ''}`}> + <div className={cn( + "flex items-center justify-between p-2 rounded border transition-all duration-200 hover:bg-gray-50 mb-1", + clause.hasComment && "border-amber-200 bg-amber-50", + clause.isExcluded && "opacity-50 border-gray-300" + )}> + <div className="flex items-center space-x-2 flex-1 min-w-0"> + {/* 확장/축소 버튼 */} + <Button + variant="ghost" + size="sm" + className="h-5 w-5 p-0 flex-shrink-0" + onClick={() => toggleExpand(clause.uniqueId)} + > + {isExpanded ? ( + <ChevronDown className="h-3 w-3" /> + ) : ( + <ChevronRight className="h-3 w-3" /> + )} + </Button> + + {/* 조항 번호 */} + <span className="font-mono text-blue-600 text-sm flex-shrink-0 min-w-0 font-medium"> + {clause.effectiveItemNumber} + </span> + + {/* 제목 */} + <span className="text-sm text-gray-800 truncate flex-1 min-w-0"> + {clause.effectiveSubtitle} + </span> + + {/* 상태 표시 */} + <div className="flex items-center space-x-1 flex-shrink-0"> + {/* {clause.hasComment && ( + <Badge variant="outline" className="text-xs px-1.5 py-0.5 h-5 bg-amber-50 text-amber-600 border-amber-200"> + <MessageSquare className="h-2.5 w-2.5" /> + </Badge> + )} */} + {clause.isExcluded && ( + <Badge variant="outline" className="text-xs px-1.5 py-0.5 h-5 bg-gray-50 text-gray-500 border-gray-300"> + 제외 + </Badge> + )} + </div> + </div> + + {/* 편집 버튼 */} + <div className="flex items-center space-x-1 flex-shrink-0 ml-2"> + {clause.isEditing ? ( + <div className="flex items-center space-x-1"> + <Button + variant="ghost" + size="sm" + onClick={() => saveComment(clause.uniqueId)} + disabled={clause.isSaving} + className="h-6 w-6 p-0 text-green-600 hover:text-green-700 hover:bg-green-50" + > + {clause.isSaving ? ( + <Loader2 className="h-3 w-3 animate-spin" /> + ) : ( + <Save className="h-3 w-3" /> + )} + </Button> + <Button + variant="ghost" + size="sm" + onClick={() => cancelEdit(clause.uniqueId)} + disabled={clause.isSaving} + className="h-6 w-6 p-0 text-gray-500 hover:text-gray-700 hover:bg-gray-50" + > + <X className="h-3 w-3" /> + </Button> + </div> + ) : ( + <Button + variant="ghost" + size="sm" + onClick={() => toggleEdit(clause.uniqueId)} + className={cn( + "h-6 w-6 p-0 transition-colors", + clause.hasComment + ? "text-amber-600 hover:text-amber-700 hover:bg-amber-50" + : "text-gray-500 hover:text-gray-700 hover:bg-gray-50" + )} + > + {clause.hasComment ? ( + <MessageSquare className="h-3 w-3" /> + ) : ( + <Edit3 className="h-3 w-3" /> + )} + </Button> + )} + </div> + </div> + + {/* 확장된 내용 */} + {isExpanded && ( + <div className="mt-1 ml-5 p-3 bg-white rounded border border-gray-200"> + {/* 카테고리 */} + {clause.effectiveCategory && ( + <div className="mb-2"> + <span className="text-sm text-gray-500 font-medium">카테고리: </span> + <span className="text-sm text-gray-700">{clause.effectiveCategory}</span> + </div> + )} + + {/* 내용 */} + {clause.effectiveContent && ( + <p className="text-sm text-gray-700 leading-relaxed mb-3 whitespace-pre-wrap"> + {clause.effectiveContent} + </p> + )} + + {/* 코멘트 편집 영역 */} + {clause.isEditing && ( + <div className="mb-3 p-2.5 bg-amber-50 rounded border border-amber-200"> + <div className="flex items-center text-sm font-medium text-amber-800 mb-2"> + <MessageSquare className="h-4 w-4 mr-2" /> + 협의 코멘트 + </div> + <Textarea + value={clause.tempComment || ""} + onChange={(e) => updateTempComment(clause.uniqueId, e.target.value)} + placeholder="이 조항에 대한 의견이나 수정 요청 사항을 입력해주세요..." + className="min-h-[60px] text-sm bg-white border-amber-200 focus:border-amber-300" + disabled={clause.isSaving} + /> + <p className="text-xs text-amber-600 mt-1"> + 코멘트를 입력하면 이 계약서는 서명할 수 없게 됩니다. + </p> + </div> + )} + + {/* 기존 코멘트 표시 */} + {!clause.isEditing && clause.hasComment && clause.negotiationNote && ( + <div className="mb-2 p-2.5 bg-amber-50 rounded border border-amber-200"> + <div className="flex items-center text-sm font-medium text-amber-800 mb-2"> + <MessageSquare className="h-4 w-4 mr-2" /> + 협의 코멘트 + </div> + <p className="text-sm text-amber-700 whitespace-pre-wrap"> + {clause.negotiationNote} + </p> + </div> + )} + </div> + )} + + {/* 자식 조항들 */} + {hasChildren && ( + <div className={isExpanded ? "mt-1 border-l border-gray-200 pl-3" : ""}> + {children.map(child => renderCompactClause(child, depth + 1))} + </div> + )} + </div> + ); + }, [expandedItems, groupedClauses, toggleExpand, saveComment, cancelEdit, toggleEdit, updateTempComment]); + + // 기본 모드 렌더링 (기존 코드 최적화) + const renderNormalClause = 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 ( + <div key={clause.uniqueId} className={`mb-1 ${depth > 0 ? 'ml-4' : ''}`}> + <Card className={cn( + "transition-all duration-200", + clause.hasComment && "border-amber-200 bg-amber-50", + clause.isExcluded && "opacity-50 border-gray-300" + )}> + <CardHeader className="pb-1 pt-2 px-3"> + <div className="flex items-start justify-between"> + <div className="flex items-start space-x-2 flex-1 min-w-0"> + {/* 확장/축소 버튼 */} + <Button + variant="ghost" + size="sm" + className="h-5 w-5 p-0 flex-shrink-0" + onClick={() => toggleExpand(clause.uniqueId)} + > + {isExpanded ? ( + <ChevronDown className="h-3 w-3" /> + ) : ( + <ChevronRight className="h-3 w-3" /> + )} + </Button> + + <div className="flex-1 min-w-0"> + <CardTitle className="text-sm font-medium text-gray-800 flex items-center"> + <Hash className="h-3 w-3 mr-1 text-blue-500" /> + {clause.effectiveItemNumber} + {clause.hasComment && ( + <Badge variant="outline" className="ml-2 text-xs bg-amber-50 text-amber-700 border-amber-200"> + <MessageSquare className="h-2 w-2 mr-1" /> + 코멘트 + </Badge> + )} + {clause.isExcluded && ( + <Badge variant="outline" className="ml-2 text-xs bg-gray-50 text-gray-500 border-gray-300"> + 제외됨 + </Badge> + )} + </CardTitle> + <p className="text-sm font-medium text-gray-700 mt-0.5"> + {clause.effectiveSubtitle} + </p> + </div> + + {/* 코멘트 버튼 */} + <div className="flex items-center space-x-0.5 flex-shrink-0"> + {clause.isEditing ? ( + <div className="flex items-center space-x-0.5"> + <Button + variant="ghost" + size="sm" + onClick={() => saveComment(clause.uniqueId)} + disabled={clause.isSaving} + className="h-6 px-2 text-green-600 hover:text-green-700 hover:bg-green-50" + > + {clause.isSaving ? ( + <Loader2 className="h-3 w-3 animate-spin" /> + ) : ( + <Save className="h-3 w-3" /> + )} + </Button> + <Button + variant="ghost" + size="sm" + onClick={() => cancelEdit(clause.uniqueId)} + disabled={clause.isSaving} + className="h-6 px-2 text-gray-500 hover:text-gray-700 hover:bg-gray-50" + > + <X className="h-3 w-3" /> + </Button> + </div> + ) : ( + <Button + variant="ghost" + size="sm" + onClick={() => toggleEdit(clause.uniqueId)} + className={cn( + "h-6 px-2 transition-colors", + clause.hasComment + ? "text-amber-600 hover:text-amber-700 hover:bg-amber-50" + : "text-gray-500 hover:text-gray-700 hover:bg-gray-50" + )} + > + {clause.hasComment ? ( + <MessageSquare className="h-3 w-3" /> + ) : ( + <Edit3 className="h-3 w-3" /> + )} + </Button> + )} + </div> + </div> + </div> + </CardHeader> + + {isExpanded && ( + <CardContent className="pt-0 px-3 pb-2"> + {/* 카테고리 */} + {clause.effectiveCategory && ( + <div className="mb-2"> + <div className="flex items-center text-xs text-gray-500 mb-1"> + <BookOpen className="h-3 w-3 mr-1" /> + 카테고리 + </div> + <p className="text-sm text-gray-700 bg-gray-50 p-2 rounded"> + {clause.effectiveCategory} + </p> + </div> + )} + + {/* 내용 */} + {clause.effectiveContent && ( + <div className="mb-2"> + <div className="flex items-center text-xs text-gray-500 mb-1"> + <FileText className="h-3 w-3 mr-1" /> + 내용 + </div> + <p className="text-sm text-gray-700 bg-gray-50 p-2 rounded whitespace-pre-wrap leading-tight"> + {clause.effectiveContent} + </p> + </div> + )} + + {/* 코멘트 편집 영역 */} + {clause.isEditing && ( + <div className="mb-2 p-2 bg-amber-50 rounded border border-amber-200"> + <div className="flex items-center text-sm font-medium text-amber-800 mb-1"> + <MessageSquare className="h-4 w-4 mr-2" /> + 협의 코멘트 + </div> + <Textarea + value={clause.tempComment || ""} + onChange={(e) => updateTempComment(clause.uniqueId, e.target.value)} + placeholder="이 조항에 대한 의견이나 수정 요청 사항을 입력해주세요..." + className="min-h-[60px] text-sm bg-white border-amber-200 focus:border-amber-300" + disabled={clause.isSaving} + /> + <p className="text-xs text-amber-600 mt-1"> + 코멘트를 입력하면 이 계약서는 서명할 수 없게 됩니다. + </p> + </div> + )} + + {/* 기존 코멘트 표시 */} + {!clause.isEditing && clause.hasComment && clause.negotiationNote && ( + <div className="mb-2 p-2 bg-amber-50 rounded border border-amber-200"> + <div className="flex items-center text-sm font-medium text-amber-800 mb-1"> + <MessageSquare className="h-4 w-4 mr-2" /> + 협의 코멘트 + </div> + <p className="text-sm text-amber-700 whitespace-pre-wrap"> + {clause.negotiationNote} + </p> + </div> + )} + + {/* 자식 조항들 */} + {hasChildren && ( + <div className="mt-2 border-l-2 border-gray-200 pl-2"> + {children.map(child => renderNormalClause(child, depth + 1))} + </div> + )} + </CardContent> + )} + </Card> + + {/* 확장되지 않았을 때 자식 조항들 */} + {!isExpanded && hasChildren && ( + <div className="ml-4"> + {children.map(child => renderNormalClause(child, depth + 1))} + </div> + )} + </div> + ); + }, [expandedItems, groupedClauses, toggleExpand, saveComment, cancelEdit, toggleEdit, updateTempComment]); + + if (loading) { + return ( + <div className="flex items-center justify-center h-64"> + <div className="text-center"> + <Loader2 className="h-8 w-8 text-blue-500 animate-spin mx-auto mb-4" /> + <p className="text-sm text-gray-500">GTC 조항을 불러오는 중...</p> + </div> + </div> + ); + } + + if (error) { + return ( + <div className="flex items-center justify-center h-64"> + <div className="text-center"> + <AlertTriangle className="h-8 w-8 text-red-500 mx-auto mb-4" /> + <p className="text-sm text-gray-700 font-medium mb-2">GTC 데이터 로드 실패</p> + <p className="text-sm text-gray-500 mb-4">{error}</p> + <Button onClick={loadGtcData} size="sm"> + 다시 시도 + </Button> + </div> + </div> + ); + } + + if (!gtcData) { + return ( + <div className="flex items-center justify-center h-64"> + <div className="text-center"> + <FileText className="h-8 w-8 text-gray-400 mx-auto mb-4" /> + <p className="text-sm text-gray-500">GTC 데이터가 없습니다.</p> + </div> + </div> + ); + } + + const totalComments = clauses.filter(c => c.hasComment).length; + + return ( + <div className="h-full flex flex-col"> + {/* 헤더 */} + <div className={cn( + "flex-shrink-0 border-b bg-gray-50", + compactMode ? "p-2.5" : "p-3" + )}> + <div className={cn( + "flex items-center justify-between", + compactMode ? "mb-2" : "mb-2" + )}> + <div className="flex-1 min-w-0"> + <h3 className={cn( + "font-semibold text-gray-800 flex items-center", + compactMode ? "text-sm" : "text-base" + )}> + <FileText className={cn( + "mr-2 text-blue-500", + compactMode ? "h-4 w-4" : "h-5 w-5" + )} /> + {gtcData.vendorDocument.name} + </h3> + {!compactMode && ( + <p className="text-sm text-gray-500 mt-0.5"> + {gtcData.vendorDocument.description || "GTC 조항 검토 및 협의"} + </p> + )} + </div> + <div className="flex items-center space-x-1.5"> + {/* 모드 전환 버튼 */} + <Button + variant="ghost" + size="sm" + onClick={() => setCompactMode(!compactMode)} + className={cn( + "transition-colors", + compactMode ? "h-7 px-2" : "h-7 px-2" + )} + title={compactMode ? "일반 모드로 전환" : "컴팩트 모드로 전환"} + > + {compactMode ? ( + <Maximize2 className="h-3 w-3" /> + ) : ( + <Minimize2 className="h-3 w-3" /> + )} + </Button> + + <Badge variant="outline" className={cn( + "bg-blue-50 text-blue-700 border-blue-200", + compactMode ? "text-xs px-1.5 py-0.5" : "text-xs" + )}> + 총 {clauses.length}개 조항 + </Badge> + {totalComments > 0 && ( + <Badge variant="outline" className={cn( + "bg-amber-50 text-amber-700 border-amber-200", + compactMode ? "text-xs px-1.5 py-0.5" : "text-xs" + )}> + <MessageSquare className={cn( + "mr-1", + compactMode ? "h-2.5 w-2.5" : "h-3 w-3" + )} /> + {totalComments}개 코멘트 + </Badge> + )} + </div> + </div> + + {/* 검색 */} + <div className="relative"> + <div className="absolute inset-y-0 left-2 flex items-center pointer-events-none"> + <Search className={cn( + "text-gray-400", + compactMode ? "h-3.5 w-3.5" : "h-4 w-4" + )} /> + </div> + <Input + placeholder={compactMode ? "조항 검색..." : "조항 번호, 제목, 내용, 코멘트로 검색..."} + className={cn( + "bg-white text-gray-700", + compactMode ? "pl-8 text-sm h-8" : "pl-8 text-sm" + )} + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + /> + </div> + + {/* 안내 메시지 */} + {totalComments > 0 && ( + <div className={cn( + "bg-amber-50 rounded border border-amber-200", + compactMode ? "mt-2 p-2" : "mt-2 p-2" + )}> + <div className={cn( + "flex items-center text-amber-800", + compactMode ? "text-sm" : "text-sm" + )}> + <AlertTriangle className={cn( + "mr-2", + compactMode ? "h-4 w-4" : "h-4 w-4" + )} /> + <span className="font-medium">코멘트가 있어 서명할 수 없습니다.</span> + </div> + {!compactMode && ( + <p className="text-sm text-amber-700 mt-0.5"> + 모든 코멘트를 삭제하거나 협의를 완료한 후 서명해주세요. + </p> + )} + </div> + )} + </div> + + {/* 조항 목록 */} + <ScrollArea className="flex-1"> + <div className={compactMode ? "p-2.5" : "p-3"}> + {filteredClauses.length === 0 ? ( + <div className="text-center py-6"> + <FileText className="h-6 w-6 text-gray-300 mx-auto mb-2" /> + <p className="text-sm text-gray-500"> + {searchTerm ? "검색 결과가 없습니다." : "조항이 없습니다."} + </p> + </div> + ) : ( + <div className={compactMode ? "space-y-0.5" : "space-y-1"}> + {(groupedClauses[0] || []).map(clause => + compactMode ? renderCompactClause(clause) : renderNormalClause(clause) + )} + </div> + )} + </div> + </ScrollArea> + </div> + ); +}
\ No newline at end of file |
