@@ -37,7 +54,7 @@ const HeaderDesktop = ({
key={href}
className={`flex items-center justify-center px-3 first:pr-3 last:pl-3 ${
hasDivider
- ? "relative before:absolute before:left-0 before:h-4 before:w-[1px] before:bg-white after:absolute after:right-0 after:h-4 after:w-[1px] after:bg-white"
+ ? "relative before:absolute before:left-0 before:h-4 before:w-px before:bg-white after:absolute after:right-0 after:h-4 after:w-px after:bg-white"
: ""
} `}
>
@@ -47,15 +64,19 @@ const HeaderDesktop = ({
onClick={() => onOpenChat()}
className="flex cursor-pointer items-center justify-center"
>
-

+
{label}
) : (
-

+
{label}
)}
diff --git a/src/widgets/header/ui/MobileSideMenu.tsx b/src/widgets/header/ui/MobileSideMenu.tsx
index 45c0b632..1947532f 100644
--- a/src/widgets/header/ui/MobileSideMenu.tsx
+++ b/src/widgets/header/ui/MobileSideMenu.tsx
@@ -6,6 +6,9 @@ import Link from "next/link";
import { useAuthStore } from "@/features/auth/model/auth.store";
import cn from "@/shared/lib/cn";
import { apiFetch } from "@/shared/api/fetcher";
+import { useQueryClient } from "@tanstack/react-query";
+import { getMyProfile } from "@/entities/user/api/mypage";
+import { useDebouncedCallback } from "@/shared/lib/useDebouncedCallback";
export default function MobileSideMenu({
onClose,
@@ -22,6 +25,18 @@ export default function MobileSideMenu({
const { isLogined, logout } = useAuthStore();
const [isVisible, setIsVisible] = useState(false);
+ const queryClient = useQueryClient();
+
+ const handleProfilePrefetch = () => {
+ queryClient.prefetchQuery({
+ queryKey: ["userProfile"],
+ queryFn: getMyProfile,
+ });
+ };
+
+ const { debouncedCallback: debouncedPrefetch, cancel: cancelPrefetch } =
+ useDebouncedCallback(handleProfilePrefetch, 500);
+
useEffect(() => {
const timer = requestAnimationFrame(() => setIsVisible(true));
return () => cancelAnimationFrame(timer);
@@ -99,6 +114,8 @@ export default function MobileSideMenu({
href="/my"
className="block px-4 py-3 text-white hover:bg-white/10"
onClick={handleClose}
+ onMouseEnter={debouncedPrefetch}
+ onMouseLeave={cancelPrefetch}
>
마이페이지
diff --git a/src/widgets/header/ui/SearchForm.tsx b/src/widgets/header/ui/SearchForm.tsx
index f8c3cc66..4b0a0d18 100644
--- a/src/widgets/header/ui/SearchForm.tsx
+++ b/src/widgets/header/ui/SearchForm.tsx
@@ -2,9 +2,7 @@
import { useState, useEffect } from "react";
import cn from "@/shared/lib/cn";
-import { useRouter, usePathname } from "next/navigation";
-import { useSearchStore } from "@/shared/model/search.store";
-import { SearchCommands } from "@/shared/commands/SearchCommands";
+import { useRouter, useSearchParams } from "next/navigation";
const SearchForm = ({
className,
@@ -22,29 +20,27 @@ const SearchForm = ({
onSubmitted?: () => void;
}) => {
const router = useRouter();
- const pathname = usePathname();
- const { category, keyword } = useSearchStore();
+ const searchParams = useSearchParams();
+ const currentKeyword = searchParams.get("keyword") ?? "";
const [value, setValue] = useState("");
useEffect(() => {
setValue("");
- SearchCommands.changeKeyword("");
- }, [category]);
+ }, [searchParams.get("category")]);
const onSubmit = (e: React.FormEvent
) => {
e.preventDefault();
const q = value.trim();
- if (q === keyword) return;
+ if (q === currentKeyword) return;
- if (pathname === "/") {
- SearchCommands.changeKeyword(q);
+ const params = new URLSearchParams(searchParams.toString());
+
+ if (q) {
+ params.set("keyword", q);
} else {
- const params = new URLSearchParams({
- category,
- keyword: q,
- });
- router.push(`/?${params.toString()}`);
+ params.delete("keyword");
}
+ router.push(`/?${params.toString()}`);
onSubmitted?.();
};
diff --git a/src/widgets/main/ui/Client/HomePage.client.tsx b/src/widgets/main/ui/Client/HomePage.client.tsx
index a47b434c..7e81cb07 100644
--- a/src/widgets/main/ui/Client/HomePage.client.tsx
+++ b/src/widgets/main/ui/Client/HomePage.client.tsx
@@ -1,10 +1,8 @@
"use client";
-import SideMenuWrapper from "@/widgets/main/ui/SideMenu/SideMenuWrapper";
+import SideMenu from "@/widgets/main/ui/SideMenu/SideMenu";
import PostList from "@/entities/post/ui/list/PostList";
-import { useSearchStore } from "@/shared/model/search.store";
-import { useSearchMediator } from "@/shared/lib/useSearchMediator";
-
+import { CATEGORY_LIST } from "../../model/constants";
interface Props {
initialCategory: string;
initialSort: string;
@@ -16,22 +14,20 @@ export default function HomePageClient({
initialSort,
initialKeyword,
}: Props) {
- useSearchMediator({ initialCategory, initialSort, initialKeyword });
-
- const { category, keyword, sort, setSort } = useSearchStore();
-
return (
diff --git a/src/widgets/main/ui/SideMenu/SideMenu.tsx b/src/widgets/main/ui/SideMenu/SideMenu.tsx
index 2b6826cf..b1c89f29 100644
--- a/src/widgets/main/ui/SideMenu/SideMenu.tsx
+++ b/src/widgets/main/ui/SideMenu/SideMenu.tsx
@@ -1,45 +1,92 @@
+"use client";
+
+import { useQueryClient } from "@tanstack/react-query";
+import { getPosts } from "@/entities/post/api/getPosts";
import cn from "@/shared/lib/cn";
+import { useRouter } from "next/navigation";
+import { useDebouncedCallback } from "@/shared/lib/useDebouncedCallback";
+import { useSearchParams } from "next/navigation";
interface SideMenuProps {
categories: string[];
selectedCategory: string;
- onSelect: (category: string) => void;
}
-const SideMenu = ({
- categories,
- selectedCategory,
- onSelect,
-}: SideMenuProps) => {
+const SideMenu = ({ categories, selectedCategory }: SideMenuProps) => {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const queryClient = useQueryClient();
+
+ const prefetchData = async (category: string) => {
+ const existing = queryClient.getQueryData([
+ "posts",
+ category,
+ "latest",
+ "",
+ ]);
+ if (existing) return;
+
+ queryClient.prefetchInfiniteQuery({
+ queryKey: ["posts", category, "latest", ""],
+ queryFn: ({ pageParam = 1 }) =>
+ getPosts({
+ category,
+ sort: "latest",
+ page: pageParam,
+ keyword: "",
+ }),
+ initialPageParam: 1,
+ staleTime: 1000 * 60 * 5,
+ });
+ };
+
+ const { debouncedCallback: handleHover, cancel: cancelHover } =
+ useDebouncedCallback(prefetchData, 200);
+
+ const handleSelect = async (category: string) => {
+ if (category === selectedCategory) return;
+
+ const params = new URLSearchParams(searchParams.toString());
+ params.set("category", category);
+ params.delete("sort");
+ params.delete("keyword");
+
+ router.push(`/?${params.toString()}`);
+
+ cancelHover();
+ };
+
return (
카테고리
-
+
+
{categories?.map((category) => (
-
@@ -49,4 +96,5 @@ const SideMenu = ({
);
};
+
export default SideMenu;
diff --git a/src/widgets/main/ui/SideMenu/SideMenuWrapper.tsx b/src/widgets/main/ui/SideMenu/SideMenuWrapper.tsx
deleted file mode 100644
index aa2c36e3..00000000
--- a/src/widgets/main/ui/SideMenu/SideMenuWrapper.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-"use client";
-
-import SideMenu from "./SideMenu";
-import { CATEGORY_LIST } from "@/widgets/main/model/constants";
-import { SearchCommands } from "@/shared/commands/SearchCommands";
-
-interface Props {
- selectedCategory: string;
-}
-
-export default function SideMenuWrapper({ selectedCategory }: Props) {
- const handleSelect = (category: string) => {
- if (category === selectedCategory) return;
- SearchCommands.changeCategory(category);
- SearchCommands.changeKeyword("");
- };
-
- return (
-
- );
-}
diff --git a/src/widgets/postDetail/ui/DetailPage.client.tsx b/src/widgets/postDetail/ui/DetailPage.client.tsx
new file mode 100644
index 00000000..90a23f61
--- /dev/null
+++ b/src/widgets/postDetail/ui/DetailPage.client.tsx
@@ -0,0 +1,38 @@
+"use client";
+
+import { useQuery } 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 { PostDetailSkeleton } from "@/widgets/postDetail/ui/DetailSkeleton";
+
+export default function PostDetailPageClient({
+ postingId,
+}: {
+ postingId: string;
+}) {
+ const id = Number(postingId);
+
+ const { data: post, isLoading } = useQuery({
+ queryKey: ["postDetail", id],
+ queryFn: () => getPostDetail(id),
+ staleTime: Infinity,
+ });
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (!post) {
+ return (
+ 게시글 정보를 찾을 수 없습니다.
+ );
+ }
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/widgets/postDetail/ui/DetailSkeleton.tsx b/src/widgets/postDetail/ui/DetailSkeleton.tsx
new file mode 100644
index 00000000..181d68df
--- /dev/null
+++ b/src/widgets/postDetail/ui/DetailSkeleton.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+export function PostDetailSkeleton() {
+ return (
+
+ );
+}