diff --git a/src/domains/community/api/fetchPost.ts b/src/domains/community/api/fetchPost.ts index 29fdb672..3cab12ed 100644 --- a/src/domains/community/api/fetchPost.ts +++ b/src/domains/community/api/fetchPost.ts @@ -1,13 +1,17 @@ import { getApi } from '@/app/api/config/appConfig'; import { Post } from '@/domains/community/types/post'; import { ParamValue } from 'next/dist/server/request/params'; +import { tabItem } from '../main/CommunityTab'; -export const fetchPost = async (): Promise => { +export const fetchPost = async (lastId?: number | null): Promise => { try { - const res = await fetch(`${getApi}/posts?postSortStatus=LATEST`, { - method: 'GET', - cache: 'no-store', - }); + const res = await fetch( + `${getApi}/posts?${lastId ? `lastId=${lastId}&` : ''}postSortStatus=LATEST`, + { + method: 'GET', + cache: 'no-store', + } + ); const data = await res.json(); return data.data; } catch (err) { @@ -29,15 +33,57 @@ export const fetchPostById = async (postId: ParamValue) => { } }; -export const fetchPostByTab = async (selectedTab: string): Promise => { +export const fetchPostByTab = async ({ + category, + filter = 'LATEST', + lastId, + lastLikeCount, + lastCommentCount, +}: { + category?: string; + filter?: 'LATEST' | 'POPULAR' | 'COMMENTS'; + lastId?: number; + lastLikeCount?: number | null; + lastCommentCount?: number | null; +}): Promise => { try { - const data = await fetchPost(); + const params = new URLSearchParams(); + + if (category && category !== 'all') { + const categoryId = tabItem.findIndex((tab) => tab.key === category); + if (categoryId >= 0) { + params.set('categoryId', categoryId.toString()); + } + } + + if (lastId) params.set('lastId', lastId.toString()); + + switch (filter) { + case 'POPULAR': + if (lastLikeCount) params.set('lastLikeCount', lastLikeCount.toString()); + params.set('postSortStatus', 'POPULAR'); + break; + case 'COMMENTS': + if (lastCommentCount) params.set('lastCommentCount', lastCommentCount.toString()); + params.set('postSortStatus', 'COMMENTS'); + break; + case 'LATEST': + default: + params.set('postSortStatus', 'LATEST'); + break; + } + + const res = await fetch(`${getApi}/posts?${params.toString()}`, { + method: 'GET', + cache: 'no-store', + }); + + const data = await res.json(); if (!data) return null; - const filtered = data.filter((post) => post.categoryName === selectedTab); - return filtered; + return data.data; // 필요하다면 filter 추가 가능 } catch (err) { - console.error('글 목록 필터링 실패', err); + console.error('글 목록 가져오기 실패', err); return null; } }; diff --git a/src/domains/community/main/Community.tsx b/src/domains/community/main/Community.tsx index 878d412b..008aab1d 100644 --- a/src/domains/community/main/Community.tsx +++ b/src/domains/community/main/Community.tsx @@ -1,27 +1,100 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import CommunityFilter from './CommunityFilter'; import CommunityTab from './CommunityTab'; import PostCard from './PostCard'; import WriteBtn from './WriteBtn'; import { Post } from '../types/post'; -import { fetchPost } from '../api/fetchPost'; +import { fetchPostByTab } from '../api/fetchPost'; +import { useSearchParams } from 'next/navigation'; function Community() { - const [posts, setPosts] = useState([]); + const [posts, setPosts] = useState([]); const [isLoading, setIsLoading] = useState(true); + const [lastLoadedId, setLastLoadedId] = useState(null); + + const searchParams = useSearchParams(); + + const category = useMemo(() => searchParams.get('category') || 'all', [searchParams]); + const filter = useMemo( + () => (searchParams.get('postSortStatus') as 'LATEST' | 'POPULAR' | 'COMMENTS') || 'LATEST', + [searchParams] + ); + + const [isEnd, setIsEnd] = useState(false); useEffect(() => { - const fetchData = async () => { - setIsLoading(true); - const data = await fetchPost(); - if (!data) return; - setPosts(data); + setPosts([]); + setIsEnd(false); + setLastLoadedId(null); + loadInitialPosts(); + }, [category, filter]); + + const loadInitialPosts = async () => { + const category = searchParams.get('category') || 'all'; + const filter = + (searchParams.get('postSortStatus') as 'LATEST' | 'POPULAR' | 'COMMENTS') || 'LATEST'; + + setIsLoading(true); + setIsEnd(false); + + const lastLikeCount = + posts && posts.length > 0 ? Math.min(...posts.map((post) => post.likeCount)) : null; + + const lastCommentCount = + posts && posts.length > 0 ? Math.min(...posts.map((post) => post.commentCount)) : null; + + try { + const newPosts = await fetchPostByTab({ + category, + filter, + lastLikeCount, + lastCommentCount, + }); + + if (!newPosts || newPosts.length === 0) { + setIsEnd(true); + setPosts([]); + } else { + setPosts(newPosts); + } + } finally { + setIsLoading(false); + } + }; + + const loadMorePosts = async (lastPostId: number) => { + if (isEnd || isLoading) return; + if (!posts || posts.length === 0) return; + console.log('시작', lastPostId); + + const lastPost = posts[posts.length - 1]; + if (lastPostId === lastPost.postId) return; + setLastLoadedId(lastPost.postId); + + setIsLoading(true); + try { + const category = searchParams.get('category') || 'all'; + const filter = + (searchParams.get('postSortStatus') as 'LATEST' | 'POPULAR' | 'COMMENTS') || 'LATEST'; + + const newPosts = await fetchPostByTab({ + category, + filter, + lastId: lastPostId, + }); + + if (!newPosts || newPosts?.length === 0) { + setIsEnd(true); + console.log('끝'); + } else { + setPosts((prev) => [...(prev ?? []), ...(newPosts ?? [])]); + } + } finally { setIsLoading(false); - }; - fetchData(); - }, [setPosts]); + } + }; return ( <> @@ -29,13 +102,20 @@ function Community() { aria-label="탭과 글쓰기" className="flex justify-between item-center sm:flex-row flex-col gap-4 mt-1" > - +
- - + +
); diff --git a/src/domains/community/main/CommunityFilter.tsx b/src/domains/community/main/CommunityFilter.tsx index d497b3f0..25dfd906 100644 --- a/src/domains/community/main/CommunityFilter.tsx +++ b/src/domains/community/main/CommunityFilter.tsx @@ -2,19 +2,59 @@ import { Post } from '../types/post'; import SelectBox from '@/shared/components/select-box/SelectBox'; +import { Dispatch, SetStateAction, useEffect } from 'react'; +import { fetchPostByTab } from '../api/fetchPost'; +import { useRouter, useSearchParams } from 'next/navigation'; type Props = { - posts: Post[]; + posts: Post[] | null; + setPosts: Dispatch>; }; -function CommunityFilter({ posts }: Props) { +const sortMap = { + 최신순: 'LATEST', + 인기순: 'POPULAR', + 댓글순: 'COMMENTS', +} as const; + +function CommunityFilter({ posts, setPosts }: Props) { + const searchParams = useSearchParams(); + const query = searchParams.get('category'); + const router = useRouter(); + + useEffect(() => { + console.log(query); + }, [query]); + + const handleChange = async (selectTitle: string) => { + if (!query) return; + + console.log(selectTitle); + + const data = await fetchPostByTab({ + category: query, + filter: sortMap[selectTitle as keyof typeof sortMap], + }); + if (!data) return; + setPosts(data); + }; + return (
-

{posts.length}개

- +

{posts && posts.length}개

+ { + const sortValue = sortMap[value as keyof typeof sortMap]; + + handleChange(value); + router.push(`?category=${query || '전체'}&postSortStatus=${sortValue}`); + }} + />
); } diff --git a/src/domains/community/main/CommunityTab.tsx b/src/domains/community/main/CommunityTab.tsx index 8ed73756..a277cc52 100644 --- a/src/domains/community/main/CommunityTab.tsx +++ b/src/domains/community/main/CommunityTab.tsx @@ -1,56 +1,60 @@ 'use client'; import tw from '@/shared/utills/tw'; -import { useEffect, useState } from 'react'; -import { fetchPost, fetchPostByTab } from '../api/fetchPost'; import { Post } from '../types/post'; +import { useState } from 'react'; +import { fetchPost, fetchPostByTab } from '../api/fetchPost'; +import { useRouter, useSearchParams } from 'next/navigation'; type Props = { - setPosts: (value: Post[]) => void; + setPosts: (value: Post[] | null) => void; + setIsLoading: (value: boolean) => void; + setIsEnd: (value: boolean) => void; }; -const tabItem = [ - { title: '전체' }, - { title: '레시피' }, - { title: '팁' }, - { title: '질문' }, - { title: '자유' }, +export const tabItem = [ + { key: 'all', label: '전체' }, + { key: 'recipe', label: '레시피' }, + { key: 'tip', label: '팁' }, + { key: 'question', label: '질문' }, + { key: 'chat', label: '자유' }, ]; -function CommunityTab({ setPosts }: Props) { - const [selectedIdx, setSelectedIdx] = useState(0); - - useEffect(() => { - const fetchData = async () => { - const selectedTab = tabItem[selectedIdx].title; +function CommunityTab({ setPosts, setIsLoading, setIsEnd }: Props) { + const searchParams = useSearchParams(); + const router = useRouter(); - let data; - if (selectedTab === '전체') data = await fetchPost(); - else data = await fetchPostByTab(selectedTab); + const currentSort = searchParams.get('postSortStatus') || 'LATEST'; - if (!data) return; - setPosts(data); - }; - fetchData(); - }, [selectedIdx, setPosts]); + const [selectedCategory, setSelectedCategory] = useState(() => { + const param = searchParams.get('category') || 'all'; + const exists = tabItem.some(({ key }) => key === param); + return exists ? param : 'all'; + }); return (
- {tabItem.map(({ title }, idx) => ( + {tabItem.map(({ key, label }) => ( ))}
diff --git a/src/domains/community/main/PostCard.tsx b/src/domains/community/main/PostCard.tsx index 952e4850..b1fe3878 100644 --- a/src/domains/community/main/PostCard.tsx +++ b/src/domains/community/main/PostCard.tsx @@ -8,86 +8,113 @@ import Label from '@/domains/shared/components/label/Label'; import { Post } from '@/domains/community/types/post'; import { useRouter } from 'next/navigation'; import SkeletonPostCard from '@/domains/shared/skeleton/SkeletonPostCard'; +import { useInfiniteScrollObserver } from '@/shared/hook/useInfiniteScrollObserver'; +import { useRef } from 'react'; type Props = { - posts: Post[]; + posts: Post[] | null; + setPost?: (value: Post[] | null) => void; isLoading: boolean; + setIsLoading?: (value: boolean) => void; + isEnd?: boolean; + onLoadMore?: (lastCommentId: number) => void; }; -function PostCard({ posts, isLoading }: Props) { +function PostCard({ posts, isLoading, isEnd, onLoadMore }: Props) { const router = useRouter(); + const firstItemRef = useRef(null); const handlePost = (id: number) => { router.push(`/community/${id}`); }; + const observeLastItem = useInfiniteScrollObserver({ + items: posts, + isEnd, + onLoadMore: onLoadMore ?? (() => {}), + }); + if (isLoading) return ; - if (posts.length === 0) + if (posts && posts.length === 0) return (
작성된 글이 없습니다.
); return ( <> - {posts.map( - ({ - postId, - categoryName, - title, - content, - userNickName, - viewCount, - createdAt, - commentCount, - imageUrl, - }) => ( -
-
+ ); + } + )} ); } diff --git a/src/domains/shared/components/comment/CommentList.tsx b/src/domains/shared/components/comment/CommentList.tsx index 2dbd9ffc..09604016 100644 --- a/src/domains/shared/components/comment/CommentList.tsx +++ b/src/domains/shared/components/comment/CommentList.tsx @@ -15,6 +15,7 @@ type Props = { onLoadMore?: (lastCommentId: number) => void; // ← 무한스크롤 콜백 isEnd?: boolean; isLoading: boolean; + myPage?: boolean; }; function CommentList({ @@ -24,6 +25,7 @@ function CommentList({ onDeleteComment, onLoadMore, isEnd, + myPage = false, }: Props) { const parentRef = useRef(null); const [editCommentId, setEditCommentId] = useState(null); @@ -107,6 +109,7 @@ function CommentList({ commentTime={createdAt} isMyComment={isMyComment} isEditing={isEditing} + myPage={myPage} onSubmitEdit={() => { const updatedContent = editedContentMap[commentId]; if (!updatedContent) return; diff --git a/src/domains/shared/components/comment/CommentTitle.tsx b/src/domains/shared/components/comment/CommentTitle.tsx index a66b99eb..b3993a44 100644 --- a/src/domains/shared/components/comment/CommentTitle.tsx +++ b/src/domains/shared/components/comment/CommentTitle.tsx @@ -11,6 +11,7 @@ type Props = { onSubmitEdit: () => void; onDelete: () => void; isMyComment: boolean | null; + myPage: boolean; }; function CommentTitle({ @@ -22,6 +23,7 @@ function CommentTitle({ onSubmitEdit, onDelete, isMyComment, + myPage, }: Props) { return (
@@ -30,7 +32,7 @@ function CommentTitle({ |

{elapsedTime(commentTime)}

- {isMyComment && ( + {isMyComment && !myPage && (