diff --git a/app/layout.tsx b/app/layout.tsx index 5e7ad48..b2d1164 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,9 +8,14 @@ import { HomeLandingRender } from "@/src/shared/ui/globalRender/globalRender"; import { FrameBottomNav } from "@/src/shared/ui/bottomNavigation/frameBottomNavigation"; import { ClientOnly } from "@/src/shared/ui/clientOnly/clientOnly"; + + export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: "pinhouse", + description: "pinhosue-fe", + icons: { + icon: "/BrandLogo.svg", + }, }; export default function RootLayout({ diff --git a/public/BrandLogo.svg b/public/BrandLogo.svg new file mode 100644 index 0000000..463ff8d --- /dev/null +++ b/public/BrandLogo.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/entities/listings/hooks/useListingDetailHooks.ts b/src/entities/listings/hooks/useListingDetailHooks.ts index f86d439..a1a35b3 100644 --- a/src/entities/listings/hooks/useListingDetailHooks.ts +++ b/src/entities/listings/hooks/useListingDetailHooks.ts @@ -206,7 +206,7 @@ export const useListingRouteDetail = ({ export const useListingFilterDetail = () => { return useQuery, Error, T>({ - queryKey: ["pinpoint"], + queryKey: ["pinpointSettings"], staleTime: 1000 * 60 * 5, placeholderData: previousData => previousData, queryFn: () => PostBasicRequest, {}, IResponse>(endPoint["pinpoint"], "get"), diff --git a/src/features/home/hooks/hooks.tsx b/src/features/home/hooks/hooks.tsx index 5a2d3a3..c6e3515 100644 --- a/src/features/home/hooks/hooks.tsx +++ b/src/features/home/hooks/hooks.tsx @@ -3,6 +3,8 @@ import { useRouter } from "next/navigation"; import { useHomeSheetStore } from "@/src/features/home/model/homeStore"; import { useOAuthStore } from "@/src/features/login/model"; import { PinPointPlace } from "@/src/entities/listings/model/type"; +import { useMemo } from "react"; +import { homeSheetParseObject } from "@/src/features/listings/model"; export const useHomeGlobalSearch = (globalData?: GlobalListType): GlobalSearchSection[] => { if (!globalData) return []; @@ -36,7 +38,7 @@ export const useHomeGlobalSearch = (globalData?: GlobalListType): GlobalSearchSe ]; }; -export const usePinhouseRouter = () => { +export const usePinhouseRouter = (searchParams: URLSearchParams) => { const router = useRouter(); const closeSheet = useHomeSheetStore(s => s.closeSheet); const replaceRouter = () => { @@ -46,11 +48,17 @@ export const usePinhouseRouter = () => { const handleSetPinpoint = () => { router.push("/mypage/pinpoints"); + closeSheet(); }; + const mode = useMemo(() => { + return homeSheetParseObject(searchParams); + }, [searchParams]); + return { replaceRouter, handleSetPinpoint, + mode, }; }; diff --git a/src/features/home/ui/components/components/pinpointSelectedButton.tsx b/src/features/home/ui/components/components/pinpointSelectedButton.tsx index 6f62fde..3ecc552 100644 --- a/src/features/home/ui/components/components/pinpointSelectedButton.tsx +++ b/src/features/home/ui/components/components/pinpointSelectedButton.tsx @@ -4,9 +4,14 @@ import { cn } from "@/lib/utils"; type PinpointSelectedButtonType = { mode: "pinpoints" | "maxTime"; handleSetPinpoint: () => void; + replaceRouter:() => void; }; -export const PinpointSelectedButton = ({ mode, handleSetPinpoint }: PinpointSelectedButtonType) => { +export const PinpointSelectedButton = ({ + mode, + handleSetPinpoint, + replaceRouter, +}: PinpointSelectedButtonType) => { return (
-
diff --git a/src/features/home/ui/components/homeFullSheet.tsx b/src/features/home/ui/components/homeFullSheet.tsx index fa7c3e0..4c86b30 100644 --- a/src/features/home/ui/components/homeFullSheet.tsx +++ b/src/features/home/ui/components/homeFullSheet.tsx @@ -1,23 +1,21 @@ "use client"; -import { homeSheetParseObject } from "@/src/features/listings/model"; + import { AnimatePresence, motion } from "framer-motion"; import { useSearchParams } from "next/navigation"; import { useHomeSheetStore } from "../../model/homeStore"; -import { useMemo } from "react"; import { PinpointRowBox } from "./pinpointRowBoxs"; import { MaxTimeSliderBox } from "./maxTime"; -import { Button } from "@/src/shared/lib/headlessUi"; -import { cn } from "@/lib/utils"; import { usePinhouseRouter } from "@/src/features/home/hooks/hooks"; import { PinpointSelectedButton } from "@/src/features/home/ui/components/components/pinpointSelectedButton"; +import type { ReadonlyURLSearchParams } from "next/navigation"; export const HomeSheet = () => { const open = useHomeSheetStore(s => s.open); const searchParams = useSearchParams(); - const mode = useMemo(() => { - return homeSheetParseObject(searchParams); - }, [searchParams]); - const { replaceRouter, handleSetPinpoint } = usePinhouseRouter(); + + const { replaceRouter, handleSetPinpoint, mode } = usePinhouseRouter( + searchParams as ReadonlyURLSearchParams + ); return ( @@ -38,7 +36,6 @@ export const HomeSheet = () => { exit={{ y: "100%" }} transition={{ type: "spring", stiffness: 260, damping: 30 }} > - {/*
*/}

{mode?.label}

@@ -57,27 +54,12 @@ export const HomeSheet = () => { {mode?.key === "pinpoints" && } {mode?.key === "maxTime" && } {!mode ? null : ( - + )} - {/*
*/} - {/* */} - {/* 핀포인트 설정*/} - {/* */} - {/* */} - {/* 저장하기*/} - {/* */} - {/*
*/}
diff --git a/src/features/home/ui/components/pinpointRowBoxs.tsx b/src/features/home/ui/components/pinpointRowBoxs.tsx index 393a090..d43f7b7 100644 --- a/src/features/home/ui/components/pinpointRowBoxs.tsx +++ b/src/features/home/ui/components/pinpointRowBoxs.tsx @@ -5,7 +5,7 @@ import { PinpointRowBoxSkeleton } from "./skeleton/skeleton"; import { usePinpointRowBox } from "@/src/features/home/hooks/hooks"; export const PinpointRowBox = () => { - const { data, isLoading } = useListingFilterDetail(); + const { data } = useListingFilterDetail(); const pin = data?.pinPoints ?? null; const { pinpoints, pinPointId, onChangePinpoint } = usePinpointRowBox(pin); diff --git a/src/features/home/ui/homeHeader.tsx b/src/features/home/ui/homeHeader.tsx index 50c182a..acedefc 100644 --- a/src/features/home/ui/homeHeader.tsx +++ b/src/features/home/ui/homeHeader.tsx @@ -3,13 +3,10 @@ import { HomeScreenLogo } from "@/src/assets/icons/home/homeScreenLogo"; import { PinhouseLogo } from "@/src/assets/icons/logo"; import { useRouter } from "next/navigation"; import { SearchLine } from "@/src/assets/icons/home"; +import { useHomeHeaderHooks } from "@/src/widgets/homeSection/hooks/homeHeaderHooks"; export const HomeHeader = () => { - const router = useRouter(); - - const pageRouter = () => { - router.push("/home/search"); - }; + const { onRouteChange } = useHomeHeaderHooks(); return (
@@ -18,7 +15,7 @@ export const HomeHeader = () => {
diff --git a/src/features/home/ui/homeQuickStatsList.tsx b/src/features/home/ui/homeQuickStatsList.tsx index 06774cd..bcf3524 100644 --- a/src/features/home/ui/homeQuickStatsList.tsx +++ b/src/features/home/ui/homeQuickStatsList.tsx @@ -6,22 +6,12 @@ import { useHomeMaxTime, useHomeSheetStore } from "../model/homeStore"; import { useRouter, useSearchParams } from "next/navigation"; import { useOAuthStore } from "../../login/model"; import { splitAddress, transTime } from "@/src/shared/lib/utils"; +import { useHomeUseHooks } from "@/src/features/home/ui/homeUseHooks/homeUseHooks"; export const QuickStatsList = () => { - const openSheet = useHomeSheetStore(s => s.openSheet); - const { pinPointId, pinPointName } = useOAuthStore(); - const { maxTime } = useHomeMaxTime(); - const [line1, line2] = splitAddress(pinPointName ?? "핀포인트 이름 설정해주세요"); - const searchParams = useSearchParams(); - const router = useRouter(); - const onSelectSection = (key: string) => { - const params = new URLSearchParams(searchParams.toString()); - params.set("mode", key); - params.set("id", pinPointId ?? ""); - router.push(`?${params.toString()}`, { scroll: false }); - openSheet(); - }; + const { maxTime } = useHomeMaxTime(); + const {line2, line1 , onSelectSection} = useHomeUseHooks(); return (
diff --git a/src/features/home/ui/homeUseHooks/homeUseHooks.ts b/src/features/home/ui/homeUseHooks/homeUseHooks.ts new file mode 100644 index 0000000..592597e --- /dev/null +++ b/src/features/home/ui/homeUseHooks/homeUseHooks.ts @@ -0,0 +1,25 @@ +import { useHomeSheetStore } from "@/src/features/home/model/homeStore"; +import { useOAuthStore } from "@/src/features/login/model"; +import { splitAddress } from "@/src/shared/lib/utils"; +import { useRouter, useSearchParams } from "next/navigation"; + +export const useHomeUseHooks = () => { + const openSheet = useHomeSheetStore(s => s.openSheet); + const { pinPointId, pinPointName } = useOAuthStore(); + const [line1, line2] = splitAddress(pinPointName ?? "핀포인트 이름 설정해주세요"); + const searchParams = useSearchParams(); + const router = useRouter(); + const onSelectSection = (key: string) => { + const params = new URLSearchParams(searchParams.toString()); + params.set("mode", key); + params.set("id", pinPointId ?? ""); + router.push(`?${params.toString()}`, { scroll: false }); + openSheet(); + }; + + return { + line1, + line2, + onSelectSection + } +} \ No newline at end of file diff --git a/src/features/listings/ui/listingsCardDetail/infra/components/roomTypeDetail.tsx b/src/features/listings/ui/listingsCardDetail/infra/components/roomTypeDetail.tsx index a2cdc3c..c83a5e6 100644 --- a/src/features/listings/ui/listingsCardDetail/infra/components/roomTypeDetail.tsx +++ b/src/features/listings/ui/listingsCardDetail/infra/components/roomTypeDetail.tsx @@ -78,8 +78,8 @@ export const RoomTypeDetail = ({ listingId }: { listingId: string }) => { {current?.thumbnail ? ( ) : ( -
- +
+

도면 이미지를 준비하고 있어요

)} diff --git a/src/features/listings/ui/listingsFilter/hooks.ts b/src/features/listings/ui/listingsFilter/hooks.ts new file mode 100644 index 0000000..e987c5b --- /dev/null +++ b/src/features/listings/ui/listingsFilter/hooks.ts @@ -0,0 +1,25 @@ +import { useFilterSheetStore, useListingsFilterStore } from "@/src/features/listings/model"; +import { useRouter, useSearchParams } from "next/navigation"; + +export const ListingHooks = () => { + const openSheet = useFilterSheetStore(state => state.openSheet); + const router = useRouter(); + const searchParams = useSearchParams(); + const params = new URLSearchParams(searchParams.toString()); + const onOpenSheet = () => { + openSheet(); + router.push(`listings?${params}`); + }; + + const hasSelectedFilters = useListingsFilterStore(state => + [state.regionType, state.rentalTypes, state.supplyTypes, state.houseTypes].some( + list => list.length > 0 + ) + ); + + return { + openSheet, + hasSelectedFilters, + onOpenSheet, + }; +}; diff --git a/src/features/listings/ui/listingsFilter/listingsFilterPanel.tsx b/src/features/listings/ui/listingsFilter/listingsFilterPanel.tsx index c45d807..d05970c 100644 --- a/src/features/listings/ui/listingsFilter/listingsFilterPanel.tsx +++ b/src/features/listings/ui/listingsFilter/listingsFilterPanel.tsx @@ -1,25 +1,12 @@ "use client"; -import { useRouter, useSearchParams } from "next/navigation"; + import { FILTER_OPTIONS, filterMap, getAllFilterIcon } from "../../model"; -import { useFilterSheetStore, useListingsFilterStore } from "../../model/listingsStore"; +import { useListingsFilterStore } from "../../model/listingsStore"; import { ListingTagButton } from "../listingsButton/listingsTagButton"; +import { ListingHooks } from "@/src/features/listings/ui/listingsFilter/hooks"; export const ListingFilterPanel = () => { - const openSheet = useFilterSheetStore(state => state.openSheet); - const router = useRouter(); - const searchParams = useSearchParams(); - const params = new URLSearchParams(searchParams.toString()); - params.set("tab", params.get("tab") ?? "region"); - const onOpenSheet = () => { - openSheet(); - router.push(`listings?${params}`); - }; - - const hasSelectedFilters = useListingsFilterStore(state => - [state.regionType, state.rentalTypes, state.supplyTypes, state.houseTypes].some( - list => list.length > 0 - ) - ); + const { onOpenSheet, hasSelectedFilters } = ListingHooks(); return (
diff --git a/src/features/listings/ui/listingsFullSheet/hooks.ts b/src/features/listings/ui/listingsFullSheet/hooks.ts new file mode 100644 index 0000000..970c734 --- /dev/null +++ b/src/features/listings/ui/listingsFullSheet/hooks.ts @@ -0,0 +1,92 @@ +import { useFilterSheetStore, useHasRouter } from "@/src/features/listings/model"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useListingListInfiniteQuery } from "@/src/entities/listings/hooks/useListingHooks"; + +const useListingTotal = () => { + const { data } = useListingListInfiniteQuery(); + const prevTotalRef = useRef(null); + const newTotal = data?.pages?.[0]?.totalCount; + + if (newTotal !== undefined && newTotal !== null) { + prevTotalRef.current = newTotal; + } + + return prevTotalRef.current; +}; + +const useScrollBottom = () => { + const scrollRef = useRef(null); + const [isAtBottom, setIsAtBottom] = useState(true); + + // useEffect 의존성을 안정화해서 불필요한 cleanup 반복을 막는다. + const handleScroll = useCallback(() => { + const el = scrollRef.current; + if (!el) return; + // 스크롤 위치에 따라 하단 그라데이션 표시를 정확히 맞추기 위함. + const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 5; + setIsAtBottom(atBottom); + }, []); + + return { scrollRef, isAtBottom, handleScroll }; +}; + +const useListingsTabSync = (open: boolean, handleScroll: () => void) => { + const searchParams = useSearchParams(); + const { setHasListingsTab, reset } = useHasRouter(); + + useEffect(() => { + if (!open) return; + + // 시트가 열릴 때 URL 상태를 반영해 하단 네비 같은 UI 분기 기준을 맞춘다. + handleScroll(); + const hasTab = searchParams.has("tab"); + setHasListingsTab(hasTab); + + // 시트가 닫힐 때 router 관련 상태를 원복한다. + return () => { + reset(); + }; + }, [open, handleScroll, searchParams, setHasListingsTab, reset]); +}; + +export const ListingFilterPartialSheetHooks = () => { + const open = useFilterSheetStore(s => s.open); + const closeSheet = useFilterSheetStore(s => s.closeSheet); + const router = useRouter(); + const searchParams = useSearchParams(); + const displayTotal = useListingTotal(); + const { scrollRef, isAtBottom, handleScroll } = useScrollBottom(); + useListingsTabSync(open, handleScroll); + + const resetListingsQuery = () => { + try { + // tab만 제거하고 나머지 쿼리는 그대로 유지한다. + const params = new URLSearchParams(searchParams.toString()); + params.delete("tab"); + const query = params.toString(); + router.replace(query ? `/listings?${query}` : "/listings", { scroll: false }); + } catch (error) { + console.error("[ListingFilterPartialSheet] Failed to reset query", error); + } + }; + + const handleCloseSheet = () => { + try { + // UI를 먼저 닫고, 이어서 URL을 정리하는 흐름으로 고정한다. + closeSheet(); + resetListingsQuery(); + } catch (error) { + console.error("[ListingFilterPartialSheet] Failed to close sheet", error); + } + }; + + return { + open, + scrollRef, + isAtBottom, + displayTotal, + handleScroll, + handleCloseSheet, + }; +}; diff --git a/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx b/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx index 5f16da2..e23796e 100644 --- a/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx +++ b/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx @@ -1,62 +1,16 @@ "use client"; import { motion, AnimatePresence } from "framer-motion"; -import { - useFilterSheetStore, - useHasRouter, - useListingsFilterStore, -} from "../../model/listingsStore"; +import { useListingsFilterStore } from "../../model/listingsStore"; import { FILTER_TABS, FilterTabKey, TAB_CONFIG } from "../../model"; -import { useEffect, useRef, useState } from "react"; import { CloseButton } from "@/src/assets/icons/button"; import { useRouter, useSearchParams } from "next/navigation"; import { getIndicatorLeft, getIndicatorWidth } from "../../hooks/listingsHooks"; -import { useListingListInfiniteQuery } from "@/src/entities/listings/hooks/useListingHooks"; import { Checkbox } from "@/src/shared/lib/headlessUi/checkBox/checkbox"; +import { ListingFilterPartialSheetHooks } from "./hooks"; export const ListingFilterPartialSheet = () => { - const open = useFilterSheetStore(s => s.open); - const closeSheet = useFilterSheetStore(s => s.closeSheet); - const router = useRouter(); - const searchParams = useSearchParams(); - const scrollRef = useRef(null); - const [isAtBottom, setIsAtBottom] = useState(true); - const { data } = useListingListInfiniteQuery(); - const prevTotalRef = useRef(null); - const newTotal = data?.pages?.[0]?.totalCount; - const { setHasListingsTab, reset } = useHasRouter(); - - if (newTotal !== undefined && newTotal !== null) { - prevTotalRef.current = newTotal; - } - const displayTotal = prevTotalRef.current; - const handleScroll = () => { - const el = scrollRef.current; - if (!el) return; - const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 5; - setIsAtBottom(atBottom); - }; - useEffect(() => { - if (open) { - handleScroll(); - const hasTab = window.location.search.includes("tab="); - setHasListingsTab(hasTab); - - return () => { - reset(); - }; - } - }, [open]); - const resetListingsQuery = () => { - const params = new URLSearchParams(searchParams.toString()); - params.delete("tab"); - const query = params.toString(); - router.replace(query ? `/listings?${query}` : "/listings", { scroll: false }); - }; - - const handleCloseSheet = () => { - closeSheet(); - resetListingsQuery(); - }; + const { open, scrollRef, isAtBottom, displayTotal, handleScroll, handleCloseSheet } = + ListingFilterPartialSheetHooks(); return ( diff --git a/src/features/mypage/hooks/useAddPinpoint.ts b/src/features/mypage/hooks/useAddPinpoint.ts index 6ddd54a..feac8ec 100644 --- a/src/features/mypage/hooks/useAddPinpoint.ts +++ b/src/features/mypage/hooks/useAddPinpoint.ts @@ -1,6 +1,6 @@ "use client"; -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { requestSetPinpoint } from "@/src/entities/address/api"; export interface IPinpointData { @@ -18,10 +18,14 @@ interface IUseAddPinpointParams { * 핀포인트만 추가하는 커스텀 훅 (마이페이지 등에서 사용) */ export const useAddPinpoint = ({ onSuccess, onError }: IUseAddPinpointParams = {}) => { + const queryClient = useQueryClient(); + const mutation = useMutation({ mutationFn: (data: IPinpointData) => requestSetPinpoint(data), onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["pinpointSettings"] }); onSuccess?.(); + }, onError: error => { console.error("핀포인트 추가 실패:", error); diff --git a/src/widgets/homeSection/hooks/homeHeaderHooks.ts b/src/widgets/homeSection/hooks/homeHeaderHooks.ts new file mode 100644 index 0000000..3a57532 --- /dev/null +++ b/src/widgets/homeSection/hooks/homeHeaderHooks.ts @@ -0,0 +1,13 @@ +import { useRouter } from "next/navigation"; +import { useCallback } from "react"; + +export const useHomeHeaderHooks = () => { + const router = useRouter(); + + const onRouteChange = useCallback(() => { + router.push("/home/search"); + },[]) + return { + onRouteChange, + } +} \ No newline at end of file