Skip to content

Commit 4fbf265

Browse files
authored
Merge pull request #134 from FE9-2/feat/albatalk-list
Feat: ์•Œ๋ฐ” ํ† ํฌ ๋ชฉ๋ก, ๋งˆ์ดํŽ˜์ด์ง€ ๋‚ด๊ฐ€ ์“ด ๊ธ€ ๋“ฑ
2 parents 7d138b3 + 1ca4b3e commit 4fbf265

File tree

18 files changed

+496
-127
lines changed

18 files changed

+496
-127
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React, { Suspense } from "react";
2+
3+
export default function AlbaTalkLayout({ children }: { children: React.ReactNode }) {
4+
return (
5+
<div className="mx-auto max-w-screen-xl px-4 py-8">
6+
<Suspense
7+
fallback={
8+
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
9+
<div>๋กœ๋”ฉ ์ค‘...</div>
10+
</div>
11+
}
12+
>
13+
{children}
14+
</Suspense>
15+
</div>
16+
);
17+
}
Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,137 @@
11
"use client";
22

3+
import React, { useEffect } from "react";
4+
import { useInView } from "react-intersection-observer";
5+
import { usePosts } from "@/hooks/queries/post/usePosts";
6+
import { usePathname, useSearchParams } from "next/navigation";
7+
import SortSection from "@/app/components/layout/posts/SortSection";
8+
import SearchSection from "@/app/components/layout/posts/SearchSection";
9+
import { useUser } from "@/hooks/queries/user/me/useUser";
10+
import Link from "next/link";
11+
import { RiEdit2Fill } from "react-icons/ri";
12+
import FloatingBtn from "@/app/components/button/default/FloatingBtn";
13+
import CardBoard from "@/app/components/card/board/CardBoard";
14+
15+
const POSTS_PER_PAGE = 10;
16+
317
export default function AlbaTalk() {
4-
return <div>AlbaTalk</div>;
18+
const pathname = usePathname();
19+
const searchParams = useSearchParams();
20+
const { user } = useUser();
21+
22+
// URL ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ์—์„œ ํ‚ค์›Œ๋“œ์™€ ์ •๋ ฌ ๊ธฐ์ค€ ๊ฐ€์ ธ์˜ค๊ธฐ
23+
const keyword = searchParams.get("keyword");
24+
const orderBy = searchParams.get("orderBy");
25+
26+
// ๋ฌดํ•œ ์Šคํฌ๋กค์„ ์œ„ํ•œ Intersection Observer ์„ค์ •
27+
const { ref, inView } = useInView({
28+
threshold: 0.1,
29+
triggerOnce: false,
30+
rootMargin: "100px",
31+
});
32+
33+
// ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ์กฐํšŒ
34+
const { data, isLoading, error, hasNextPage, fetchNextPage, isFetchingNextPage } = usePosts({
35+
limit: POSTS_PER_PAGE,
36+
keyword: keyword || undefined,
37+
orderBy: orderBy || undefined,
38+
});
39+
40+
// ์Šคํฌ๋กค์ด ํ•˜๋‹จ์— ๋„๋‹ฌํ•˜๋ฉด ๋‹ค์Œ ํŽ˜์ด์ง€ ๋กœ๋“œ
41+
useEffect(() => {
42+
if (inView && hasNextPage && !isFetchingNextPage) {
43+
fetchNextPage();
44+
}
45+
}, [inView, hasNextPage, fetchNextPage, isFetchingNextPage]);
46+
47+
// ์—๋Ÿฌ ์ƒํƒœ ์ฒ˜๋ฆฌ
48+
if (error) {
49+
return (
50+
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
51+
<p className="text-red-500">๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.</p>
52+
</div>
53+
);
54+
}
55+
56+
// ๋กœ๋”ฉ ์ƒํƒœ ์ฒ˜๋ฆฌ
57+
if (isLoading) {
58+
return (
59+
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
60+
<div>๋กœ๋”ฉ ์ค‘...</div>
61+
</div>
62+
);
63+
}
64+
65+
return (
66+
<div className="flex min-h-screen flex-col items-center">
67+
{/* ๊ฒ€์ƒ‰ ์„น์…˜๊ณผ ์ •๋ ฌ ์˜ต์…˜์„ ๊ณ ์ • ์œ„์น˜๋กœ ์„ค์ • */}
68+
<div className="fixed left-0 right-0 top-16 z-40 bg-white shadow-sm">
69+
{/* ๊ฒ€์ƒ‰ ์„น์…˜ */}
70+
<div className="w-full border-b border-grayscale-100">
71+
<div className="mx-auto flex max-w-screen-2xl flex-col gap-4 px-4 py-4 md:px-6 lg:px-8">
72+
<div className="flex items-center justify-between">
73+
<SearchSection />
74+
</div>
75+
</div>
76+
</div>
77+
78+
{/* ์ •๋ ฌ ์˜ต์…˜ ์„น์…˜ */}
79+
<div className="w-full border-b border-grayscale-100">
80+
<div className="mx-auto flex max-w-screen-2xl items-center justify-end gap-2 px-4 py-4 md:px-6 lg:px-8">
81+
<div className="flex items-center gap-4">
82+
<SortSection pathname={pathname} searchParams={searchParams} />
83+
</div>
84+
</div>
85+
</div>
86+
</div>
87+
88+
{/* ๋ฉ”์ธ ์ฝ˜ํ…์ธ  ์˜์—ญ */}
89+
<div className="w-full pt-[132px]">
90+
{/* ๊ธ€์“ฐ๊ธฐ ๋ฒ„ํŠผ - ๊ณ ์ • ์œ„์น˜ */}
91+
{user && (
92+
<Link href="/albatalk/addtalk" className="fixed bottom-[50%] right-4 z-[9999] translate-y-1/2">
93+
<FloatingBtn icon={<RiEdit2Fill className="size-6" />} variant="orange" />
94+
</Link>
95+
)}
96+
97+
{!data?.pages?.[0]?.data?.length ? (
98+
<div className="flex h-[calc(100vh-200px)] flex-col items-center justify-center">
99+
<p className="text-grayscale-500">๋“ฑ๋ก๋œ ๊ฒŒ์‹œ๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.</p>
100+
</div>
101+
) : (
102+
<div className="mx-auto mt-4 w-full max-w-screen-xl px-3">
103+
<div className="flex flex-col gap-4">
104+
{data?.pages.map((page) => (
105+
<React.Fragment key={page.nextCursor}>
106+
{page.data.map((post) => (
107+
<div key={post.id} className="rounded-lg border border-grayscale-100 p-4 hover:bg-grayscale-50">
108+
<Link href={`/albatalk/${post.id}`}>
109+
<CardBoard
110+
title={post.title}
111+
content={post.content}
112+
nickname={post.writer.nickname}
113+
updatedAt={post.updatedAt}
114+
commentCount={post.commentCount}
115+
likeCount={post.likeCount}
116+
/>
117+
</Link>
118+
</div>
119+
))}
120+
</React.Fragment>
121+
))}
122+
</div>
123+
124+
{/* ๋ฌดํ•œ ์Šคํฌ๋กค ํŠธ๋ฆฌ๊ฑฐ ์˜์—ญ */}
125+
<div ref={ref} className="h-4 w-full">
126+
{isFetchingNextPage && (
127+
<div className="flex justify-center py-4">
128+
<div className="h-6 w-6 animate-spin rounded-full border-2 border-primary-orange-300 border-t-transparent" />
129+
</div>
130+
)}
131+
</div>
132+
</div>
133+
)}
134+
</div>
135+
</div>
136+
);
5137
}

โ€Žsrc/app/(pages)/mypage/components/sections/CommentsSection.tsxโ€Ž

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ import React from "react";
44
import { useState } from "react";
55
import { useMyComments } from "@/hooks/queries/user/me/useMyComments";
66
import Pagination from "@/app/components/pagination/Pagination";
7-
import type { MyCommentType } from "@/types/response/user";
7+
import Comment from "@/app/components/card/board/Comment";
8+
import Link from "next/link";
89
import LoadingSpinner from "@/app/components/loading-spinner/LoadingSpinner";
10+
import { useUser } from "@/hooks/queries/user/me/useUser";
911

1012
// ํ•œ ํŽ˜์ด์ง€๋‹น ๋Œ“๊ธ€ ์ˆ˜
1113
const COMMENTS_PER_PAGE = 10;
1214

1315
export default function CommentsSection() {
16+
const { user } = useUser();
17+
1418
// ํ˜„์žฌ ํŽ˜์ด์ง€ ์ƒํƒœ ๊ด€๋ฆฌ
1519
const [currentPage, setCurrentPage] = useState(1);
1620

@@ -57,18 +61,22 @@ export default function CommentsSection() {
5761
}
5862

5963
return (
60-
<div className="space-y-4">
64+
<div className="mx-auto w-full max-w-screen-xl space-y-4 px-3">
6165
{/* ๋Œ“๊ธ€ ๋ชฉ๋ก ๋ Œ๋”๋ง */}
62-
{data.data.map((comment: MyCommentType) => (
63-
<div key={comment.id} className="rounded-lg border p-4">
64-
<h3 className="text-grayscale-900 mb-2 font-medium">{comment.post.title}</h3>
65-
<p className="text-grayscale-600">{comment.content}</p>
66-
<div className="mt-2 text-sm text-grayscale-500">
67-
<time>{new Date(comment.createdAt).toLocaleDateString()}</time>
68-
{comment.updatedAt !== comment.createdAt && <span className="ml-2 text-grayscale-400">(์ˆ˜์ •๋จ)</span>}
66+
<div className="flex flex-col gap-4">
67+
{data.data.map((comment) => (
68+
<div key={comment.id} className="rounded-lg border border-grayscale-100 p-4 hover:bg-grayscale-50">
69+
<Link href={`/albatalk/${comment.post.id}`}>
70+
<Comment
71+
nickname={user?.nickname || ""}
72+
updatedAt={comment.updatedAt}
73+
content={comment.content}
74+
onKebabClick={() => console.log("์ผ€๋ฐฅ ๋ฉ”๋‰ด ํด๋ฆญ", comment.id)}
75+
/>
76+
</Link>
6977
</div>
70-
</div>
71-
))}
78+
))}
79+
</div>
7280

7381
{/* ํŽ˜์ด์ง€๋„ค์ด์…˜ */}
7482
{totalPages > 1 && (

โ€Žsrc/app/(pages)/mypage/components/sections/PostsSection.tsxโ€Ž

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,54 +4,31 @@ import React, { useEffect } from "react";
44
import { useInView } from "react-intersection-observer";
55
import { useMyPosts } from "@/hooks/queries/user/me/useMyPosts";
66
import { useMySortStore } from "@/store/mySortStore";
7-
import type { PostListType } from "@/types/response/post";
87
import { useProfileStringValue } from "@/hooks/queries/user/me/useProfileStringValue";
8+
import CardBoard from "@/app/components/card/board/CardBoard";
9+
import Link from "next/link";
910
import LoadingSpinner from "@/app/components/loading-spinner/LoadingSpinner";
1011

1112
// ํ•œ ํŽ˜์ด์ง€๋‹น ๊ฒŒ์‹œ๊ธ€ ์ˆ˜
1213
const POSTS_PER_PAGE = 10;
1314

14-
// ์ปดํฌ๋„ŒํŠธ
15+
// ์ƒํƒœ ๋ฉ”์‹œ์ง€ ์ปดํฌ๋„ŒํŠธ
1516
const StatusMessage = ({ message, className = "text-grayscale-500" }: { message: string; className?: string }) => (
1617
<div className="flex h-[calc(100vh-200px)] items-center justify-center">
1718
<p className={className}>{message}</p>
1819
</div>
1920
);
2021

21-
const PostCard = ({ post }: { post: PostListType }) => (
22-
<div className="rounded-lg border p-4 transition-all hover:border-primary-orange-200">
23-
<h3 className="font-bold">{post.title}</h3>
24-
<p className="text-grayscale-600">{post.content}</p>
25-
<div className="mt-2 text-sm text-grayscale-500">
26-
<span>๋Œ“๊ธ€ {post.commentCount}</span>
27-
<span className="mx-2">โ€ข</span>
28-
<span>์ข‹์•„์š” {post.likeCount}</span>
29-
</div>
30-
</div>
31-
);
32-
33-
const PostList = ({ pages }: { pages: any[] }) => (
34-
<>
35-
{pages.map((page, index) => (
36-
<React.Fragment key={index}>
37-
{page.data.map((post: PostListType) => (
38-
<PostCard key={post.id} post={post} />
39-
))}
40-
</React.Fragment>
41-
))}
42-
</>
43-
);
44-
4522
export default function PostsSection() {
4623
// ์ •๋ ฌ ์ƒํƒœ ๊ด€๋ฆฌ
4724
const { orderBy } = useMySortStore();
4825
useProfileStringValue();
4926

5027
// ๋ฌดํ•œ ์Šคํฌ๋กค์„ ์œ„ํ•œ Intersection Observer ์„ค์ •
5128
const { ref, inView } = useInView({
52-
threshold: 0.1, // 10% ์ •๋„ ๋ณด์ด๋ฉด ํŠธ๋ฆฌ๊ฑฐ
53-
triggerOnce: true, // ํ•œ ๋ฒˆ๋งŒ ํŠธ๋ฆฌ๊ฑฐ (๋ถˆํ•„์š”ํ•œ API ํ˜ธ์ถœ ๋ฐฉ์ง€)
54-
rootMargin: "100px", // ํ•˜๋‹จ 100px ์ „์— ๋ฏธ๋ฆฌ ๋กœ๋“œ
29+
threshold: 0.1,
30+
triggerOnce: false,
31+
rootMargin: "100px",
5532
});
5633

5734
// ๋‚ด๊ฐ€ ์ž‘์„ฑํ•œ ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ์กฐํšŒ
@@ -74,8 +51,29 @@ export default function PostsSection() {
7451
if (!data?.pages[0]?.data?.length) return <StatusMessage message="์ž‘์„ฑํ•œ ๊ฒŒ์‹œ๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค." />;
7552

7653
return (
77-
<div className="mt-10 space-y-4">
78-
<PostList pages={data.pages} />
54+
<div className="mx-auto w-full max-w-screen-xl space-y-4 px-3">
55+
{/* ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ๋ Œ๋”๋ง */}
56+
<div className="flex flex-col gap-4">
57+
{data.pages.map((page) => (
58+
<React.Fragment key={page.nextCursor}>
59+
{page.data.map((post) => (
60+
<div key={post.id} className="rounded-lg border border-grayscale-100 p-4 hover:bg-grayscale-50">
61+
<Link href={`/albatalk/${post.id}`}>
62+
<CardBoard
63+
title={post.title}
64+
content={post.content}
65+
nickname={post.writer.nickname}
66+
updatedAt={post.updatedAt}
67+
commentCount={post.commentCount}
68+
likeCount={post.likeCount}
69+
onKebabClick={() => console.log("์ผ€๋ฐฅ ๋ฉ”๋‰ด ํด๋ฆญ", post.id)}
70+
/>
71+
</Link>
72+
</div>
73+
))}
74+
</React.Fragment>
75+
))}
76+
</div>
7977

8078
{/* ๋ฌดํ•œ ์Šคํฌ๋กค ํŠธ๋ฆฌ๊ฑฐ ์˜์—ญ */}
8179
<div ref={ref} className="h-4 w-full">

0 commit comments

Comments
ย (0)