diff --git a/src/pages/Post/PostBase/LikeCommentBottomSheetContent/CommentItem/index.tsx b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/CommentItem/index.tsx index 3da48992..307180df 100644 --- a/src/pages/Post/PostBase/LikeCommentBottomSheetContent/CommentItem/index.tsx +++ b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/CommentItem/index.tsx @@ -14,7 +14,7 @@ import { import { StyledText } from '@components/Text/StyledText'; -import { CommentItemProps } from './dto'; +import type { CommentItemProps } from './dto'; import More from '@assets/default/more.svg'; diff --git a/src/pages/Post/PostBase/LikeCommentBottomSheetContent/MenuButtonList/index.tsx b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/MenuButtonList/index.tsx index d30526a6..ae7ae51a 100644 --- a/src/pages/Post/PostBase/LikeCommentBottomSheetContent/MenuButtonList/index.tsx +++ b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/MenuButtonList/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { MenuButtonListProps } from './dto'; +import type { MenuButtonListProps } from './dto'; import { MenuListWrapper, MenuListContainer, MenuButtonItem } from './styles'; import { StyledText } from '@components/Text/StyledText'; diff --git a/src/pages/Post/PostBase/LikeCommentBottomSheetContent/index.tsx b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/index.tsx index d7b5985f..edc16761 100644 --- a/src/pages/Post/PostBase/LikeCommentBottomSheetContent/index.tsx +++ b/src/pages/Post/PostBase/LikeCommentBottomSheetContent/index.tsx @@ -14,13 +14,13 @@ import { StyledText } from '@components/Text/StyledText'; import theme from '@styles/theme'; import Loading from '@components/Loading'; import Modal from '@components/Modal'; -import CommentItem from './CommentItem'; -import MenuButtonList from './MenuButtonList'; +import CommentItem from './CommentItem/index'; +import MenuButtonList from './MenuButtonList/index'; -import { LikeCommentBottomSheetProps } from '../dto'; -import { ModalProps } from '@components/Modal/dto'; -import { GetPostLikeListResponse } from '@apis/post-like/dto'; -import { Comment, GetCommentListResponse } from '@apis/post-comment/dto'; +import type { LikeCommentBottomSheetProps } from '../dto'; +import type { ModalProps } from '@components/Modal/dto'; +import type { GetPostLikeListResponse } from '@apis/post-like/dto'; +import type { Comment, GetCommentListResponse } from '@apis/post-comment/dto'; import Delete from '@assets/default/delete.svg'; import Block from '@assets/default/block.svg'; @@ -36,15 +36,11 @@ import { getCurrentUserId } from '@utils/getCurrentUserId'; const LikeCommentBottomSheetContent: React.FC = ({ tab, likeCount, commentCount }) => { const [activeTab, setActiveTab] = useState<'likes' | 'comments'>(tab); - const { postId } = useParams<{ postId: string }>(); const [likes, setLikes] = useState([]); const [postLikeCount, setPostLikeCount] = useState(likeCount); const [comments, setComments] = useState([]); const [postCommentCount, setPostCommentCount] = useState(commentCount); - const [isBlockConfirmationModalOpen, setIsBlockConfirmationModalOpen] = useState(false); - const [isStatusModalOpen, setIsStatusModalOpen] = useState(false); - const [modalContent, setModalContent] = useState('알 수 없는 오류입니다.\n관리자에게 문의해 주세요.'); const [isLoading, setIsLoading] = useState(false); const [page, setPage] = useState(1); @@ -60,50 +56,43 @@ const LikeCommentBottomSheetContent: React.FC = ({ const [isMenuVisible, setIsMenuVisible] = useState(false); const [menuPosition, setMenuPosition] = useState<{ top: number; left: number }>({ top: 0, left: 0 }); + const [isBlockConfirmationModalOpen, setIsBlockConfirmationModalOpen] = useState(false); + const [isStatusModalOpen, setIsStatusModalOpen] = useState(false); const [isCommentDeleteConfirmationModalOpen, setIsCommentDeleteConfirmationModalOpen] = useRecoilState( IsCommentDeleteConfirmationModalOpenAtom, ); const [, setIsCommentReportModalOpen] = useRecoilState(IsCommentReportModalOpenAtom); + const [modalContent, setModalContent] = useState('알 수 없는 오류입니다.\n관리자에게 문의해 주세요.'); + const { postId } = useParams<{ postId: string }>(); const nav = useNavigate(); - useEffect(() => { - setPage(1); - setReachedEnd(false); - setLikes([]); - setComments([]); - - if (activeTab === 'likes') { - getPostLikeList(1); - } else if (activeTab === 'comments') { - getPostCommentList(); - } - }, [activeTab]); - - // IntersectionObserver를 활용하여 무한 스크롤 감지 - useEffect(() => { - if (observerRef.current) observerRef.current.disconnect(); - - const handleIntersection = (entries: IntersectionObserverEntry[]) => { - const [entry] = entries; - if (entry.isIntersecting && !isLoading) { - console.log('호출'); - getPostLikeList(page); - } - }; + // 댓글 메뉴 클릭한 경우 + const handleMenuOpen = (comment: Comment, event: React.MouseEvent) => { + setSelectedComment(comment); + const rect = event.currentTarget.getBoundingClientRect(); + setMenuPosition({ top: rect.bottom + window.scrollY - 90, left: rect.left + window.scrollX - 100 }); + setIsMenuVisible(true); + }; - observerRef.current = new IntersectionObserver(handleIntersection, { - root: null, - rootMargin: '0px', - threshold: 1.0, - }); + // 유저 클릭한 경우 + const handleUserClick = (userId: number) => { + // 로컬 스토리지에서 사용자 ID 가져오기 + const myUserId = getCurrentUserId(); // 로컬 스토리지에 저장된 사용자 ID를 가져옴 - if (loadMoreRef.current) observerRef.current.observe(loadMoreRef.current); + if (String(myUserId) === String(userId)) { + // 나인 경우 + nav(`/profile/${userId}`); + } else { + // 다른 유저인 경우 + nav(`/users/${userId}`); + } + }; - return () => { - if (observerRef.current) observerRef.current.disconnect(); - }; - }, [page, reachedEnd, loadMoreRef.current, activeTab]); + // 댓글 작성 Input + const handleInputChange = useCallback((e: React.ChangeEvent) => { + setInputValue(e.target.value); + }, []); // 좋아요 리스트 불러오기 api const getPostLikeList = async (currentPage: number) => { @@ -156,11 +145,6 @@ const LikeCommentBottomSheetContent: React.FC = ({ } }; - // 댓글 작성 Input - const handleInputChange = useCallback((e: React.ChangeEvent) => { - setInputValue(e.target.value); - }, []); - // 댓글 작성 api const createComment = async () => { if (isSubmitting) return; // 중복 요청 방지 @@ -233,6 +217,44 @@ const LikeCommentBottomSheetContent: React.FC = ({ } }; + useEffect(() => { + setPage(1); + setReachedEnd(false); + setLikes([]); + setComments([]); + + if (activeTab === 'likes') { + getPostLikeList(1); + } else if (activeTab === 'comments') { + getPostCommentList(); + } + }, [activeTab]); + + // IntersectionObserver를 활용하여 무한 스크롤 감지 + useEffect(() => { + if (observerRef.current) observerRef.current.disconnect(); + + const handleIntersection = (entries: IntersectionObserverEntry[]) => { + const [entry] = entries; + if (entry.isIntersecting && !isLoading) { + console.log('호출'); + getPostLikeList(page); + } + }; + + observerRef.current = new IntersectionObserver(handleIntersection, { + root: null, + rootMargin: '0px', + threshold: 1.0, + }); + + if (loadMoreRef.current) observerRef.current.observe(loadMoreRef.current); + + return () => { + if (observerRef.current) observerRef.current.disconnect(); + }; + }, [page, reachedEnd, loadMoreRef.current, activeTab]); + // 본인 댓글 메뉴 항목 const MyCommentMenuItems = [ { @@ -307,28 +329,6 @@ const LikeCommentBottomSheetContent: React.FC = ({ }, }; - // 댓글 메뉴 클릭한 경우 - const handleMenuOpen = (comment: Comment, event: React.MouseEvent) => { - setSelectedComment(comment); - const rect = event.currentTarget.getBoundingClientRect(); - setMenuPosition({ top: rect.bottom + window.scrollY - 90, left: rect.left + window.scrollX - 100 }); - setIsMenuVisible(true); - }; - - // 유저 클릭한 경우 - const handleUserClick = (userId: number) => { - // 로컬 스토리지에서 사용자 ID 가져오기 - const myUserId = getCurrentUserId(); // 로컬 스토리지에 저장된 사용자 ID를 가져옴 - - if (String(myUserId) === String(userId)) { - // 나인 경우 - nav(`/profile/${userId}`); - } else { - // 다른 유저인 경우 - nav(`/users/${userId}`); - } - }; - return ( <> diff --git a/src/pages/Post/PostBase/index.tsx b/src/pages/Post/PostBase/index.tsx index 4f4ad540..5d2908aa 100644 --- a/src/pages/Post/PostBase/index.tsx +++ b/src/pages/Post/PostBase/index.tsx @@ -15,8 +15,8 @@ import TopBar from '@components/TopBar'; import NavBar from '@components/NavBar'; import BottomSheet from '@components/BottomSheet'; import ClothingInfoItem from '@components/ClothingInfoItem'; -import ImageSwiper from './ImageSwiper'; -import LikeCommentBottomSheetContent from './LikeCommentBottomSheetContent'; +import ImageSwiper from './ImageSwiper/index'; +import LikeCommentBottomSheetContent from './LikeCommentBottomSheetContent/index'; import { PostLayout, @@ -36,22 +36,21 @@ import { ClothingInfoList, } from './styles'; -import Left from '../../../assets/arrow/left.svg'; -import Like from '../../../assets/default/like.svg'; -import LikeFill from '../../../assets/default/like-fill.svg'; -import Message from '../../../assets/default/message.svg'; -import More from '../../../assets/default/more.svg'; +import Left from '@assets/arrow/left.svg'; +import Like from '@assets/default/like.svg'; +import LikeFill from '@assets/default/like-fill.svg'; +import Message from '@assets/default/message.svg'; +import More from '@assets/default/more.svg'; -import { BottomSheetProps } from '@components/BottomSheet/dto'; -import { PostBaseProps } from './dto'; -import { GetPostDetailResponse } from '@apis/post/dto'; +import type { BottomSheetProps } from '@components/BottomSheet/dto'; +import type { PostBaseProps } from './dto'; +import type { GetPostDetailResponse } from '@apis/post/dto'; import { getPostDetailApi } from '@apis/post'; import { togglePostLikeStatusApi } from '@apis/post-like'; import { getCurrentUserId } from '@utils/getCurrentUserId'; const PostBase: React.FC = ({ onClickMenu }) => { - const { postId } = useParams<{ postId: string }>(); const [, setPostId] = useRecoilState(postIdAtom); const [post, setPost] = useState(); const [user, setUser] = useRecoilState(userAtom); @@ -62,9 +61,56 @@ const PostBase: React.FC = ({ onClickMenu }) => { const [isLikeCommentBottomSheetOpen, setIsLikeCommentBottomSheetOpen] = useState(false); const [activeTab, setActiveTab] = useState<'likes' | 'comments'>('likes'); // activeTab state - const nav = useNavigate(); + const { postId } = useParams<{ postId: string }>(); const userId = getCurrentUserId(); + const nav = useNavigate(); + + const handleLikeCommentOpen = (tab: 'likes' | 'comments') => { + setActiveTab(tab); // 클릭한 버튼에 따라 activeTab 설정 + setIsLikeCommentBottomSheetOpen(true); + }; + + const handleUserClick = () => { + if (post?.isPostWriter) { + // 내 게시물인 경우 + nav(`/profile/${userId}`); + } else { + // 다른 유저의 게시물인 경우 + nav(`/users/${post?.user.id}`); + } + }; + + const contentRef = useRef(null); + + const toggleTextDisplay = () => { + setShowFullText((prev) => !prev); + }; + + // 게시글 좋아요 누르기/취소하기 api + const togglePostLikeStatus = async () => { + if (!post || !postId) return; + + const prevPost = { ...post }; // 현재 상태 저장 + setPost({ + ...post, + isPostLike: !post.isPostLike, + postLikesCount: post.isPostLike ? post.postLikesCount - 1 : post.postLikesCount + 1, + }); //사용자가 좋아요를 누르면 먼저 클라이언트에서 post 상태를 변경(낙관적 업데이트) + + try { + const response = await togglePostLikeStatusApi(Number(postId)); + setPost({ + ...post, + isPostLike: response.data.isPostLike, + postLikesCount: response.data.postLikesCount, + }); // 서버로 요청 후 성공하면 그대로 유지 + } catch (error) { + console.error('Error toggling like status:', error); + setPost(prevPost); // 실패하면 원래 상태로 롤백 + } + }; + useEffect(() => { setPostId(Number(postId)); @@ -85,8 +131,6 @@ const PostBase: React.FC = ({ onClickMenu }) => { getPost(); }, [postId]); - const contentRef = useRef(null); - useEffect(() => { if (contentRef.current) { // 실제 높이와 줄 제한 높이 비교 @@ -95,25 +139,6 @@ const PostBase: React.FC = ({ onClickMenu }) => { } }, [post?.content]); - const toggleTextDisplay = () => { - setShowFullText((prev) => !prev); - }; - - const handleUserClick = () => { - if (post?.isPostWriter) { - // 내 게시물인 경우 - nav(`/profile/${userId}`); - } else { - // 다른 유저의 게시물인 경우 - nav(`/users/${post?.user.id}`); - } - }; - - const handleLikeCommentOpen = (tab: 'likes' | 'comments') => { - setActiveTab(tab); // 클릭한 버튼에 따라 activeTab 설정 - setIsLikeCommentBottomSheetOpen(true); - }; - const likeCommentbottomSheetProps: BottomSheetProps = { isOpenBottomSheet: isLikeCommentBottomSheetOpen, isHandlerVisible: true, @@ -128,30 +153,6 @@ const PostBase: React.FC = ({ onClickMenu }) => { }, }; - // 게시글 좋아요 누르기/취소하기 - const togglePostLikeStatus = async () => { - if (!post || !postId) return; - - const prevPost = { ...post }; // 현재 상태 저장 - setPost({ - ...post, - isPostLike: !post.isPostLike, - postLikesCount: post.isPostLike ? post.postLikesCount - 1 : post.postLikesCount + 1, - }); //사용자가 좋아요를 누르면 먼저 클라이언트에서 post 상태를 변경(낙관적 업데이트) - - try { - const response = await togglePostLikeStatusApi(Number(postId)); - setPost({ - ...post, - isPostLike: response.data.isPostLike, - postLikesCount: response.data.postLikesCount, - }); // 서버로 요청 후 성공하면 그대로 유지 - } catch (error) { - console.error('Error toggling like status:', error); - setPost(prevPost); // 실패하면 원래 상태로 롤백 - } - }; - return ( diff --git a/src/pages/Post/index.tsx b/src/pages/Post/index.tsx index 7b48cd1c..e96ada8b 100644 --- a/src/pages/Post/index.tsx +++ b/src/pages/Post/index.tsx @@ -5,24 +5,25 @@ import { useRecoilValue, useRecoilState } from 'recoil'; import { postIdAtom, userAtom } from '@recoil/Post/PostAtom.ts'; import { isPostRepresentativeAtom } from '@recoil/Post/PostAtom'; -import PostBase from './PostBase/index.tsx'; -import OptionsBottomSheet from '@components/BottomSheet/OptionsBottomSheet/index.tsx'; -import { OptionsBottomSheetProps } from '@components/BottomSheet/OptionsBottomSheet/dto.ts'; -import Modal from '@components/Modal'; -import { ModalProps } from '@components/Modal/dto'; +import OptionsBottomSheet from '@components/BottomSheet/OptionsBottomSheet'; import BottomSheet from '@components/BottomSheet'; -import { BottomSheetProps } from '@components/BottomSheet/dto'; import BottomSheetMenu from '@components/BottomSheetMenu'; -import { BottomSheetMenuProps } from '@components/BottomSheetMenu/dto'; +import Modal from '@components/Modal'; +import PostBase from './PostBase/index'; + +import type { OptionsBottomSheetProps } from '@components/BottomSheet/OptionsBottomSheet/dto.ts'; +import type { BottomSheetProps } from '@components/BottomSheet/dto'; +import type { BottomSheetMenuProps } from '@components/BottomSheetMenu/dto'; +import type { ModalProps } from '@components/Modal/dto'; import Edit from '@assets/default/edit.svg'; import Pin from '@assets/default/pin.svg'; import Delete from '@assets/default/delete.svg'; import { modifyPostRepresentativeStatusApi, deletePostApi } from '@apis/post'; +import { getCurrentUserId } from '@utils/getCurrentUserId'; const Post: React.FC = () => { - const userId = localStorage.getItem('current_user_id'); const user = useRecoilValue(userAtom); const postId = useRecoilValue(postIdAtom); const [isMyPost, setIsMyPost] = useState(false); @@ -35,8 +36,54 @@ const Post: React.FC = () => { const [isApiResponseModalOpen, setIsApiResponseModalOpen] = useState(false); const [modalContent, setModalContent] = useState(''); const [postPinStatus, setPostPinStatus] = useState<'지정' | '해제'>('지정'); + + const userId = getCurrentUserId(); const navigate = useNavigate(); + const handleMenuOpen = () => { + { + isMyPost ? setIsMyPostMenuBottomSheetOpen(true) : setIsOptionsBottomSheetOpen(true); + } + }; + + const modifyPostRepresentativeStatus = async () => { + try { + const response = await modifyPostRepresentativeStatusApi(Number(postId)); + + if (response.isSuccess) { + setModalContent(`대표 OOTD ${postPinStatus}에 성공했어요`); + setIsPostRepresentative((prev) => !prev); + } else { + setModalContent(`대표 OOTD ${postPinStatus}에 실패했어요\n잠시 뒤 다시 시도해 보세요`); + } + } catch (error) { + console.error('Error pinning post:', error); + } finally { + setIsApiResponseModalOpen(true); + } + }; + + const deletePost = async () => { + try { + const response = await deletePostApi(Number(postId)); + + if (response.isSuccess) { + setModalContent('OOTD 삭제에 성공했어요'); + + setTimeout(() => { + navigate(`/profile/${userId}`); + }, 1000); + } else { + setModalContent(`OOTD 삭제에 실패했어요\n잠시 뒤 다시 시도해 보세요`); + } + } catch (error) { + console.error('Error deleting post:', error); + } finally { + setIsApiResponseModalOpen(true); + setIsDeleteConfirmationModalOpen(false); // 확인 모달을 닫음 + } + }; + useEffect(() => { // 현재 게시글이 내 게시글인지 확인 if (user?.id && postId) { @@ -44,12 +91,6 @@ const Post: React.FC = () => { } }, [user, postId]); - const handleMenuOpen = () => { - { - isMyPost ? setIsMyPostMenuBottomSheetOpen(true) : setIsOptionsBottomSheetOpen(true); - } - }; - useEffect(() => { setPostPinStatus(isPostRepresentative ? '해제' : '지정'); }, [isPostRepresentative]); @@ -105,44 +146,6 @@ const Post: React.FC = () => { onCloseBottomSheet: () => setIsMyPostMenuBottomSheetOpen(false), }; - const modifyPostRepresentativeStatus = async () => { - try { - const response = await modifyPostRepresentativeStatusApi(Number(postId)); - - if (response.isSuccess) { - setModalContent(`대표 OOTD ${postPinStatus}에 성공했어요`); - setIsPostRepresentative((prev) => !prev); - } else { - setModalContent(`대표 OOTD ${postPinStatus}에 실패했어요\n잠시 뒤 다시 시도해 보세요`); - } - } catch (error) { - console.error('Error pinning post:', error); - } finally { - setIsApiResponseModalOpen(true); - } - }; - - const deletePost = async () => { - try { - const response = await deletePostApi(Number(postId)); - - if (response.isSuccess) { - setModalContent('OOTD 삭제에 성공했어요'); - - setTimeout(() => { - navigate(`/profile/${userId}`); - }, 1000); - } else { - setModalContent(`OOTD 삭제에 실패했어요\n잠시 뒤 다시 시도해 보세요`); - } - } catch (error) { - console.error('Error deleting post:', error); - } finally { - setIsApiResponseModalOpen(true); - setIsDeleteConfirmationModalOpen(false); // 확인 모달을 닫음 - } - }; - const deleteConfirmationModalProps: ModalProps = { isCloseButtonVisible: true, onClose: () => setIsDeleteConfirmationModalOpen(false),