From 2acf5f8966a40c1c9a97680c8dc263ee3f1ad3d1 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 2 Jul 2025 00:45:49 +0000 Subject: (대표님/최겸) 20250702 변경사항 업데이트 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/error-boundary.tsx | 45 ++ components/layout/SessionManager.tsx | 2 +- components/qna/comment-section.tsx | 166 ++++++ components/qna/richTextEditor.tsx | 676 ++++++++++++++++++++++ components/qna/tiptap-editor.tsx | 287 +++++++++ components/tech-vendors/tech-vendor-container.tsx | 7 +- 6 files changed, 1180 insertions(+), 3 deletions(-) create mode 100644 components/error-boundary.tsx create mode 100644 components/qna/comment-section.tsx create mode 100644 components/qna/richTextEditor.tsx create mode 100644 components/qna/tiptap-editor.tsx (limited to 'components') diff --git a/components/error-boundary.tsx b/components/error-boundary.tsx new file mode 100644 index 00000000..41334eb7 --- /dev/null +++ b/components/error-boundary.tsx @@ -0,0 +1,45 @@ +"use client"; + +import * as React from "react"; + +interface ErrorBoundaryProps { + children: React.ReactNode; + fallback: React.ComponentType<{ error: Error; reset: () => void }>; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; +} + +export class ErrorBoundary extends React.Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error("Dashboard error boundary caught an error:", error, errorInfo); + } + + render() { + if (this.state.hasError && this.state.error) { + const Fallback = this.props.fallback; + return ( + this.setState({ hasError: false, error: null })} + /> + ); + } + + return this.props.children; + } +} \ No newline at end of file diff --git a/components/layout/SessionManager.tsx b/components/layout/SessionManager.tsx index c917c5f3..0aca82fb 100644 --- a/components/layout/SessionManager.tsx +++ b/components/layout/SessionManager.tsx @@ -96,7 +96,7 @@ export function SessionManager({ lng }: SessionManagerProps) { const handleAutoLogout = useCallback(() => { setShowExpiredModal(false) setShowWarning(false) - window.location.href = `/${lng}/evcp?reason=expired` + window.location.href = `/${lng}/${session?.user.domain}?reason=expired` }, [lng]) // 세션 만료 체크 diff --git a/components/qna/comment-section.tsx b/components/qna/comment-section.tsx new file mode 100644 index 00000000..2ea358e2 --- /dev/null +++ b/components/qna/comment-section.tsx @@ -0,0 +1,166 @@ +import * as React from "react"; +import { useSession } from "next-auth/react"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Badge } from "@/components/ui/badge"; +import { format } from "date-fns"; +import { Comment } from "@/lib/qna/types"; +import { Trash2, Pencil, Check, X } from "lucide-react"; + +interface CommentSectionProps { + answerId: string; + comments: Comment[]; + onAddComment: (content: string) => Promise; + onDeleteComment: (commentId: string) => Promise; + onUpdateComment?: (commentId: string, content: string) => Promise; +} + +export function CommentSection({ answerId, comments, onAddComment, onDeleteComment, onUpdateComment }: CommentSectionProps) { + const { data: session } = useSession(); + const [content, setContent] = React.useState(""); + const [isSubmitting, setIsSubmitting] = React.useState(false); + const [editingId, setEditingId] = React.useState(null); + const [editContent, setEditContent] = React.useState(""); + + const handleSubmit = async () => { + if (!content.trim() || !session?.user?.name) return; + setIsSubmitting(true); + try { + await onAddComment(content); + setContent(""); + } finally { + setIsSubmitting(false); + } + }; + + const handleEditStart = (comment: Comment) => { + setEditingId(comment.id); + setEditContent(comment.content); + }; + + const handleEditCancel = () => { + setEditingId(null); + setEditContent(""); + }; + + const handleEditSave = async (commentId: string) => { + if (!editContent.trim() || !onUpdateComment) return; + try { + await onUpdateComment(commentId, editContent); + setEditingId(null); + } catch (error) { + console.error("댓글 수정 실패:", error); + } + }; + + return ( +
+
+

댓글

+ {comments.length > 0 && ( + + {comments.length} + + )} +
+ + {/* 댓글 목록 */} +
+ {comments.map((comment) => ( +
+
+
+ {comment.author} + + {format(new Date(comment.createdAt), "yyyy.MM.dd HH:mm")} + +
+ {editingId === comment.id ? ( +