|
|
@@ -0,0 +1,390 @@
|
|
|
+'use client';
|
|
|
+
|
|
|
+import './style.scss';
|
|
|
+import Link from 'next/link';
|
|
|
+import Image from 'next/image';
|
|
|
+import { redirect, useRouter } from 'next/navigation';
|
|
|
+import { useState, useEffect, useCallback, MouseEvent } from 'react';
|
|
|
+import { Menu } from 'lucide-react';
|
|
|
+import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
|
|
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
|
+import { faBookmark as nBookmark, faThumbsUp as nThumbsUp, faThumbsDown as nThumbsDown, faFlag as nFlag, faPenToSquare, faTrashCan } from '@fortawesome/free-regular-svg-icons';
|
|
|
+import { faQrcode, faPrint, faLink, faShareNodes, faBookmark as yBookmark, faThumbsUp as yThumbsUp, faThumbsDown as yThumbsDown, faFlag as yFlag } from '@fortawesome/free-solid-svg-icons';
|
|
|
+import Loading from '@/app/component/Loading';
|
|
|
+import Comment from '@/app/(forum)/comment/view';
|
|
|
+import LatestList from '@/app/(forum)/post/_component/LatestPosts';
|
|
|
+import BoardResponse from '@/dtos/response/forum/board/boardResponse';
|
|
|
+import PostResponse from '@/dtos/response/forum/post/postResponse';
|
|
|
+import PostReactionRequest from '@/dtos/request/forum/post/postReactionRequest';
|
|
|
+import PostBookmarkRequest from '@/dtos/request/forum/post/postReactionRequest';
|
|
|
+import Content from '../_component/Content';
|
|
|
+import QRCode from '../_component/QRCode';
|
|
|
+import Copied from '../_component/Copied';
|
|
|
+import SnsShare from '../_component/SnsShare';
|
|
|
+import Report from '../_component/Report';
|
|
|
+import { Reaction } from '@/constants/forum';
|
|
|
+import { fetchPostReaction, fetchPostBookmark, fetchPostDelete } from '@/lib/api/forum/post';
|
|
|
+import { getDateTime, throwError, formatDate, isDateOverdue } from '@/lib/utils/client';
|
|
|
+import useAuth from '@/hooks/useAuth';
|
|
|
+
|
|
|
+type Props = {
|
|
|
+ _board: BoardResponse,
|
|
|
+ _post: PostResponse
|
|
|
+};
|
|
|
+
|
|
|
+export default function View({ _board, _post }: Props)
|
|
|
+{
|
|
|
+ useEffect(() => {
|
|
|
+
|
|
|
+ // 신고 횟수 초과 게시글은 접근 불가
|
|
|
+ if (_post.reports > _board.boardMeta.view.blameHideCount && _board.boardMeta.view.blameHideCount > 0) {
|
|
|
+ alert('비공개 게시글입니다.');
|
|
|
+ redirect(`/board/${_post.boardCode}${window.location.search}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const router = useRouter();
|
|
|
+ const { member, isLogined } = useAuth();
|
|
|
+ const [error, setError] = useState<string>('');
|
|
|
+ const [loading, setLoading] = useState<boolean>(false);
|
|
|
+ const [qrCode, setQrCode] = useState<boolean>(false);
|
|
|
+ const [copied, setCopied] = useState<boolean>(false);
|
|
|
+ const [snsShare, setSnsShare] = useState<boolean>(false);
|
|
|
+ const [report, setReport] = useState<boolean>(false);
|
|
|
+ const [hasLike, setHasLike] = useState<boolean>(_post.hasLike);
|
|
|
+ const [hasDisLike, setHasDisLike] = useState<boolean>(_post.hasDislike);
|
|
|
+ const [hasBookmark, setHasBookmark] = useState<boolean>(_post.hasBookmark);
|
|
|
+ const [hasReport, setHasReport] = useState<boolean>(_post.hasReport);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (error) {
|
|
|
+ alert(error);
|
|
|
+ setError('');
|
|
|
+ }
|
|
|
+ }, [error]);
|
|
|
+
|
|
|
+ const toggleQRCode = useCallback((e: MouseEvent<HTMLAnchorElement>) => {
|
|
|
+ e.preventDefault();
|
|
|
+ setQrCode((prev) => !prev);
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const handlePrint = useCallback(() => {
|
|
|
+ window.print();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const toggleCopied = useCallback((e: MouseEvent<HTMLAnchorElement>) => {
|
|
|
+ e.preventDefault();
|
|
|
+ setCopied((prev) => !prev);
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const toggleSnsShare = useCallback((e: MouseEvent<HTMLAnchorElement>) => {
|
|
|
+ e.preventDefault();
|
|
|
+ setSnsShare((prev) => !prev);
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // 좋아요/싫어요
|
|
|
+ const handleReaction = useCallback(async (e: MouseEvent<HTMLButtonElement>) => {
|
|
|
+ const reaction = Number(e.currentTarget.value);
|
|
|
+
|
|
|
+ if (!await isLogined()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ fetchPostReaction({ postID: _post.id, reaction: reaction } as PostReactionRequest).then(res => {
|
|
|
+ if (res.ok) {
|
|
|
+ switch (reaction) {
|
|
|
+ case Reaction.Like:
|
|
|
+ setHasLike(!hasLike);
|
|
|
+ setHasDisLike(false);
|
|
|
+ break;
|
|
|
+ case Reaction.Dislike:
|
|
|
+ setHasDisLike(!hasDisLike);
|
|
|
+ setHasLike(false);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ throwError(res);
|
|
|
+ }
|
|
|
+ }).catch((err) => {
|
|
|
+ setError(err.message);
|
|
|
+ }).finally(() => {
|
|
|
+ setLoading(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ }, [member, hasLike, hasDisLike]);
|
|
|
+
|
|
|
+ // 즐겨찾기
|
|
|
+ const handleBookmark = useCallback(async () => {
|
|
|
+ if (!await isLogined()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ fetchPostBookmark({ postID: _post.id } as PostBookmarkRequest).then(res => {
|
|
|
+ if (res.ok) {
|
|
|
+ setHasBookmark(!hasBookmark);
|
|
|
+ } else {
|
|
|
+ throwError(res);
|
|
|
+ }
|
|
|
+ }).catch((err) => {
|
|
|
+ setError(err.message);
|
|
|
+ }).finally(() => {
|
|
|
+ setLoading(false);
|
|
|
+ });
|
|
|
+
|
|
|
+ }, [member, hasBookmark]);
|
|
|
+
|
|
|
+ // 신고하기 시작
|
|
|
+ const handleReport = useCallback(async () => {
|
|
|
+ if (hasReport) {
|
|
|
+ alert('이미 신고하셨습니다.');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!await isLogined()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ setReport((prev) => !prev);
|
|
|
+ }, [member, hasReport]);
|
|
|
+
|
|
|
+ // 수정하기
|
|
|
+ const handleEdit = useCallback(async () => {
|
|
|
+ if (!await isLogined()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 게시글 삭제 보호 확인
|
|
|
+ if (_board.boardMeta.general.allowUpdateProtection && !member?.isAdmin) {
|
|
|
+ if (isDateOverdue(_post.createdAt, _board.boardMeta.general.updateProtectionDays)) {
|
|
|
+ return alert(`게시글 작성 후 ${_board.boardMeta.general.updateProtectionDays}일이 지나 수정이 불가능합니다.`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ router.push(`/post/edit/${_post.id}`);
|
|
|
+ }, [member]);
|
|
|
+
|
|
|
+ // 게시글 삭제
|
|
|
+ const handleDelete = useCallback(async () => {
|
|
|
+ if (!await isLogined()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 게시글 삭제 보호 확인
|
|
|
+ if (_board.boardMeta.general.allowDeleteProtection && !member?.isAdmin) {
|
|
|
+ if (isDateOverdue(_post.createdAt, _board.boardMeta.general.deleteProtectionDays)) {
|
|
|
+ return alert(`게시글 작성 후 ${_board.boardMeta.general.deleteProtectionDays}일이 지나 삭제가 불가능합니다.`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (confirm('정말 삭제하시겠습니까?')) {
|
|
|
+ fetchPostDelete(_post.id).then(res => {
|
|
|
+ if (res.ok) {
|
|
|
+ alert('게시글이 삭제되었습니다.');
|
|
|
+ router.push(`/board/${_post.boardCode}`);
|
|
|
+ } else {
|
|
|
+ throwError(res);
|
|
|
+ }
|
|
|
+ }).catch((err) => {
|
|
|
+ setError(err.message);
|
|
|
+ }).finally(() => {
|
|
|
+ setLoading(false);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }, [member]);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div id='postView'>
|
|
|
+ {loading && <Loading />}
|
|
|
+
|
|
|
+ <QRCode isEnable={true} open={qrCode} onChange={setQrCode} />
|
|
|
+ <Copied isEnable={true} open={copied} onChange={setCopied} />
|
|
|
+ <SnsShare isEnable={true} open={snsShare} onChange={setSnsShare} />
|
|
|
+ <Report isEnable={true} open={report} onChange={setReport} onComplete={setHasReport} postID={_post.id} memberID={member?.id} />
|
|
|
+
|
|
|
+ {/* 글 제목 */}
|
|
|
+ <section className='subject whitespace-normal break-words'>
|
|
|
+ {_post.boardPrefixID && ("[" + _post.boardPrefix.name + "]")} {_post.subject}
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <hr />
|
|
|
+
|
|
|
+ {/* 글 작성자/작성일시/부가기능들 */}
|
|
|
+ <section className='attribution'>
|
|
|
+ {_board.boardMeta.view.showMemberPhoto && (
|
|
|
+ <div>
|
|
|
+ <article className='writer-thumb'>
|
|
|
+ <Image src='/resources/thumb.gif' alt='회원 사진' width={84} height={0} />
|
|
|
+ </article>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <div>
|
|
|
+ <article className='writer-info'>
|
|
|
+ <ul>
|
|
|
+ <li>※ {_post.writer.name}</li>
|
|
|
+ {_board.boardMeta.view.showMemberRegDate && <li>{formatDate(_post.writer.createdAt)} 가입</li>}
|
|
|
+ {_board.boardMeta.view.showMemberSummary && <li>{_post.writer.summary}</li>}
|
|
|
+ </ul>
|
|
|
+ </article>
|
|
|
+
|
|
|
+ <article className='post-info'>
|
|
|
+ <ul>
|
|
|
+ <li>조회: {_post.views}</li>
|
|
|
+ <li>댓글: {_post.comments}</li>
|
|
|
+ {_board.boardMeta.view.allowLike && (
|
|
|
+ <li>좋아요: {_post.likes}</li>
|
|
|
+ )}
|
|
|
+ {_board.boardMeta.view.allowDislike && (
|
|
|
+ <li>싫어요: {_post.dislikes}</li>
|
|
|
+ )}
|
|
|
+ <li>IP: {_post.ipAddress}</li>
|
|
|
+ </ul>
|
|
|
+ </article>
|
|
|
+
|
|
|
+ <article className='post-date'>
|
|
|
+ 작성일시 : {getDateTime(_post.createdAt)}
|
|
|
+ </article>
|
|
|
+
|
|
|
+ <article className='functions'>
|
|
|
+ <ul>
|
|
|
+ {_board.boardMeta.view.allowPostUrlQrCode && (
|
|
|
+ <li>
|
|
|
+ <a href='#' rel='noreferrer' onClick={toggleQRCode}><FontAwesomeIcon icon={faQrcode} /> QR</a>
|
|
|
+ </li>
|
|
|
+ )}
|
|
|
+ {_board.boardMeta.view.allowPrint && (
|
|
|
+ <li>
|
|
|
+ <a href='#' rel='noreferrer' onClick={handlePrint}><FontAwesomeIcon icon={faPrint} /> 인쇄</a>
|
|
|
+ </li>
|
|
|
+ )}
|
|
|
+ {_board.boardMeta.view.allowPostUrlCopy && (
|
|
|
+ <li>
|
|
|
+ <a href='#' rel='noreferrer' onClick={toggleCopied}><FontAwesomeIcon icon={faLink} /> 주소</a>
|
|
|
+ </li>
|
|
|
+ )}
|
|
|
+ {_board.boardMeta.view.allowSnsShare && (
|
|
|
+ <li>
|
|
|
+ <a href='#' rel='noreferrer' onClick={toggleSnsShare}><FontAwesomeIcon icon={faShareNodes} /> 공유</a>
|
|
|
+ </li>
|
|
|
+ )}
|
|
|
+ </ul>
|
|
|
+ </article>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <hr />
|
|
|
+
|
|
|
+ {/* 글 내용 */}
|
|
|
+ <section className='content'>
|
|
|
+ <Content boardMeta={_board.boardMeta} content={_post.content}></Content>
|
|
|
+
|
|
|
+ {_post.tagList.length > 0 && (
|
|
|
+ <article>
|
|
|
+ {/* 태그 표시 */}
|
|
|
+ {_post.tagList.map((row, i) => (
|
|
|
+ <span key={i}>
|
|
|
+ <Link href={`/tag/${row.slug}`}>#{row.slug}</Link>
|
|
|
+ </span>
|
|
|
+ ))}
|
|
|
+ </article>
|
|
|
+ )}
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <hr />
|
|
|
+
|
|
|
+ {/* 제어 버튼들 */}
|
|
|
+ <section className='controls'>
|
|
|
+
|
|
|
+ <article>
|
|
|
+ <Link href={`/board/${_post.boardCode}${window.location.search}`} className='btn btn-default'>목록</Link>
|
|
|
+
|
|
|
+ {_board.boardMeta.view.allowPrevNextBotton && (
|
|
|
+ <>
|
|
|
+ {!!_post.prevID && (
|
|
|
+ <Link href={`/post/${_post.prevID}`} className='btn btn-default'>이전</Link>
|
|
|
+ )}
|
|
|
+ {!!_post.nextID && (
|
|
|
+ <Link href={`/post/${_post.nextID}`} className='btn btn-default'>다음</Link>
|
|
|
+ )}
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </article>
|
|
|
+
|
|
|
+ <article className='functions'>
|
|
|
+ {_board.boardMeta.view.allowLike && (
|
|
|
+ <div className='hidden sm:block'>
|
|
|
+ <button className='btn btn-default' title='좋아요' value={Reaction.Like} onClick={handleReaction}>
|
|
|
+ <FontAwesomeIcon icon={!hasLike ? nThumbsUp : yThumbsUp } />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {_board.boardMeta.view.allowDislike && (
|
|
|
+ <div className='hidden sm:block'>
|
|
|
+ <button className='btn btn-default' title='싫어요' value={Reaction.Dislike} onClick={handleReaction}>
|
|
|
+ <FontAwesomeIcon icon={!hasDisLike ? nThumbsDown : yThumbsDown } />
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {_board.boardMeta.view.allowBookmark && (
|
|
|
+ <div className='hidden md:block'>
|
|
|
+ <button className='btn btn-default' onClick={handleBookmark}>
|
|
|
+ <FontAwesomeIcon icon={!hasBookmark ? nBookmark : yBookmark } /> 즐겨찾기
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {_board.boardMeta.view.allowBlame && (
|
|
|
+ <div className='hidden xlm:block'>
|
|
|
+ <button className='btn btn-default' onClick={handleReport}>
|
|
|
+ <FontAwesomeIcon icon={!hasReport ? nFlag : yFlag } /> 신고
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ <div className='hidden xl:block'>
|
|
|
+ <button className='btn btn-default' onClick={handleEdit}>수정</button>
|
|
|
+ </div>
|
|
|
+ <div className='hidden xl:block'>
|
|
|
+ <button className='btn btn-default' onClick={handleDelete}>삭제</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className='block xl:hidden'>
|
|
|
+ <DropdownMenu>
|
|
|
+ <DropdownMenuTrigger asChild>
|
|
|
+ <button className='btn btn-default' title='더보기'>
|
|
|
+ <Menu className='w-5 h-5' />
|
|
|
+ </button>
|
|
|
+ </DropdownMenuTrigger>
|
|
|
+ <DropdownMenuContent align='end'>
|
|
|
+ {_board.boardMeta.view.allowLike && (
|
|
|
+ <DropdownMenuItem className='block sm:hidden'>
|
|
|
+ <button type="button" value={Reaction.Like} onClick={handleReaction}><FontAwesomeIcon icon={!hasLike ? nThumbsUp : yThumbsUp } /> 좋아요</button>
|
|
|
+ </DropdownMenuItem>
|
|
|
+ )}
|
|
|
+ {_board.boardMeta.view.allowDislike && (
|
|
|
+ <DropdownMenuItem className='block sm:hidden'>
|
|
|
+ <button type="button" value={Reaction.Dislike} onClick={handleReaction}><FontAwesomeIcon icon={!hasDisLike ? nThumbsDown : yThumbsDown } /> 좋아요</button>
|
|
|
+ </DropdownMenuItem>
|
|
|
+ )}
|
|
|
+ {_board.boardMeta.view.allowBookmark && (
|
|
|
+ <DropdownMenuItem className='block md:hidden' onClick={handleBookmark}><FontAwesomeIcon icon={!hasBookmark ? nBookmark : yBookmark } /> 즐겨찾기</DropdownMenuItem>
|
|
|
+ )}
|
|
|
+ {_board.boardMeta.view.allowBlame && (
|
|
|
+ <DropdownMenuItem className='block xlm:hidden' onClick={handleReport}><FontAwesomeIcon icon={!hasReport ? nFlag : yFlag } /> 신고</DropdownMenuItem>
|
|
|
+ )}
|
|
|
+ <DropdownMenuItem className='block xl:hidden' onClick={handleEdit}><FontAwesomeIcon icon={faPenToSquare} /> 수정</DropdownMenuItem>
|
|
|
+ <DropdownMenuItem className='block xl:hidden' onClick={handleDelete}><FontAwesomeIcon icon={faTrashCan} /> 삭제</DropdownMenuItem>
|
|
|
+ </DropdownMenuContent>
|
|
|
+ </DropdownMenu>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <br/>
|
|
|
+
|
|
|
+ {/* 댓글 */}
|
|
|
+ <Comment board={_board} post={_post} />
|
|
|
+
|
|
|
+ {/* 게시판 최근 글 */}
|
|
|
+ <LatestList boardListMeta={_board.boardMeta.list} boardID={_board.id} boardCode={_board.code} postID={_post.id} />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|