Skip to content

Commit c63aa72

Browse files
committed
Feat/communityscroll#23 (#114)
* [feat] 스크롤링구현 * [feat] 주소, api설정 * [feat] 커뮤니티 탭, 필터 패치로직 * [feat] lastLikeCount, lastCommentCount, 추가 * [fix] 코멘트 삭제수정 마이페이지에선 뗄수있게 myPage props 추가 * 옵셔널로 수정 * 오류 수정
1 parent 0692f9d commit c63aa72

File tree

7 files changed

+322
-120
lines changed

7 files changed

+322
-120
lines changed

src/domains/community/api/fetchPost.ts

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import { getApi } from '@/app/api/config/appConfig';
22
import { Post } from '@/domains/community/types/post';
33
import { ParamValue } from 'next/dist/server/request/params';
4+
import { tabItem } from '../main/CommunityTab';
45

5-
export const fetchPost = async (): Promise<Post[] | null> => {
6+
export const fetchPost = async (lastId?: number | null): Promise<Post[] | null> => {
67
try {
7-
const res = await fetch(`${getApi}/posts?postSortStatus=LATEST`, {
8-
method: 'GET',
9-
cache: 'no-store',
10-
});
8+
const res = await fetch(
9+
`${getApi}/posts?${lastId ? `lastId=${lastId}&` : ''}postSortStatus=LATEST`,
10+
{
11+
method: 'GET',
12+
cache: 'no-store',
13+
}
14+
);
1115
const data = await res.json();
1216
return data.data;
1317
} catch (err) {
@@ -29,15 +33,57 @@ export const fetchPostById = async (postId: ParamValue) => {
2933
}
3034
};
3135

32-
export const fetchPostByTab = async (selectedTab: string): Promise<Post[] | null> => {
36+
export const fetchPostByTab = async ({
37+
category,
38+
filter = 'LATEST',
39+
lastId,
40+
lastLikeCount,
41+
lastCommentCount,
42+
}: {
43+
category?: string;
44+
filter?: 'LATEST' | 'POPULAR' | 'COMMENTS';
45+
lastId?: number;
46+
lastLikeCount?: number | null;
47+
lastCommentCount?: number | null;
48+
}): Promise<Post[] | null> => {
3349
try {
34-
const data = await fetchPost();
50+
const params = new URLSearchParams();
51+
52+
if (category && category !== 'all') {
53+
const categoryId = tabItem.findIndex((tab) => tab.key === category);
54+
if (categoryId >= 0) {
55+
params.set('categoryId', categoryId.toString());
56+
}
57+
}
58+
59+
if (lastId) params.set('lastId', lastId.toString());
60+
61+
switch (filter) {
62+
case 'POPULAR':
63+
if (lastLikeCount) params.set('lastLikeCount', lastLikeCount.toString());
64+
params.set('postSortStatus', 'POPULAR');
65+
break;
66+
case 'COMMENTS':
67+
if (lastCommentCount) params.set('lastCommentCount', lastCommentCount.toString());
68+
params.set('postSortStatus', 'COMMENTS');
69+
break;
70+
case 'LATEST':
71+
default:
72+
params.set('postSortStatus', 'LATEST');
73+
break;
74+
}
75+
76+
const res = await fetch(`${getApi}/posts?${params.toString()}`, {
77+
method: 'GET',
78+
cache: 'no-store',
79+
});
80+
81+
const data = await res.json();
3582
if (!data) return null;
3683

37-
const filtered = data.filter((post) => post.categoryName === selectedTab);
38-
return filtered;
84+
return data.data; // 필요하다면 filter 추가 가능
3985
} catch (err) {
40-
console.error('글 목록 필터링 실패', err);
86+
console.error('글 목록 가져오기 실패', err);
4187
return null;
4288
}
4389
};

src/domains/community/main/Community.tsx

Lines changed: 94 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,121 @@
11
'use client';
22

3-
import { useEffect, useState } from 'react';
3+
import { useEffect, useMemo, useState } from 'react';
44
import CommunityFilter from './CommunityFilter';
55
import CommunityTab from './CommunityTab';
66
import PostCard from './PostCard';
77
import WriteBtn from './WriteBtn';
88
import { Post } from '../types/post';
9-
import { fetchPost } from '../api/fetchPost';
9+
import { fetchPostByTab } from '../api/fetchPost';
10+
import { useSearchParams } from 'next/navigation';
1011

1112
function Community() {
12-
const [posts, setPosts] = useState<Post[]>([]);
13+
const [posts, setPosts] = useState<Post[] | null>([]);
1314
const [isLoading, setIsLoading] = useState(true);
15+
const [lastLoadedId, setLastLoadedId] = useState<number | null>(null);
16+
17+
const searchParams = useSearchParams();
18+
19+
const category = useMemo(() => searchParams.get('category') || 'all', [searchParams]);
20+
const filter = useMemo(
21+
() => (searchParams.get('postSortStatus') as 'LATEST' | 'POPULAR' | 'COMMENTS') || 'LATEST',
22+
[searchParams]
23+
);
24+
25+
const [isEnd, setIsEnd] = useState(false);
1426

1527
useEffect(() => {
16-
const fetchData = async () => {
17-
setIsLoading(true);
18-
const data = await fetchPost();
19-
if (!data) return;
20-
setPosts(data);
28+
setPosts([]);
29+
setIsEnd(false);
30+
setLastLoadedId(null);
31+
loadInitialPosts();
32+
}, [category, filter]);
33+
34+
const loadInitialPosts = async () => {
35+
const category = searchParams.get('category') || 'all';
36+
const filter =
37+
(searchParams.get('postSortStatus') as 'LATEST' | 'POPULAR' | 'COMMENTS') || 'LATEST';
38+
39+
setIsLoading(true);
40+
setIsEnd(false);
41+
42+
const lastLikeCount =
43+
posts && posts.length > 0 ? Math.min(...posts.map((post) => post.likeCount)) : null;
44+
45+
const lastCommentCount =
46+
posts && posts.length > 0 ? Math.min(...posts.map((post) => post.commentCount)) : null;
47+
48+
try {
49+
const newPosts = await fetchPostByTab({
50+
category,
51+
filter,
52+
lastLikeCount,
53+
lastCommentCount,
54+
});
55+
56+
if (!newPosts || newPosts.length === 0) {
57+
setIsEnd(true);
58+
setPosts([]);
59+
} else {
60+
setPosts(newPosts);
61+
}
62+
} finally {
63+
setIsLoading(false);
64+
}
65+
};
66+
67+
const loadMorePosts = async (lastPostId: number) => {
68+
if (isEnd || isLoading) return;
69+
if (!posts || posts.length === 0) return;
70+
console.log('시작', lastPostId);
71+
72+
const lastPost = posts[posts.length - 1];
73+
if (lastPostId === lastPost.postId) return;
74+
setLastLoadedId(lastPost.postId);
75+
76+
setIsLoading(true);
77+
try {
78+
const category = searchParams.get('category') || 'all';
79+
const filter =
80+
(searchParams.get('postSortStatus') as 'LATEST' | 'POPULAR' | 'COMMENTS') || 'LATEST';
81+
82+
const newPosts = await fetchPostByTab({
83+
category,
84+
filter,
85+
lastId: lastPostId,
86+
});
87+
88+
if (!newPosts || newPosts?.length === 0) {
89+
setIsEnd(true);
90+
console.log('끝');
91+
} else {
92+
setPosts((prev) => [...(prev ?? []), ...(newPosts ?? [])]);
93+
}
94+
} finally {
2195
setIsLoading(false);
22-
};
23-
fetchData();
24-
}, [setPosts]);
96+
}
97+
};
2598

2699
return (
27100
<>
28101
<section
29102
aria-label="탭과 글쓰기"
30103
className="flex justify-between item-center sm:flex-row flex-col gap-4 mt-1"
31104
>
32-
<CommunityTab setPosts={setPosts} />
105+
<CommunityTab setPosts={setPosts} setIsLoading={setIsLoading} setIsEnd={setIsEnd} />
33106
<WriteBtn />
34107
</section>
35108

36109
<section aria-label="게시물 목록">
37-
<CommunityFilter posts={posts} />
38-
<PostCard posts={posts} isLoading={isLoading} />
110+
<CommunityFilter posts={posts} setPosts={setPosts} />
111+
<PostCard
112+
posts={posts}
113+
setPost={setPosts}
114+
isLoading={isLoading}
115+
setIsLoading={setIsLoading}
116+
isEnd={isEnd}
117+
onLoadMore={loadMorePosts}
118+
/>
39119
</section>
40120
</>
41121
);

src/domains/community/main/CommunityFilter.tsx

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,59 @@
22

33
import { Post } from '../types/post';
44
import SelectBox from '@/shared/components/select-box/SelectBox';
5+
import { Dispatch, SetStateAction, useEffect } from 'react';
6+
import { fetchPostByTab } from '../api/fetchPost';
7+
import { useRouter, useSearchParams } from 'next/navigation';
58

69
type Props = {
7-
posts: Post[];
10+
posts: Post[] | null;
11+
setPosts: Dispatch<SetStateAction<Post[] | null>>;
812
};
913

10-
function CommunityFilter({ posts }: Props) {
14+
const sortMap = {
15+
최신순: 'LATEST',
16+
인기순: 'POPULAR',
17+
댓글순: 'COMMENTS',
18+
} as const;
19+
20+
function CommunityFilter({ posts, setPosts }: Props) {
21+
const searchParams = useSearchParams();
22+
const query = searchParams.get('category');
23+
const router = useRouter();
24+
25+
useEffect(() => {
26+
console.log(query);
27+
}, [query]);
28+
29+
const handleChange = async (selectTitle: string) => {
30+
if (!query) return;
31+
32+
console.log(selectTitle);
33+
34+
const data = await fetchPostByTab({
35+
category: query,
36+
filter: sortMap[selectTitle as keyof typeof sortMap],
37+
});
38+
if (!data) return;
39+
setPosts(data);
40+
};
41+
1142
return (
1243
<section
1344
className="w-full flex justify-between items-center border-b-1 border-gray-light pb-1.5"
1445
aria-label="커뮤니티 정렬 필터"
1546
>
16-
<p aria-live="polite">{posts.length}</p>
17-
<SelectBox option={['최신순', '인기순', '댓글순']} title={'최신순'} />
47+
<p aria-live="polite">{posts && posts.length}</p>
48+
<SelectBox
49+
option={['최신순', '인기순', '댓글순']}
50+
title={'최신순'}
51+
onChange={(value) => {
52+
const sortValue = sortMap[value as keyof typeof sortMap];
53+
54+
handleChange(value);
55+
router.push(`?category=${query || '전체'}&postSortStatus=${sortValue}`);
56+
}}
57+
/>
1858
</section>
1959
);
2060
}

src/domains/community/main/CommunityTab.tsx

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,60 @@
11
'use client';
22

33
import tw from '@/shared/utills/tw';
4-
import { useEffect, useState } from 'react';
5-
import { fetchPost, fetchPostByTab } from '../api/fetchPost';
64
import { Post } from '../types/post';
5+
import { useState } from 'react';
6+
import { fetchPost, fetchPostByTab } from '../api/fetchPost';
7+
import { useRouter, useSearchParams } from 'next/navigation';
78

89
type Props = {
9-
setPosts: (value: Post[]) => void;
10+
setPosts: (value: Post[] | null) => void;
11+
setIsLoading: (value: boolean) => void;
12+
setIsEnd: (value: boolean) => void;
1013
};
1114

12-
const tabItem = [
13-
{ title: '전체' },
14-
{ title: '레시피' },
15-
{ title: '팁' },
16-
{ title: '질문' },
17-
{ title: '자유' },
15+
export const tabItem = [
16+
{ key: 'all', label: '전체' },
17+
{ key: 'recipe', label: '레시피' },
18+
{ key: 'tip', label: '팁' },
19+
{ key: 'question', label: '질문' },
20+
{ key: 'chat', label: '자유' },
1821
];
1922

20-
function CommunityTab({ setPosts }: Props) {
21-
const [selectedIdx, setSelectedIdx] = useState(0);
22-
23-
useEffect(() => {
24-
const fetchData = async () => {
25-
const selectedTab = tabItem[selectedIdx].title;
23+
function CommunityTab({ setPosts, setIsLoading, setIsEnd }: Props) {
24+
const searchParams = useSearchParams();
25+
const router = useRouter();
2626

27-
let data;
28-
if (selectedTab === '전체') data = await fetchPost();
29-
else data = await fetchPostByTab(selectedTab);
27+
const currentSort = searchParams.get('postSortStatus') || 'LATEST';
3028

31-
if (!data) return;
32-
setPosts(data);
33-
};
34-
fetchData();
35-
}, [selectedIdx, setPosts]);
29+
const [selectedCategory, setSelectedCategory] = useState(() => {
30+
const param = searchParams.get('category') || 'all';
31+
const exists = tabItem.some(({ key }) => key === param);
32+
return exists ? param : 'all';
33+
});
3634

3735
return (
3836
<section className="relative sm:w-[70%] w-full" aria-label="커뮤니티 탭">
3937
<div className="w-full overflow-x-scroll no-scrollbar scroll-smooth">
4038
<div className="flex gap-3 w-max" aria-label="커뮤니티 카테고리">
41-
{tabItem.map(({ title }, idx) => (
39+
{tabItem.map(({ key, label }) => (
4240
<button
43-
key={title + idx}
41+
key={key}
4442
role="tab"
45-
aria-selected={selectedIdx === idx}
46-
tabIndex={selectedIdx === idx ? 0 : -1}
47-
onClick={() => setSelectedIdx(idx)}
43+
aria-selected={selectedCategory === key}
44+
tabIndex={selectedCategory === key ? 0 : -1}
45+
onClick={() => {
46+
setSelectedCategory(key);
47+
const params = new URLSearchParams();
48+
params.set('category', key);
49+
params.set('postSortStatus', currentSort); // ✅ 현재 필터 상태 유지
50+
router.push(`?${params.toString()}`);
51+
}}
4852
className={tw(
4953
`border-1 py-1 px-3 rounded-2xl transition-colors ease-in min-w-18`,
50-
selectedIdx === idx ? 'bg-secondary text-primary' : 'hover:bg-secondary/20'
54+
selectedCategory === key ? 'bg-secondary text-primary' : 'hover:bg-secondary/20'
5155
)}
5256
>
53-
{title}
57+
{label}
5458
</button>
5559
))}
5660
</div>

0 commit comments

Comments
 (0)