Skip to content

Commit b22ddcc

Browse files
authored
Merge pull request #38 from boostcampwm-2024/feature/tag-component
✨ feat: 게시글 상세 모달/ 페이지 구현
2 parents 7c793a6 + 3c9594e commit b22ddcc

28 files changed

+1978
-125
lines changed

client/package-lock.json

Lines changed: 1378 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"@radix-ui/react-tabs": "^1.1.1",
2727
"@radix-ui/react-toast": "^1.2.2",
2828
"@radix-ui/react-toggle": "^1.1.0",
29-
"@radix-ui/react-tooltip": "^1.1.3",
29+
"@radix-ui/react-tooltip": "^1.1.8",
30+
"@tailwindcss/typography": "^0.5.16",
3031
"@tanstack/react-query": "^5.59.20",
3132
"@tanstack/react-query-devtools": "^5.59.20",
3233
"avvvatars-react": "^0.4.2",
@@ -39,6 +40,7 @@
3940
"react": "^18.3.1",
4041
"react-dom": "^18.3.1",
4142
"react-helmet": "^6.1.0",
43+
"react-markdown": "^9.0.3",
4244
"react-router-dom": "^6.27.0",
4345
"recharts": "^2.13.3",
4446
"socket.io-client": "^4.8.1",

client/src/App.tsx

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { lazy, Suspense, useEffect } from "react";
2-
import { Routes, Route } from "react-router-dom";
2+
import { Routes, Route, useLocation } from "react-router-dom";
33

4-
import LoadingPage from "@/pages/Loading.tsx";
5-
import NotFound from "@/pages/NotFound";
4+
import PostDetail from "@/components/common/Card/PostDetail";
5+
6+
import Loading from "@/pages/Loading.tsx";
7+
import PostDetailPage from "@/pages/PostDetailPage";
68

79
import { useMediaQuery } from "@/hooks/common/useMediaQuery";
810

@@ -21,51 +23,67 @@ const queryClient = new QueryClient();
2123
export default function App() {
2224
const setIsMobile = useMediaStore((state) => state.setIsMobile);
2325
const isMobile = useMediaQuery("(max-width: 767px)");
26+
const location = useLocation();
27+
const state =
28+
location.state && location.state.backgroundLocation
29+
? { backgroundLocation: location.state.backgroundLocation }
30+
: null;
2431

2532
useEffect(() => {
2633
console.log(denamuAscii);
2734
}, []);
2835

36+
useEffect(() => {
37+
if (location.state?.backgroundLocation) {
38+
window.history.replaceState({}, document.title, window.location.pathname + window.location.search);
39+
}
40+
}, [location]);
41+
2942
useEffect(() => {
3043
setIsMobile(isMobile);
3144
}, [isMobile]);
3245

3346
return (
3447
<QueryClientProvider client={queryClient}>
35-
<Routes>
48+
<Routes location={state?.backgroundLocation || location}>
3649
<Route
3750
path="/"
3851
element={
39-
<Suspense fallback={<LoadingPage />}>
52+
<Suspense fallback={<Loading />}>
4053
<Home />
4154
</Suspense>
4255
}
4356
/>
4457
<Route
4558
path="/admin"
4659
element={
47-
<Suspense fallback={<LoadingPage />}>
60+
<Suspense fallback={<Loading />}>
4861
<Admin />
4962
</Suspense>
5063
}
5164
/>
5265
<Route
5366
path="/about"
5467
element={
55-
<Suspense fallback={<LoadingPage />}>
68+
<Suspense fallback={<Loading />}>
5669
<AboutService />
5770
</Suspense>
5871
}
5972
/>
6073
<Route
61-
path="*"
74+
path="/:id"
6275
element={
63-
<Suspense fallback={<LoadingPage />}>
64-
<NotFound />
76+
<Suspense fallback={<Loading />}>
77+
<PostDetailPage />
6578
</Suspense>
6679
}
6780
/>
6881
</Routes>
82+
{state?.backgroundLocation && (
83+
<Routes>
84+
<Route path="/:id" element={<PostDetail />} />
85+
</Routes>
86+
)}
6987
<ReactQueryDevtools />
7088
</QueryClientProvider>
7189
);

client/src/api/mocks/data/posts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export const generateMockPost = (id: number): Post => ({
1111
author: `작성자 ${(id % 5) + 1}`,
1212
thumbnail: `https://picsum.photos/640/480?random=${id}`,
1313
blogPlatform: "etc",
14+
summary: "",
15+
tag: [],
1416
});
1517

1618
export const TOTAL_POSTS = Array.from({ length: 100 }, (_, i) => generateMockPost(i + 1));

client/src/api/mocks/handlers/posts.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { POST_MODAL_DATA } from "@/constants/dummyData";
2+
13
import { TOTAL_POSTS, PAGE_SIZE_POSTS } from "@/api/mocks/data/posts";
24
import { mock } from "@/api/mocks/mockSetup";
35

@@ -23,3 +25,11 @@ export const setupPostHandlers = () => {
2325
];
2426
});
2527
};
28+
29+
export const mockDetailPost = (feedId: number) => {
30+
POST_MODAL_DATA.data.id = feedId;
31+
console.log(feedId);
32+
mock.onGet(`/feed/${feedId}`).reply(() => {
33+
return [200, POST_MODAL_DATA];
34+
});
35+
};

client/src/api/services/posts.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { BLOG } from "@/constants/endpoints";
22

33
import { axiosInstance } from "@/api/instance";
4-
import { InfiniteScrollResponse, LatestPostsApiResponse, Post } from "@/types/post";
4+
import { InfiniteScrollResponse, LatestPostsApiResponse, Post, PostDetailType } from "@/types/post";
55

66
export const posts = {
77
latest: async (params: { limit: number; lastId: number }): Promise<InfiniteScrollResponse<Post>> => {
@@ -17,4 +17,8 @@ export const posts = {
1717
lastId: response.data.data.lastId,
1818
};
1919
},
20+
detail: async (postId: number): Promise<PostDetailType> => {
21+
const response = await axiosInstance.get<PostDetailType>(`${BLOG.POST}/detail/${postId}`);
22+
return response.data;
23+
},
2024
};

client/src/components/chat/ChatButton.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function OpenChat() {
2727
return (
2828
<button
2929
onClick={toggleSidebar}
30-
className="fixed text-white bottom-[14.5rem] right-7 bg-[#3498DB] hover:bg-[#2980B9] !rounded-full p-3"
30+
className="fixed text-white bottom-[14.5rem] right-7 bg-[#3498DB] hover:bg-[#2980B9] !rounded-full p-3 side-btn"
3131
>
3232
<MessageCircleMore size={25} />
3333
</button>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
2+
3+
type AvatarType = {
4+
author: string;
5+
className: string;
6+
blogPlatform: string;
7+
};
8+
9+
export default function PostAvatar({ author, className, blogPlatform }: AvatarType) {
10+
const isValidPlatform = (platform: string): boolean => {
11+
const validPlatforms = ["tistory", "velog", "medium"];
12+
return validPlatforms.includes(platform);
13+
};
14+
const authorInitial = author?.charAt(0)?.toUpperCase() || "?";
15+
16+
return (
17+
<Avatar className="h-8 w-8 ring-2 ring-background cursor-pointer">
18+
{isValidPlatform(blogPlatform) ? (
19+
<img src={`https://denamu.site/files/${blogPlatform}-icon.svg`} alt={author} className={className} />
20+
) : (
21+
<AvatarFallback className="text-xs bg-slate-200">{authorInitial}</AvatarFallback>
22+
)}
23+
</Avatar>
24+
);
25+
}

client/src/components/common/Card/PostCard.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Card, MCard } from "@/components/ui/card";
1+
import { useNavigate, useLocation } from "react-router-dom";
22

3-
import { usePostCardActions } from "@/hooks/common/usePostCardActions";
3+
import { Card, MCard } from "@/components/ui/card";
44

55
import { PostCardContent } from "./PostCardContent";
66
import { PostCardImage } from "./PostCardImage";
@@ -19,13 +19,17 @@ export const PostCard = ({ post, className }: PostCardProps) => {
1919
};
2020

2121
const DesktopCard = ({ post, className }: PostCardProps) => {
22-
const { handlePostClick } = usePostCardActions(post);
22+
const navigate = useNavigate();
23+
const location = useLocation();
2324

25+
const openPostDetail = (modalUrl: string) => {
26+
navigate(modalUrl, { state: { backgroundLocation: location } });
27+
};
2428
return (
2529
<Card
26-
onClick={handlePostClick}
30+
onClick={() => openPostDetail(`/${post.id}`)}
2731
className={cn(
28-
"h-[240px] group shadow-sm hover:shadow-xl transition-all duration-300 hover:-translate-y-0.5 border-none rounded-xl cursor-pointer",
32+
"h-[270px] group shadow-md hover:shadow-xl transition-all duration-300 hover:-translate-y-0.5 border-none rounded-xl cursor-pointer",
2933
className
3034
)}
3135
>
@@ -35,11 +39,11 @@ const DesktopCard = ({ post, className }: PostCardProps) => {
3539
);
3640
};
3741
const MobileCard = ({ post, className }: PostCardProps) => {
38-
const { handlePostClick } = usePostCardActions(post);
42+
const navigate = useNavigate();
3943

4044
return (
4145
<MCard
42-
onClick={handlePostClick}
46+
onClick={() => navigate(`/${post.id}`)}
4347
className={cn("aspect-[5/3] transition-all duration-300 flex flex-col gap-2", className)}
4448
>
4549
<PostCardImage thumbnail={post.thumbnail} alt={post.title} />

client/src/components/common/Card/PostCardContent.tsx

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,61 @@
1-
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
1+
import PostAvatar from "@/components/common/Card/PostAvatar";
2+
import PostTag from "@/components/common/Card/PostTag";
23
import { CardContent } from "@/components/ui/card";
34

45
import { formatDate } from "@/utils/date";
56

67
import { useMediaStore } from "@/store/useMediaStore";
78
import { Post } from "@/types/post";
89

9-
const isValidPlatform = (platform: string): boolean => {
10-
const validPlatforms = ["tistory", "velog", "medium"];
11-
return validPlatforms.includes(platform);
12-
};
1310
interface PostCardContentProps {
1411
post: Post;
1512
}
1613

1714
export const PostCardContent = ({ post }: PostCardContentProps) => {
1815
const isMobile = useMediaStore((state) => state.isMobile);
19-
2016
return isMobile ? <MobileCardContent post={post} /> : <DesktopCardContent post={post} />;
2117
};
2218

2319
const MobileCardContent = ({ post }: PostCardContentProps) => {
24-
const authorInitial = post.author?.charAt(0)?.toUpperCase() || "?";
25-
2620
return (
2721
<CardContent className="p-0">
2822
<div className="flex items-center ml-4 mb-3 gap-3">
29-
<Avatar className="h-8 w-8 ring-2 ring-background cursor-pointer">
30-
{isValidPlatform(post.blogPlatform) ? (
31-
<img
32-
src={`https://denamu.site/files/${post.blogPlatform}-icon.svg`}
33-
alt={post.author}
34-
className="w-full h-full"
35-
/>
36-
) : (
37-
<AvatarFallback className="text-xs bg-slate-200">{authorInitial}</AvatarFallback>
38-
)}
39-
</Avatar>
23+
<PostAvatar
24+
blogPlatform={post.blogPlatform}
25+
className="h-8 w-8 ring-2 ring-background cursor-pointer"
26+
author={post.author}
27+
/>
4028
<p className="font-bold text-sm">{post.author}</p>
4129
</div>
4230
<div className="px-4 pb-4">
4331
<p className="h-[48px] font-bold text-md group-hover:text-primary transition-colors line-clamp-2">
4432
{post.title}
4533
</p>
34+
{post.tag && <PostTag tags={post.tag} />}
35+
4636
<p className="text-[12px] text-gray-400 pt-2">{formatDate(post.createdAt)}</p>
4737
</div>
4838
</CardContent>
4939
);
5040
};
5141

5242
const DesktopCardContent = ({ post }: PostCardContentProps) => {
53-
const authorInitial = post.author?.charAt(0)?.toUpperCase() || "?";
54-
5543
return (
5644
<CardContent className="p-0">
57-
<div className="relative -mt-6 ml-4 mb-3">
58-
<Avatar className="h-8 w-8 ring-2 ring-background cursor-pointer">
59-
{isValidPlatform(post.blogPlatform) ? (
60-
<img
61-
src={`https://denamu.site/files/${post.blogPlatform}-icon.svg`}
62-
alt={post.author}
63-
className="w-full h-full"
64-
/>
65-
) : (
66-
<AvatarFallback className="text-xs bg-slate-200">{authorInitial}</AvatarFallback>
67-
)}
68-
</Avatar>
45+
<div className="relative -mt-4 ml-4 mb-3">
46+
<PostAvatar
47+
blogPlatform={post.blogPlatform}
48+
className="h-8 w-8 ring-2 ring-background cursor-pointer"
49+
author={post.author}
50+
/>
6951
</div>
7052
<div className="px-4 pb-4">
7153
<p className="font-bold text-xs text-gray-400 pb-1 line-clamp-1">{post.author}</p>
7254
<p className="h-[40px] font-bold text-sm group-hover:text-primary transition-colors line-clamp-2">
7355
{post.title}
7456
</p>
7557
<p className="text-[10px] text-gray-400 pt-2">{formatDate(post.createdAt)}</p>
58+
{post.tag && <PostTag tags={post.tag} />}
7659
</div>
7760
</CardContent>
7861
);

0 commit comments

Comments
 (0)