Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
144 changes: 54 additions & 90 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";

const options = [
{ label: "판매중 상품", value: "selling" },
Expand All @@ -15,76 +19,42 @@ const options = [
{ label: "관심 상품", value: "favorite" },
];

interface PostListItem {
postingId: number;
sellerId: number;
title: string;
price: number;
content: string;
category: string;
createdAt: string;
likeCount: number;
chatCount: number;
viewCount: number;
thumbnail: string;
}
const emptyMessageMap: Record<string, string> = {
selling: "등록되어 있는 상품이 없습니다.",
sold: "판매 완료된 상품이 없습니다.",
purchased: "구매 완료된 상품이 없습니다.",
favorite: "즐겨찾기한 상품이 없습니다.",
};

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 @@ -101,7 +71,7 @@ const Mypage = () => {
},
});
}, 100);
await fetchUserProfile();
await refetchUserProfile();
},
onError: () => {
closeModal();
Expand All @@ -114,35 +84,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 @@ -154,6 +120,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";

interface PostCardProps {
postingId: number;
Expand All @@ -23,8 +28,26 @@ const PostCard = ({
viewCount,
thumbnail,
}: 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 @@ -49,14 +72,12 @@ const PostCard = ({
</h1>

<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