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
39 changes: 18 additions & 21 deletions src/app/detail/[postingId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,29 @@
"use client";

import { useParams } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import {
QueryClient,
dehydrate,
HydrationBoundary,
} from "@tanstack/react-query";
import { getPostDetail } from "@/entities/post/api/getPostDetail";
import { PostDetailSection } from "@/widgets/postDetail/ui/PostDetailSection";
import { SellerPostsSection } from "@/widgets/postDetail/ui/SellerPostsSection";
import PostDetailPageClient from "@/widgets/postDetail/ui/DetailPage.client";

export default function PostDetailPage() {
const { postingId } = useParams<{ postingId: string }>();
export default async function Page({
params,
}: {
params: { postingId: string };
}) {
const { postingId } = params;
const id = Number(postingId);
const queryClient = new QueryClient();

const {
data: post,
isLoading,
isError,
} = useQuery({
await queryClient.prefetchQuery({
queryKey: ["postDetail", id],
queryFn: () => getPostDetail(id),
staleTime: Infinity,
});

if (isLoading) return <p className="text-center text-white">로딩 중...</p>;
if (isError || !post)
return <p className="text-center text-white">게시글을 찾을 수 없습니다.</p>;

return (
<main className="text-white">
<PostDetailSection post={post} />
<SellerPostsSection sellerId={post.sellerId} />
</main>
<HydrationBoundary state={dehydrate(queryClient)}>
<PostDetailPageClient postingId={postingId} />
</HydrationBoundary>
);
}
31 changes: 12 additions & 19 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import Header from "@/widgets/header/ui/Header";
import { ModalContainer } from "@/shared/ui/ModalContainer/ModalContainer";
import ChatContainer from "@/features/chat/ui/ChatContainer";
import { ReactQueryProvider } from "@/shared/lib/provider";
import { ClientLayout } from "@/shared/lib/ClientLayout";

const myFont = localFont({
src: "../shared/fonts/PretendardVariable.woff2",
Expand All @@ -24,29 +23,23 @@ export default function RootLayout({
}) {
return (
<html lang="kr" className={myFont.className}>
<body className="mx-auto max-w-[1200px] bg-[#1c1c22] pt-[70px] md:pt-[80px] xl:pt-[100px]">
<body className="mx-auto max-w-[1200px] bg-[#1c1c22] pt-[70px] md:pt-20 xl:pt-[100px]">
<ReactQueryProvider>
<ClientLayout>
<Suspense fallback={null}>
<Header />
</Suspense>
<Suspense fallback={null}>
<Header />
</Suspense>

<Suspense
fallback={<div className="p-4 text-gray-400">Loading...</div>}
>
{children}
</Suspense>
{children}

<div id="modal-root" />
<div id="modal-root" />

<Suspense fallback={null}>
<ChatContainer />
</Suspense>
<Suspense fallback={null}>
<ChatContainer />
</Suspense>

<Suspense fallback={null}>
<ModalContainer />
</Suspense>
</ClientLayout>
<Suspense fallback={null}>
<ModalContainer />
</Suspense>
</ReactQueryProvider>
</body>
</html>
Expand Down
131 changes: 54 additions & 77 deletions src/app/my/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
"use client";
import { useState, useEffect } from "react";

import { useState, useCallback } from "react";
import { useQuery } from "@tanstack/react-query";
import Profile, { ProfileProps } from "@/entities/user/ui/card/Profile";
import ProfileSkeleton from "@/entities/user/ui/card/ProfileSkeleton";
import PostCard from "@/entities/post/ui/card/PostCard";
import { PostCreateButton } from "@/features/createPost/ui/PostCreateButton/PostCreateButton";
import Tab from "@/widgets/mypage/ui/Tab.tsx/Tab";
import { apiFetch } from "@/shared/api/fetcher";
import { useModalStore } from "@/shared/model/modal.store";
import { usePostCreateModal } from "@/features/createPost/lib/usePostCreateModal";
import { getMyPosts, getMyProfile } from "@/entities/user/api/mypage";
import type { Post } from "@/entities/post/model/types/post";
import { PostStatus } from "@/entities/post/model/types/post";

const options = [
Expand All @@ -16,6 +20,12 @@ const options = [
{ label: "관심 상품", value: "favorite" },
];

const emptyMessageMap: Record<string, string> = {
selling: "등록되어 있는 상품이 없습니다.",
sold: "판매 완료된 상품이 없습니다.",
purchased: "구매 완료된 상품이 없습니다.",
favorite: "즐겨찾기한 상품이 없습니다.",
};
interface PostListItem {
postingId: number;
sellerId: number;
Expand All @@ -31,62 +41,35 @@ interface PostListItem {
status: PostStatus;
}

const Mypage = () => {
const [selected, setSelected] = useState(options[0].value);
const [userProfile, setUserProfile] = useState<ProfileProps | null>(null);
const [posts, setPosts] = useState<PostListItem[]>([]);
const [loading, setLoading] = useState(false);
export default function Page() {
const [selectedTab, setSelectedTab] = useState(options[0].value);
const { openModal, closeModal } = useModalStore();

const {
data: userProfile,
isLoading: profileLoading,
refetch: refetchUserProfile,
} = useQuery<ProfileProps>({
queryKey: ["userProfile"],
queryFn: getMyProfile,
});

const {
data: postsData,
isLoading: postsLoading,
refetch: refetchPosts,
} = useQuery<{ data: Post[] }>({
queryKey: ["myPosts", selectedTab],
queryFn: () => getMyPosts(selectedTab),
});

const { openPostCreateModal } = usePostCreateModal({
onSuccess: async () => {
await fetchPosts(selected);
await refetchPosts();
},
});

async function fetchUserProfile() {
try {
const data = await apiFetch<ProfileProps>("/api/users/me", {
method: "GET",
});
setUserProfile(data);
} catch (error) {
console.error("유저 정보 로딩 실패: ", error);
}
}

async function fetchPosts(status: string) {
//TODO: sold | purchase | favorite 기능 구현되면 지우기
if (status != "selling") {
setPosts([]);
}

try {
setLoading(true);
const res = await apiFetch<{ data: PostListItem[] }>(
`/api/postings/my?status=${status}`,
{
method: "GET",
},
);
setPosts(res.data);
} catch (error) {
console.error("현재 유저 관련 게시글 불러오기 실패 : ", error);
setPosts([]);
} finally {
setLoading(false);
}
}

useEffect(() => {
fetchUserProfile();
}, []);

useEffect(() => {
fetchPosts(selected);
}, [selected]);

// 프로필 수정 모달 열기 함수
const handleEditProfile = () => {
const handleEditProfile = useCallback(() => {
if (!userProfile) return;

openModal("editProfile", {
Expand All @@ -103,7 +86,7 @@ const Mypage = () => {
},
});
}, 100);
await fetchUserProfile();
await refetchUserProfile();
},
onError: () => {
closeModal();
Expand All @@ -116,35 +99,31 @@ const Mypage = () => {
});
},
});
};
}, [userProfile, openModal, closeModal, refetchUserProfile]);

//카테고리별 빈 상태 문구
const emptyMessageMap: Record<string, string> = {
selling: "등록되어 있는 상품이 없습니다.",
sold: "판매 완료된 상품이 없습니다.",
purchased: "구매 완료된 상품이 없습니다.",
favorite: "즐겨찾기한 상품이 없습니다.",
};
const posts = postsData?.data || [];

return (
<main className="m-auto flex max-w-[335px] flex-col items-center justify-center gap-[60px] py-[30px] md:max-w-[510px] md:py-[40px] xl:max-w-[1340px] xl:flex-row xl:items-start xl:justify-start xl:gap-[80px] xl:py-[60px]">
{userProfile && <Profile {...userProfile} onEdit={handleEditProfile} />}
<main className="m-auto flex max-w-[335px] flex-col items-center justify-center gap-[60px] py-[30px] md:max-w-[510px] md:py-10 xl:max-w-[1340px] xl:flex-row xl:items-start xl:justify-start xl:gap-20 xl:py-[60px]">
{profileLoading || !userProfile ? (
<ProfileSkeleton />
) : (
<Profile {...userProfile} onEdit={handleEditProfile} />
)}

<section className="flex flex-col gap-[30px]">
<Tab options={options} selected={selected} onChange={setSelected} />
{loading ? (
<p className="mt-10 text-center text-gray-400">로딩 중...</p>
) : selected !== "selling" ? (
<p className="mt-10 text-center text-gray-400">
{emptyMessageMap[selected]} <br />
<span className="text-sm text-gray-500">(TODO: API 구현 예정)</span>
</p>
) : posts.length == 0 ? (
<Tab
options={options}
selected={selectedTab}
onChange={setSelectedTab}
/>

{postsLoading ? null : posts.length === 0 ? (
<p className="mt-10 text-center text-gray-400">
{emptyMessageMap[selected]}
{emptyMessageMap[selectedTab]}
</p>
) : (
<ul className="grid grid-cols-2 gap-[15px] xl:grid-cols-3 xl:gap-[20px]">
<ul className="grid grid-cols-2 gap-[15px] xl:grid-cols-3 xl:gap-5">
{posts.map((post) => (
<li key={post.postingId}>
<PostCard {...post} />
Expand All @@ -156,6 +135,4 @@ const Mypage = () => {
</section>
</main>
);
};

export default Mypage;
}
2 changes: 1 addition & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
import { getPosts } from "@/entities/post/api/getPosts";
import HomePageClient from "@/widgets/main/ui/Client/HomePage.client";

export default async function HomePage({
export default async function Page({
searchParams,
}: {
searchParams: Promise<Record<string, string>>;
Expand Down
31 changes: 26 additions & 5 deletions src/entities/post/ui/card/PostCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
"use client";

import Image from "next/image";
import Link from "next/link";
import { useQueryClient } from "@tanstack/react-query";
import { getPostDetail } from "@/entities/post/api/getPostDetail";
import { useDebouncedCallback } from "@/shared/lib/useDebouncedCallback";
import PostStatusBadge from "../badge/PostStatusBadge";
import { PostStatus } from "../../model/types/post";

Expand Down Expand Up @@ -27,8 +32,26 @@ const PostCard = ({
thumbnail,
status,
}: PostCardProps) => {
const queryClient = useQueryClient();

const { debouncedCallback: handleMouseEnter, cancel: handleMouseLeave } =
useDebouncedCallback(() => {
const cached = queryClient.getQueryData(["postDetail", postingId]);
if (cached) return;

queryClient.prefetchQuery({
queryKey: ["postDetail", postingId],
queryFn: () => getPostDetail(postingId),
staleTime: 1000 * 60 * 3,
});
}, 200);

return (
<Link href={`/detail/${postingId}`}>
<Link
href={`/detail/${postingId}`}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<article className="w-full rounded-lg border border-[#353542] bg-[#252530] p-2.5 md:pb-5 xl:pb-6">
<div className="flex flex-col gap-2.5 md:gap-5 xl:gap-6">
<div className="relative h-24 w-full overflow-hidden rounded-md md:h-40 xl:h-56">
Expand All @@ -53,14 +76,12 @@ const PostCard = ({
</h1>
<PostStatusBadge status={status} className="block w-fit" />
<p className="text-xs text-[#9FA6B2] md:text-sm">
{price?.toLocaleString()} 원
{price.toLocaleString()} 원
</p>

<div className="flex w-full flex-col gap-1.5 text-xs leading-none font-light text-[#6E6E82] md:flex-row md:justify-between md:text-sm xl:text-base">
<div className="flex gap-1.5">
<span aria-label="조회 수" title="조회 수">
조회
</span>
<span>조회</span>
<span>{viewCount}</span>
</div>

Expand Down
Loading