Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,127 changes: 552 additions & 575 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.5",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
Expand All @@ -26,7 +28,8 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.7.0",
"tailwind-merge": "^3.3.1"
"tailwind-merge": "^3.3.1",
"@tanstack/react-query": "^5.83.0"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
Expand Down
32 changes: 31 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
import { Outlet } from "react-router-dom";
import { Outlet, useLocation } from "react-router-dom";
import GlobalLayout from "./components/layout/GlobalLayout";

function App() {
const location = useLocation();

const layoutGroups = {
full: ["/", "/board", "/explore", "/my"], // header + bottom nav
headerOnly: ["/developer", "/community", "/onboarding"], // only header
none: ["/login", "/search"], // no header and bottom nav
};

const currentPath = location.pathname;

// no header and bottom nav
if (layoutGroups.none.some((path) => currentPath.startsWith(path))) {
return (
<GlobalLayout showHeader={false} showBottomNavigation={false}>
<Outlet />
</GlobalLayout>
);
}

// only header
if (layoutGroups.headerOnly.some((path) => currentPath.startsWith(path))) {
return (
<GlobalLayout showBottomNavigation={false}>
<Outlet />
</GlobalLayout>
);
}

// basic(header + bommon nav)
return (
<GlobalLayout>
<Outlet />
Expand Down
51 changes: 51 additions & 0 deletions src/components/ui/avatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"

import { cn } from "@/lib/utils"

function Avatar({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
className={cn(
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
)
}

function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}

function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted flex size-full items-center justify-center rounded-full",
className
)}
{...props}
/>
)
}

export { Avatar, AvatarImage, AvatarFallback }
28 changes: 28 additions & 0 deletions src/components/ui/separator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client"

import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"

import { cn } from "@/lib/utils"

function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}

export { Separator }
64 changes: 64 additions & 0 deletions src/hooks/usePost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import type { Post, Comment } from '@/types/community';

const fetchPost = async (postId: string): Promise<Post> => {
const res = await fetch(`/api/posts/${postId}`);
if (!res.ok) throw new Error('게시글을 불러오는데 실패했습니다.');
return res.json();
};

const fetchComments = async (postId: string): Promise<Comment[]> => {
const res = await fetch(`/api/posts/${postId}/comments`);
if (!res.ok) throw new Error('댓글을 불러오는데 실패했습니다.');
return res.json();
};

export function usePost(postId: string) {
return useQuery({
queryKey: ['post', postId],
queryFn: async () => {
const [postData, commentsData] = await Promise.all([
fetchPost(postId),
fetchComments(postId),
]);
return { ...postData, comments: commentsData };
},
enabled: !!postId,
});
}

const togglePostLike = async (postId: string) => {
const res = await fetch(`/api/posts/${postId}/like`, { method: 'PUT' });
if (!res.ok) throw new Error('좋아요 클릭에 실패했습니다.');
return res.json();
};

export function useTogglePostLike() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: togglePostLike,
onSuccess: (_data, postId) => {
queryClient.invalidateQueries({ queryKey: ['post', postId] });
},
});
}

const addComment = async ({ postId, content }: { postId: string, content: string }) => {
const res = await fetch(`/api/posts/${postId}/comments`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content }),
});
if (!res.ok) throw new Error('댓글 작성에 실패했습니다.');
return res.json();
};

export function useAddComment() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: addComment,
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['post', variables.postId] });
},
});
}
18 changes: 14 additions & 4 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { BoardPage } from "./pages/home/index.tsx";
import { MyPage } from "./pages/my/index.tsx";
import { DeveloperPage } from "./pages/developer/index.tsx";
import { ExplorePage } from "./pages/explore/index.tsx";
import { OnboardingPage } from "./pages/onboarding/OnboardingPage.tsx";
import { PostDetailPage } from "./pages/community/PostDetailPage.tsx";

async function enableMocking() {
if (import.meta.env.DEV) {
Expand Down Expand Up @@ -41,12 +43,20 @@ const router = createBrowserRouter([
path: "developer/:id",
element: <DeveloperPage />,
},
{
path: "community",
element: <PostDetailPage />,
},
{
path: "login",
element: <LoginPage />,
},
{
path: "onboarding",
element: <OnboardingPage />,
},
],
},
{
path: "/login",
element: <LoginPage />,
},
]);

enableMocking().then(() => {
Expand Down
50 changes: 24 additions & 26 deletions src/pages/auth/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
import GlobalLayout from "@/components/layout/GlobalLayout";
import { Button } from "@/components/ui/button";

export function LoginPage() {
return (
<GlobalLayout
showHeader={false}
showFooter={false}
variant="black"
>
<div className="flex flex-col justify-between min-h-full p-8">
<section className="mt-20">
<p className="text-brand-primary text-lg mb-8">우리 동네 개발자 커뮤니티</p>
<h1 className="text-brand-primary text-5xl font-bold">CO-MEET</h1>
</section>
<div className="flex flex-col justify-between min-h-full p-8">
<section className="mt-20">
<p className="text-brand-primary text-lg mb-8">
우리 동네 개발자 커뮤니티
</p>
<h1 className="text-brand-primary text-5xl font-bold">CO-MEET</h1>
</section>

<section className="mx-auto">
<img src="/logo.svg" alt="CO-MEET 로고" className="w-52 h-auto" />
</section>
<section className="mx-auto">
<img src="/logo.svg" alt="CO-MEET 로고" className="w-52 h-auto" />
</section>

<section className="mb-4">
<Button
className="
<section className="mb-4">
<Button
className="
w-full
h-12
bg-brand-primary
Expand All @@ -29,13 +25,15 @@ export function LoginPage() {
text-lg
font-bold
gap-5
"
>
<img src="/github-black.svg" alt="Github 로고" className="w-8 h-auto" />
<span>Github 로그인</span>
</Button>
</section>
</div>
</GlobalLayout>
">
<img
src="/github-black.svg"
alt="Github 로고"
className="w-8 h-auto"
/>
<span>Github 로그인</span>
</Button>
</section>
</div>
);
}
}
53 changes: 53 additions & 0 deletions src/pages/community/PostDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useParams } from "react-router-dom";
import { PostContent } from "./__components/PostContent";
import { CommentSection } from "./__components/CommentSection";
import { CommentForm } from "./__components/CommentForm";
import GlobalLayout from "@/components/layout/GlobalLayout";
import { usePost, useTogglePostLike, useAddComment } from "@/hooks/usePost";

export function PostDetailPage() {
const { postId } = useParams<{ postId: string }>();

if (!postId) {
return <div className="p-4 text-white">유효하지 않은 게시글 ID입니다.</div>;
}

const { data: post, isLoading, isError, error } = usePost(postId);
const { mutate: toggleLike, isPending: isLikePending } = useTogglePostLike();
const { mutate: addNewComment, isPending: isAddingComment } = useAddComment();

if (isLoading) {
return <div className="p-4 text-white">로딩 중...</div>;
}
if (isError) {
return <div className="p-4 text-white">에러 발생: {error.message}</div>;
}
if (!post) {
return <div className="p-4 text-white">게시글이 없습니다.</div>;
}

const handleToggleCommentLike = (commentId: number) =>
console.log("like comment:", commentId);

return (
<GlobalLayout>
<div className="flex flex-col h-full">
<div className="flex-grow overflow-y-auto px-4">
<PostContent
post={post}
onLikeClick={() => toggleLike(postId)}
isLikePending={isLikePending}
/>
<CommentSection
comments={post.comments}
onToggleCommentLike={handleToggleCommentLike}
/>
</div>
<CommentForm
onSubmit={(content: string) => addNewComment({ postId, content })}
isPending={isAddingComment}
/>
</div>
</GlobalLayout>
);
}
Loading