diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b9a771f0..34caac0e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,6 +5,8 @@ import "@/shared/styles/globals.css"; 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", @@ -23,25 +25,29 @@ export default function RootLayout({ return ( - -
- + + + +
+ - Loading...} - > - {children} - + Loading...} + > + {children} + - - {isLoading && ( + {isFetchingNextPage && (

불러오는 중...

)} diff --git a/src/shared/commands/SearchCommands.ts b/src/shared/commands/SearchCommands.ts new file mode 100644 index 00000000..3b495037 --- /dev/null +++ b/src/shared/commands/SearchCommands.ts @@ -0,0 +1,11 @@ +import { useSearchStore } from "@/shared/model/search.store"; + +export const SearchCommands = { + changeCategory: (category: string) => + useSearchStore.getState().setCategory(category), + + changeSort: (sort: string) => useSearchStore.getState().setSort(sort), + + changeKeyword: (keyword: string) => + useSearchStore.getState().setKeyword(keyword), +}; diff --git a/src/shared/lib/ClientLayout.tsx b/src/shared/lib/ClientLayout.tsx new file mode 100644 index 00000000..6fcea7d2 --- /dev/null +++ b/src/shared/lib/ClientLayout.tsx @@ -0,0 +1,8 @@ +"use client"; + +import { useResetKeywordOnPathChange } from "@/shared/lib/useResetKeyword"; + +export function ClientLayout({ children }: { children: React.ReactNode }) { + useResetKeywordOnPathChange(); + return <>{children}; +} diff --git a/src/shared/lib/provider.tsx b/src/shared/lib/provider.tsx new file mode 100644 index 00000000..e7b862c7 --- /dev/null +++ b/src/shared/lib/provider.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { useState } from "react"; + +export function ReactQueryProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60, + gcTime: 1000 * 60 * 5, + refetchOnWindowFocus: false, + }, + }, + }), + ); + + return ( + + {children} + {process.env.NODE_ENV === "development" && ( + + )} + + ); +} diff --git a/src/shared/lib/useResetKeyword.ts b/src/shared/lib/useResetKeyword.ts new file mode 100644 index 00000000..b92c3af7 --- /dev/null +++ b/src/shared/lib/useResetKeyword.ts @@ -0,0 +1,18 @@ +"use client"; + +import { useEffect, useRef } from "react"; +import { usePathname } from "next/navigation"; +import { SearchCommands } from "@/shared/commands/SearchCommands"; + +export function useResetKeywordOnPathChange() { + const pathname = usePathname(); + const prevPathRef = useRef(""); + + useEffect(() => { + if (prevPathRef.current && prevPathRef.current !== pathname) { + SearchCommands.changeKeyword(""); + } + + prevPathRef.current = pathname; + }, [pathname]); +} diff --git a/src/shared/lib/useSearchMediator.ts b/src/shared/lib/useSearchMediator.ts new file mode 100644 index 00000000..e5426d21 --- /dev/null +++ b/src/shared/lib/useSearchMediator.ts @@ -0,0 +1,40 @@ +import { useEffect } from "react"; +import { useSearchStore } from "@/shared/model/search.store"; + +export function useSearchMediator({ + initialCategory, + initialKeyword, + initialSort, +}: { + initialCategory: string; + initialKeyword: string; + initialSort: string; +}) { + const { category, keyword, sort, setCategory, setKeyword, setSort } = + useSearchStore(); + + // SSR 초기값 → Zustand 반영 + useEffect(() => { + setCategory(initialCategory); + setKeyword(initialKeyword); + setSort(initialSort); + }, [ + initialCategory, + initialKeyword, + initialSort, + setCategory, + setKeyword, + setSort, + ]); + + // Zustand 상태 → URL 동기화 + useEffect(() => { + const params = new URLSearchParams(); + + if (category !== "전체") params.set("category", category); + if (sort) params.set("sort", sort); + if (keyword) params.set("keyword", keyword); + + window.history.replaceState(null, "", `/?${params.toString()}`); + }, [category, sort, keyword]); +} diff --git a/src/shared/model/search.store.ts b/src/shared/model/search.store.ts new file mode 100644 index 00000000..da2bbb5b --- /dev/null +++ b/src/shared/model/search.store.ts @@ -0,0 +1,19 @@ +import { create } from "zustand"; + +interface SearchState { + category: string; + sort: string; + keyword: string; + setCategory: (c: string) => void; + setSort: (s: string) => void; + setKeyword: (k: string) => void; +} + +export const useSearchStore = create((set) => ({ + category: "전체", + sort: "latest", + keyword: "", + setCategory: (category) => set({ category }), + setSort: (sort) => set({ sort }), + setKeyword: (keyword) => set({ keyword }), +})); diff --git a/src/widgets/header/ui/SearchForm.tsx b/src/widgets/header/ui/SearchForm.tsx index 82e152c0..f8c3cc66 100644 --- a/src/widgets/header/ui/SearchForm.tsx +++ b/src/widgets/header/ui/SearchForm.tsx @@ -1,8 +1,10 @@ "use client"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useState } from "react"; +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"; const SearchForm = ({ className, @@ -20,18 +22,30 @@ const SearchForm = ({ onSubmitted?: () => void; }) => { const router = useRouter(); - const sp = useSearchParams(); - const [value, setValue] = useState(sp.get(name) ?? ""); + const pathname = usePathname(); + const { category, keyword } = useSearchStore(); + const [value, setValue] = useState(""); + + useEffect(() => { + setValue(""); + SearchCommands.changeKeyword(""); + }, [category]); const onSubmit = (e: React.FormEvent) => { e.preventDefault(); - const params = new URLSearchParams(sp.toString()); - const q = value.trim(); - if (q) params.set(name, q); - else params.delete(name); + if (q === keyword) return; + + if (pathname === "/") { + SearchCommands.changeKeyword(q); + } else { + const params = new URLSearchParams({ + category, + keyword: q, + }); + router.push(`/?${params.toString()}`); + } - router.replace("/?" + params.toString(), { scroll: true }); onSubmitted?.(); }; @@ -63,7 +77,6 @@ const SearchForm = ({ value={value} onChange={(e) => setValue(e.target.value)} /> - diff --git a/src/widgets/main/ui/Client/HomePage.client.tsx b/src/widgets/main/ui/Client/HomePage.client.tsx new file mode 100644 index 00000000..a47b434c --- /dev/null +++ b/src/widgets/main/ui/Client/HomePage.client.tsx @@ -0,0 +1,39 @@ +"use client"; + +import SideMenuWrapper from "@/widgets/main/ui/SideMenu/SideMenuWrapper"; +import PostList from "@/entities/post/ui/list/PostList"; +import { useSearchStore } from "@/shared/model/search.store"; +import { useSearchMediator } from "@/shared/lib/useSearchMediator"; + +interface Props { + initialCategory: string; + initialSort: string; + initialKeyword: string; +} + +export default function HomePageClient({ + initialCategory, + initialSort, + initialKeyword, +}: Props) { + useSearchMediator({ initialCategory, initialSort, initialKeyword }); + + const { category, keyword, sort, setSort } = useSearchStore(); + + return ( +
+ + +
+ +
+
+ ); +} diff --git a/src/widgets/main/ui/SideMenu/SideMenuWrapper.tsx b/src/widgets/main/ui/SideMenu/SideMenuWrapper.tsx index 49a6f39e..aa2c36e3 100644 --- a/src/widgets/main/ui/SideMenu/SideMenuWrapper.tsx +++ b/src/widgets/main/ui/SideMenu/SideMenuWrapper.tsx @@ -1,24 +1,18 @@ "use client"; -import { useRouter, useSearchParams } from "next/navigation"; 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 router = useRouter(); - const searchParams = useSearchParams(); - const handleSelect = (category: string) => { - const params = new URLSearchParams(searchParams.toString()); - if (category === "전체") params.delete("category"); - else params.set("category", category); - params.delete("keyword"); - params.delete("sort"); - router.push(`/?${params.toString()}`); + if (category === selectedCategory) return; + SearchCommands.changeCategory(category); + SearchCommands.changeKeyword(""); }; return (