'use client'; import '../style.scss'; import Image from 'next/image'; import { useState, useMemo, useCallback, useEffect, useRef, MouseEvent } from 'react'; import Loading from '@/app/component/Loading'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faThumbsUp as nThumbsUp, faThumbsDown as nThumbsDown, faFlag as nFlag, faPenToSquare, faTrashCan } from '@fortawesome/free-regular-svg-icons'; import { faThumbsUp as yThumbsUp, faThumbsDown as yThumbsDown, faFlag as yFlag, faEllipsisVertical, faLock } from '@fortawesome/free-solid-svg-icons'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" import { fetchApi, formatDate } from '@/lib/utils/client'; import { isBoardAdmin } from '@/lib/utils/permission'; import useAuth from '@/hooks/useAuth'; import useErrorAlert from '@/hooks/useErrorAlert'; import { type CommentItem } from '@/types/forum/comment'; import { BoardResponse } from '@/types/response/forum/board'; import { PostResponse } from '@/types/response/forum/post'; import EditForm from './EditForm'; import Report from '@/app/(main)/(forum)/post/_component/Report'; import { Reaction } from '@/constants/forum'; type Props = { _board: BoardResponse; // 게시판 정보 _post: PostResponse; // 게시글 정보 _comment: CommentItem; // 댓글 정보 isReplying?: boolean; // 답글 버튼 클릭 여부 onReply: () => void; // 답글 버튼 클릭 시 onDelete: (commentID: number, parentID?: number) => void; // 삭제 후 onSuccess: (comment?: CommentItem) => void; // 수정/삭제 후 canWriteReply?: boolean; // 답글 작성 권한 children?: React.ReactNode; } export default function Item({ _board, _post, _comment, isReplying, onReply, onSuccess, onDelete, canWriteReply, children } : Props) { const { member, loginCheck } = useAuth(); const { setError } = useErrorAlert(); const [loading, setLoading] = useState(false); const [isEditing, setIsEditing] = useState(false); const [hasLike, setHasLike] = useState(_comment.hasLike); const [hasDisLike, setHasDisLike] = useState(_comment.hasDislike); const [likes, setLikes] = useState(_comment.likes); const [dislikes, setDislikes] = useState(_comment.dislikes); const [hasReport, setHasReport] = useState(_comment.hasReport); const [report, setReport] = useState(false); const isOwner = member?.id === _comment.memberID; const isPostOwner = member?.id === _post.memberID; const canManage = isOwner || isBoardAdmin(_board.boardManager, member); const canViewSecret = !_comment.isSecret || isOwner || isPostOwner || canManage; const contentRef = useRef(null); const writerThumb = useMemo(() => _comment.writer.thumbnail ?? '/resources/thumb.gif', [_comment.writer.thumbnail]); const writerIcon = useMemo(() => _comment.writer.icon ?? _comment.writer.gradeImage ?? null, [_comment.writer.icon, _comment.writer.gradeImage]); const writerName = useMemo(() => _comment.writer.name || _comment.writer.sid, [_comment.writer.name, _comment.writer.sid]); const createdAt = useMemo(() => formatDate(_comment.createdAt), [_comment.createdAt]); // 댓글 내 파일 다운로드 클릭 핸들러 useEffect(() => { const el = contentRef.current; if (!el) { return; } const handler = (e: globalThis.MouseEvent) => { const target = e.target as HTMLElement; const file = target.closest('section.file-embed') as HTMLElement; if (file && file.dataset.uuid) { window.location.href = `/api/forum/comment/file/${file.dataset.uuid}`; } }; el.addEventListener('click', handler); return () => { el.removeEventListener('click', handler); }; }, []); const handleStartEdit = useCallback(() => { if (!loginCheck()) { return; } if (!canManage) { alert('본인의 댓글만 수정할 수 있습니다.'); return; } setIsEditing(true); }, [loginCheck, canManage]); const handleCancelEdit = useCallback(() => { setIsEditing(false); }, []); const handleEditSuccess = useCallback((comment?: CommentItem) => { setIsEditing(false); onSuccess(comment); }, [onSuccess]); // 좋아요/싫어요 (로딩 없이 즉시 반영) const handleReaction = useCallback(async (e: MouseEvent) => { const reaction = Number(e.currentTarget.value); if (!loginCheck()) { return; } try { const res = await fetchApi('/api/forum/comments/' + _comment.id + '/reaction', { method: 'POST', body: { reaction } }); if (res.success) { switch (reaction) { case Reaction.Like: setHasLike(prev => { setLikes(c => prev ? c - 1 : c + 1); return !prev; }); setHasDisLike(prev => { if (prev) setDislikes(c => c - 1); return false; }); break; case Reaction.Dislike: setHasDisLike(prev => { setDislikes(c => prev ? c - 1 : c + 1); return !prev; }); setHasLike(prev => { if (prev) setLikes(c => c - 1); return false; }); break; } } } catch (err) { if (err instanceof Error) { setError(err.message); } } }, [_comment.id, loginCheck, setError]); // 신고하기 const handleReport = useCallback(() => { if (hasReport) { alert('이미 신고하셨습니다.'); return; } if (!loginCheck()) { return; } setReport(prev => !prev); }, [loginCheck, hasReport]); // 댓글 삭제 const handleDelete = useCallback(() => { if (!loginCheck()) { return; } if (!canManage) { alert('본인의 댓글만 삭제할 수 있습니다.'); return; } setIsEditing(false); if (confirm("댓글을 삭제하시겠습니까?")) { setLoading(true); // 댓글 삭제 호출 fetchApi('/api/forum/comments/' + _comment.id, { method: 'DELETE' }).then(() => { // 삭제 성공 시 해당 댓글 영역 삭제 onDelete(_comment.id, _comment.parentID); }).catch(err => { setError(err.message); }).finally(() => { setLoading(false); }); } }, [_comment.id, _comment.parentID, loginCheck, canManage, onDelete, setError]); return ( <> {loading ? ( ) : (
  • {_board.boardMeta.comment.showMemberThumb && (
    {writerName}
    )}
    • {_board.boardMeta.comment.showMemberIcon && writerIcon && ( )} {writerName}
    • {createdAt}
    {!canManage && ( 신고 )} {canManage && ( <> 수정 삭제 )}
    {!canViewSecret ? ( 비밀글입니다. ) : isEditing ? ( ) : ( <> {_comment.mention && ( {_comment.mention.rawHandle} )} {_comment.isSecret && ( )} )}
    {!isEditing && canViewSecret && ( <> {canWriteReply !== false && (
    )} {_board.boardMeta.comment.allowLike && ( <>
    {likes}
    )} {_board.boardMeta.comment.allowDisLike && ( <>
    {dislikes}
    )} )}
    {children}
  • )} ); }