From 4662ed5713b40baa32016d35b48363f43bacf6df Mon Sep 17 00:00:00 2001 From: chan Date: Wed, 11 Feb 2026 08:35:26 +0900 Subject: [PATCH 01/18] =?UTF-8?q?style:=ED=95=80=ED=95=98=EC=9A=B0?= =?UTF-8?q?=EC=8A=A4=20home=20/=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20Color=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/home/ui/search/homeSearchPopuler.tsx | 2 +- tailwind.config.mjs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/home/ui/search/homeSearchPopuler.tsx b/src/features/home/ui/search/homeSearchPopuler.tsx index f84272f..d40f522 100644 --- a/src/features/home/ui/search/homeSearchPopuler.tsx +++ b/src/features/home/ui/search/homeSearchPopuler.tsx @@ -24,7 +24,7 @@ export const HomeSearchPopuler = () => { size="sm" onClick={() => handleSearchTag(word.keyword)} className={cn( - "font-suit text-text-greyscale-grey-85 rounded-full border px-3 py-1 text-sm transition-all" + "font-suit text-text-greyscale-grey-85 rounded-full border px-3 py-1 text-sm transition-all hover:border-none hover:bg-primary-blue-300 hover:text-gray-200" )} > {word.keyword} diff --git a/tailwind.config.mjs b/tailwind.config.mjs index dca2081..336990a 100644 --- a/tailwind.config.mjs +++ b/tailwind.config.mjs @@ -73,7 +73,7 @@ export default { 200: "#CECED7", 300: "#BBBAC5", 400: "#9F9FAB", - 500: "#7F7FBF", + 500: "#7F7F8F", 600: "#676472", 700: "#4F4B5C", 800: "#2E293D", From c0f6195a5b55cb79bf990274879e41549e3f4e8e Mon Sep 17 00:00:00 2001 From: chan Date: Thu, 12 Feb 2026 09:40:23 +0900 Subject: [PATCH 02/18] =?UTF-8?q?feat:=EB=B0=94=ED=85=80=20=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=ED=8F=AC=EC=A7=80=EC=85=98=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?/=20=ED=95=B4=EB=8B=B9=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=20=EA=B3=B5=ED=86=B5=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/home/model/homeStore.ts | 2 +- .../home/ui/components/homeFullSheet.tsx | 28 +++++-- src/features/home/ui/components/maxTime.tsx | 1 + .../DetailSectionFilter/DetailFilterSheet.tsx | 19 +++-- .../infra/components/roomTypeDetail.tsx | 40 +++++----- .../listingsCardDetail/infra/infraSheet.tsx | 22 +++++- .../listingsFullSheet/listingsFullSheet.tsx | 4 +- src/shared/hooks/usePortalTarget/index.ts | 1 + .../hooks/usePortalTarget/usePortalTarget.ts | 13 ++++ src/shared/hooks/useScrollLock/index.ts | 1 + .../hooks/useScrollLock/useScrollLock.ts | 76 +++++++++++++++++++ src/shared/ui/globalRender/globalRender.tsx | 5 ++ 12 files changed, 174 insertions(+), 38 deletions(-) create mode 100644 src/shared/hooks/usePortalTarget/index.ts create mode 100644 src/shared/hooks/usePortalTarget/usePortalTarget.ts create mode 100644 src/shared/hooks/useScrollLock/index.ts create mode 100644 src/shared/hooks/useScrollLock/useScrollLock.ts diff --git a/src/features/home/model/homeStore.ts b/src/features/home/model/homeStore.ts index 4c53076..e2b9a3e 100644 --- a/src/features/home/model/homeStore.ts +++ b/src/features/home/model/homeStore.ts @@ -35,7 +35,7 @@ type HomeMaxSheet = { }; export const useHomeMaxTime = create(set => ({ - maxTime: 30, + maxTime: 60, setMaxTime: time => set({ maxTime: time }), reset: () => set({ maxTime: 30 }), })); diff --git a/src/features/home/ui/components/homeFullSheet.tsx b/src/features/home/ui/components/homeFullSheet.tsx index 4c86b30..0ed5cc2 100644 --- a/src/features/home/ui/components/homeFullSheet.tsx +++ b/src/features/home/ui/components/homeFullSheet.tsx @@ -2,27 +2,35 @@ import { AnimatePresence, motion } from "framer-motion"; import { useSearchParams } from "next/navigation"; -import { useHomeSheetStore } from "../../model/homeStore"; -import { PinpointRowBox } from "./pinpointRowBoxs"; -import { MaxTimeSliderBox } from "./maxTime"; +import { useRef } from "react"; +import { createPortal } from "react-dom"; import { usePinhouseRouter } from "@/src/features/home/hooks/hooks"; import { PinpointSelectedButton } from "@/src/features/home/ui/components/components/pinpointSelectedButton"; +import { usePortalTarget } from "@/src/shared/hooks/usePortalTarget"; +import { useScrollLock } from "@/src/shared/hooks/useScrollLock"; +import { useHomeSheetStore } from "../../model/homeStore"; +import { MaxTimeSliderBox } from "./maxTime"; +import { PinpointRowBox } from "./pinpointRowBoxs"; import type { ReadonlyURLSearchParams } from "next/navigation"; export const HomeSheet = () => { const open = useHomeSheetStore(s => s.open); const searchParams = useSearchParams(); + const anchorRef = useRef(null); + const portalRoot = usePortalTarget("mobile-overlay-root"); const { replaceRouter, handleSetPinpoint, mode } = usePinhouseRouter( searchParams as ReadonlyURLSearchParams ); - return ( + useScrollLock({ locked: open, anchorRef }); + + const content = ( {open && ( <> { /> -

{mode?.label}

@@ -67,4 +74,11 @@ export const HomeSheet = () => { )} ); + + return ( + <> + + {portalRoot ? createPortal(content, portalRoot) : content} + + ); }; diff --git a/src/features/home/ui/components/maxTime.tsx b/src/features/home/ui/components/maxTime.tsx index 1ff8d4a..613b341 100644 --- a/src/features/home/ui/components/maxTime.tsx +++ b/src/features/home/ui/components/maxTime.tsx @@ -22,6 +22,7 @@ export const MaxTimeSliderBox = () => { { const open = useDetailFilterSheetStore(s => s.open); const closeSheet = useDetailFilterSheetStore(s => s.closeSheet); const searchParams = useSearchParams(); + const anchorRef = useRef(null); const section = parseDetailSection(searchParams); + useScrollLock({ locked: open, anchorRef }); return ( - - {open && ( - <> + <> + + + {open && ( + <> {
- - )} -
+ + )} + + ); }; diff --git a/src/features/listings/ui/listingsCardDetail/infra/components/roomTypeDetail.tsx b/src/features/listings/ui/listingsCardDetail/infra/components/roomTypeDetail.tsx index c83a5e6..8cc7ea0 100644 --- a/src/features/listings/ui/listingsCardDetail/infra/components/roomTypeDetail.tsx +++ b/src/features/listings/ui/listingsCardDetail/infra/components/roomTypeDetail.tsx @@ -1,15 +1,16 @@ -"use cilent"; -import { useState, useEffect } from "react"; +"use client"; + +import { useEffect, useState } from "react"; +import { CompareDefaultImage } from "@/src/assets/images/compare/compare"; import { useListingRoomTypeDetail } from "@/src/entities/listings/hooks/useListingDetailHooks"; -import { SmallSpinner } from "@/src/shared/ui/spinner/small/smallSpinner"; -import { formatNumber } from "@/src/shared/lib/numberFormat"; +import { ListingUnitType } from "@/src/entities/listings/model/type"; import { toPyeong } from "@/src/features/listings/model"; +import { LikeType } from "@/src/features/listings/hooks/listingsHooks"; +import { formatNumber } from "@/src/shared/lib/numberFormat"; +import { TagButton } from "@/src/shared/ui/button/tagButton"; +import { SmallSpinner } from "@/src/shared/ui/spinner/small/smallSpinner"; import { DepositSection } from "./components/roomType/depositSection"; import { TypeInfoSection } from "./components/roomType/typeInfoSection"; -import { ListingUnitType } from "@/src/entities/listings/model/type"; -import { TagButton } from "@/src/shared/ui/button/tagButton"; -import { LikeType } from "@/src/features/listings/hooks/listingsHooks"; -import { CompareDefaultImage } from "@/src/assets/images/compare/compare"; export const RoomTypeDetail = ({ listingId }: { listingId: string }) => { const { data, isFetching } = useListingRoomTypeDetail({ @@ -17,11 +18,10 @@ export const RoomTypeDetail = ({ listingId }: { listingId: string }) => { queryK: "useListingRoomTypeDetail", url: "unit", }); + const [currentIndex, setCurrentIndex] = useState(0); const items = data ?? []; const current = items[currentIndex]; - const typeId = current?.typeId; - const liked = current?.liked; const goPrev = () => { setCurrentIndex(p => (p - 1 + items.length) % Math.max(items.length, 1)); @@ -31,15 +31,16 @@ export const RoomTypeDetail = ({ listingId }: { listingId: string }) => { setCurrentIndex(p => (p + 1) % Math.max(items.length, 1)); }; - const isLast = currentIndex + 1 < items.length; + const hasNext = currentIndex + 1 < items.length; useEffect(() => { setCurrentIndex(0); }, [listingId]); if (isFetching && !items.length) { - return ; + return ; } + if (!items.length) { return (
@@ -49,7 +50,7 @@ export const RoomTypeDetail = ({ listingId }: { listingId: string }) => { } return ( -
+
@@ -64,31 +65,34 @@ export const RoomTypeDetail = ({ listingId }: { listingId: string }) => { ))} +
+
{current?.thumbnail ? ( ) : (
- -

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

+ +

이미지 준비 중입니다.

)}
+ {items.length > 1 && ( )} diff --git a/src/features/listings/ui/listingsCardDetail/infra/infraSheet.tsx b/src/features/listings/ui/listingsCardDetail/infra/infraSheet.tsx index ea451e9..70bb479 100644 --- a/src/features/listings/ui/listingsCardDetail/infra/infraSheet.tsx +++ b/src/features/listings/ui/listingsCardDetail/infra/infraSheet.tsx @@ -1,5 +1,9 @@ import { motion, AnimatePresence } from "framer-motion"; +import { useRef } from "react"; +import { createPortal } from "react-dom"; import { CloseButton } from "@/src/assets/icons/button"; +import { usePortalTarget } from "@/src/shared/hooks/usePortalTarget"; +import { useScrollLock } from "@/src/shared/hooks/useScrollLock"; import { InfraSheetProps, RenderContentProps, @@ -31,17 +35,20 @@ const RenderContent = ({ section, listingId }: RenderContentProps) => { }; export const InfraSheet = ({ onClose, sheetState }: InfraSheetProps) => { + const anchorRef = useRef(null); + const portalRoot = usePortalTarget("mobile-overlay-root"); const roomType: RoomTitleDesType | null = sheetState.open ? ROOM_TYPE_TITLE_DES[sheetState.section] : null; + useScrollLock({ locked: sheetState.open, anchorRef }); - return ( + const content = ( {sheetState.open && ( <> { e.stopPropagation(); onClose(); @@ -53,7 +60,7 @@ export const InfraSheet = ({ onClose, sheetState }: InfraSheetProps) => { {
-
+
{sheetState.open && ( )} @@ -84,4 +91,11 @@ export const InfraSheet = ({ onClose, sheetState }: InfraSheetProps) => { )} ); + + return ( + <> + + {portalRoot ? createPortal(content, portalRoot) : content} + + ); }; diff --git a/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx b/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx index e23796e..51c95d5 100644 --- a/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx +++ b/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx @@ -238,7 +238,7 @@ const FilterSheetContainer = ({ return ( <> { + const [portalTarget, setPortalTarget] = useState(null); + + useEffect(() => { + setPortalTarget(document.getElementById(targetId)); + }, [targetId]); + + return portalTarget; +}; diff --git a/src/shared/hooks/useScrollLock/index.ts b/src/shared/hooks/useScrollLock/index.ts new file mode 100644 index 0000000..e3fed0e --- /dev/null +++ b/src/shared/hooks/useScrollLock/index.ts @@ -0,0 +1 @@ +export * from "./useScrollLock"; diff --git a/src/shared/hooks/useScrollLock/useScrollLock.ts b/src/shared/hooks/useScrollLock/useScrollLock.ts new file mode 100644 index 0000000..ad8f273 --- /dev/null +++ b/src/shared/hooks/useScrollLock/useScrollLock.ts @@ -0,0 +1,76 @@ +"use client"; + +import { RefObject, useEffect } from "react"; + +interface UseScrollLockOptions { + locked: boolean; + anchorRef?: RefObject; + lockDocument?: boolean; +} + +const findScrollableAncestor = (element: HTMLElement | null) => { + let current = element?.parentElement ?? null; + + while (current) { + const styles = window.getComputedStyle(current); + const overflowY = styles.overflowY; + const overflow = styles.overflow; + + if ( + overflowY === "auto" || + overflowY === "scroll" || + overflow === "auto" || + overflow === "scroll" + ) { + return current; + } + + current = current.parentElement; + } + + return null; +}; + +export const useScrollLock = ({ + locked, + anchorRef, + lockDocument = true, +}: UseScrollLockOptions) => { + useEffect(() => { + if (!locked) { + return; + } + + const anchor = anchorRef?.current ?? null; + const scrollContainer = findScrollableAncestor(anchor); + const html = document.documentElement; + const body = document.body; + + const prevContainerOverflow = scrollContainer?.style.overflow ?? ""; + const prevContainerOverflowY = scrollContainer?.style.overflowY ?? ""; + const prevHtmlOverflow = html.style.overflow; + const prevBodyOverflow = body.style.overflow; + + if (scrollContainer) { + scrollContainer.style.overflow = "hidden"; + scrollContainer.style.overflowY = "hidden"; + } + + if (lockDocument) { + html.style.overflow = "hidden"; + body.style.overflow = "hidden"; + } + + return () => { + if (scrollContainer) { + scrollContainer.style.overflow = prevContainerOverflow; + scrollContainer.style.overflowY = prevContainerOverflowY; + } + + if (lockDocument) { + html.style.overflow = prevHtmlOverflow; + body.style.overflow = prevBodyOverflow; + } + }; + }, [anchorRef, lockDocument, locked]); +}; diff --git a/src/shared/ui/globalRender/globalRender.tsx b/src/shared/ui/globalRender/globalRender.tsx index f36ac97..05f4e41 100644 --- a/src/shared/ui/globalRender/globalRender.tsx +++ b/src/shared/ui/globalRender/globalRender.tsx @@ -71,6 +71,11 @@ export const HomeLandingRender = ({ children, bottom }: Props) => {
{bottom}
+ +
From 3e90eff7e11e3d9e609a414e764da7346fa70798 Mon Sep 17 00:00:00 2001 From: chan Date: Fri, 13 Feb 2026 09:09:48 +0900 Subject: [PATCH 03/18] =?UTF-8?q?feat=20=EB=B0=94=ED=85=80=20=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=ED=8F=AC=EC=A7=80=EC=85=98=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?/=20=ED=95=B4=EB=8B=B9=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8?= =?UTF-8?q?=20=EA=B3=B5=ED=86=B5=20=ED=9B=85=EC=9C=BC=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/home/ui/homeQuickStatsList.tsx | 9 +++------ .../ui/listingsContents/listingsContents.tsx | 2 +- .../listingsFullSheet/listingsFullSheet.tsx | 20 ++++++++++--------- .../frameBottomNavigation.tsx | 11 +--------- 4 files changed, 16 insertions(+), 26 deletions(-) diff --git a/src/features/home/ui/homeQuickStatsList.tsx b/src/features/home/ui/homeQuickStatsList.tsx index bcf3524..4280d47 100644 --- a/src/features/home/ui/homeQuickStatsList.tsx +++ b/src/features/home/ui/homeQuickStatsList.tsx @@ -2,16 +2,13 @@ import { CaretDown } from "@/src/assets/icons/button/caretDown"; import { HomeFiveoclock } from "@/src/assets/icons/home/HomeFiveoclock"; import { HomePushPin } from "@/src/assets/icons/home/homePushpin"; -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 { useHomeMaxTime } from "../model/homeStore"; +import { transTime } from "@/src/shared/lib/utils"; import { useHomeUseHooks } from "@/src/features/home/ui/homeUseHooks/homeUseHooks"; export const QuickStatsList = () => { - const { maxTime } = useHomeMaxTime(); - const {line2, line1 , onSelectSection} = useHomeUseHooks(); + const { line2, line1, onSelectSection } = useHomeUseHooks(); return (
diff --git a/src/features/listings/ui/listingsContents/listingsContents.tsx b/src/features/listings/ui/listingsContents/listingsContents.tsx index 919700f..faf2fe0 100644 --- a/src/features/listings/ui/listingsContents/listingsContents.tsx +++ b/src/features/listings/ui/listingsContents/listingsContents.tsx @@ -2,7 +2,7 @@ import { useListingListInfiniteQuery } from "@/src/entities/listings/hooks/useListingHooks"; import { ListingsContentHeader } from "./listingsContentsHeader"; import { ListingContentsList } from "./listingsContentsList"; -import { ListingNoSearchResult } from "../listingsNoSearchResult/listingNoSearchResult"; +import { ListingNoSearchResult } from "@/src/features/listings"; import { Spinner } from "@/src/shared/ui/spinner/default"; export const ListingsContent = ({ viewSet = true }: { viewSet?: boolean }) => { diff --git a/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx b/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx index 51c95d5..1052e11 100644 --- a/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx +++ b/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx @@ -1,12 +1,14 @@ "use client"; import { motion, AnimatePresence } from "framer-motion"; -import { useListingsFilterStore } from "../../model/listingsStore"; +import { useFilterSheetStore, useListingsFilterStore } from "../../model/listingsStore"; import { FILTER_TABS, FilterTabKey, TAB_CONFIG } from "../../model"; import { CloseButton } from "@/src/assets/icons/button"; import { useRouter, useSearchParams } from "next/navigation"; import { getIndicatorLeft, getIndicatorWidth } from "../../hooks/listingsHooks"; import { Checkbox } from "@/src/shared/lib/headlessUi/checkBox/checkbox"; import { ListingFilterPartialSheetHooks } from "./hooks"; +import { ReactNode, useRef } from "react"; +import { useScrollLock } from "@/src/shared/hooks/useScrollLock"; export const ListingFilterPartialSheet = () => { const { open, scrollRef, isAtBottom, displayTotal, handleScroll, handleCloseSheet } = @@ -125,7 +127,6 @@ const UseCheckBox = () => { const searchParams = useSearchParams(); const currentTab = (searchParams.get("tab") as FilterTabKey) || "region"; const tabConfig = currentTab ? TAB_CONFIG[currentTab] : null; - const regionType = useListingsFilterStore(s => s.regionType); const rentalTypes = useListingsFilterStore(s => s.rentalTypes); const supplyTypes = useListingsFilterStore(s => s.supplyTypes); @@ -154,12 +155,9 @@ const UseCheckBox = () => { rental: supplyTypes, housing: houseTypes, }[currentTab]; - const isAllSelected = selectedList.length === totalItems.length; - const handleAllSelect = (e: boolean) => { const checked = e; - // 기존 방식 유지: 기존 값 초기화 if (currentTab === "region") resetRegionType(); if (currentTab === "target") resetRentalTypes(); @@ -233,20 +231,24 @@ const FilterSheetContainer = ({ children, }: { onDismiss: () => void; - children: React.ReactNode; + children: ReactNode; }) => { + const open = useFilterSheetStore(s => s.open); + const anchorRef = useRef(null); + useScrollLock({ locked: open, anchorRef }); + return ( <> + - { detailPageRegex.test(pathname) || compareDetailPageRegex.test(pathname) || (pathname === "/listings" && hasListingsTab); - // const searchParams = useSearchParams(); - // const tab = searchParams.get("tab"); - // const shouldHide = - // hiddenRoutes.some(route => pathname.startsWith(route)) || - // hiddenExactRoutes.includes(pathname) || - // pathname.startsWith("/home/search") || - // (pathname === "/listings" && tab !== null) || - // detailPageRegex.test(pathname) || - // compareDetailPageRegex.test(pathname) || - // (pathname === "/home" && searchParams.has("mode")); + if (shouldHide) return null; return ( From bce1d8a4bb2fbae9eef6907842f5e073fa20c549 Mon Sep 17 00:00:00 2001 From: chan Date: Fri, 13 Feb 2026 09:29:41 +0900 Subject: [PATCH 04/18] =?UTF-8?q?fix:=EB=B0=94=ED=85=80=20=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=ED=8F=AC=EC=A7=80=EC=85=98=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?/=20=EA=B8=80=EB=A1=9C=EB=B2=8C=20=EB=A0=8C=EB=8D=94=EB=A7=81?= =?UTF-8?q?=EC=97=90=20=EC=A2=85=EC=86=8D=20=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EA=B3=A0=20=20=EC=8B=9C=ED=8A=B8=EA=B0=80=20=EB=B7=B0=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=EC=85=98=EC=9D=B4=20=EC=9E=A1=ED=9E=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../listingsFullSheet/listingsFullSheet.tsx | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx b/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx index 1052e11..5d50f1e 100644 --- a/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx +++ b/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx @@ -7,8 +7,10 @@ import { useRouter, useSearchParams } from "next/navigation"; import { getIndicatorLeft, getIndicatorWidth } from "../../hooks/listingsHooks"; import { Checkbox } from "@/src/shared/lib/headlessUi/checkBox/checkbox"; import { ListingFilterPartialSheetHooks } from "./hooks"; -import { ReactNode, useRef } from "react"; +import { ReactNode, RefObject, useRef } from "react"; +import { createPortal } from "react-dom"; import { useScrollLock } from "@/src/shared/hooks/useScrollLock"; +import { usePortalTarget } from "@/src/shared/hooks/usePortalTarget"; export const ListingFilterPartialSheet = () => { const { open, scrollRef, isAtBottom, displayTotal, handleScroll, handleCloseSheet } = @@ -235,20 +237,20 @@ const FilterSheetContainer = ({ }) => { const open = useFilterSheetStore(s => s.open); const anchorRef = useRef(null); + const portalRoot = usePortalTarget("mobile-overlay-root"); useScrollLock({ locked: open, anchorRef }); - return ( + const content = ( <> - ); + + return ( + <> + + {portalRoot ? createPortal(content, portalRoot) : content} + + ); }; const FilterSheetHeader = ({ onClose }: { onClose: () => void }) => { @@ -283,8 +292,8 @@ const FilterSheetContent = ({ onScroll, isAtBottom, }: { - children: React.ReactNode; - scrollRef: React.RefObject; + children: ReactNode; + scrollRef: RefObject; onScroll: () => void; isAtBottom: boolean; }) => { From 958407fad6712b62638d0828037c9ff30132040c Mon Sep 17 00:00:00 2001 From: chan Date: Fri, 13 Feb 2026 09:53:27 +0900 Subject: [PATCH 05/18] =?UTF-8?q?fix:=EB=B0=94=ED=85=80=20=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=ED=8F=AC=EC=A7=80=EC=85=98=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?/=20=EA=B8=80=EB=A1=9C=EB=B2=8C=20=EB=A0=8C=EB=8D=94=EB=A7=81?= =?UTF-8?q?=EC=97=90=20=EC=A2=85=EC=86=8D=20=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EA=B3=A0=20=20=EC=8B=9C=ED=8A=B8=EA=B0=80=20=EB=B7=B0=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=20=EA=B8=B0=EC=A4=80=EC=9C=BC=EB=A1=9C=20=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=EC=85=98=EC=9D=B4=20=EC=9E=A1=ED=9E=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DetailSectionFilter/DetailFilterSheet.tsx | 35 ++++++++++++------- .../listingsFullSheet/listingsFullSheet.tsx | 6 +++- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DetailFilterSheet.tsx b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DetailFilterSheet.tsx index 5ad4d2e..0a37f1f 100644 --- a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DetailFilterSheet.tsx +++ b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DetailFilterSheet.tsx @@ -3,8 +3,10 @@ import { AnimatePresence, motion } from "framer-motion"; import { useSearchParams } from "next/navigation"; import { useRef } from "react"; +import { createPortal } from "react-dom"; import { useDetailFilterSheetStore } from "@/src/features/listings/model"; import { useScrollLock } from "@/src/shared/hooks/useScrollLock"; +import { usePortalTarget } from "@/src/shared/hooks/usePortalTarget"; import { DetailFilterTab } from "./DetailFilterTab"; import { parseDetailSection } from "@/src/features/listings/model"; import { DistanceFilter } from "./DistanceFilter"; @@ -17,28 +19,31 @@ export const DetailFilterSheet = () => { const closeSheet = useDetailFilterSheetStore(s => s.closeSheet); const searchParams = useSearchParams(); const anchorRef = useRef(null); + const portalRoot = usePortalTarget("mobile-overlay-root"); const section = parseDetailSection(searchParams); useScrollLock({ locked: open, anchorRef }); - return ( - <> - - - {open && ( - <> + const content = ( + + {open && ( + <> { + e.stopPropagation(); + closeSheet(); + }} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} /> e.stopPropagation()} transition={{ type: "spring", stiffness: 260, damping: 30 }} >
@@ -67,9 +72,15 @@ export const DetailFilterSheet = () => {
- - )} -
+ + )} +
+ ); + + return ( + <> + + {portalRoot ? createPortal(content, portalRoot) : content} ); }; diff --git a/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx b/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx index 5d50f1e..37f2802 100644 --- a/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx +++ b/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx @@ -244,7 +244,10 @@ const FilterSheetContainer = ({ <> { + e.stopPropagation(); + onDismiss(); + }} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} @@ -254,6 +257,7 @@ const FilterSheetContainer = ({ initial={{ y: "100%" }} animate={{ y: 0 }} exit={{ y: "100%" }} + onClick={e => e.stopPropagation()} transition={{ type: "spring", stiffness: 260, damping: 30 }} > {children} From aeb1a25d653576cdf9272726835b77ec2d4b0ca3 Mon Sep 17 00:00:00 2001 From: JW_Ahn0 Date: Fri, 13 Feb 2026 11:32:48 +0900 Subject: [PATCH 06/18] =?UTF-8?q?refactor:=20=EC=95=B1=20=EB=9D=BC?= =?UTF-8?q?=EC=9A=B0=ED=8A=B8=C2=B7=EC=9C=84=EC=A0=AF=20=EB=B6=84=EB=A6=AC?= =?UTF-8?q?=20=EB=B0=8F=20=EC=A0=91=EA=B7=BC=EC=84=B1=C2=B7=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=B2=98=EB=A6=AC=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app 라우트는 위젯 한 개만 렌더하도록 축소 (MypageSection, SettingsSection, ProfileSection, WithdrawSection, PinpointsSection) - 레이아웃·비즈니스 로직은 src/widgets/mypageSection/ui/*Section.tsx 로 이전 - 문구·라벨은 src/features/mypage/model/mypageConstants.ts 로 통합 - 공용 LoadingState, ErrorState 컴포넌트 추가 --- app/mypage/page.tsx | 80 +------------ app/mypage/pinpoints/page.tsx | 51 +-------- app/mypage/profile/page.tsx | 23 +--- app/mypage/settings/page.tsx | 32 +----- app/mypage/withdraw/page.tsx | 22 +--- next.config.ts | 24 ++++ src/features/login/model/auth.cilent.type.ts | 6 +- src/features/mypage/api/index.ts | 2 + src/features/mypage/api/mypageApi.ts | 26 +++++ src/features/mypage/api/profileImageApi.ts | 30 +++++ src/features/mypage/hooks/index.ts | 3 + src/features/mypage/hooks/useMypageUser.ts | 19 +++ src/features/mypage/hooks/useProfile.ts | 97 ++++++++++++---- .../mypage/hooks/useProfileNickname.ts | 15 +++ .../mypage/hooks/useProfilePhotoSheet.ts | 54 +++++++++ src/features/mypage/hooks/useWithdraw.ts | 2 + src/features/mypage/model/index.ts | 1 + src/features/mypage/model/mypageConstants.ts | 48 ++++++++ src/features/mypage/model/mypageUser.type.ts | 21 ++++ src/features/mypage/model/profile.type.ts | 4 +- src/features/mypage/ui/index.ts | 1 + src/features/mypage/ui/mypageSection.tsx | 42 ++++--- src/features/mypage/ui/mypageSettingsMenu.tsx | 41 +++++++ src/features/mypage/ui/pinReportSection.tsx | 73 +++++++----- src/features/mypage/ui/profileAvatar.tsx | 17 ++- src/features/mypage/ui/profileForm.tsx | 108 ++++++++++-------- src/features/mypage/ui/profileLoginInfo.tsx | 11 +- .../mypage/ui/profileNicknameInput.tsx | 21 +++- src/features/mypage/ui/userInfoCard.tsx | 87 +++++++------- src/features/mypage/ui/withdrawForm.tsx | 48 ++++---- src/shared/api/endpoints.ts | 5 + src/shared/config/queryKeys.ts | 6 + src/shared/types/auth.ts | 5 + src/shared/types/index.ts | 1 + .../ui/bottomNavigation/bottomNavigation.tsx | 2 +- .../frameBottomNavigation.tsx | 5 +- src/shared/ui/errorState/ErrorState.tsx | 73 ++++++++++++ src/shared/ui/errorState/index.ts | 1 + src/shared/ui/loadingState/LoadingState.tsx | 32 ++++++ src/shared/ui/loadingState/index.ts | 1 + src/shared/ui/modal/default/modal.tsx | 3 + src/shared/ui/modal/default/type.ts | 2 + src/widgets/mypageSection/index.ts | 5 + .../mypageSection/ui/MypageSection.tsx | 106 +++++++++++++++++ .../mypageSection/ui/PinpointsSection.tsx | 70 ++++++++++++ .../mypageSection/ui/ProfileSection.tsx | 49 ++++++++ .../mypageSection/ui/SettingsSection.tsx | 30 +++++ .../mypageSection/ui/WithdrawSection.tsx | 26 +++++ 48 files changed, 1041 insertions(+), 390 deletions(-) create mode 100644 src/features/mypage/api/mypageApi.ts create mode 100644 src/features/mypage/api/profileImageApi.ts create mode 100644 src/features/mypage/hooks/useMypageUser.ts create mode 100644 src/features/mypage/hooks/useProfileNickname.ts create mode 100644 src/features/mypage/hooks/useProfilePhotoSheet.ts create mode 100644 src/features/mypage/model/mypageUser.type.ts create mode 100644 src/features/mypage/ui/mypageSettingsMenu.tsx create mode 100644 src/shared/types/auth.ts create mode 100644 src/shared/ui/errorState/ErrorState.tsx create mode 100644 src/shared/ui/errorState/index.ts create mode 100644 src/shared/ui/loadingState/LoadingState.tsx create mode 100644 src/shared/ui/loadingState/index.ts create mode 100644 src/widgets/mypageSection/index.ts create mode 100644 src/widgets/mypageSection/ui/MypageSection.tsx create mode 100644 src/widgets/mypageSection/ui/PinpointsSection.tsx create mode 100644 src/widgets/mypageSection/ui/ProfileSection.tsx create mode 100644 src/widgets/mypageSection/ui/SettingsSection.tsx create mode 100644 src/widgets/mypageSection/ui/WithdrawSection.tsx diff --git a/app/mypage/page.tsx b/app/mypage/page.tsx index 7e5c58d..fc01f77 100644 --- a/app/mypage/page.tsx +++ b/app/mypage/page.tsx @@ -1,83 +1,7 @@ "use client"; -import { SearchLine } from "@/src/assets/icons/home"; -import FootPrintIcon from "@/src/assets/icons/mypage/footPrintIcon"; -import MapPinIcon from "@/src/assets/icons/mypage/mapPinIcon"; -import RecentIcon from "@/src/assets/icons/mypage/recentIcon"; -import PinsetIcon from "@/src/assets/icons/mypage/pinsetIcon"; -import { MypageSection, UserInfoCard, PinReportSection } from "@/src/features/mypage/ui"; -import { useRouter } from "next/navigation"; -import { useOAuthStore } from "@/src/features/login/model/authStore"; - +import { MypageSection } from "@/src/widgets/mypageSection"; export default function MypagePage() { - const { userName } = useOAuthStore(); - const imageUrl = null; - const router = useRouter(); - return ( -
- {/* 사용자 정보 카드 */} - { - router.push("/mypage/settings"); - }} - /> - - {/* 핀 보고서 섹션 */} - { - router.push("/eligibility"); - }} - /> - - {/* 내 정보 섹션 */} - , - label: "관심 주변 환경 설정", - onClick: () => { - alert("관심 주변 환경 설정 미구현 상태"); - // TODO: 네비게이션 구현 - }, - }, - { - icon: , - label: "핀포인트 설정", - onClick: () => { - router.push("/mypage/pinpoints"); - }, - }, - ]} - /> - - {/* 내 활동 섹션 */} - , - label: "저장 목록", - onClick: () => { - alert("저장 목록 이동 미구현 상태"); - // TODO: 네비게이션 구현 - }, - }, - { - icon: , - label: "최근 본 공고", - onClick: () => { - alert("최근 본 공고 이동 미구현 상태"); - // TODO: 네비게이션 구현 - }, - }, - ]} - /> -
- ); + return ; } - diff --git a/app/mypage/pinpoints/page.tsx b/app/mypage/pinpoints/page.tsx index 0ca841f..816257f 100644 --- a/app/mypage/pinpoints/page.tsx +++ b/app/mypage/pinpoints/page.tsx @@ -1,50 +1,7 @@ "use client"; -import { MapPin } from "@/src/assets/icons/onboarding"; -import { useAddressStore } from "@/src/entities/address"; -import { AddressSearch } from "@/src/features/addressSearch"; -import { useAddPinpoint } from "@/src/features/mypage/hooks/useAddPinpoint"; -import { Button } from "@/src/shared/lib/headlessUi"; -import { useRouter } from "next/navigation"; +import { PinpointsSection } from "@/src/widgets/mypageSection"; -const MypagePinpointsPage = () => { - const title = "핀포인트 설정"; - const description = "나만의 핀포인트를 찍고\n원하는 거리 안의 임재주택을 찾아보세요!"; - const image = ; - const { address, pinPoint } = useAddressStore(); - const router = useRouter(); - const { addPinpoint, isLoading, isError, error } = useAddPinpoint({ - onSuccess: () => { - router.push("/mypage/settings"); // 주소 선택 후 이전 화면으로 - // 또는 목록 새로고침 등 - }, - onError: err => { - // 토스트 등 에러 처리 - }, - }); - return ( -
-
-
- {image} -
- {title &&

{title}

} -

{description}

-
- -
-
- {address ? ( - - ) : null} -
- ); -}; - -export default MypagePinpointsPage; +export default function MypagePinpointsPage() { + return ; +} diff --git a/app/mypage/profile/page.tsx b/app/mypage/profile/page.tsx index dd19974..6554475 100644 --- a/app/mypage/profile/page.tsx +++ b/app/mypage/profile/page.tsx @@ -1,26 +1,7 @@ "use client"; -import { ProfileForm } from "@/src/features/mypage/ui/profileForm"; -import { LeftButton } from "@/src/assets/icons/button/leftButton"; -import { useRouter } from "next/navigation"; -import { useOAuthStore } from "@/src/features/login/model/authStore"; +import { ProfileSection } from "@/src/widgets/mypageSection"; export default function ProfilePage() { - const { userName } = useOAuthStore(); - - // TODO: 로그인 시 or 프로필 조회 API로 실제 사용자 이메일, Provider 정보 필요 - const initialEmail = "로그인할때or프로필조회API로이메일_필요@naver.com"; // 예시 - const initialProvider = "kakao" as const; - return ( -
- {/* 프로필 폼 */} -
- -
-
- ); + return ; } diff --git a/app/mypage/settings/page.tsx b/app/mypage/settings/page.tsx index c497281..0313e43 100644 --- a/app/mypage/settings/page.tsx +++ b/app/mypage/settings/page.tsx @@ -1,35 +1,7 @@ "use client"; -import Link from "next/link"; -import { logout } from "@/src/features/login/utils/logout"; +import { SettingsSection } from "@/src/widgets/mypageSection"; export default function MypageSettingsPage() { - return ( -
-
- - 프로필 설정 - -
- - - - - 회원 탈퇴 - -
-
- ); + return ; } diff --git a/app/mypage/withdraw/page.tsx b/app/mypage/withdraw/page.tsx index 1825891..b5c48ba 100644 --- a/app/mypage/withdraw/page.tsx +++ b/app/mypage/withdraw/page.tsx @@ -1,25 +1,7 @@ "use client"; -import { WithdrawForm } from "@/src/features/mypage/ui/withdrawForm"; -import { WithdrawBanner } from "@/src/features/mypage/ui"; +import { WithdrawSection } from "@/src/widgets/mypageSection"; export default function WithdrawPage() { - return ( -
- {/* 상단 아이콘 및 문구 */} - - - {/* 구분선 */} -
- - {/* 탈퇴 폼 */} -
- -
-
- ); + return ; } diff --git a/next.config.ts b/next.config.ts index 130bac2..46abc39 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,6 +3,30 @@ import type { Configuration, RuleSetRule } from "webpack"; const nextConfig: NextConfig = { reactStrictMode: false, + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "img1.kakaocdn.net", + pathname: "/**", + }, + { + protocol: "http", + hostname: "img1.kakaocdn.net", + pathname: "/**", + }, + { + protocol: "https", + hostname: "t1.kakaocdn.net", + pathname: "/**", + }, + { + protocol: "http", + hostname: "t1.kakaocdn.net", + pathname: "/**", + }, + ], + }, webpack(config: Configuration) { const fileLoaderRule = config.module?.rules?.find( (rule): rule is RuleSetRule => diff --git a/src/features/login/model/auth.cilent.type.ts b/src/features/login/model/auth.cilent.type.ts index 63ff6c6..ffd5a2a 100644 --- a/src/features/login/model/auth.cilent.type.ts +++ b/src/features/login/model/auth.cilent.type.ts @@ -1,6 +1,8 @@ -export type OAuthProviderType = "KAKAO" | "NAVER"; +import { OAuthProviderType } from "@/src/shared/types"; -/*로그인폼*/ +export type { OAuthProviderType }; + +/** 로그인폼 */ export interface ILoginFormProps { onOuth2Login: (provider: OAuthProviderType) => void; } diff --git a/src/features/mypage/api/index.ts b/src/features/mypage/api/index.ts index da26b1a..dba7a7e 100644 --- a/src/features/mypage/api/index.ts +++ b/src/features/mypage/api/index.ts @@ -1 +1,3 @@ export * from "./withdrawApi"; +export * from "./mypageApi"; +export * from "./profileImageApi"; diff --git a/src/features/mypage/api/mypageApi.ts b/src/features/mypage/api/mypageApi.ts new file mode 100644 index 0000000..c4cfd27 --- /dev/null +++ b/src/features/mypage/api/mypageApi.ts @@ -0,0 +1,26 @@ +import { http } from "@/src/shared/api/http"; +import { + USER_MYPAGE_ENDPOINT, + USER_EDIT_MY_INFO_ENDPOINT, +} from "@/src/shared/api"; +import { MypageUserResponse } from "../model"; + +export const getMypageUser = () => { + return http.get(USER_MYPAGE_ENDPOINT); +}; + +export interface PatchMypageUserBody { + nickname?: string; + imageUrl?: string; +} + +/** + * 마이페이지 개인정보 수정 (닉네임 / 프로필 이미지 URL) + * PATCH /users/mypage + */ +export const patchMypageUser = (body: PatchMypageUserBody) => { + return http.patch( + USER_EDIT_MY_INFO_ENDPOINT, + body + ); +}; diff --git a/src/features/mypage/api/profileImageApi.ts b/src/features/mypage/api/profileImageApi.ts new file mode 100644 index 0000000..f64fc0f --- /dev/null +++ b/src/features/mypage/api/profileImageApi.ts @@ -0,0 +1,30 @@ +import { http } from "@/src/shared/api/http"; +import { IMAGES_PRESIGNED_URL_ENDPOINT } from "@/src/shared/api/endpoints"; +import type { IResponse } from "@/src/shared/types/response"; + +export interface PresignedUrlRequest { + fileName: string; + contentType: string; +} + +export interface PresignedUrlData { + /** PUT 업로드에 사용할 presigned URL */ + presignedUrl: string; + /** 업로드 완료 후 접근할 이미지 URL */ + imageUrl: string; + /** presigned URL 만료 시간(초) */ + expiresIn: number; +} + +export type PresignedUrlResponse = IResponse; + +/** + * 프로필 이미지 업로드용 presigned URL 발급 + * POST /v1/images/presigned-url + */ +export const getPresignedUrl = (body: PresignedUrlRequest) => { + return http.post( + IMAGES_PRESIGNED_URL_ENDPOINT, + body + ); +}; diff --git a/src/features/mypage/hooks/index.ts b/src/features/mypage/hooks/index.ts index 96d20b8..7889b91 100644 --- a/src/features/mypage/hooks/index.ts +++ b/src/features/mypage/hooks/index.ts @@ -1,2 +1,5 @@ export * from "./useWithdraw"; export * from "./useProfile"; +export * from "./useProfileNickname"; +export * from "./useProfilePhotoSheet"; +export * from "./useMypageUser"; diff --git a/src/features/mypage/hooks/useMypageUser.ts b/src/features/mypage/hooks/useMypageUser.ts new file mode 100644 index 0000000..0aaf429 --- /dev/null +++ b/src/features/mypage/hooks/useMypageUser.ts @@ -0,0 +1,19 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { getMypageUser } from "../api/mypageApi"; +import { mypageKeys } from "@/src/shared/config/queryKeys"; + +/** + * 마이페이지 진입 시 사용자 정보 조회 훅 + * GET /users/mypage + */ +export const useMypageUser = () => { + return useQuery({ + queryKey: mypageKeys.user(), + queryFn: getMypageUser, + select: (response) => response.data, // MypageUserData | undefined + staleTime: 5 * 60 * 1000, // 5분 + gcTime: 10 * 60 * 1000, // 10분 + }); +}; diff --git a/src/features/mypage/hooks/useProfile.ts b/src/features/mypage/hooks/useProfile.ts index 597529d..c39b9e2 100644 --- a/src/features/mypage/hooks/useProfile.ts +++ b/src/features/mypage/hooks/useProfile.ts @@ -1,39 +1,73 @@ import { useState } from "react"; -import { useRouter } from "next/navigation"; +import { useQueryClient } from "@tanstack/react-query"; +import { toast } from "sonner"; +import { mypageKeys } from "@/src/shared/config/queryKeys"; +import { getPresignedUrl } from "../api/profileImageApi"; +import { patchMypageUser } from "../api/mypageApi"; + +const MAX_PROFILE_IMAGE_SIZE = 10 * 1024 * 1024; // 10MB + +function getFileName(file: File): string { + const base = file.name ? file.name.replace(/\s+/g, "-") : "profile"; + const ext = file.type === "image/png" ? "png" : "jpg"; + return base.endsWith(`.${ext}`) ? base : `${base.split(".")[0] || "profile"}.${ext}`; +} export const useProfile = () => { - const router = useRouter(); + const queryClient = useQueryClient(); const [isLoading, setIsLoading] = useState(false); + const [isImageUploading, setIsImageUploading] = useState(false); const [profileImage, setProfileImage] = useState(null); const [profileImageUrl, setProfileImageUrl] = useState(null); - // TODO: 프로필 이미지 URL은 ReactQuery로 조회한 API 응답을 사용하도록 교체 - const handleImageUpload = (file: File) => { - // 파일 유효성 검사 + const handleImageUpload = async (file: File) => { if (!file.type.startsWith("image/")) { - console.error("이미지 파일만 업로드 가능합니다."); + toast.error("이미지 파일만 업로드 가능합니다."); return; } - - // 파일 크기 제한 (예: 5MB) - if (file.size > 10 * 1024 * 1024) { - console.error("파일 크기는 5MB 이하여야 합니다."); + if (file.size > MAX_PROFILE_IMAGE_SIZE) { + toast.error("파일 크기는 10MB 이하여야 합니다."); return; } - setProfileImage(file); + setIsImageUploading(true); + try { + // 1. presigned URL 요청 + const fileName = getFileName(file); + const contentType = file.type || "image/jpeg"; + const { data } = await getPresignedUrl({ fileName, contentType }); + const presignedUrl = data?.presignedUrl; + const imageUrl = data?.imageUrl; + if (!presignedUrl || !imageUrl) { + toast.error("이미지 업로드 준비에 실패했어요. 잠시 후 다시 시도해주세요."); + return; + } - // 미리보기 URL 생성 - const url = URL.createObjectURL(file); - setProfileImageUrl(prevUrl => { - if (prevUrl) { - URL.revokeObjectURL(prevUrl); + // 2. presigned URL로 PUT 업로드 + const putRes = await fetch(presignedUrl, { + method: "PUT", + body: file, + headers: { "Content-Type": contentType }, + }); + if (!putRes.ok) { + toast.error("이미지 업로드에 실패했어요. 잠시 후 다시 시도해주세요."); + return; } - return url; - }); - // TODO: 선택된 파일을 백엔드에 업로드하는 API 호출로 교체 - // ex: await uploadProfileImage(file); + // 3. PATCH로 서버에 반영 + await patchMypageUser({ imageUrl }); + + queryClient.invalidateQueries({ queryKey: mypageKeys.user() }); + + // 3단계 모두 성공 시에만 이미지 반영 + setProfileImage(file); + setProfileImageUrl(imageUrl); + } catch (err) { + console.error("프로필 이미지 업로드 실패:", err); + toast.error("프로필 이미지 변경에 실패했어요. 잠시 후 다시 시도해주세요."); + } finally { + setIsImageUploading(false); + } }; const handleRemovePhoto = () => { @@ -49,12 +83,18 @@ export const useProfile = () => { // ex: await removeProfileImage(); }; - const handleProfileUpdate = async (nickname: string) => { + const updateMypageProfile = async (payload: { + nickname?: string; + imageUrl?: string; + }) => { + if (!payload.nickname && !payload.imageUrl) return; setIsLoading(true); try { - // TODO: 실제 API 호출 - // await updateProfile({ nickname, profileImage }); - console.log("프로필 업데이트:", { nickname, profileImage }); + await patchMypageUser({ + ...(payload.nickname !== undefined && { nickname: payload.nickname }), + ...(payload.imageUrl !== undefined && { imageUrl: payload.imageUrl }), + }); + queryClient.invalidateQueries({ queryKey: mypageKeys.user() }); } catch (error) { console.error("프로필 업데이트 실패:", error); } finally { @@ -62,12 +102,21 @@ export const useProfile = () => { } }; + const handleProfileUpdate = async (nickname: string) => { + await updateMypageProfile({ + nickname, + ...(profileImageUrl && { imageUrl: profileImageUrl }), + }); + }; + return { profileImage, profileImageUrl, isLoading, + isImageUploading, handleImageUpload, handleRemovePhoto, handleProfileUpdate, + updateMypageProfile, }; }; diff --git a/src/features/mypage/hooks/useProfileNickname.ts b/src/features/mypage/hooks/useProfileNickname.ts new file mode 100644 index 0000000..e0e7f6b --- /dev/null +++ b/src/features/mypage/hooks/useProfileNickname.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from "react"; + +/** + * 유저 닉네임과 동기화되는 편집 가능한 닉네임 state + * initialNickname이 바뀌면 로컬 state도 갱신됨 + */ +export function useProfileNickname(initialNickname: string) { + const [nickname, setNickname] = useState(initialNickname); + + useEffect(() => { + setNickname(initialNickname); + }, [initialNickname]); + + return [nickname, setNickname] as const; +} diff --git a/src/features/mypage/hooks/useProfilePhotoSheet.ts b/src/features/mypage/hooks/useProfilePhotoSheet.ts new file mode 100644 index 0000000..3331c07 --- /dev/null +++ b/src/features/mypage/hooks/useProfilePhotoSheet.ts @@ -0,0 +1,54 @@ +"use client"; + +import { useRef, useState } from "react"; +import { useProfile } from "./useProfile"; + +/** + * 프로필 사진 변경 시트 + 앨범 선택/삭제 핸들러 + * useProfile 기반으로 시트 열기/닫기 및 파일 입력 처리 + */ +export function useProfilePhotoSheet() { + const [isSheetOpen, setIsSheetOpen] = useState(false); + const fileInputRef = useRef(null); + const { + handleImageUpload, + handleRemovePhoto, + profileImageUrl, + isImageUploading, + isLoading, + handleProfileUpdate, + } = useProfile(); + + const handleCameraClick = () => setIsSheetOpen(true); + + const handleSelectFromAlbum = () => { + fileInputRef.current?.click(); + }; + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + handleImageUpload(file); + setIsSheetOpen(false); + } + }; + + const handleRemovePhotoClick = () => { + handleRemovePhoto(); + setIsSheetOpen(false); + }; + + return { + isSheetOpen, + setIsSheetOpen, + fileInputRef, + profileImageUrl, + isImageUploading, + isLoading, + handleCameraClick, + handleSelectFromAlbum, + handleFileChange, + handleRemovePhotoClick, + handleProfileUpdate, + }; +} diff --git a/src/features/mypage/hooks/useWithdraw.ts b/src/features/mypage/hooks/useWithdraw.ts index 96819ad..8f4a261 100644 --- a/src/features/mypage/hooks/useWithdraw.ts +++ b/src/features/mypage/hooks/useWithdraw.ts @@ -1,5 +1,6 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; +import { toast } from "sonner"; import { withdrawUser } from "../api/withdrawApi"; import { logout } from "@/src/features/login/utils/logout"; import { WITHDRAW_REASONS } from "../model/mypageConstants"; @@ -29,6 +30,7 @@ export const useWithdraw = () => { //router.push("/login"); } catch (error) { console.error("탈퇴 실패:", error); + toast.error("탈퇴 처리에 실패했어요. 잠시 후 다시 시도해주세요."); setIsLoading(false); setIsModalOpen(false); } diff --git a/src/features/mypage/model/index.ts b/src/features/mypage/model/index.ts index 521caa7..0a6c498 100644 --- a/src/features/mypage/model/index.ts +++ b/src/features/mypage/model/index.ts @@ -1,2 +1,3 @@ export * from "./withdraw.type"; export * from "./profile.type"; +export * from "./mypageUser.type"; diff --git a/src/features/mypage/model/mypageConstants.ts b/src/features/mypage/model/mypageConstants.ts index 5be44b6..307ab34 100644 --- a/src/features/mypage/model/mypageConstants.ts +++ b/src/features/mypage/model/mypageConstants.ts @@ -1,3 +1,51 @@ +/** 마이페이지 메인 화면 문구 */ +export const MYPAGE_LOADING_TITLE = "마이페이지 불러오는 중"; +export const MYPAGE_LOADING_DESCRIPTION = "잠시만 기다려주세요."; +export const MYPAGE_ERROR_TEXT = + "정보를 가져오지 못했어요
네트워크 상태를 확인하거나 잠시 후 다시 시도해주세요."; + +/** 프로필 화면 문구 */ +export const MYPAGE_PROFILE_LOADING_TITLE = "프로필 불러오는 중"; + +export const MYPAGE_SECTION_MY_INFO = "내 정보"; +export const MYPAGE_SECTION_MY_ACTIVITY = "내 활동"; + +export const MYPAGE_LABEL_INTEREST_ENV = "관심 주변 환경 설정"; +export const MYPAGE_LABEL_PINPOINTS = "핀포인트 설정"; +/** 핀포인트 설정 페이지 (pinpoints) */ +export const MYPAGE_PINPOINTS_DESCRIPTION = + "나만의 핀포인트를 찍고\n원하는 거리 안의 임대주택을 찾아보세요!"; +export const MYPAGE_PINPOINTS_ADD_BUTTON = "핀포인트 추가"; +export const MYPAGE_PINPOINTS_DEFAULT_NAME = "핀 포인트"; + +export const MYPAGE_LABEL_SAVED_LIST = "저장 목록"; +export const MYPAGE_LABEL_RECENT_ADS = "최근 본 공고"; + +/** PinReportSection */ +export const MYPAGE_PIN_REPORT_TITLE = "핀 보고서"; +export const MYPAGE_PIN_REPORT_DESCRIPTION_LINES = [ + "자격진단으로", + "임대주택 지원 가능 여부를 확인하고", + "맞춤 보고서를 받아보세요", +] as const; +export const MYPAGE_PIN_REPORT_BUTTON = "자격진단 하러가기"; + +/** 설정 화면 (settings) */ +export const MYPAGE_SETTINGS_PROFILE = "프로필 설정"; +export const MYPAGE_SETTINGS_LOGOUT = "로그아웃"; +export const MYPAGE_SETTINGS_WITHDRAW = "회원 탈퇴"; + +/** UserInfoCard 등 기본값/접근성 문구 */ +export const MYPAGE_DEFAULT_USER_NAME = "유저명"; +export const MYPAGE_DEFAULT_USER_EMAIL = "userid@email.com"; +export const MYPAGE_PROFILE_IMAGE_ALT = "프로필 사진"; + +/** 탈퇴 관련 */ +export const WITHDRAW_BANNER_TITLE = + "그동안 핀하우스를 이용해 주셔서 감사합니다."; +export const WITHDRAW_BANNER_DESCRIPTION = + "탈퇴 사유를 알려주시면 서비스 개선에 참고하겠습니다."; + export const WITHDRAW_REASONS = [ { id: "1", label: "추천 결과가 내 조건과 잘 맞지 않아요" }, { id: "2", label: "원하는 공고/단지가 부족해요" }, diff --git a/src/features/mypage/model/mypageUser.type.ts b/src/features/mypage/model/mypageUser.type.ts new file mode 100644 index 0000000..d1250ec --- /dev/null +++ b/src/features/mypage/model/mypageUser.type.ts @@ -0,0 +1,21 @@ +import { IResponse, OAuthProviderType } from "@/src/shared/types"; + +/** GET /users/mypage 응답의 data 필드 */ +export interface MypageUserData { + userId: string; + provider: OAuthProviderType; + name: string; + nickName: string; + email: string; + phoneNumber: string | null; + role: string; + gender: string; + profileImage: string; + birthday: string; + facilityTypes: string[]; +} + +/** 공통 응답 확장 */ +export interface MypageUserResponse extends IResponse { + data: MypageUserData; +} diff --git a/src/features/mypage/model/profile.type.ts b/src/features/mypage/model/profile.type.ts index 6b9bb73..3d88b4f 100644 --- a/src/features/mypage/model/profile.type.ts +++ b/src/features/mypage/model/profile.type.ts @@ -1,8 +1,10 @@ +import { OAuthProviderType } from "@/src/shared/types"; + export interface ProfileData { nickname: string; email: string; profileImageUrl?: string | null; - provider?: "naver" | "kakao" | "google"; + provider?: OAuthProviderType; badgeCount?: number; } diff --git a/src/features/mypage/ui/index.ts b/src/features/mypage/ui/index.ts index 8edd366..a762c4b 100644 --- a/src/features/mypage/ui/index.ts +++ b/src/features/mypage/ui/index.ts @@ -9,3 +9,4 @@ export * from "./mypageSection"; export * from "./userInfoCard"; export * from "./pinReportSection"; export * from "./mypageMenuItem"; +export * from "./mypageSettingsMenu"; diff --git a/src/features/mypage/ui/mypageSection.tsx b/src/features/mypage/ui/mypageSection.tsx index 62f09ae..cb9c81e 100644 --- a/src/features/mypage/ui/mypageSection.tsx +++ b/src/features/mypage/ui/mypageSection.tsx @@ -1,25 +1,33 @@ "use client"; -import { ReactNode } from "react"; +import { useId } from "react"; import { MypageMenuItem, MypageMenuItemProps } from "./mypageMenuItem"; -export interface MypageSectionProps { - title: string; - items: MypageMenuItemProps[]; +export interface MypageMenuSectionProps { + title: string; + items: MypageMenuItemProps[]; } -export const MypageSection = ({ title, items }: MypageSectionProps) => { - return ( -
-
-

- {title} -

-
- {items.map((item, index) => ( - - ))} -
- ); +export const MypageMenuSection = ({ title, items }: MypageMenuSectionProps) => { + const headingId = useId(); + + return ( +
+
+

+ {title} +

+
+ {items.map((item, index) => ( + + ))} +
+ ); }; diff --git a/src/features/mypage/ui/mypageSettingsMenu.tsx b/src/features/mypage/ui/mypageSettingsMenu.tsx new file mode 100644 index 0000000..f91c5da --- /dev/null +++ b/src/features/mypage/ui/mypageSettingsMenu.tsx @@ -0,0 +1,41 @@ +"use client"; + +import Link from "next/link"; + +const ITEM_CLASS = + "font-regular block w-full px-5 py-3 text-base leading-[140%] tracking-[-0.02em] text-greyscale-grey-900 hover:no-underline"; +const DIVIDER_CLASS = + "-mx-[20px] h-[9px] border-t border-greyscale-grey-50 bg-greyscale-grey-25"; + +export type MypageSettingsMenuItem = + | { type: "link"; label: string; href: string } + | { type: "button"; label: string; onClick: () => void }; + +export interface MypageSettingsMenuProps { + items: MypageSettingsMenuItem[]; +} + +export const MypageSettingsMenu = ({ items }: MypageSettingsMenuProps) => { + return ( +
+ {items.map((item, index) => ( + + {index == 1 &&
} + {item.type === "link" ? ( + + {item.label} + + ) : ( + + )} + + ))} +
+ ); +}; diff --git a/src/features/mypage/ui/pinReportSection.tsx b/src/features/mypage/ui/pinReportSection.tsx index 1009481..79ac1fe 100644 --- a/src/features/mypage/ui/pinReportSection.tsx +++ b/src/features/mypage/ui/pinReportSection.tsx @@ -1,35 +1,56 @@ "use client"; import { Button } from "@/src/shared/lib/headlessUi"; +import { + MYPAGE_PIN_REPORT_BUTTON, + MYPAGE_PIN_REPORT_DESCRIPTION_LINES, + MYPAGE_PIN_REPORT_TITLE, +} from "@/src/features/mypage/model/mypageConstants"; interface PinReportSectionProps { - onDiagnosisClick?: () => void; + onDiagnosisClick?: () => void; } -export const PinReportSection = ({ - onDiagnosisClick -}: PinReportSectionProps) => { - return ( -
-
-

- 핀 보고서 -

-
-
-

- 자격진단으로 -
- 임대주택 지원 가능 여부를 확인하고 -
- 맞춤 보고서를 받아보세요 -

- -
- +const PIN_REPORT_HEADING_ID = "pin-report-heading"; -
- ); +export const PinReportSection = ({ + onDiagnosisClick, +}: PinReportSectionProps) => { + return ( +
+
+

+ {MYPAGE_PIN_REPORT_TITLE} +

+
+
+

+ {MYPAGE_PIN_REPORT_DESCRIPTION_LINES.map((line, i) => ( + + {i > 0 &&
} + {line} +
+ ))} +

+ +
+
+ ); }; diff --git a/src/features/mypage/ui/profileAvatar.tsx b/src/features/mypage/ui/profileAvatar.tsx index 9bfb1bf..a824088 100644 --- a/src/features/mypage/ui/profileAvatar.tsx +++ b/src/features/mypage/ui/profileAvatar.tsx @@ -8,13 +8,20 @@ import { ProfileDefaultImg } from "@/src/assets/images/mypage/ProfileDefaultImg" export interface ProfileAvatarProps { /** 프로필 이미지 URL */ imageUrl?: string | null; + /** 이미지 업로드 중 여부 (로딩 오버레이 표시) */ + isUploading?: boolean; /** 카메라 아이콘 클릭 핸들러 */ onCameraClick?: () => void; /** 추가 클래스명 */ className?: string; } -export const ProfileAvatar = ({ imageUrl, onCameraClick, className }: ProfileAvatarProps) => { +export const ProfileAvatar = ({ + imageUrl, + isUploading = false, + onCameraClick, + className, +}: ProfileAvatarProps) => { return (
{/* 프로필 이미지 */} @@ -26,12 +33,18 @@ export const ProfileAvatar = ({ imageUrl, onCameraClick, className }: ProfileAva
)} + {isUploading && ( +
+ +
+ )}
{/* 카메라 아이콘 */} -
- ); +export const UserInfoCard = ({ user, onSettingsClick }: UserInfoCardProps) => { + const profileImageUrl = user?.profileImage?.trim() || null; + const userName = user?.nickName ?? MYPAGE_DEFAULT_USER_NAME; + const userEmail = user?.email ?? MYPAGE_DEFAULT_USER_EMAIL; + + return ( +
+
+ {profileImageUrl ? ( + {MYPAGE_PROFILE_IMAGE_ALT} + ) : ( + + )} +
+
+ + {userName} + + + {userEmail} + +
+ +
+ ); }; diff --git a/src/features/mypage/ui/withdrawForm.tsx b/src/features/mypage/ui/withdrawForm.tsx index fe906eb..f8976a4 100644 --- a/src/features/mypage/ui/withdrawForm.tsx +++ b/src/features/mypage/ui/withdrawForm.tsx @@ -6,6 +6,8 @@ import { WITHDRAW_BUTTON_TEXT, WITHDRAW_REASONS, WITHDRAW_TITLE } from "../model import { SurveyButton } from "@/src/shared/ui/button/surveyButton"; import { Modal } from "@/src/shared/ui/modal/default/modal"; +const WITHDRAW_REASONS_HEADING_ID = "withdraw-reasons-heading"; + export const WithdrawForm = () => { const { selectedReasons, @@ -14,41 +16,35 @@ export const WithdrawForm = () => { handleWithdrawConfirm, handleModalCancel, isModalOpen, + isLoading, } = useWithdraw(); - const handleModalButtonClick = (buttonIndex: number, buttonLabel: string) => { + const handleModalButtonClick = (buttonIndex: number, _buttonLabel: string) => { if (buttonIndex === 0) { - // 취소 버튼 handleModalCancel(); } else if (buttonIndex === 1) { - // 탈퇴하기 버튼 handleWithdrawConfirm(); } }; const handleOptionClick = (optionId: string) => { - let newSelectedIds: string[]; - - if (selectedReasons.includes(optionId)) { - // 이미 선택된 경우 제거 - newSelectedIds = selectedReasons.filter(id => id !== optionId); - } else { - // 선택되지 않은 경우 추가 - newSelectedIds = [...selectedReasons, optionId]; - } - + const newSelectedIds = selectedReasons.includes(optionId) + ? selectedReasons.filter(id => id !== optionId) + : [...selectedReasons, optionId]; handleReasonsChange(newSelectedIds); }; return ( <>
- {/* 탈퇴 사유 선택 */} -
-

+
+

{WITHDRAW_TITLE}

-
+
{WITHDRAW_REASONS.map(reason => { const isSelected = selectedReasons.includes(reason.id); return ( @@ -57,29 +53,31 @@ export const WithdrawForm = () => { title={reason.label} pressed={isSelected} onPressedChange={() => handleOptionClick(reason.id)} - className={"w-full pl-5 text-sm"} + className="w-full pl-5 text-sm" /> ); })}
-
+
- {/* 탈퇴하기 버튼 */}

- {/* 탈퇴 확인 모달 */} - + ); }; diff --git a/src/shared/api/endpoints.ts b/src/shared/api/endpoints.ts index e7ea4c4..b24c214 100644 --- a/src/shared/api/endpoints.ts +++ b/src/shared/api/endpoints.ts @@ -138,3 +138,8 @@ export const SCHOOL_SEARCH_ENDPOINT = "/school/search"; * 좋아요 API */ export const LIKE_ENDPOINT = "/likes"; + +/** + * 이미지 API + */ +export const IMAGES_PRESIGNED_URL_ENDPOINT = "/images/presigned-url"; diff --git a/src/shared/config/queryKeys.ts b/src/shared/config/queryKeys.ts index ea893ae..ce38fb2 100644 --- a/src/shared/config/queryKeys.ts +++ b/src/shared/config/queryKeys.ts @@ -18,6 +18,12 @@ export const pinPointKeys = { detail: (id: string) => [...pinPointKeys.details(), id] as const, } as const; +// Mypage 관련 QueryKeys +export const mypageKeys = { + all: ["mypage"] as const, + user: () => [...mypageKeys.all, "user"] as const, +} as const; + // Listing 관련 QueryKeys export const listingKeys = { all: ["listing"] as const, diff --git a/src/shared/types/auth.ts b/src/shared/types/auth.ts new file mode 100644 index 0000000..54417ac --- /dev/null +++ b/src/shared/types/auth.ts @@ -0,0 +1,5 @@ +/** + * OAuth 로그인 제공자 타입 + * 로그인/마이페이지 등에서 공통 사용 + */ +export type OAuthProviderType = "KAKAO" | "NAVER"; diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 9d2cfb1..b2607bb 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -1 +1,2 @@ export * from "./response"; +export * from "./auth"; diff --git a/src/shared/ui/bottomNavigation/bottomNavigation.tsx b/src/shared/ui/bottomNavigation/bottomNavigation.tsx index a551641..e2a9f9a 100644 --- a/src/shared/ui/bottomNavigation/bottomNavigation.tsx +++ b/src/shared/ui/bottomNavigation/bottomNavigation.tsx @@ -40,7 +40,7 @@ function BottomNavigationContent() { compareDetailPageRegex.test(pathname) || (pathname === "/home" && searchParams.has("mode")); - const isMypageActive = pathname === "/mypage" || pathname.startsWith("/mypage/"); + const isMypageActive = pathname === "/mypage" || pathname.startsWith("/mypage/"); if (shouldHide) return null; return (
diff --git a/src/shared/ui/bottomNavigation/frameBottomNavigation.tsx b/src/shared/ui/bottomNavigation/frameBottomNavigation.tsx index 3af48d3..d821b49 100644 --- a/src/shared/ui/bottomNavigation/frameBottomNavigation.tsx +++ b/src/shared/ui/bottomNavigation/frameBottomNavigation.tsx @@ -47,6 +47,7 @@ export const FrameBottomNav = () => { // detailPageRegex.test(pathname) || // compareDetailPageRegex.test(pathname) || // (pathname === "/home" && searchParams.has("mode")); + const isMypageActive = pathname === "/mypage" || pathname.startsWith("/mypage/"); if (shouldHide) return null; return ( @@ -77,13 +78,13 @@ export const FrameBottomNav = () => { diff --git a/src/shared/ui/errorState/ErrorState.tsx b/src/shared/ui/errorState/ErrorState.tsx new file mode 100644 index 0000000..a375d6c --- /dev/null +++ b/src/shared/ui/errorState/ErrorState.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { SearchEmpty } from "@/src/assets/icons/home/searchEmpty"; +import { Button } from "@/src/shared/lib/headlessUi"; +import { AnimatePresence, motion } from "framer-motion"; + +export interface ErrorStateProps { + /** 에러 메시지 (
로 줄바꿈) */ + text?: string; + /** 버튼 클릭 시 호출 (미전달 시 /home 이동) */ + onClick?: () => void; + /** wrapper className */ + className?: string; +} + +/** + * 공용 에러 UI + * ListingNoSearchResult 스타일: 아이콘 + 메시지 + home으로 돌아가기 버튼 + */ +export const ErrorState = ({ + text = "에러가 발생했습니다.", + onClick, + className, +}: ErrorStateProps) => { + const router = useRouter(); + const lines = text.split("
"); + const handleButtonClick = onClick ?? (() => router.push("/home")); + + return ( +
+ + + + {lines.map((line, idx) => ( +

+ {line} +

+ ))} + +
+
+
+ ); +}; diff --git a/src/shared/ui/errorState/index.ts b/src/shared/ui/errorState/index.ts new file mode 100644 index 0000000..fd710f3 --- /dev/null +++ b/src/shared/ui/errorState/index.ts @@ -0,0 +1 @@ +export * from "./ErrorState"; diff --git a/src/shared/ui/loadingState/LoadingState.tsx b/src/shared/ui/loadingState/LoadingState.tsx new file mode 100644 index 0000000..facada1 --- /dev/null +++ b/src/shared/ui/loadingState/LoadingState.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { Spinner } from "@/src/shared/ui/spinner/default"; + +export interface LoadingStateProps { + /** 로딩 제목 */ + title?: string; + /** 로딩 설명 */ + description?: string; + /** 최소 높이 (기본: 화면 전체) */ + className?: string; +} + +/** + * 공용 로딩 UI + * 섹션/전체 화면 로딩 시 사용 + */ +export const LoadingState = ({ + title = "로딩 중", + description = "잠시만 기다려주세요.", + className, +}: LoadingStateProps) => { + return ( +
+ +
+ ); +}; diff --git a/src/shared/ui/loadingState/index.ts b/src/shared/ui/loadingState/index.ts new file mode 100644 index 0000000..47d4edd --- /dev/null +++ b/src/shared/ui/loadingState/index.ts @@ -0,0 +1 @@ +export * from "./LoadingState"; diff --git a/src/shared/ui/modal/default/modal.tsx b/src/shared/ui/modal/default/modal.tsx index f0df3bb..ceb8746 100644 --- a/src/shared/ui/modal/default/modal.tsx +++ b/src/shared/ui/modal/default/modal.tsx @@ -12,6 +12,7 @@ export const Modal = ({ className, overlayClassName, onButtonClick, + confirmButtonDisabled = false, }: ModalProps) => { if (!open) return null; @@ -33,10 +34,12 @@ export const Modal = ({ {modalScript?.btnlabel?.map((item, index) => ( + ) : null} +
+ ); +}; diff --git a/src/widgets/mypageSection/ui/ProfileSection.tsx b/src/widgets/mypageSection/ui/ProfileSection.tsx new file mode 100644 index 0000000..c03750f --- /dev/null +++ b/src/widgets/mypageSection/ui/ProfileSection.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { useMypageUser } from "@/src/features/mypage/hooks"; +import { + MYPAGE_ERROR_TEXT, + MYPAGE_LOADING_DESCRIPTION, + MYPAGE_PROFILE_LOADING_TITLE, +} from "@/src/features/mypage/model/mypageConstants"; +import { ProfileForm } from "@/src/features/mypage/ui"; +import { ErrorState } from "@/src/shared/ui/errorState"; +import { LoadingState } from "@/src/shared/ui/loadingState"; + +/** + * 마이페이지 프로필 화면 위젯 + * - useMypageUser 호출 및 로딩/에러 처리 + * - ProfileForm 렌더 + */ +export const ProfileSection = () => { + const { data, isLoading, isError } = useMypageUser(); + + if (isLoading) { + return ( + + ); + } + + if (isError) { + return ( + + ); + } + + return ( +
+
+ +
+
+ ); +}; diff --git a/src/widgets/mypageSection/ui/SettingsSection.tsx b/src/widgets/mypageSection/ui/SettingsSection.tsx new file mode 100644 index 0000000..c042b52 --- /dev/null +++ b/src/widgets/mypageSection/ui/SettingsSection.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { logout } from "@/src/features/login/utils/logout"; +import { + MYPAGE_SETTINGS_LOGOUT, + MYPAGE_SETTINGS_PROFILE, + MYPAGE_SETTINGS_WITHDRAW, +} from "@/src/features/mypage/model/mypageConstants"; +import { + MypageSettingsMenu, + type MypageSettingsMenuItem, +} from "@/src/features/mypage/ui"; + +/** + * 마이페이지 설정 화면 위젯 + * - 프로필 설정 / 로그아웃 / 회원 탈퇴 메뉴 + */ +export const SettingsSection = () => { + const menuItems: MypageSettingsMenuItem[] = [ + { type: "link", label: MYPAGE_SETTINGS_PROFILE, href: "/mypage/profile" }, + { type: "button", label: MYPAGE_SETTINGS_LOGOUT, onClick: logout }, + { type: "link", label: MYPAGE_SETTINGS_WITHDRAW, href: "/mypage/withdraw" }, + ]; + + return ( +
+ +
+ ); +}; diff --git a/src/widgets/mypageSection/ui/WithdrawSection.tsx b/src/widgets/mypageSection/ui/WithdrawSection.tsx new file mode 100644 index 0000000..160ac94 --- /dev/null +++ b/src/widgets/mypageSection/ui/WithdrawSection.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { WithdrawBanner, WithdrawForm } from "@/src/features/mypage/ui"; +import { + WITHDRAW_BANNER_DESCRIPTION, + WITHDRAW_BANNER_TITLE, +} from "@/src/features/mypage/model/mypageConstants"; + +/** + * 마이페이지 회원 탈퇴 화면 위젯 + */ +export const WithdrawSection = () => { + return ( +
+ +
+
+ +
+
+ ); +}; From 00ed533760c1960f30fb05e7c159b834e2ceb94f Mon Sep 17 00:00:00 2001 From: JW_Ahn0 Date: Fri, 13 Feb 2026 13:12:50 +0900 Subject: [PATCH 07/18] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B3=B5=ED=86=B5=20=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/mypage/model/mypageConstants.ts | 8 +++ src/features/mypage/ui/index.ts | 1 + src/features/mypage/ui/myPageHeader.tsx | 24 ++++++++ .../homeSearchSection/homeSearchSection.tsx | 3 - .../mypageSection/ui/MypageSection.tsx | 6 +- .../mypageSection/ui/PinpointsSection.tsx | 58 +++++++++++-------- .../mypageSection/ui/ProfileSection.tsx | 16 +++-- .../mypageSection/ui/SettingsSection.tsx | 12 +++- .../mypageSection/ui/WithdrawSection.tsx | 6 ++ 9 files changed, 98 insertions(+), 36 deletions(-) create mode 100644 src/features/mypage/ui/myPageHeader.tsx diff --git a/src/features/mypage/model/mypageConstants.ts b/src/features/mypage/model/mypageConstants.ts index 307ab34..6b83e51 100644 --- a/src/features/mypage/model/mypageConstants.ts +++ b/src/features/mypage/model/mypageConstants.ts @@ -1,10 +1,15 @@ /** 마이페이지 메인 화면 문구 */ +export const MYPAGE_TITLE = "마이페이지"; +/** 마이페이지 헤더에 표시할 제목 (공백 포함) */ +export const MYPAGE_HEADER_TITLE = "마이 페이지"; +export const MYPAGE_HEADER_SEARCH_PLACEHOLDER = "검색"; export const MYPAGE_LOADING_TITLE = "마이페이지 불러오는 중"; export const MYPAGE_LOADING_DESCRIPTION = "잠시만 기다려주세요."; export const MYPAGE_ERROR_TEXT = "정보를 가져오지 못했어요
네트워크 상태를 확인하거나 잠시 후 다시 시도해주세요."; /** 프로필 화면 문구 */ +export const MYPAGE_PROFILE_HEADER_TITLE = "프로필 설정"; export const MYPAGE_PROFILE_LOADING_TITLE = "프로필 불러오는 중"; export const MYPAGE_SECTION_MY_INFO = "내 정보"; @@ -13,6 +18,7 @@ export const MYPAGE_SECTION_MY_ACTIVITY = "내 활동"; export const MYPAGE_LABEL_INTEREST_ENV = "관심 주변 환경 설정"; export const MYPAGE_LABEL_PINPOINTS = "핀포인트 설정"; /** 핀포인트 설정 페이지 (pinpoints) */ +export const MYPAGE_PINPOINTS_HEADER_TITLE = "핀포인트 설정"; export const MYPAGE_PINPOINTS_DESCRIPTION = "나만의 핀포인트를 찍고\n원하는 거리 안의 임대주택을 찾아보세요!"; export const MYPAGE_PINPOINTS_ADD_BUTTON = "핀포인트 추가"; @@ -31,6 +37,7 @@ export const MYPAGE_PIN_REPORT_DESCRIPTION_LINES = [ export const MYPAGE_PIN_REPORT_BUTTON = "자격진단 하러가기"; /** 설정 화면 (settings) */ +export const MYPAGE_SETTINGS_TITLE = "설정"; export const MYPAGE_SETTINGS_PROFILE = "프로필 설정"; export const MYPAGE_SETTINGS_LOGOUT = "로그아웃"; export const MYPAGE_SETTINGS_WITHDRAW = "회원 탈퇴"; @@ -41,6 +48,7 @@ export const MYPAGE_DEFAULT_USER_EMAIL = "userid@email.com"; export const MYPAGE_PROFILE_IMAGE_ALT = "프로필 사진"; /** 탈퇴 관련 */ +export const MYPAGE_WITHDRAW_HEADER_TITLE = "회원 탈퇴"; export const WITHDRAW_BANNER_TITLE = "그동안 핀하우스를 이용해 주셔서 감사합니다."; export const WITHDRAW_BANNER_DESCRIPTION = diff --git a/src/features/mypage/ui/index.ts b/src/features/mypage/ui/index.ts index a762c4b..6dd5a39 100644 --- a/src/features/mypage/ui/index.ts +++ b/src/features/mypage/ui/index.ts @@ -1,5 +1,6 @@ export * from "./withdrawForm"; export * from "./withdrawBanner"; +export * from "./myPageHeader"; export * from "./profileForm"; export * from "./profileAvatar"; export * from "./profileNicknameInput"; diff --git a/src/features/mypage/ui/myPageHeader.tsx b/src/features/mypage/ui/myPageHeader.tsx new file mode 100644 index 0000000..1d4e8a5 --- /dev/null +++ b/src/features/mypage/ui/myPageHeader.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { SearchLine } from "@/src/assets/icons/home"; +import { MYPAGE_HEADER_TITLE } from "@/src/features/mypage/model/mypageConstants"; +import { useHomeHeaderHooks } from "@/src/widgets/homeSection/hooks/homeHeaderHooks"; +import { useRouter } from "next/navigation"; + +/** + * 마이페이지 전용 헤더 (로고 없음, 제목 텍스트만) + */ +export const MyPageHeader = () => { + const router = useRouter(); + + return ( +
+

{MYPAGE_HEADER_TITLE}

+
+ +
+
+ ); +}; diff --git a/src/widgets/homeSection/homeSearchSection/homeSearchSection.tsx b/src/widgets/homeSection/homeSearchSection/homeSearchSection.tsx index 1512c06..b4cf009 100644 --- a/src/widgets/homeSection/homeSearchSection/homeSearchSection.tsx +++ b/src/widgets/homeSection/homeSearchSection/homeSearchSection.tsx @@ -1,7 +1,4 @@ "use client"; -import { LeftButton } from "@/src/assets/icons/button"; -import { SearchBar } from "@/src/features/home"; -import { useRouter } from "next/navigation"; import { SearchHeader } from "@/src/shared/ui/header/header/searchHeader/searchHeader"; import { useSearchState } from "@/src/shared/hooks/store"; diff --git a/src/widgets/mypageSection/ui/MypageSection.tsx b/src/widgets/mypageSection/ui/MypageSection.tsx index 8d4ad3b..f7e57cc 100644 --- a/src/widgets/mypageSection/ui/MypageSection.tsx +++ b/src/widgets/mypageSection/ui/MypageSection.tsx @@ -19,6 +19,7 @@ import { } from "@/src/features/mypage/model/mypageConstants"; import { MypageMenuSection, + MyPageHeader, PinReportSection, UserInfoCard, } from "@/src/features/mypage/ui"; @@ -54,7 +55,9 @@ export const MypageSection = () => { } return ( -
+
+ +
router.push("/mypage/settings")} @@ -101,6 +104,7 @@ export const MypageSection = () => { }, ]} /> +
); }; diff --git a/src/widgets/mypageSection/ui/PinpointsSection.tsx b/src/widgets/mypageSection/ui/PinpointsSection.tsx index 33f9490..95b3e3e 100644 --- a/src/widgets/mypageSection/ui/PinpointsSection.tsx +++ b/src/widgets/mypageSection/ui/PinpointsSection.tsx @@ -11,8 +11,10 @@ import { MYPAGE_PINPOINTS_ADD_BUTTON, MYPAGE_PINPOINTS_DEFAULT_NAME, MYPAGE_PINPOINTS_DESCRIPTION, + MYPAGE_PINPOINTS_HEADER_TITLE, } from "@/src/features/mypage/model/mypageConstants"; import { Button } from "@/src/shared/lib/headlessUi"; +import { DefaultHeader } from "@/src/shared/ui/header"; /** * 마이페이지 핀포인트 설정 화면 위젯 @@ -39,32 +41,38 @@ export const PinpointsSection = () => { }; return ( -
-
-
- +
+
+ +
+
+
+
+ +
+

+ {MYPAGE_LABEL_PINPOINTS} +

+

+ {MYPAGE_PINPOINTS_DESCRIPTION} +

+
+ +
-

- {MYPAGE_LABEL_PINPOINTS} -

-

- {MYPAGE_PINPOINTS_DESCRIPTION} -

-
- -
-
- {address ? ( - - ) : null} + {address ? ( +
+ +
+ ) : null}
); }; diff --git a/src/widgets/mypageSection/ui/ProfileSection.tsx b/src/widgets/mypageSection/ui/ProfileSection.tsx index c03750f..4cb5040 100644 --- a/src/widgets/mypageSection/ui/ProfileSection.tsx +++ b/src/widgets/mypageSection/ui/ProfileSection.tsx @@ -4,10 +4,12 @@ import { useMypageUser } from "@/src/features/mypage/hooks"; import { MYPAGE_ERROR_TEXT, MYPAGE_LOADING_DESCRIPTION, + MYPAGE_PROFILE_HEADER_TITLE, MYPAGE_PROFILE_LOADING_TITLE, } from "@/src/features/mypage/model/mypageConstants"; import { ProfileForm } from "@/src/features/mypage/ui"; import { ErrorState } from "@/src/shared/ui/errorState"; +import { DefaultHeader } from "@/src/shared/ui/header"; import { LoadingState } from "@/src/shared/ui/loadingState"; /** @@ -39,11 +41,15 @@ export const ProfileSection = () => { return (
-
- -
+
+ +
+
+
+ +
); }; diff --git a/src/widgets/mypageSection/ui/SettingsSection.tsx b/src/widgets/mypageSection/ui/SettingsSection.tsx index c042b52..ff19a9c 100644 --- a/src/widgets/mypageSection/ui/SettingsSection.tsx +++ b/src/widgets/mypageSection/ui/SettingsSection.tsx @@ -4,12 +4,14 @@ import { logout } from "@/src/features/login/utils/logout"; import { MYPAGE_SETTINGS_LOGOUT, MYPAGE_SETTINGS_PROFILE, + MYPAGE_SETTINGS_TITLE, MYPAGE_SETTINGS_WITHDRAW, } from "@/src/features/mypage/model/mypageConstants"; import { MypageSettingsMenu, type MypageSettingsMenuItem, } from "@/src/features/mypage/ui"; +import { DefaultHeader } from "@/src/shared/ui/header"; /** * 마이페이지 설정 화면 위젯 @@ -23,8 +25,14 @@ export const SettingsSection = () => { ]; return ( -
- +
+
+ +
+
+
+ +
); }; diff --git a/src/widgets/mypageSection/ui/WithdrawSection.tsx b/src/widgets/mypageSection/ui/WithdrawSection.tsx index 160ac94..df11155 100644 --- a/src/widgets/mypageSection/ui/WithdrawSection.tsx +++ b/src/widgets/mypageSection/ui/WithdrawSection.tsx @@ -2,9 +2,11 @@ import { WithdrawBanner, WithdrawForm } from "@/src/features/mypage/ui"; import { + MYPAGE_WITHDRAW_HEADER_TITLE, WITHDRAW_BANNER_DESCRIPTION, WITHDRAW_BANNER_TITLE, } from "@/src/features/mypage/model/mypageConstants"; +import { DefaultHeader } from "@/src/shared/ui/header"; /** * 마이페이지 회원 탈퇴 화면 위젯 @@ -12,6 +14,10 @@ import { export const WithdrawSection = () => { return (
+
+ +
+
Date: Fri, 13 Feb 2026 14:25:29 +0900 Subject: [PATCH 08/18] =?UTF-8?q?feat:=20info=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20=EC=97=B0=EB=8F=99=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8F=B0=20=ED=94=84=EB=A0=88=EC=9E=84=20=EB=82=B4?= =?UTF-8?q?=20=EC=8B=9C=ED=8A=B8/=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 버튼+시트를 한 컴포넌트로 통합, 기존 eligibilityInfoButton.tsx 제거 - sheetContentType 'asset' | 'car' | 'house' 시 클릭하면 바텀시트 오픈 - decision tree: 해당 infoButton 6곳에 sheetContentType 추가 --- public/info/info_asset.png | Bin 0 -> 102800 bytes public/info/info_car.png | Bin 0 -> 37850 bytes public/info/info_house.png | Bin 0 -> 39271 bytes .../model/eligibilityDecisionTree.ts | 6 + .../common/eligibilityComponentRenderer.tsx | 18 ++- .../ui/common/eligibilityInfoButton.tsx | 46 ------ .../common/eligibilityInfoButtonWithSheet.tsx | 144 ++++++++++++++++++ src/features/eligibility/ui/common/index.ts | 7 +- .../context/mobileSheetPortalContext.tsx | 24 +++ .../headlessUi/bottomSheet/bottomSheet.tsx | 56 ++++--- src/shared/ui/globalRender/globalRender.tsx | 24 +-- .../mobileFrameWithSheetPortal.tsx | 54 +++++++ 12 files changed, 283 insertions(+), 96 deletions(-) create mode 100644 public/info/info_asset.png create mode 100644 public/info/info_car.png create mode 100644 public/info/info_house.png delete mode 100644 src/features/eligibility/ui/common/eligibilityInfoButton.tsx create mode 100644 src/features/eligibility/ui/common/eligibilityInfoButtonWithSheet.tsx create mode 100644 src/shared/context/mobileSheetPortalContext.tsx create mode 100644 src/shared/ui/globalRender/mobileFrameWithSheetPortal.tsx diff --git a/public/info/info_asset.png b/public/info/info_asset.png new file mode 100644 index 0000000000000000000000000000000000000000..4a563498af9976ec77168d3884d4604ac25ac163 GIT binary patch literal 102800 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

_OLZ+rD>plEh=UaK}?VT(Yh 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 LDpLtG|MWiqj^66f literal 0 HcmV?d00001 diff --git a/public/info/info_car.png b/public/info/info_car.png new file mode 100644 index 0000000000000000000000000000000000000000..4f26abc30fe8946985fb96287e291d8158c9b932 GIT binary patch literal 37850 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

3qEM+$HIaB{) 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 HJ>vfZD$DFY literal 0 HcmV?d00001 diff --git a/src/features/eligibility/model/eligibilityDecisionTree.ts b/src/features/eligibility/model/eligibilityDecisionTree.ts index 5d889b1..2d39d6a 100644 --- a/src/features/eligibility/model/eligibilityDecisionTree.ts +++ b/src/features/eligibility/model/eligibilityDecisionTree.ts @@ -1033,6 +1033,7 @@ export const eligibilityDecisionTree: StepConfig[] = [ props: { title: "세대주, 세대원의 차이가 궁금하다면?", description: "", + sheetContentType: "house", }, }, ], @@ -1172,6 +1173,7 @@ export const eligibilityDecisionTree: StepConfig[] = [ props: { title: "총자산 계산법이 궁금하다면?", description: "", + sheetContentType: "asset", }, }, ], @@ -1294,6 +1296,7 @@ export const eligibilityDecisionTree: StepConfig[] = [ props: { title: "인정되는 자동차 기준이 궁금하다면?", description: "", + sheetContentType: "car", }, }, { @@ -1314,6 +1317,7 @@ export const eligibilityDecisionTree: StepConfig[] = [ props: { title: "총자산 계산법이 궁금하다면?", description: "", + sheetContentType: "asset", }, }, ], @@ -1369,6 +1373,7 @@ export const eligibilityDecisionTree: StepConfig[] = [ props: { title: "세대주, 세대원의 차이가 궁금하다면?", description: "", + sheetContentType: "house", }, }, ], @@ -1745,6 +1750,7 @@ export const eligibilityDecisionTree: StepConfig[] = [ props: { title: "인정되는 자동차 기준이 궁금하다면?", description: "", + sheetContentType: "car", }, }, { diff --git a/src/features/eligibility/ui/common/eligibilityComponentRenderer.tsx b/src/features/eligibility/ui/common/eligibilityComponentRenderer.tsx index db5e96c..931e906 100644 --- a/src/features/eligibility/ui/common/eligibilityComponentRenderer.tsx +++ b/src/features/eligibility/ui/common/eligibilityComponentRenderer.tsx @@ -9,7 +9,7 @@ import { EligibilityOptionSelector } from "./eligibilityOptionSelector"; import { EligibilitySelect } from "./eligibilitySelect"; import { EligibilityPriceInput } from "./eligibilityPriceInput"; import { EligibilityNumberInputList } from "./eligibilityNumberInputList"; -import { EligibilityInfoButton } from "./eligibilityInfoButton"; +import { EligibilityInfoButtonWithSheet } from "./eligibilityInfoButtonWithSheet"; import { DatePicker } from "@/src/shared/ui/datePicker/datePicker"; import { Checkbox } from "@/src/shared/lib/headlessUi/checkBox/checkbox"; import { motion, AnimatePresence } from "framer-motion"; @@ -296,17 +296,21 @@ export const EligibilityComponentRenderer = ({ config }: EligibilityComponentRen } case "infoButton": { - // action prop이 있으면 동적으로 핸들러 생성 + // sheetContentType이 있으면 클릭 시 바텀시트, 없으면 onClick/action으로 라우팅 + const sheetContentType = config.props.sheetContentType; let onClick = config.props.onClick; - if (config.props.action === "home") { - onClick = () => router.push("/home"); - } else if (config.props.action === "back") { - onClick = () => router.back(); + if (!sheetContentType) { + if (config.props.action === "home") { + onClick = () => router.push("/home"); + } else if (config.props.action === "back") { + onClick = () => router.back(); + } } return ( - diff --git a/src/features/eligibility/ui/common/eligibilityInfoButton.tsx b/src/features/eligibility/ui/common/eligibilityInfoButton.tsx deleted file mode 100644 index e3b1efa..0000000 --- a/src/features/eligibility/ui/common/eligibilityInfoButton.tsx +++ /dev/null @@ -1,46 +0,0 @@ -"use client"; - -import { cn } from "@/lib/utils"; -import { Button } from "@/src/shared/lib/headlessUi/button/button"; -import { ChevronRight } from "lucide-react"; -import { InfoButoonImg } from "@/src/assets/images/eligibility/InfoButoonImg"; - -export interface EligibilityInfoButtonProps { - /** 버튼 텍스트 (길이에 따라 자동으로 2줄로 표시) */ - text: string; - /** 클릭 핸들러 */ - onClick?: () => void; - /** 추가 클래스명 */ - className?: string; -} - -export const EligibilityInfoButton = ({ text, onClick, className }: EligibilityInfoButtonProps) => { - return ( - - ); -}; diff --git a/src/features/eligibility/ui/common/eligibilityInfoButtonWithSheet.tsx b/src/features/eligibility/ui/common/eligibilityInfoButtonWithSheet.tsx new file mode 100644 index 0000000..dc4fe8f --- /dev/null +++ b/src/features/eligibility/ui/common/eligibilityInfoButtonWithSheet.tsx @@ -0,0 +1,144 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import Image from "next/image"; +import { cn } from "@/lib/utils"; +import { useMobileSheetPortal } from "@/src/shared/context/mobileSheetPortalContext"; +import { Button } from "@/src/shared/lib/headlessUi/button/button"; +import { + BottomSheet, + BottomSheetContent, + BottomSheetTitle, +} from "@/src/shared/lib/headlessUi"; +import { ChevronRight } from "lucide-react"; +import { InfoButoonImg } from "@/src/assets/images/eligibility/InfoButoonImg"; + +/** 바텀시트에 표시할 콘텐츠 타입 (타입별 제목·이미지) */ +export type EligibilityInfoSheetContentType = "asset" | "car" | "house"; + +const INFO_SHEET_CONFIG: Record< + EligibilityInfoSheetContentType, + { title: string; imageSrc: string } +> = { + asset: { + title: "총자산 계산법 한눈에 보기", + imageSrc: "/info/info_asset.png", + }, + car: { + title: "자동차 기준 한눈에 보기", + imageSrc: "/info/info_car.png", + }, + house: { + title: "세대주,세대원 기준 한눈에 보기", + imageSrc: "/info/info_house.png", + }, +}; + +export interface EligibilityInfoButtonWithSheetProps { + /** 버튼 텍스트 */ + text: string; + /** 바텀시트 콘텐츠 타입. 있으면 클릭 시 시트 오픈, 없으면 onClick 사용 */ + sheetContentType?: EligibilityInfoSheetContentType; + /** 시트를 쓰지 않을 때 클릭 핸들러 (action: home/back 또는 커스텀) */ + onClick?: () => void; + /** 추가 클래스명 */ + className?: string; +} + +export const EligibilityInfoButtonWithSheet = ({ + text, + sheetContentType, + onClick, + className, +}: EligibilityInfoButtonWithSheetProps) => { + const [sheetOpen, setSheetOpen] = useState(false); + const portalRef = useMobileSheetPortal(); + const container = portalRef?.current ?? undefined; + const scrollRef = useRef(null); + + useEffect(() => { + if (sheetOpen) { + scrollRef.current?.scrollTo(0, 0); + } + }, [sheetOpen]); + + const handleClick = () => { + if (sheetContentType) { + setSheetOpen(true); + } else { + onClick?.(); + } + }; + + const config = sheetContentType ? INFO_SHEET_CONFIG[sheetContentType] : null; + + return ( + <> + + + {config && ( + + + {config.title} +

+
+ +
+ + + )} + + ); +}; diff --git a/src/features/eligibility/ui/common/index.ts b/src/features/eligibility/ui/common/index.ts index 48b8183..ecaa49d 100644 --- a/src/features/eligibility/ui/common/index.ts +++ b/src/features/eligibility/ui/common/index.ts @@ -8,8 +8,11 @@ export type { export { EligibilityHelpButton } from "./eligibilityHelpButton"; export type { EligibilityHelpButtonProps } from "./eligibilityHelpButton"; -export { EligibilityInfoButton } from "./eligibilityInfoButton"; -export type { EligibilityInfoButtonProps } from "./eligibilityInfoButton"; +export { EligibilityInfoButtonWithSheet } from "./eligibilityInfoButtonWithSheet"; +export type { + EligibilityInfoButtonWithSheetProps, + EligibilityInfoSheetContentType, +} from "./eligibilityInfoButtonWithSheet"; export { EligibilityPriceInput } from "./eligibilityPriceInput"; export type { EligibilityPriceInputProps } from "./eligibilityPriceInput"; diff --git a/src/shared/context/mobileSheetPortalContext.tsx b/src/shared/context/mobileSheetPortalContext.tsx new file mode 100644 index 0000000..1ad0ab8 --- /dev/null +++ b/src/shared/context/mobileSheetPortalContext.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { createContext, useContext, useRef, type RefObject } from "react"; + +const MobileSheetPortalContext = createContext | null>(null); + +export function useMobileSheetPortal(): RefObject | null { + return useContext(MobileSheetPortalContext); +} + +/** 폰 프레임 내부 시트 포탈 ref를 제공 (globalRender에서 사용) */ +export function MobileSheetPortalProvider({ + children, + portalRef, +}: { + children: React.ReactNode; + portalRef: RefObject; +}) { + return ( + + {children} + + ); +} diff --git a/src/shared/lib/headlessUi/bottomSheet/bottomSheet.tsx b/src/shared/lib/headlessUi/bottomSheet/bottomSheet.tsx index 3658e41..9fb1b02 100644 --- a/src/shared/lib/headlessUi/bottomSheet/bottomSheet.tsx +++ b/src/shared/lib/headlessUi/bottomSheet/bottomSheet.tsx @@ -14,15 +14,21 @@ const BottomSheetClose = SheetPrimitive.Close; const BottomSheetPortal = SheetPrimitive.Portal; +const overlayClass = (isInsideContainer: boolean) => + cn( + "z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", + isInsideContainer ? "absolute inset-0" : "fixed inset-0" + ); + const BottomSheetOverlay = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { + /** 포탈 컨테이너 안에 렌더될 때 true → absolute 사용 */ + isInsideContainer?: boolean; + } +>(({ className, isInsideContainer, ...props }, ref) => ( @@ -33,26 +39,34 @@ interface BottomSheetContentProps extends React.ComponentPropsWithoutRef { showCloseButton?: boolean; showOverlay?: boolean; + /** 지정 시 이 엘리먼트 안에 포탈하고, overlay/content를 absolute로 배치 (폰 프레임 내 시트용) */ + container?: HTMLElement | null; } +const contentClass = (isInsideContainer: boolean) => + cn( + "z-50 gap-4 rounded-t-3xl border-t bg-white p-6 shadow-[0px_-16px_24px_-10px_rgba(48,111,255,0.15)] transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + isInsideContainer ? "absolute inset-x-0 bottom-0 left-0 right-0" : "fixed inset-x-0 bottom-0" + ); + const BottomSheetContent = React.forwardRef< React.ElementRef, BottomSheetContentProps ->(({ className, children, showCloseButton = false, showOverlay = true, ...props }, ref) => ( - - {showOverlay && } - - {children} - - -)); +>(({ className, children, showCloseButton = false, showOverlay = true, container, ...props }, ref) => { + const isInsideContainer = Boolean(container); + return ( + + {showOverlay && } + + {children} + + + ); +}); BottomSheetContent.displayName = SheetPrimitive.Content.displayName; const BottomSheetHeader = ({ className, ...props }: React.HTMLAttributes) => ( diff --git a/src/shared/ui/globalRender/globalRender.tsx b/src/shared/ui/globalRender/globalRender.tsx index 05f4e41..5535e94 100644 --- a/src/shared/ui/globalRender/globalRender.tsx +++ b/src/shared/ui/globalRender/globalRender.tsx @@ -1,9 +1,9 @@ -import { cn } from "@/lib/utils"; import { HomeBottomRender } from "@/src/assets/images/render/homeBottom"; import { HomeRectangleRender } from "@/src/assets/images/render/homeRectangle"; import { HomeRectangleRender2 } from "@/src/assets/images/render/homeRectangle_1"; import { HomeStarRender } from "@/src/assets/images/render/homeStar"; import { SecondaryLogoRender } from "@/src/assets/images/render/secondaryLogo"; +import { MobileFrameWithSheetPortal } from "@/src/shared/ui/globalRender/mobileFrameWithSheetPortal"; import { ReactNode } from "react"; interface Props { @@ -58,25 +58,9 @@ export const HomeLandingRender = ({ children, bottom }: Props) => {
-
-
- -
- {children} -
- -
{bottom}
- -
-
+ + {children} +
diff --git a/src/shared/ui/globalRender/mobileFrameWithSheetPortal.tsx b/src/shared/ui/globalRender/mobileFrameWithSheetPortal.tsx new file mode 100644 index 0000000..70c66c4 --- /dev/null +++ b/src/shared/ui/globalRender/mobileFrameWithSheetPortal.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { useRef } from "react"; +import { cn } from "@/lib/utils"; +import { MobileSheetPortalProvider } from "@/src/shared/context/mobileSheetPortalContext"; + +interface MobileFrameWithSheetPortalProps { + children: React.ReactNode; + bottom?: React.ReactNode; + hasBottom: boolean; +} + +/** + * 홈 폰 프레임(375px) 내부에 시트 포탈 컨테이너를 두고, + * 바텀시트가 이 영역 안에서만 뜨도록 ref를 context로 제공합니다. + */ +export function MobileFrameWithSheetPortal({ + children, + bottom, + hasBottom, +}: MobileFrameWithSheetPortalProps) { + const portalRef = useRef(null); + + return ( + +
+
+ +
+ {children} +
+ +
{bottom}
+ +
+ + {/* 바텀시트가 이 컨테이너에만 렌더되도록 포탈 타깃 */} + + + ); +} From 2e5686727daa57d5b1d7eb5359aece43622c55e6 Mon Sep 17 00:00:00 2001 From: JW_Ahn0 Date: Fri, 13 Feb 2026 14:27:43 +0900 Subject: [PATCH 09/18] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/test/page.tsx | 118 ---------------------------------------------- 1 file changed, 118 deletions(-) diff --git a/app/test/page.tsx b/app/test/page.tsx index bbdd191..60128a3 100644 --- a/app/test/page.tsx +++ b/app/test/page.tsx @@ -1,127 +1,9 @@ "use client"; -import { useState } from "react"; -import { - EligibilityInfoButton, - EligibilityNumberInputList, - EligibilityOptionSelector, - EligibilityPriceInput, - EligibilitySelect, -} from "@/src/features/eligibility"; -import { DatePicker } from "@/src/shared/ui/datePicker/datePicker"; - export default function DefaultTest() { - const [income, setIncome] = useState(""); - const [incomeError, setIncomeError] = useState(false); - - // 유효성 검증 예시: 값이 비어있거나 0이면 에러 - const validateIncome = (value: string) => { - if (!value || value === "0") { - setIncomeError(true); - } else { - setIncomeError(false); - } - }; return (
- - - console.log("정보 클릭")} /> - - - ( - <> - 총{" "} - - {Number(values.under6 || 0) + Number(values.over7 || 0)} - {" "} - 명의 미성년 자녀가 있어요 - - )} - /> - - console.log("옵션 선택")} - /> - console.log("옵션 선택")} - /> - console.log("옵션 선택")} - /> - console.log("옵션 선택")} - /> - { - setIncome(value); - validateIncome(value); - console.log("소득 입력:", value); - }} - onBlur={() => validateIncome(income)} - />
); } From ddf2a4b212df54ac3e74de7eda5210c097ff341a Mon Sep 17 00:00:00 2001 From: JW_Ahn0 Date: Fri, 13 Feb 2026 14:55:18 +0900 Subject: [PATCH 10/18] =?UTF-8?q?feat:=20=EC=A7=84=EB=8B=A8=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EB=94=A9=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20UI=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - layout children 래퍼에 높이 전달 --- app/layout.tsx | 6 +- app/test/page.tsx | 8 +- .../eligibility/eligibilityLoadingImg.tsx | 145 ++++++++++++++++++ .../eligibility/eligibilityLoadingImg.svg | 46 ++++++ .../eligibility/model/eligibilityConstants.ts | 4 + src/features/eligibility/model/index.ts | 5 + .../ui/common/eligibilityLoadingState.tsx | 28 ++++ 7 files changed, 234 insertions(+), 8 deletions(-) create mode 100644 src/assets/images/eligibility/eligibilityLoadingImg.tsx create mode 100644 src/assets/images/svgFile/eligibility/eligibilityLoadingImg.svg create mode 100644 src/features/eligibility/model/eligibilityConstants.ts create mode 100644 src/features/eligibility/ui/common/eligibilityLoadingState.tsx diff --git a/app/layout.tsx b/app/layout.tsx index b2d1164..ba95ad1 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -8,8 +8,6 @@ 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: "pinhouse", description: "pinhosue-fe", @@ -43,9 +41,7 @@ export default function RootLayout({ } > - -
{children}
-
+ {children}
diff --git a/app/test/page.tsx b/app/test/page.tsx index 60128a3..e0e33bc 100644 --- a/app/test/page.tsx +++ b/app/test/page.tsx @@ -1,9 +1,11 @@ "use client"; -export default function DefaultTest() { +import EligibilityLoadingState from "@/src/features/eligibility/ui/common/eligibilityLoadingState"; +export default function DefaultTest() { return ( -
-
+
+ +
); } diff --git a/src/assets/images/eligibility/eligibilityLoadingImg.tsx b/src/assets/images/eligibility/eligibilityLoadingImg.tsx new file mode 100644 index 0000000..ca78e88 --- /dev/null +++ b/src/assets/images/eligibility/eligibilityLoadingImg.tsx @@ -0,0 +1,145 @@ +import { SVGProps } from "react"; + +export const EligibilityLoadingImg = (props: SVGProps) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default EligibilityLoadingImg; diff --git a/src/assets/images/svgFile/eligibility/eligibilityLoadingImg.svg b/src/assets/images/svgFile/eligibility/eligibilityLoadingImg.svg new file mode 100644 index 0000000..422375d --- /dev/null +++ b/src/assets/images/svgFile/eligibility/eligibilityLoadingImg.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/features/eligibility/model/eligibilityConstants.ts b/src/features/eligibility/model/eligibilityConstants.ts new file mode 100644 index 0000000..e46afa0 --- /dev/null +++ b/src/features/eligibility/model/eligibilityConstants.ts @@ -0,0 +1,4 @@ +/** 자격 진단 로딩 화면 문구 */ +export const ELIGIBILITY_LOADING_TITLE = "맞춤 보고서를 작성 중이에요!"; +export const ELIGIBILITY_LOADING_SUBTITLE_LINE1 = "모두의 꿈인 내 집 마련"; +export const ELIGIBILITY_LOADING_SUBTITLE_LINE2 = "나에게 맞는 추천 집은?"; diff --git a/src/features/eligibility/model/index.ts b/src/features/eligibility/model/index.ts index ab98a8e..d8a9c22 100644 --- a/src/features/eligibility/model/index.ts +++ b/src/features/eligibility/model/index.ts @@ -1,2 +1,7 @@ export { eligibilityContentMap, ELIGIBILITY_STEPS } from "./eligibilityContentMap"; +export { + ELIGIBILITY_LOADING_SUBTITLE_LINE1, + ELIGIBILITY_LOADING_SUBTITLE_LINE2, + ELIGIBILITY_LOADING_TITLE, +} from "./eligibilityConstants"; export type { EligibilityStepContent } from "./eligibilityContentMap"; diff --git a/src/features/eligibility/ui/common/eligibilityLoadingState.tsx b/src/features/eligibility/ui/common/eligibilityLoadingState.tsx new file mode 100644 index 0000000..8e66b4b --- /dev/null +++ b/src/features/eligibility/ui/common/eligibilityLoadingState.tsx @@ -0,0 +1,28 @@ +import EligibilityLoadingImg from "@/src/assets/images/eligibility/eligibilityLoadingImg"; +import { + ELIGIBILITY_LOADING_SUBTITLE_LINE1, + ELIGIBILITY_LOADING_SUBTITLE_LINE2, + ELIGIBILITY_LOADING_TITLE, +} from "@/src/features/eligibility/model/eligibilityConstants"; + +const EligibilityLoadingState = () => { + return ( +
+
+ +
+

+ {ELIGIBILITY_LOADING_TITLE} +

+

+ {ELIGIBILITY_LOADING_SUBTITLE_LINE1} +
+ {ELIGIBILITY_LOADING_SUBTITLE_LINE2} +

+
+
+
+ ); +}; + +export default EligibilityLoadingState; From c1f7d30e67b72d16e63be280de7fedcab2ab694b Mon Sep 17 00:00:00 2001 From: chan Date: Fri, 13 Feb 2026 22:33:06 +0900 Subject: [PATCH 11/18] =?UTF-8?q?fix:=EA=B3=B5=EA=B3=A0=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=8B=9C=ED=8A=B8=20=ED=8F=AC=EC=A7=80=EC=85=98=20?= =?UTF-8?q?=EB=B0=8F=20=EB=B2=84=ED=8A=BC=20=ED=81=B4=EB=A6=AD=EC=8B=9C=20?= =?UTF-8?q?=EB=9D=BC=EC=9A=B0=ED=84=B0=20=EC=9D=B4=EB=8F=99=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/listingsCardDetail/button/button.tsx | 19 +++++++++ .../DetailSectionFilter/DetailFilterSheet.tsx | 39 ++++++++++-------- .../DetailSectionFilter/DistanceFilter.tsx | 11 +---- .../components/CostFilter.tsx | 20 ++++------ .../components/areaFilter.tsx | 10 +---- .../components/regionFilter.tsx | 10 +---- ...listingsCardDetailOutOfCriteriaSection.tsx | 6 ++- .../components/listingsCardDetailSummary.tsx | 8 ++-- .../ui/listingsCardDetail/hooks/hooks.ts | 40 +++++++++++++++++++ .../listingsCardDetailSection.tsx | 8 +++- 10 files changed, 111 insertions(+), 60 deletions(-) create mode 100644 src/features/listings/ui/listingsCardDetail/button/button.tsx create mode 100644 src/features/listings/ui/listingsCardDetail/hooks/hooks.ts diff --git a/src/features/listings/ui/listingsCardDetail/button/button.tsx b/src/features/listings/ui/listingsCardDetail/button/button.tsx new file mode 100644 index 0000000..58e000d --- /dev/null +++ b/src/features/listings/ui/listingsCardDetail/button/button.tsx @@ -0,0 +1,19 @@ +import { useDetailFilterResultButton } from "@/src/features/listings/ui/listingsCardDetail/hooks/hooks"; + +type ListingCardDetailProps = { + filteredCount: number; + handleCloseSheet: () => void; +}; +export const ListingCardDetailOut = ({ filteredCount, handleCloseSheet }: ListingCardDetailProps) => { + return ( +
+ +
+ ); +}; \ No newline at end of file diff --git a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DetailFilterSheet.tsx b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DetailFilterSheet.tsx index 0a37f1f..0088d21 100644 --- a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DetailFilterSheet.tsx +++ b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DetailFilterSheet.tsx @@ -1,5 +1,4 @@ "use client"; - import { AnimatePresence, motion } from "framer-motion"; import { useSearchParams } from "next/navigation"; import { useRef } from "react"; @@ -13,15 +12,17 @@ import { DistanceFilter } from "./DistanceFilter"; import { CostFilter } from "./components/CostFilter"; import { RegionFilter } from "./components/regionFilter"; import { AreaFilter } from "./components/areaFilter"; +import { ListingCardDetailOut } from "@/src/features/listings/ui/listingsCardDetail/button/button"; +import { useDetailFilterResultButton } from "@/src/features/listings/ui/listingsCardDetail/hooks/hooks"; export const DetailFilterSheet = () => { const open = useDetailFilterSheetStore(s => s.open); - const closeSheet = useDetailFilterSheetStore(s => s.closeSheet); const searchParams = useSearchParams(); const anchorRef = useRef(null); const portalRoot = usePortalTarget("mobile-overlay-root"); const section = parseDetailSection(searchParams); useScrollLock({ locked: open, anchorRef }); + const { filteredCount, handleCloseSheet } = useDetailFilterResultButton(); const content = ( @@ -31,7 +32,7 @@ export const DetailFilterSheet = () => { className="pointer-events-auto absolute inset-0 bg-black/40" onClick={e => { e.stopPropagation(); - closeSheet(); + handleCloseSheet(); }} initial={{ opacity: 0 }} animate={{ opacity: 1 }} @@ -39,7 +40,7 @@ export const DetailFilterSheet = () => { /> {

단지 필터

- +
-
- + +
{section === "distance" && } {section === "cost" && } {section === "region" && } {section === "area" && } {/* {section === "around" && } */} - -
+
+
+ +
+
)} diff --git a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DistanceFilter.tsx b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DistanceFilter.tsx index 149cce2..3fd3c2c 100644 --- a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DistanceFilter.tsx +++ b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DistanceFilter.tsx @@ -8,6 +8,7 @@ import { getDefaultPinPointLabel, mapPinPointToOptions, } from "@/src/features/listings/hooks/listingsHooks"; +import { ListingCardDetailOut } from "@/src/features/listings/ui/listingsCardDetail/button/button"; const SLIDER_MIN = 0; const SLIDER_MAX = 120; @@ -20,7 +21,7 @@ export const DistanceFilter = () => { const hasPinPoints = pinPointList.myPinPoint.length > 0; const { setPinPointId } = useOAuthStore(); const { distance, setDistance } = useListingDetailFilter(); - const { filteredCount } = useListingDetailCountStore(); + const onChageValue = (selectedKey: string) => { setPinPointId(selectedKey); @@ -75,14 +76,6 @@ export const DistanceFilter = () => { />
-
- -
); }; diff --git a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/components/CostFilter.tsx b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/components/CostFilter.tsx index 39e7004..6d710b5 100644 --- a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/components/CostFilter.tsx +++ b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/components/CostFilter.tsx @@ -8,6 +8,7 @@ import { useParams } from "next/navigation"; import { useListingDetailNoticeSheet } from "@/src/entities/listings/hooks/useListingDetailSheetHooks"; import { CostResponse } from "@/src/entities/listings/model/type"; import { useListingDetailCountStore, useListingDetailFilter } from "@/src/features/listings/model"; +import { ListingCardDetailOut } from "@/src/features/listings/ui/listingsCardDetail/button/button"; const DEPOSIT_STEP = 10; const WON_UNIT = 1; @@ -94,7 +95,7 @@ export const CostFilter = () => { if (!isManualDeposit) { setMaxDeposit(deposit); } else { - setMaxDeposit(handleDepositInput); + setMaxDeposit(handleDepositInput === "" ? "0" :handleDepositInput); } }, [deposit, handleDepositInput]); @@ -110,6 +111,10 @@ export const CostFilter = () => { // 직접 입력 시 숫자만 추려서 포맷 const handleDepositChangeText = (event: ChangeEvent) => { const values = event.target.value; + console.log(values) + if(values === ""){ + return setHandleDepositInput(""); + } const numericValue = Number(values.replace(/[^0-9]/g, "")); setHandleDepositInput(formatNumber(toKRW(numericValue))); }; @@ -126,7 +131,7 @@ export const CostFilter = () => { }; return ( -
+

@@ -169,7 +174,7 @@ export const CostFilter = () => { {

- -
- -
); }; diff --git a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/components/areaFilter.tsx b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/components/areaFilter.tsx index 4ec727d..63f5e2d 100644 --- a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/components/areaFilter.tsx +++ b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/components/areaFilter.tsx @@ -10,6 +10,7 @@ import { Checkbox } from "@/src/shared/lib/headlessUi/checkBox/checkbox"; import { TagButton } from "@/src/shared/ui/button/tagButton"; import { Spinner } from "@/src/shared/ui/spinner/default"; import { useParams } from "next/navigation"; +import { ListingCardDetailOut } from "@/src/features/listings/ui/listingsCardDetail/button/button"; export const AreaFilter = () => { const { id } = useParams() as { id: string }; @@ -56,14 +57,7 @@ export const AreaFilter = () => { ))} -
- -
+ ); }; diff --git a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/components/regionFilter.tsx b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/components/regionFilter.tsx index 4288e9f..9de6352 100644 --- a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/components/regionFilter.tsx +++ b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/components/regionFilter.tsx @@ -11,6 +11,7 @@ import { TagButton } from "@/src/shared/ui/button/tagButton"; import { Spinner } from "@/src/shared/ui/spinner/default"; import { useParams } from "next/navigation"; import { useState } from "react"; +import { ListingCardDetailOut } from "@/src/features/listings/ui/listingsCardDetail/button/button"; export const RegionFilter = () => { const { id } = useParams() as { id: string }; @@ -57,14 +58,7 @@ export const RegionFilter = () => { ))} -
- -
+ ); }; diff --git a/src/features/listings/ui/listingsCardDetail/components/listingsCardDetailOutOfCriteriaSection.tsx b/src/features/listings/ui/listingsCardDetail/components/listingsCardDetailOutOfCriteriaSection.tsx index e09ee4a..ea7d366 100644 --- a/src/features/listings/ui/listingsCardDetail/components/listingsCardDetailOutOfCriteriaSection.tsx +++ b/src/features/listings/ui/listingsCardDetail/components/listingsCardDetailOutOfCriteriaSection.tsx @@ -2,11 +2,13 @@ import { ListingsCardTile } from "./listingsCardTile"; import { ComplexList } from "@/src/entities/listings/model/type"; type ListingsCardDetailOutOfCriteriaSectionProps = { - listings: ComplexList; + listings: ComplexList, + className?: string }; export const ListingsCardDetailOutOfCriteriaSection = ({ - listings, + listings, + className, }: ListingsCardDetailOutOfCriteriaSectionProps) => { if (listings.complexes.length === 0) return; return ( diff --git a/src/features/listings/ui/listingsCardDetail/components/listingsCardDetailSummary.tsx b/src/features/listings/ui/listingsCardDetail/components/listingsCardDetailSummary.tsx index 8d6e250..04cbb3e 100644 --- a/src/features/listings/ui/listingsCardDetail/components/listingsCardDetailSummary.tsx +++ b/src/features/listings/ui/listingsCardDetail/components/listingsCardDetailSummary.tsx @@ -3,9 +3,11 @@ import { TagButton } from "@/src/shared/ui/button/tagButton"; import { cn } from "@/lib/utils"; export const ListingsCardDetailSummary = ({ - basicInfo, -}: { - basicInfo: ListingDetailResponseWithColor["data"]["basicInfo"]; + basicInfo, + className, + }: { + basicInfo: ListingDetailResponseWithColor["data"]["basicInfo"], + className?: string }) => { return (
diff --git a/src/features/listings/ui/listingsCardDetail/hooks/hooks.ts b/src/features/listings/ui/listingsCardDetail/hooks/hooks.ts new file mode 100644 index 0000000..36c6cc2 --- /dev/null +++ b/src/features/listings/ui/listingsCardDetail/hooks/hooks.ts @@ -0,0 +1,40 @@ +import { + useDetailFilterSheetStore, + useListingDetailCountStore, +} from "@/src/features/listings/model"; +import { useParams, useRouter, useSearchParams } from "next/navigation"; + +export const useDetailFilterResultButton = () => { + const { filteredCount } = useListingDetailCountStore(); + const router = useRouter(); + const { id } = useParams() as { id: string }; + const searchParams = useSearchParams(); + const closeSheet = useDetailFilterSheetStore(s => s.closeSheet); + + const resetListingsQuery = () => { + try { + const params = new URLSearchParams(searchParams.toString()); + params.delete("section"); + router.replace(`/listings/${id}`); + } catch (error) { + console.error("[ListingFilterPartialSheet] Failed to reset query", error); + } + }; + + const handleCloseSheet = () => { + try { + closeSheet(); + resetListingsQuery(); + } catch (error) { + console.error("[ListingFilterPartialSheet] Failed to close sheet", error); + } + + }; + + + return { + filteredCount, + handleCloseSheet, + } + +}; diff --git a/src/widgets/listingsSection/ui/listingsCardDetailSection/listingsCardDetailSection.tsx b/src/widgets/listingsSection/ui/listingsCardDetailSection/listingsCardDetailSection.tsx index 590c653..8ecf956 100644 --- a/src/widgets/listingsSection/ui/listingsCardDetailSection/listingsCardDetailSection.tsx +++ b/src/widgets/listingsSection/ui/listingsCardDetailSection/listingsCardDetailSection.tsx @@ -48,7 +48,9 @@ export const ListingsCardDetailSection = ({ id }: { id: string }) => { <>
- {!open && } + @@ -58,7 +60,9 @@ export const ListingsCardDetailSection = ({ id }: { id: string }) => { onFilteredCount={nonFiltered.totalCount} /> - {!open && } +
)} From 5a6cee2e3e86bb07d716a7516c5f3428e0cd4f0f Mon Sep 17 00:00:00 2001 From: chan Date: Sat, 14 Feb 2026 00:19:05 +0900 Subject: [PATCH 12/18] =?UTF-8?q?refactor:=EA=B3=B5=EA=B3=A0=EC=83=81?= =?UTF-8?q?=EC=84=B8=EC=A1=B0=ED=9A=8C=20/=20=ED=95=84=ED=84=B0=20?= =?UTF-8?q?=EC=8B=9C=ED=8A=B8=20=EC=97=AD=ED=95=A0,=EC=B1=85=EC=9E=84=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/useListingDetailSheetHooks.ts | 2 +- .../ui/listingsCardDetail/button/button.tsx | 9 +- .../DetailSectionFilter/DetailFilterSheet.tsx | 2 +- .../DetailSectionFilter/DistanceFilter.tsx | 35 ++-- .../components/CostFilter.tsx | 153 +++------------- .../components/regionFilter.tsx | 41 +---- .../ui/listingsCardDetail/hooks/costHooks.ts | 169 ++++++++++++++++++ .../listingsCardDetail/hooks/distanceHooks.ts | 45 +++++ .../listingsCardDetail/hooks/regionHooks.tsx | 29 +++ .../hooks/{hooks.ts => routerHooks.ts} | 11 +- 10 files changed, 290 insertions(+), 206 deletions(-) create mode 100644 src/features/listings/ui/listingsCardDetail/hooks/costHooks.ts create mode 100644 src/features/listings/ui/listingsCardDetail/hooks/distanceHooks.ts create mode 100644 src/features/listings/ui/listingsCardDetail/hooks/regionHooks.tsx rename src/features/listings/ui/listingsCardDetail/hooks/{hooks.ts => routerHooks.ts} (93%) diff --git a/src/entities/listings/hooks/useListingDetailSheetHooks.ts b/src/entities/listings/hooks/useListingDetailSheetHooks.ts index a75e163..af15d20 100644 --- a/src/entities/listings/hooks/useListingDetailSheetHooks.ts +++ b/src/entities/listings/hooks/useListingDetailSheetHooks.ts @@ -9,7 +9,7 @@ export const useListingDetailNoticeSheet = ({ id, url }: UseListingsHooksWith return useQuery({ queryKey: [url], - enabled: !!id, + enabled: !!id && !!url, staleTime: 1000 * 60 * 5, queryFn: () => diff --git a/src/features/listings/ui/listingsCardDetail/button/button.tsx b/src/features/listings/ui/listingsCardDetail/button/button.tsx index 58e000d..0d419aa 100644 --- a/src/features/listings/ui/listingsCardDetail/button/button.tsx +++ b/src/features/listings/ui/listingsCardDetail/button/button.tsx @@ -1,10 +1,13 @@ -import { useDetailFilterResultButton } from "@/src/features/listings/ui/listingsCardDetail/hooks/hooks"; +import { useDetailFilterResultButton } from "@/src/features/listings/ui/listingsCardDetail/hooks/routerHooks"; type ListingCardDetailProps = { filteredCount: number; handleCloseSheet: () => void; }; -export const ListingCardDetailOut = ({ filteredCount, handleCloseSheet }: ListingCardDetailProps) => { +export const ListingCardDetailOut = ({ + filteredCount, + handleCloseSheet, +}: ListingCardDetailProps) => { return (
); -}; \ No newline at end of file +}; diff --git a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DetailFilterSheet.tsx b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DetailFilterSheet.tsx index 0088d21..2c5cca9 100644 --- a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DetailFilterSheet.tsx +++ b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DetailFilterSheet.tsx @@ -13,7 +13,7 @@ import { CostFilter } from "./components/CostFilter"; import { RegionFilter } from "./components/regionFilter"; import { AreaFilter } from "./components/areaFilter"; import { ListingCardDetailOut } from "@/src/features/listings/ui/listingsCardDetail/button/button"; -import { useDetailFilterResultButton } from "@/src/features/listings/ui/listingsCardDetail/hooks/hooks"; +import { useDetailFilterResultButton } from "@/src/features/listings/ui/listingsCardDetail/hooks/routerHooks"; export const DetailFilterSheet = () => { const open = useDetailFilterSheetStore(s => s.open); diff --git a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DistanceFilter.tsx b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DistanceFilter.tsx index 3fd3c2c..fe7db87 100644 --- a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DistanceFilter.tsx +++ b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/DistanceFilter.tsx @@ -9,34 +9,22 @@ import { mapPinPointToOptions, } from "@/src/features/listings/hooks/listingsHooks"; import { ListingCardDetailOut } from "@/src/features/listings/ui/listingsCardDetail/button/button"; +import { + useDistanceHooks, + useDistanceVariable, +} from "@/src/features/listings/ui/listingsCardDetail/hooks/distanceHooks"; const SLIDER_MIN = 0; const SLIDER_MAX = 120; export const DistanceFilter = () => { const { data, isFetching } = useListingFilterDetail(); - const pinPointData = data?.pinPoints; - const pinPointList = mapPinPointToOptions(pinPointData); - const dropDownTriggerLabel = getDefaultPinPointLabel(pinPointList); - const hasPinPoints = pinPointList.myPinPoint.length > 0; - const { setPinPointId } = useOAuthStore(); - const { distance, setDistance } = useListingDetailFilter(); - - - const onChageValue = (selectedKey: string) => { - setPinPointId(selectedKey); - }; - - const handleDistanceChange = (values: number[]) => { - const [nextValue] = values; - if (typeof nextValue === "number") { - setDistance(nextValue); - } - }; - - const sliderValue = [distance]; - const formatMinutes = (value: number) => value.toString().padStart(1, "0"); - const formattedDistance = formatMinutes(distance); + const emptyPinPoint: PinPointPlace = { userName: "", pinPoints: [] }; + const { pinPointList, dropDownTriggerLabel, hasPinPoints } = useDistanceVariable( + data ?? emptyPinPoint + ); + const { onChangeValue, handleDistanceChange, sliderValue, formattedDistance } = + useDistanceHooks(); return (
@@ -54,7 +42,7 @@ export const DistanceFilter = () => { types="myPinPoint" data={pinPointList} size="lg" - onChange={onChageValue} + onChange={onChangeValue} disabled={isFetching || !hasPinPoints} > {dropDownTriggerLabel} @@ -75,7 +63,6 @@ export const DistanceFilter = () => { labelSuffix="분" />
- ); }; diff --git a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/components/CostFilter.tsx b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/components/CostFilter.tsx index 6d710b5..5a58865 100644 --- a/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/components/CostFilter.tsx +++ b/src/features/listings/ui/listingsCardDetail/components/DetailSectionFilter/components/CostFilter.tsx @@ -1,134 +1,28 @@ "use client"; - -import { useEffect, useLayoutEffect, useRef, useState, type ChangeEvent } from "react"; import { Checkbox } from "@/src/shared/lib/headlessUi/checkBox/checkbox"; import { Input } from "@/src/shared/ui/input/deafult"; import { HistogramSlider } from "./HistogramSlider"; -import { useParams } from "next/navigation"; -import { useListingDetailNoticeSheet } from "@/src/entities/listings/hooks/useListingDetailSheetHooks"; -import { CostResponse } from "@/src/entities/listings/model/type"; -import { useListingDetailCountStore, useListingDetailFilter } from "@/src/features/listings/model"; -import { ListingCardDetailOut } from "@/src/features/listings/ui/listingsCardDetail/button/button"; - -const DEPOSIT_STEP = 10; -const WON_UNIT = 1; -const GAP = 2; -const BAR_COUNT = 21; -const MAX_INDEX = BAR_COUNT - 1; - -export const HISTOGRAM_VALUES = [ - 10, 13, 15, 16, 17, 15, 14, 13, 14, 15, 16, 17, 18, 15, 12, 10, 14, 13, 12, 11, 10, -]; - -const formatNumber = (value: number) => { - const normalized = Number.isFinite(value) ? value : 0; - return Math.round(normalized).toLocaleString("ko-KR"); -}; -const toKRW = (valueInMan: number) => valueInMan * WON_UNIT; +import { useCostFilter } from "@/src/features/listings/ui/listingsCardDetail/hooks/costHooks"; export const CostFilter = () => { - const [activeIndex, setActiveIndex] = useState(DEPOSIT_STEP); - const { id } = useParams() as { id: string }; - const { data } = useListingDetailNoticeSheet({ - id: id, - url: "cost", - }); - const DEPOSIT_MIN = data?.minPrice ?? 0; - const DEPOSIT_MAX = data?.maxPrice ?? 0; - const HISTOGRAM_MIN = formatNumber(toKRW(DEPOSIT_MIN)); - const HISTOGRAM_MAX = formatNumber(toKRW(DEPOSIT_MAX)); - const AVG_COST = data?.avgPrice ?? 0; - const [isManualDeposit, setIsManualDeposit] = useState(false); - const { setMaxDeposit, maxDeposit, maxMonthPay, setMaxMonthPay } = useListingDetailFilter(); - const [handleDepositInput, setHandleDepositInput] = useState("0"); - const [deposit, setDeposit] = useState("0"); - const { filteredCount } = useListingDetailCountStore(); - - // 슬라이더 인덱스를 가격 범위에 맞춰 실제 보증금 값으로 변환 - const getDepositByIndex = (index: number) => { - if (DEPOSIT_MAX <= DEPOSIT_MIN) return DEPOSIT_MIN; - const step = (DEPOSIT_MAX - DEPOSIT_MIN) / MAX_INDEX; - return Math.round(DEPOSIT_MIN + step * index); - }; - - // 보증금 값을 현재 범위에 맞는 슬라이더 인덱스로 역변환 - const getIndexByDepositValue = (value: number) => { - if (DEPOSIT_MAX <= DEPOSIT_MIN) return 0; - const ratio = (value - DEPOSIT_MIN) / (DEPOSIT_MAX - DEPOSIT_MIN); - const clamped = Math.min(1, Math.max(0, ratio)); - return Math.round(clamped * MAX_INDEX); - }; - - const sliderRef = useRef(null); - const [containerWidth, setContainerWidth] = useState(0); - const maxValue = Math.max(...HISTOGRAM_VALUES); - const normalized = HISTOGRAM_VALUES.map(v => (v / maxValue) * 100); - const barCount = normalized.length; - - // 히스토그램 컨테이너 폭 변화에 맞춰 막대 폭/위치 재계산 - useLayoutEffect(() => { - if (!sliderRef.current) return; - const observer = new ResizeObserver(entries => { - setContainerWidth(entries[0].contentRect.width); - }); - observer.observe(sliderRef.current); - return () => observer.disconnect(); - }, []); - - // 전체 gap/막대 폭 계산 후 슬라이더 핸들의 픽셀/퍼센트 위치 산출 - const totalGap = GAP * (barCount - 1); - const barWidth = barCount ? Math.max(0, (containerWidth - totalGap) / barCount) : 0; - const handleLeftPx = barWidth * activeIndex + GAP * activeIndex + barWidth / 2; - const handleLeftPct = containerWidth ? (handleLeftPx / containerWidth) * 100 : 0; - const maxlength = HISTOGRAM_VALUES.length - 1; - - // API 데이터가 도착하면 평균값을 기준으로 슬라이더·입력 초기화 - useEffect(() => { - if (!data) return; - const baseDeposit = data.avgPrice ?? data.minPrice ?? 0; - const formatted = formatNumber(toKRW(baseDeposit)); - setDeposit(formatted); - setActiveIndex(getIndexByDepositValue(baseDeposit)); - }, [AVG_COST, DEPOSIT_MIN, DEPOSIT_MAX, data]); - - useEffect(() => { - if (!isManualDeposit) { - setMaxDeposit(deposit); - } else { - setMaxDeposit(handleDepositInput === "" ? "0" :handleDepositInput); - } - }, [deposit, handleDepositInput]); - - // 슬라이더 인덱스를 실 보증금으로 변환 - const handleDepositChange = (value: string) => { - const index = Number(value); - if (Number.isNaN(index)) return; - setActiveIndex(index); - const depositValue = getDepositByIndex(index); - setDeposit(formatNumber(toKRW(depositValue))); - }; - - // 직접 입력 시 숫자만 추려서 포맷 - const handleDepositChangeText = (event: ChangeEvent) => { - const values = event.target.value; - console.log(values) - if(values === ""){ - return setHandleDepositInput(""); - } - const numericValue = Number(values.replace(/[^0-9]/g, "")); - setHandleDepositInput(formatNumber(toKRW(numericValue))); - }; - - const handleManualDepositChange = (event: ChangeEvent) => { - const rawValue = event.target.value; - const numericValue = Number(rawValue.replace(/[^0-9]/g, "")); - setMaxMonthPay(rawValue === "" ? "" : formatNumber(numericValue)); - }; - - const handleManualToggle = (checked: boolean | "indeterminate") => { - const nextValue = checked === true; - setIsManualDeposit(nextValue); - }; + const { + avgCostLabel, + histogramMaxLabel, + histogramMinLabel, + isManualDeposit, + maxDeposit, + maxMonthPay, + activeIndex, + deposit, + normalized, + handleLeftPct, + maxlength, + sliderRef, + handleDepositChange, + handleDepositChangeText, + handleManualDepositChange, + handleManualToggle, + } = useCostFilter(); return (
@@ -139,18 +33,15 @@ export const CostFilter = () => {

이 공고의 평균 보증금은{" "} - - {data ? `${formatNumber(toKRW(AVG_COST))}만원` : "정보 없음"} - {" "} - 입니다. + {avgCostLabel} 입니다.

{ const { id } = useParams() as { id: string }; const regionType = useListingDetailFilter(state => state.region); const setRegion = useListingDetailFilter(state => state.toggleRegionType); - const { filteredCount } = useListingDetailCountStore(); const { data } = useListingDetailNoticeSheet({ id: id, url: "districts", @@ -58,35 +50,6 @@ export const RegionFilter = () => {
))}
- ); }; - -const Tag = ({ - label, - selected, - onClick, -}: { - label: string; - selected: boolean; - onClick: () => void; -}) => { - console.log(selected); - return ( - <> - - {label} - - - ); -}; diff --git a/src/features/listings/ui/listingsCardDetail/hooks/costHooks.ts b/src/features/listings/ui/listingsCardDetail/hooks/costHooks.ts new file mode 100644 index 0000000..3cbc0bb --- /dev/null +++ b/src/features/listings/ui/listingsCardDetail/hooks/costHooks.ts @@ -0,0 +1,169 @@ +import { + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, + useCallback, + ChangeEvent, +} from "react"; +import { useParams } from "next/navigation"; +import { useListingDetailNoticeSheet } from "@/src/entities/listings/hooks/useListingDetailSheetHooks"; +import { CostResponse } from "@/src/entities/listings/model/type"; +import { useListingDetailFilter } from "@/src/features/listings/model"; + +const DEPOSIT_STEP = 10; +const WON_UNIT = 1; +const GAP = 2; +const BAR_COUNT = 21; +const MAX_INDEX = BAR_COUNT - 1; + +export const HISTOGRAM_VALUES = [ + 10, 13, 15, 16, 17, 15, 14, 13, 14, 15, 16, 17, 18, 15, 12, 10, 14, 13, 12, 11, 10, +]; + +const formatNumber = (value: number) => { + const normalized = Number.isFinite(value) ? value : 0; + return Math.round(normalized).toLocaleString("ko-KR"); +}; + +const toKRW = (valueInMan: number) => valueInMan * WON_UNIT; + +export const useCostFilter = () => { + const [activeIndex, setActiveIndex] = useState(DEPOSIT_STEP); + const { id } = useParams() as { id: string }; + const { data } = useListingDetailNoticeSheet({ + id, + url: "cost", + }); + const DEPOSIT_MIN = data?.minPrice ?? 0; + const DEPOSIT_MAX = data?.maxPrice ?? 0; + const AVG_COST = data?.avgPrice ?? 0; + const [isManualDeposit, setIsManualDeposit] = useState(false); + const { setMaxDeposit, maxDeposit, maxMonthPay, setMaxMonthPay } = useListingDetailFilter(); + const [handleDepositInput, setHandleDepositInput] = useState("0"); + const [deposit, setDeposit] = useState("0"); + + const sliderRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + + const normalized = useMemo(() => { + const maxValue = Math.max(...HISTOGRAM_VALUES); + return HISTOGRAM_VALUES.map(v => (v / maxValue) * 100); + }, []); + + const barCount = normalized.length; + const totalGap = GAP * (barCount - 1); + const barWidth = barCount ? Math.max(0, (containerWidth - totalGap) / barCount) : 0; + const handleLeftPx = barWidth * activeIndex + GAP * activeIndex + barWidth / 2; + const handleLeftPct = containerWidth ? (handleLeftPx / containerWidth) * 100 : 0; + const maxlength = HISTOGRAM_VALUES.length - 1; + + const histogramMinLabel = `${formatNumber(toKRW(DEPOSIT_MIN))} 만`; + const histogramMaxLabel = `${formatNumber(toKRW(DEPOSIT_MAX))} 만`; + const avgCostLabel = data ? `${formatNumber(toKRW(AVG_COST))}만원` : "정보 없음"; + + const getDepositByIndex = useCallback( + (index: number) => { + if (DEPOSIT_MAX <= DEPOSIT_MIN) return DEPOSIT_MIN; + const step = (DEPOSIT_MAX - DEPOSIT_MIN) / MAX_INDEX; + return Math.round(DEPOSIT_MIN + step * index); + }, + [DEPOSIT_MAX, DEPOSIT_MIN] + ); + + const getIndexByDepositValue = useCallback( + (value: number) => { + if (DEPOSIT_MAX <= DEPOSIT_MIN) return 0; + const ratio = (value - DEPOSIT_MIN) / (DEPOSIT_MAX - DEPOSIT_MIN); + const clamped = Math.min(1, Math.max(0, ratio)); + return Math.round(clamped * MAX_INDEX); + }, + [DEPOSIT_MAX, DEPOSIT_MIN] + ); + + useLayoutEffect(() => { + if (!sliderRef.current) return; + const observer = new ResizeObserver(entries => { + setContainerWidth(entries[0].contentRect.width); + }); + observer.observe(sliderRef.current); + return () => observer.disconnect(); + }, []); + + useEffect(() => { + if (!data) return; + const baseDeposit = data.avgPrice ?? data.minPrice ?? 0; + const formatted = formatNumber(toKRW(baseDeposit)); + setDeposit(formatted); + setActiveIndex(getIndexByDepositValue(baseDeposit)); + }, [data, getIndexByDepositValue]); + + useEffect(() => { + if (!isManualDeposit) { + setMaxDeposit(deposit); + } else { + setMaxDeposit(handleDepositInput === "" ? "0" : handleDepositInput); + } + }, [deposit, handleDepositInput, isManualDeposit, setMaxDeposit]); + + const handleDepositChange = useCallback( + (value: string) => { + const index = Number(value); + if (Number.isNaN(index)) return; + setActiveIndex(index); + const depositValue = getDepositByIndex(index); + setDeposit(formatNumber(toKRW(depositValue))); + }, + [getDepositByIndex] + ); + + const handleDepositChangeText = useCallback((event: ChangeEvent) => { + const values = event.target.value; + if (values === "") { + setHandleDepositInput(""); + return; + } + const numericValue = Number(values.replace(/[^0-9]/g, "")); + setHandleDepositInput(formatNumber(toKRW(numericValue))); + }, []); + + const handleManualDepositChange = useCallback( + (event: ChangeEvent) => { + const rawValue = event.target.value; + const numericValue = Number(rawValue.replace(/[^0-9]/g, "")); + setMaxMonthPay(rawValue === "" ? "" : formatNumber(numericValue)); + }, + [setMaxMonthPay] + ); + + const handleManualToggle = useCallback( + (checked: boolean | "indeterminate") => { + const nextValue = checked === true; + setIsManualDeposit(nextValue); + if (nextValue) { + setHandleDepositInput(maxDeposit || "0"); + } + }, + [maxDeposit] + ); + + return { + avgCostLabel, + histogramMaxLabel, + histogramMinLabel, + isManualDeposit, + maxDeposit, + maxMonthPay, + activeIndex, + deposit, + normalized, + handleLeftPct, + maxlength, + sliderRef, + handleDepositChange, + handleDepositChangeText, + handleManualDepositChange, + handleManualToggle, + }; +}; diff --git a/src/features/listings/ui/listingsCardDetail/hooks/distanceHooks.ts b/src/features/listings/ui/listingsCardDetail/hooks/distanceHooks.ts new file mode 100644 index 0000000..7a42e47 --- /dev/null +++ b/src/features/listings/ui/listingsCardDetail/hooks/distanceHooks.ts @@ -0,0 +1,45 @@ +import { + getDefaultPinPointLabel, + mapPinPointToOptions, +} from "@/src/features/listings/hooks/listingsHooks"; +import { useOAuthStore } from "@/src/features/login/model"; +import { useListingDetailFilter } from "@/src/features/listings/model"; +import { PinPointPlace } from "@/src/entities/listings/model/type"; + +export const useDistanceHooks = () => { + const { setPinPointId } = useOAuthStore(); + const { distance, setDistance } = useListingDetailFilter(); + + const onChangeValue = (selectedKey: string) => { + setPinPointId(selectedKey); + }; + + const handleDistanceChange = (values: number[]) => { + const [nextValue] = values; + setDistance(nextValue); + }; + + const sliderValue = [distance]; + const formatMinutes = (value: number) => value.toString().padStart(1, "0"); + const formattedDistance = formatMinutes(distance); + + return { + onChangeValue, + handleDistanceChange, + sliderValue, + formattedDistance, + }; +}; + +export const useDistanceVariable = (data?: PinPointPlace) => { + const pinPointData = data?.pinPoints; + const pinPointList = mapPinPointToOptions(pinPointData); + const dropDownTriggerLabel = getDefaultPinPointLabel(pinPointList); + const hasPinPoints = pinPointList.myPinPoint.length > 0; + + return { + pinPointList, + dropDownTriggerLabel, + hasPinPoints, + }; +}; diff --git a/src/features/listings/ui/listingsCardDetail/hooks/regionHooks.tsx b/src/features/listings/ui/listingsCardDetail/hooks/regionHooks.tsx new file mode 100644 index 0000000..194f4b7 --- /dev/null +++ b/src/features/listings/ui/listingsCardDetail/hooks/regionHooks.tsx @@ -0,0 +1,29 @@ +import { cn } from "@/lib/utils"; +import { TagButton } from "@/src/shared/ui/button/tagButton"; + +export const Tag = ({ + label, + selected, + onClick, +}: { + label: string; + selected: boolean; + onClick: () => void; +}) => { + return ( + <> + + {label} + + + ); +}; diff --git a/src/features/listings/ui/listingsCardDetail/hooks/hooks.ts b/src/features/listings/ui/listingsCardDetail/hooks/routerHooks.ts similarity index 93% rename from src/features/listings/ui/listingsCardDetail/hooks/hooks.ts rename to src/features/listings/ui/listingsCardDetail/hooks/routerHooks.ts index 36c6cc2..2cabf1c 100644 --- a/src/features/listings/ui/listingsCardDetail/hooks/hooks.ts +++ b/src/features/listings/ui/listingsCardDetail/hooks/routerHooks.ts @@ -28,13 +28,10 @@ export const useDetailFilterResultButton = () => { } catch (error) { console.error("[ListingFilterPartialSheet] Failed to close sheet", error); } - }; - - return { - filteredCount, - handleCloseSheet, - } - + return { + filteredCount, + handleCloseSheet, + }; }; From 13ebdfd07448a052abfba9c1d75304d0eb3fcc1e Mon Sep 17 00:00:00 2001 From: JW_Ahn0 Date: Thu, 19 Feb 2026 10:01:52 +0900 Subject: [PATCH 13/18] =?UTF-8?q?feat:=20=EC=9E=90=EA=B2=A9=EC=A7=84?= =?UTF-8?q?=EB=8B=A8=20=EA=B2=B0=EA=B3=BC(=EC=9E=85=EB=A0=A5=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=99=95=EC=9D=B8)=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=B0=8F=20=EC=A7=84=EB=8B=A8=20=EC=A2=85=EB=A3=8C=20=EC=8B=9C?= =?UTF-8?q?=20=EA=B2=B0=EA=B3=BC=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 결과 화면 (/eligibility/result) - EligibilityResultSection: 헤더(입력 정보 확인) + Banner + ResultList + 결과보기 버튼 - EligibilityResultBanner: 결과 배너 이미지 + "{{user}}님의 진단 결과입니다" + 부제 - EligibilityResultList: store 기반 결과 리스트 (성별/나이, 소득 등 10항목), shadow-result-card 적용 - eligibilityResultModel: getEligibilityResultListItems로 store → 표시용 리스트 변환 (모델에서만 처리) - eligibilityConstants: 결과 화면 문구 상수 추가 진단 종료 플로우 - decision tree에서 diagnosisEnd 스텝 제거 - getNextStep이 "diagnosisEnd" 반환하던 4곳 → null 반환 - EligibilityNextButton: 마지막 단계에서 "결과 확인" 클릭 시 /eligibility/result 이동 (기존 diagnosisEnd 페이지 대체) --- app/eligibility/result/page.tsx | 11 ++ .../images/eligibility/resultBannerImg.tsx | 100 ++++++++++++++++++ .../svgFile/eligibility/resultBannerImg.svg | 35 ++++++ .../eligibility/model/eligibilityConstants.ts | 8 ++ .../model/eligibilityDecisionTree.ts | 25 +---- .../model/eligibilityResultModel.ts | 82 ++++++++++++++ src/features/eligibility/model/index.ts | 7 ++ .../eligibility/ui/eligibilityNextButton.tsx | 11 +- src/features/eligibility/ui/index.ts | 1 + .../ui/result/eligibilityResultBanner.tsx | 30 ++++++ .../ui/result/eligibilityResultList.tsx | 29 +++++ src/features/eligibility/ui/result/index.ts | 4 + src/widgets/eligibilitySection/index.ts | 1 + .../ui/eligibilityResultSection.tsx | 48 +++++++++ tailwind.config.mjs | 1 + 15 files changed, 364 insertions(+), 29 deletions(-) create mode 100644 app/eligibility/result/page.tsx create mode 100644 src/assets/images/eligibility/resultBannerImg.tsx create mode 100644 src/assets/images/svgFile/eligibility/resultBannerImg.svg create mode 100644 src/features/eligibility/model/eligibilityResultModel.ts create mode 100644 src/features/eligibility/ui/result/eligibilityResultBanner.tsx create mode 100644 src/features/eligibility/ui/result/eligibilityResultList.tsx create mode 100644 src/features/eligibility/ui/result/index.ts create mode 100644 src/widgets/eligibilitySection/ui/eligibilityResultSection.tsx diff --git a/app/eligibility/result/page.tsx b/app/eligibility/result/page.tsx new file mode 100644 index 0000000..25f08c8 --- /dev/null +++ b/app/eligibility/result/page.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { EligibilityResultSection } from "@/src/widgets/eligibilitySection"; + +export default function EligibilityResultPage() { + return ( +
+ +
+ ); +} diff --git a/src/assets/images/eligibility/resultBannerImg.tsx b/src/assets/images/eligibility/resultBannerImg.tsx new file mode 100644 index 0000000..c4b89dc --- /dev/null +++ b/src/assets/images/eligibility/resultBannerImg.tsx @@ -0,0 +1,100 @@ +const ResultBannerImg = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ResultBannerImg; diff --git a/src/assets/images/svgFile/eligibility/resultBannerImg.svg b/src/assets/images/svgFile/eligibility/resultBannerImg.svg new file mode 100644 index 0000000..2195dd9 --- /dev/null +++ b/src/assets/images/svgFile/eligibility/resultBannerImg.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/features/eligibility/model/eligibilityConstants.ts b/src/features/eligibility/model/eligibilityConstants.ts index e46afa0..1eb4552 100644 --- a/src/features/eligibility/model/eligibilityConstants.ts +++ b/src/features/eligibility/model/eligibilityConstants.ts @@ -2,3 +2,11 @@ export const ELIGIBILITY_LOADING_TITLE = "맞춤 보고서를 작성 중이에요!"; export const ELIGIBILITY_LOADING_SUBTITLE_LINE1 = "모두의 꿈인 내 집 마련"; export const ELIGIBILITY_LOADING_SUBTITLE_LINE2 = "나에게 맞는 추천 집은?"; + +/** 자격 진단 결과(입력 정보 확인) 화면 문구 */ +export const ELIGIBILITY_RESULT_PAGE_TITLE = "입력 정보 확인"; +export const ELIGIBILITY_RESULT_BANNER_TITLE = (userName: string) => + `${userName}님의 진단 결과입니다`; +export const ELIGIBILITY_RESULT_BANNER_SUBTITLE = + "입력하신 정보가 맞는지 확인해 보세요"; +export const ELIGIBILITY_RESULT_BUTTON = "결과보기"; diff --git a/src/features/eligibility/model/eligibilityDecisionTree.ts b/src/features/eligibility/model/eligibilityDecisionTree.ts index 2d39d6a..66e303a 100644 --- a/src/features/eligibility/model/eligibilityDecisionTree.ts +++ b/src/features/eligibility/model/eligibilityDecisionTree.ts @@ -402,7 +402,7 @@ export const eligibilityDecisionTree: StepConfig[] = [ return null; }, getNextStep: () => { - return "diagnosisEnd"; + return null; }, }, @@ -1184,7 +1184,7 @@ export const eligibilityDecisionTree: StepConfig[] = [ return null; }, getNextStep: () => { - return "diagnosisEnd"; + return null; }, }, @@ -1339,7 +1339,7 @@ export const eligibilityDecisionTree: StepConfig[] = [ }, getNextStep: data => { // 다음 단계로 이동 (추후 결정) - return "diagnosisEnd"; + return null; }, }, @@ -1790,26 +1790,9 @@ export const eligibilityDecisionTree: StepConfig[] = [ }, getNextStep: data => { // 다음 단계로 이동 (추후 결정) - return "diagnosisEnd"; + return null; }, }, - - // diagnosisEnd - { - id: "diagnosisEnd", - groupId: "diagnosisEnd", - components: [ - { - type: "statusBanner", - props: { - title: "진단이 종료되었습니다", - description: "", - }, - }, - ], - validation: () => null, - getNextStep: () => null, - }, ]; /** diff --git a/src/features/eligibility/model/eligibilityResultModel.ts b/src/features/eligibility/model/eligibilityResultModel.ts new file mode 100644 index 0000000..fa5fd9d --- /dev/null +++ b/src/features/eligibility/model/eligibilityResultModel.ts @@ -0,0 +1,82 @@ +import type { EligibilityData } from "./eligibilityStore"; +import { calculateAge } from "./eligibilityDecisionTree"; + +export interface EligibilityResultListItem { + label: string; + value: string; +} + +const formatGender = (v: string | null) => (v === "1" ? "남성" : v === "2" ? "여성" : "-"); +const formatHouseholdRole = (v: string | null) => + v === "1" ? "세대주" : v === "2" ? "세대원" : "-"; +const formatHouseholdComposition = (v: string | null) => + v === "1" ? "1인 가구" : v === "2" ? "가족과 함께" : v === "3" ? "공동생활가정" : "-"; +const formatHousing = (v: string | null) => + v === "1" ? "무주택가구" : v === "2" ? "주택 소유" : v ? "해당 없음" : "-"; +const formatCar = (v: string | null) => (v === "1" ? "있음" : v === "2" ? "없음" : "-"); + +/** + * store에 저장된 EligibilityData를 결과 확인 화면용 리스트 아이템으로 변환 + */ +export function getEligibilityResultListItems(data: EligibilityData): EligibilityResultListItem[] { + const age = calculateAge(data.birthDate); + const ageStr = age != null ? `만 ${age}세` : "-"; + const genderStr = formatGender(data.gender); + const incomeStr = data.benefitTypes?.length + ? `소득 1분위, 주거급여 수급자` + : data.monthlyIncome + ? `월소득 ${data.monthlyIncome}만원` + : "-"; + const studentStr = + data.youngSingleStudentStatus && data.youngSingleStudentStatus !== "1" + ? "대학생, 부모님 소득 1분위" + : "-"; + const subscriptionStr = + data.hasHousingSubscriptionSavings === "1" + ? `${data.housingSubscriptionPeriod ?? "00"}년 ${data.housingSubscriptionPaymentCount ?? "00"}회 납입, ${data.totalPaymentAmount === "2" ? "6000만원 이하" : "6000만원 이상"}` + : data.hasHousingSubscriptionSavings === "2" + ? "해당 없음" + : "-"; + const marriageStr = + data.isNewlyMarried === true + ? `기혼, 기간 ${data.marriagePeriod ?? "00"}년` + : data.marriageStatus === "1" + ? "예정" + : "-"; + const childrenStr = data.childrenInfo + ? `${data.childrenInfo.under6 + data.childrenInfo.over7}명, 대리 양육 가정 외` + : data.hasRegisteredChildren === "1" + ? "있음" + : "-"; + const householdStr = + [ + data.householdRole ? formatHouseholdRole(data.householdRole) : null, + data.householdComposition ? formatHouseholdComposition(data.householdComposition) : null, + ] + .filter(Boolean) + .join(", ") || "-"; + const housingStr = + data.hasOwnHousing === "2" || data.householdHousingOwnershipStatus === "2" + ? "무주택가구" + : formatHousing(data.hasOwnHousing); + const carStr = formatCar(data.hasHouseholdCar ?? data.hasCar); + const assetStr = + data.isTotalAssetUnder337Million === "1" || data.isHouseholdTotalAssetUnder337Million === "1" + ? "3억 3천 7백만원 이하" + : data.financialAssetValue || data.householdFinancialAssetValue + ? "해당" + : "-"; + + return [ + { label: "성별/나이", value: `${genderStr}, ${ageStr}` }, + { label: "소득", value: incomeStr }, + { label: "대학생 여부", value: studentStr }, + { label: "청약저축", value: subscriptionStr }, + { label: "결혼 여부", value: marriageStr }, + { label: "자녀 여부", value: childrenStr }, + { label: "세대 정보", value: householdStr }, + { label: "주택 소유 여부", value: housingStr }, + { label: "자동차 소유 여부", value: carStr }, + { label: "총자산", value: assetStr }, + ]; +} diff --git a/src/features/eligibility/model/index.ts b/src/features/eligibility/model/index.ts index d8a9c22..f7ec0b0 100644 --- a/src/features/eligibility/model/index.ts +++ b/src/features/eligibility/model/index.ts @@ -3,5 +3,12 @@ export { ELIGIBILITY_LOADING_SUBTITLE_LINE1, ELIGIBILITY_LOADING_SUBTITLE_LINE2, ELIGIBILITY_LOADING_TITLE, + ELIGIBILITY_RESULT_BANNER_SUBTITLE, + ELIGIBILITY_RESULT_BUTTON, + ELIGIBILITY_RESULT_PAGE_TITLE, } from "./eligibilityConstants"; export type { EligibilityStepContent } from "./eligibilityContentMap"; +export { + getEligibilityResultListItems, + type EligibilityResultListItem, +} from "./eligibilityResultModel"; diff --git a/src/features/eligibility/ui/eligibilityNextButton.tsx b/src/features/eligibility/ui/eligibilityNextButton.tsx index ced9872..786d412 100644 --- a/src/features/eligibility/ui/eligibilityNextButton.tsx +++ b/src/features/eligibility/ui/eligibilityNextButton.tsx @@ -70,14 +70,9 @@ export const EligibilityNextButton = () => { } return; } - // diagnosisEnd에서 다음 클릭 시 홈으로 이동 - if (currentStepId === "diagnosisEnd" && isLastStep) { - router.push("/home"); - return; - } - // 마지막 단계인 경우 진단종료 페이지로 이동 + // 마지막 단계인 경우 입력 정보 확인(결과) 페이지로 이동 if (isLastStep) { - router.push("/eligibility?step=diagnosisEnd"); + router.push("/eligibility/result"); return; } @@ -95,7 +90,7 @@ export const EligibilityNextButton = () => { onClick={handleClick} disabled={isDisabled} > - {currentStepId === "diagnosisEnd" ? "홈으로 이동하기" : "다음"} + {isLastStep ? "결과 확인" : "다음"} ); }; diff --git a/src/features/eligibility/ui/index.ts b/src/features/eligibility/ui/index.ts index 81abb22..316cbcb 100644 --- a/src/features/eligibility/ui/index.ts +++ b/src/features/eligibility/ui/index.ts @@ -1,2 +1,3 @@ export * from "./common"; export { EligibilityNextButton } from "./eligibilityNextButton"; +export * from "./result"; diff --git a/src/features/eligibility/ui/result/eligibilityResultBanner.tsx b/src/features/eligibility/ui/result/eligibilityResultBanner.tsx new file mode 100644 index 0000000..7abbbe8 --- /dev/null +++ b/src/features/eligibility/ui/result/eligibilityResultBanner.tsx @@ -0,0 +1,30 @@ +"use client"; + +import ResultBannerImg from "@/src/assets/images/eligibility/resultBannerImg"; +import { + ELIGIBILITY_RESULT_BANNER_SUBTITLE, + ELIGIBILITY_RESULT_BANNER_TITLE, +} from "@/src/features/eligibility/model/eligibilityConstants"; + +export interface EligibilityResultBannerProps { + /** 진단 결과 문구에 넣을 사용자 이름 + * TODO: 추후 닉네임으로 API 변경 필요 - 백엔드에 추후 요청 필요 + */ + userName: string; +} + +export const EligibilityResultBanner = ({ userName }: EligibilityResultBannerProps) => { + return ( +
+ +
+

+ {ELIGIBILITY_RESULT_BANNER_TITLE(userName)} +

+

+ {ELIGIBILITY_RESULT_BANNER_SUBTITLE} +

+
+
+ ); +}; diff --git a/src/features/eligibility/ui/result/eligibilityResultList.tsx b/src/features/eligibility/ui/result/eligibilityResultList.tsx new file mode 100644 index 0000000..1433720 --- /dev/null +++ b/src/features/eligibility/ui/result/eligibilityResultList.tsx @@ -0,0 +1,29 @@ +"use client"; + +import type { EligibilityData } from "@/src/features/eligibility/model/eligibilityStore"; +import { getEligibilityResultListItems } from "@/src/features/eligibility/model/eligibilityResultModel"; + +export interface EligibilityResultListProps { + data: EligibilityData; +} + +export const EligibilityResultList = ({ data }: EligibilityResultListProps) => { + const items = getEligibilityResultListItems(data); + + return ( +
+
    + {items.map(({ label, value }) => ( +
  • + + {label} + + + {value} + +
  • + ))} +
+
+ ); +}; diff --git a/src/features/eligibility/ui/result/index.ts b/src/features/eligibility/ui/result/index.ts new file mode 100644 index 0000000..b79c9d2 --- /dev/null +++ b/src/features/eligibility/ui/result/index.ts @@ -0,0 +1,4 @@ +export { EligibilityResultBanner } from "./eligibilityResultBanner"; +export type { EligibilityResultBannerProps } from "./eligibilityResultBanner"; +export { EligibilityResultList } from "./eligibilityResultList"; +export type { EligibilityResultListProps } from "./eligibilityResultList"; diff --git a/src/widgets/eligibilitySection/index.ts b/src/widgets/eligibilitySection/index.ts index 5de9319..26e470b 100644 --- a/src/widgets/eligibilitySection/index.ts +++ b/src/widgets/eligibilitySection/index.ts @@ -1 +1,2 @@ export { EligibilitySection } from "./ui/eligibilitySection"; +export { EligibilityResultSection } from "./ui/eligibilityResultSection"; diff --git a/src/widgets/eligibilitySection/ui/eligibilityResultSection.tsx b/src/widgets/eligibilitySection/ui/eligibilityResultSection.tsx new file mode 100644 index 0000000..aefab08 --- /dev/null +++ b/src/widgets/eligibilitySection/ui/eligibilityResultSection.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useEligibilityStore } from "@/src/features/eligibility/model/eligibilityStore"; +import { + ELIGIBILITY_RESULT_BUTTON, + ELIGIBILITY_RESULT_PAGE_TITLE, +} from "@/src/features/eligibility/model/eligibilityConstants"; +import { + EligibilityResultBanner, + EligibilityResultList, +} from "@/src/features/eligibility/ui/result"; +import { DefaultHeader } from "@/src/shared/ui/header"; +import { Button } from "@/src/shared/lib/headlessUi/button/button"; +import { useOAuthStore } from "@/src/features/login/model/authStore"; + +export const EligibilityResultSection = () => { + const router = useRouter(); + const data = useEligibilityStore(); + const { userName } = useOAuthStore(); + + return ( +
+
+ +
+
+ + +
+
+ +
+
+ ); +}; diff --git a/tailwind.config.mjs b/tailwind.config.mjs index 336990a..c70ab3a 100644 --- a/tailwind.config.mjs +++ b/tailwind.config.mjs @@ -217,6 +217,7 @@ export default { }, boxShadow: { "md-16": "0 8px 16px 0 rgba(0, 0, 0, 0.15)", + "result-card": "0px 2px 12px -8px rgba(17, 12, 34, 0.06)", }, }, }, From 8c14ce906201ada1229f18c088d1999d010e29d8 Mon Sep 17 00:00:00 2001 From: chan Date: Thu, 19 Feb 2026 10:18:14 +0900 Subject: [PATCH 14/18] =?UTF-8?q?fix:=EA=B3=B5=EA=B3=A0=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20/=20=EA=B3=B5=EA=B3=A0=20=EC=B9=B4?= =?UTF-8?q?=EB=93=9C=20=EC=84=B9=EC=85=98=20/=20=EC=8B=9C=ED=8A=B8=20?= =?UTF-8?q?=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/ui/components/homeFullSheet.tsx | 6 +++--- src/features/home/ui/homeUrgentNoticeList.tsx | 2 +- src/features/listings/hooks/listingsHooks.tsx | 13 ++++++------ .../ui/listingsContents/listingsBookMark.tsx | 17 +++++++++++++-- .../listingsContents/listingsContentCard.tsx | 21 ++++++++----------- .../listingsFullSheet/listingsFullSheet.tsx | 10 ++++++--- .../mobileFrameWithSheetPortal.tsx | 2 +- 7 files changed, 43 insertions(+), 28 deletions(-) diff --git a/src/features/home/ui/components/homeFullSheet.tsx b/src/features/home/ui/components/homeFullSheet.tsx index 0ed5cc2..a2215aa 100644 --- a/src/features/home/ui/components/homeFullSheet.tsx +++ b/src/features/home/ui/components/homeFullSheet.tsx @@ -30,7 +30,7 @@ export const HomeSheet = () => { {open && ( <> { /> { animate={{ x: 0, opacity: 1 }} exit={{ x: -100, opacity: 0 }} transition={{ duration: 0.5, ease: "easeInOut" }} - className="flex h-full flex-col justify-between" + className="z-11 flex h-full flex-col justify-between" > {mode?.key === "pinpoints" && } {mode?.key === "maxTime" && } diff --git a/src/features/home/ui/homeUrgentNoticeList.tsx b/src/features/home/ui/homeUrgentNoticeList.tsx index dd64225..f73b259 100644 --- a/src/features/home/ui/homeUrgentNoticeList.tsx +++ b/src/features/home/ui/homeUrgentNoticeList.tsx @@ -34,7 +34,7 @@ export const UrgentNoticeList = () => {

전체보기 diff --git a/src/features/listings/hooks/listingsHooks.tsx b/src/features/listings/hooks/listingsHooks.tsx index 9d72bf0..e1b40c0 100644 --- a/src/features/listings/hooks/listingsHooks.tsx +++ b/src/features/listings/hooks/listingsHooks.tsx @@ -37,9 +37,9 @@ const normalizeRentType = (rentType: string) => { }; export const getListingIcon = (type: string, housingType: string, size = 78) => { - const Nyear = normalizeRentType(type); + const Near = normalizeRentType(type); - const IconComp = LISTING_ICON_MAP[Nyear]?.[housingType]; + const IconComp = LISTING_ICON_MAP[Near]?.[housingType]; if (!IconComp) return null; return ; @@ -160,14 +160,15 @@ export const HouseICons = (item: ListingNormalized) => { type HouseRentalProps = ListingNormalized & { query: "listingListInfinite" | "listingSearchInfinite" | "notice"; }; -// ListingNormalized +// ListingNormalized export const HouseRental = ({ query, ...item }: HouseRentalProps) => { - const rantalText = getListingsRental(item.type); - if (!rantalText) return null; + const Near = normalizeRentType(item.type); + const rentalText = getListingsRental(Near); + if (!rentalText) return null; return ( - + ); diff --git a/src/features/listings/ui/listingsContents/listingsBookMark.tsx b/src/features/listings/ui/listingsContents/listingsBookMark.tsx index 41c2e6a..4c3b8bd 100644 --- a/src/features/listings/ui/listingsContents/listingsBookMark.tsx +++ b/src/features/listings/ui/listingsContents/listingsBookMark.tsx @@ -6,13 +6,26 @@ export const ListingBookMark = ({ item, border }: { item: string; border: string aria-label="Toggle bookmark" size="sm" variant="outline" - className={`data-[state=on]:*:[svg]:fill-blue-500 data-[state=on]:*:[svg]:stroke-blue-500 ${border}`} + className={` ${border} data-[state=on]:*:[svg]:fill-blue-500 data-[state=on]:*:[svg]:stroke-blue-500 max-w-full overflow-hidden rounded-[4px]`} > -

{item}

+

{item}

); }; +// export const ListingBookMark = ({ item, border }: { item: string; border: string }) => { +// return ( +// +//

{item}

+//
+// ); +// }; + export const ListingBgBookMark = ({ item, bg, diff --git a/src/features/listings/ui/listingsContents/listingsContentCard.tsx b/src/features/listings/ui/listingsContents/listingsContentCard.tsx index 6410a06..8b6a01e 100644 --- a/src/features/listings/ui/listingsContents/listingsContentCard.tsx +++ b/src/features/listings/ui/listingsContents/listingsContentCard.tsx @@ -27,9 +27,9 @@ export const ListingContentsCard = ({ data }: { data: T[ onClick={() => handleRouter(normalized.id)} >
-
+
-

+

{normalized.supplier}

@@ -38,26 +38,23 @@ export const ListingContentsCard = ({ data }: { data: T[
-
-
{ - e.stopPropagation(); - }} - > +
+
e.stopPropagation()}>
+
-

+

+
-

모집일정

-

+

모집일정

+

{formatApplyPeriod(normalized.applyPeriod)}

diff --git a/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx b/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx index 37f2802..743d300 100644 --- a/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx +++ b/src/features/listings/ui/listingsFullSheet/listingsFullSheet.tsx @@ -253,7 +253,7 @@ const FilterSheetContainer = ({ exit={{ opacity: 0 }} /> void }) => { <>
-
+

공고 필터

-
diff --git a/src/shared/ui/globalRender/mobileFrameWithSheetPortal.tsx b/src/shared/ui/globalRender/mobileFrameWithSheetPortal.tsx index 70c66c4..1a3f979 100644 --- a/src/shared/ui/globalRender/mobileFrameWithSheetPortal.tsx +++ b/src/shared/ui/globalRender/mobileFrameWithSheetPortal.tsx @@ -39,7 +39,7 @@ export function MobileFrameWithSheetPortal({
{/* 바텀시트가 이 컨테이너에만 렌더되도록 포탈 타깃 */} From b08ee63580aa47f61eb64fea6c3f352769d56ac0 Mon Sep 17 00:00:00 2001 From: JW_Ahn0 Date: Thu, 19 Feb 2026 14:00:36 +0900 Subject: [PATCH 15/18] =?UTF-8?q?feat:=20=EC=9E=90=EA=B2=A9=EC=A7=84?= =?UTF-8?q?=EB=8B=A8=20API=20=EC=97=B0=EB=8F=99=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모달: 모바일 프레임 내부 렌더 옵션, 300px대·버튼 140px, X 버튼 플래그 - 홈: 자격진단 기준 N건(totalElements), 100%/0% 완료 표시 - 마이페이지: 자격진단 하러가기 확인 모달, 핀 보고서 결과 유무별 카드/빈 상태 - 자격진단 입력 완료 - 로딩 - 결과 임시 화면 개발 완료 - 자격진단 V2/API 연동 완료 - 기존 자격진단 결과 있는 경우 modal 출력하도록 개발 완료 --- app/eligibility/page.tsx | 51 +++- app/eligibility/result/final/page.tsx | 11 + src/features/eligibility/api/diagnosisApi.ts | 20 ++ .../eligibility/api/diagnosisTypes.ts | 121 ++++++++++ .../api/mapEligibilityToDiagnosisRequest.ts | 220 ++++++++++++++++++ .../eligibility/hooks/useDiagnosisLatest.ts | 26 +++ .../eligibility/hooks/useDiagnosisResult.ts | 39 ++++ .../eligibility/model/diagnosisResultStore.ts | 22 ++ .../ui/result/diagnosisResultBanner.tsx | 65 ++++++ .../ui/result/diagnosisResultConstants.ts | 31 +++ .../ui/result/diagnosisResultItem.tsx | 64 +++++ .../ui/result/diagnosisResultList.tsx | 77 ++++++ src/features/eligibility/ui/result/index.ts | 7 + src/features/home/ui/homeActionCardList.tsx | 7 +- src/features/mypage/model/mypageConstants.ts | 5 + src/features/mypage/ui/pinReportSection.tsx | 124 ++++++++++ src/shared/api/endpoints.ts | 4 +- src/shared/api/http.ts | 6 + src/shared/lib/headlessUi/modal/dialog.tsx | 55 +++-- src/shared/ui/errorState/ErrorState.tsx | 7 +- .../rightIconDefaultHeader.tsx | 22 ++ src/shared/ui/header/index.ts | 1 + src/shared/ui/modal/default/modal.tsx | 56 +++-- src/shared/ui/modal/default/type.ts | 4 + src/shared/ui/modal/preset.ts | 14 +- src/widgets/eligibilitySection/index.ts | 1 + .../ui/eligibilityFinalResultSection.tsx | 91 ++++++++ .../ui/eligibilityResultSection.tsx | 80 +++++-- .../ui/eligibilitySection.tsx | 30 +-- .../mypageSection/ui/MypageSection.tsx | 128 ++++++---- .../mypageSection/ui/PinpointsSection.tsx | 44 ++-- .../mypageSection/ui/ProfileSection.tsx | 14 +- .../mypageSection/ui/SettingsSection.tsx | 22 +- .../mypageSection/ui/WithdrawSection.tsx | 30 ++- 34 files changed, 1319 insertions(+), 180 deletions(-) create mode 100644 app/eligibility/result/final/page.tsx create mode 100644 src/features/eligibility/api/diagnosisApi.ts create mode 100644 src/features/eligibility/api/diagnosisTypes.ts create mode 100644 src/features/eligibility/api/mapEligibilityToDiagnosisRequest.ts create mode 100644 src/features/eligibility/hooks/useDiagnosisLatest.ts create mode 100644 src/features/eligibility/hooks/useDiagnosisResult.ts create mode 100644 src/features/eligibility/model/diagnosisResultStore.ts create mode 100644 src/features/eligibility/ui/result/diagnosisResultBanner.tsx create mode 100644 src/features/eligibility/ui/result/diagnosisResultConstants.ts create mode 100644 src/features/eligibility/ui/result/diagnosisResultItem.tsx create mode 100644 src/features/eligibility/ui/result/diagnosisResultList.tsx create mode 100644 src/shared/ui/header/header/rightIconDefaultHeader/rightIconDefaultHeader.tsx create mode 100644 src/widgets/eligibilitySection/ui/eligibilityFinalResultSection.tsx diff --git a/app/eligibility/page.tsx b/app/eligibility/page.tsx index 01e43c4..9544940 100644 --- a/app/eligibility/page.tsx +++ b/app/eligibility/page.tsx @@ -1,23 +1,66 @@ "use client"; -import { Suspense, useEffect } from "react"; +import { Suspense, useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; import { EligibilitySection } from "@/src/widgets/eligibilitySection"; import { useEligibilityStore } from "@/src/features/eligibility/model/eligibilityStore"; +import { useDiagnosisResultStore } from "@/src/features/eligibility/model/diagnosisResultStore"; +import { getDiagnosisLatest } from "@/src/features/eligibility/api/diagnosisApi"; +import type { DiagnosisResultData } from "@/src/features/eligibility/api/diagnosisTypes"; +import { Modal } from "@/src/shared/ui/modal/default/modal"; import { Spinner } from "@/src/shared/ui/spinner/default"; export default function EligibilityPage() { + const router = useRouter(); const reset = useEligibilityStore(state => state.reset); + const setDiagnosisResult = useDiagnosisResultStore(s => s.setResult); + const [isModalOpen, setIsModalOpen] = useState(false); useEffect(() => { - // 자격진단 페이지 진입 시 store 초기화 - reset(); - }, [reset]); + let mounted = true; + + const checkLatest = async () => { + try { + const response = await getDiagnosisLatest(); + if (!mounted) return; + const data = response?.data; + if (data != null && typeof data === "object" && "eligible" in data) { + setDiagnosisResult(data as DiagnosisResultData); + setIsModalOpen(true); + } else { + reset(); + } + } catch { + if (mounted) reset(); + } + }; + + checkLatest(); + return () => { + mounted = false; + }; + }, [reset, setDiagnosisResult]); + + const handleModalButtonClick = (index: number) => { + setIsModalOpen(false); + if (index === 0) { + setDiagnosisResult(null); + reset(); + } else { + router.push("/eligibility/result/final"); + } + }; return (
}> +
); } diff --git a/app/eligibility/result/final/page.tsx b/app/eligibility/result/final/page.tsx new file mode 100644 index 0000000..325086a --- /dev/null +++ b/app/eligibility/result/final/page.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { EligibilityFinalResultSection } from "@/src/widgets/eligibilitySection"; + +export default function EligibilityFinalResultPage() { + return ( +
+ +
+ ); +} diff --git a/src/features/eligibility/api/diagnosisApi.ts b/src/features/eligibility/api/diagnosisApi.ts new file mode 100644 index 0000000..4bb125e --- /dev/null +++ b/src/features/eligibility/api/diagnosisApi.ts @@ -0,0 +1,20 @@ +import { http, API_BASE_URL_V2 } from "@/src/shared/api/http"; +import { + DIAGNOSIS_ENDPOINT, + DIAGNOSIS_LATEST_ENDPOINT, +} from "@/src/shared/api/endpoints"; +import type { IResponse } from "@/src/shared/types/response"; +import type { DiagnosisResultData } from "./diagnosisTypes"; +import type { DiagnosisPostRequest } from "./diagnosisTypes"; + +const v2Options = { baseURL: API_BASE_URL_V2 }; + +/** GET /v2/diagnosis/latest - 청약 진단 최신 결과 조회 */ +export function getDiagnosisLatest() { + return http.get>(DIAGNOSIS_LATEST_ENDPOINT, undefined, v2Options); +} + +/** POST /v2/diagnosis - 청약 진단 제출 */ +export function postDiagnosis(data: D) { + return http.post, D>(DIAGNOSIS_ENDPOINT, data, v2Options); +} diff --git a/src/features/eligibility/api/diagnosisTypes.ts b/src/features/eligibility/api/diagnosisTypes.ts new file mode 100644 index 0000000..0c728d7 --- /dev/null +++ b/src/features/eligibility/api/diagnosisTypes.ts @@ -0,0 +1,121 @@ +/** POST /v2/diagnosis 요청 body - API DTO Enum 타입 */ + +export const DIAGNOSIS_GENDER = ["남성", "여성", "미정"] as const; +export type DiagnosisGender = (typeof DIAGNOSIS_GENDER)[number]; + +export const DIAGNOSIS_ACCOUNT_YEARS = [ + "6개월 미만", + "6개월 이상 ~ 1년 미만", + "1년 이상 ~ 2년 미만", + "2년 이상", +] as const; +export type DiagnosisAccountYears = (typeof DIAGNOSIS_ACCOUNT_YEARS)[number]; + +export const DIAGNOSIS_ACCOUNT_DEPOSIT = [ + "0회 ~ 5회", + "6회 ~ 11회", + "12회 ~ 23회", + "24회 ~ 35회", + "36회 ~ 48회", + "49회 ~ 59회", + "60회 이상", +] as const; +export type DiagnosisAccountDeposit = (typeof DIAGNOSIS_ACCOUNT_DEPOSIT)[number]; + +export const DIAGNOSIS_ACCOUNT = ["600만원 이하", "600만원 이상"] as const; +export type DiagnosisAccount = (typeof DIAGNOSIS_ACCOUNT)[number]; + +export const DIAGNOSIS_EDUCATION_STATUS = [ + "대학교 재학 중이거나 다음 학기에 입학 예정", + "대학교 휴학 중이며 다음 학기 복학 예정", + "대학교 혹은 고등학교 졸업/중퇴 후 2년 이내", + "졸업/중퇴 후 2년이 지났지만 대학원에 재학 중", + "해당 사항 없음", +] as const; +export type DiagnosisEducationStatus = (typeof DIAGNOSIS_EDUCATION_STATUS)[number]; + +export const DIAGNOSIS_INCOME_LEVEL = [ + "1구간", + "2구간", + "3구간", + "4구간", + "5구간", + "6구간", + "기타", +] as const; +export type DiagnosisIncomeLevel = (typeof DIAGNOSIS_INCOME_LEVEL)[number]; + +export const DIAGNOSIS_HOUSING_STATUS = [ + "나는 무주택자지만 우리 가구원중 주택 소유자가 있어요", + "우리집 가구원 모두 주택을 소유하고 있지 않아요", + "주택을 소유하고 있어요", +] as const; +export type DiagnosisHousingStatus = (typeof DIAGNOSIS_HOUSING_STATUS)[number]; + +export const DIAGNOSIS_SPECIAL_CATEGORY = [ + "주거급여 수급자", + "생계/의료급여 수급자", + "한부모 가정", + "보호대상 한부모 가정", + "친인척 위탁가정", + "대리양육가정", + "철거민", + "국가 유공자 본인/가구", + "위안부 피해자 본인/가구", + "북한이탈주민 본인", + "장애인 등록자/장애인 가구", + "영구임대 퇴거자", + "장기복무 제대군인", + "주거 취약계층/긴급 주거지원 대상자", + "위탁가정/보육원 시설종료2년이내, 종료예정자", + "귀한 국군포로 본인", + "교통사고 유자녀 가정", + "산단근로자", + "보증 거절자", + "나 또는 배우자가 노부모를 1년 이상 부양", + "그룹홈 거주", + "조손가족", +] as const; +export type DiagnosisSpecialCategory = (typeof DIAGNOSIS_SPECIAL_CATEGORY)[number]; + +/** POST /v2/diagnosis 요청 body */ +export interface DiagnosisPostRequest { + gender: DiagnosisGender; + birthday: string; + monthPay: number; + hasAccount: boolean; + accountYears: DiagnosisAccountYears; + accountDeposit: DiagnosisAccountDeposit; + account: DiagnosisAccount; + maritalStatus: boolean; + marriageYears: number; + unbornChildrenCount: number; + under6ChildrenCount: number; + over7MinorChildrenCount: number; + educationStatus: DiagnosisEducationStatus; + hasCar: boolean; + carValue: number; + isHouseholdHead: boolean; + isSingle: boolean; + fetusCount: number; + minorCount: number; + adultCount: number; + incomeLevel: DiagnosisIncomeLevel; + housingStatus: DiagnosisHousingStatus; + housingYears: number; + propertyAsset: number; + carAsset: number; + financialAsset: number; + hasSpecialCategory: DiagnosisSpecialCategory[]; +} + +/** POST /v2/diagnosis 응답 data (공통 제외). GET /diagnosis/latest 응답에 소득분위·지원대상이 포함 */ +export interface DiagnosisResultData { + eligible: boolean; + decisionMessage: string; + recommended: string[]; + /** 내 소득분위 (예: "1분위")*/ + incomeLevel?: string; + /** 나의 지원 가능 대상 */ + targetGroups?: string[]; +} diff --git a/src/features/eligibility/api/mapEligibilityToDiagnosisRequest.ts b/src/features/eligibility/api/mapEligibilityToDiagnosisRequest.ts new file mode 100644 index 0000000..3561d9a --- /dev/null +++ b/src/features/eligibility/api/mapEligibilityToDiagnosisRequest.ts @@ -0,0 +1,220 @@ +import type { EligibilityData } from "../model/eligibilityStore"; +import type { DiagnosisPostRequest, DiagnosisSpecialCategory } from "./diagnosisTypes"; +import { + DIAGNOSIS_ACCOUNT_YEARS, + DIAGNOSIS_ACCOUNT_DEPOSIT, + DIAGNOSIS_ACCOUNT, + DIAGNOSIS_EDUCATION_STATUS, + DIAGNOSIS_HOUSING_STATUS, + DIAGNOSIS_INCOME_LEVEL, +} from "./diagnosisTypes"; + +/** Store housingSubscriptionPeriod key → API accountYears */ +const ACCOUNT_YEARS_MAP: Record = { + "1": "6개월 미만", + "2": "6개월 이상 ~ 1년 미만", + "3": "1년 이상 ~ 2년 미만", + "4": "2년 이상", +}; + +/** Store housingSubscriptionPaymentCount key → API accountDeposit */ +const ACCOUNT_DEPOSIT_MAP: Record = { + "1": "0회 ~ 5회", + "2": "6회 ~ 11회", + "3": "12회 ~ 23회", + "4": "24회 ~ 35회", + "5": "36회 ~ 48회", + "6": "49회 ~ 59회", + "7": "60회 이상", +}; + +/** Store totalPaymentAmount id → API account (600만원) */ +const ACCOUNT_MAP: Record = { + "1": "600만원 이상", + "2": "600만원 이하", +}; + +/** Store householdHousingOwnershipStatus → API housingStatus */ +const HOUSING_STATUS_MAP: Record = { + "1": "나는 무주택자지만 우리 가구원중 주택 소유자가 있어요", + "2": "우리집 가구원 모두 주택을 소유하고 있지 않아요", + "3": "주택을 소유하고 있어요", +}; + +/** Store youngSingleStudentStatus id → API educationStatus */ +const EDUCATION_STATUS_MAP: Record = { + "1": "해당 사항 없음", + "2": "대학교 재학 중이거나 다음 학기에 입학 예정", + "3": "대학교 휴학 중이며 다음 학기 복학 예정", + "4": "대학교 혹은 고등학교 졸업/중퇴 후 2년 이내", + "5": "졸업/중퇴 후 2년이 지났지만 대학원에 재학 중", +}; + +function toNum(s: string | null | undefined): number { + if (s == null || s === "") return 0; + const n = Number(s); + return Number.isFinite(n) ? n : 0; +} + +function formatBirthday(date: Date | null): string { + if (!date) return ""; + const y = date.getFullYear(); + const m = String(date.getMonth() + 1).padStart(2, "0"); + const d = String(date.getDate()).padStart(2, "0"); + return `${y}-${m}-${d}`; +} + +/** + * 스토어 monthlyIncome → 원 단위. + * (UI에서 만원으로 넣으면 150, 원으로 넣으면 1500000 저장되는 경우 모두 처리) + */ +function monthlyIncomeToWon(monthlyIncome: string | null): number { + const n = toNum(monthlyIncome); + if (n <= 0) return 0; + return n <= 10000 ? n * 10000 : n; +} + +/** 월소득(만원 기준 비교) + 수급자 여부 → API incomeLevel */ +function toIncomeLevel( + monthlyIncome: string | null, + benefitTypes: string[] +): (typeof DIAGNOSIS_INCOME_LEVEL)[number] { + if (benefitTypes?.length) return "1구간"; + const won = monthlyIncomeToWon(monthlyIncome); + const manwon = won / 10000; + if (manwon <= 0) return "1구간"; + if (manwon <= 150) return "1구간"; + if (manwon <= 250) return "2구간"; + if (manwon <= 350) return "3구간"; + if (manwon <= 450) return "4구간"; + if (manwon <= 550) return "5구간"; + if (manwon <= 650) return "6구간"; + return "기타"; +} + +/** benefitTypes store id → API hasSpecialCategory */ +const BENEFIT_TO_SPECIAL: Record = { + "1": "주거급여 수급자", + "2": "생계/의료급여 수급자", +}; + +/** familyTypes store id → API hasSpecialCategory */ +const FAMILY_TO_SPECIAL: Record = { + "1": "친인척 위탁가정", + "2": "대리양육가정", + "3": "한부모 가정", + "4": "보호대상 한부모 가정", +}; + +/** specialEligibilityTypes store id → API hasSpecialCategory */ +const SPECIAL_ELIGIBILITY_TO_API: Record = { + "1": "국가 유공자 본인/가구", + "2": "위안부 피해자 본인/가구", + "3": "북한이탈주민 본인", + "4": "장애인 등록자/장애인 가구", + "5": "교통사고 유자녀 가정", + "6": "영구임대 퇴거자", + "7": "영구임대 퇴거자", + "8": "주거 취약계층/긴급 주거지원 대상자", + "9": "산단근로자", + "10": "보증 거절자", +}; + +/** marriedHouseholdFamilyTypes store id → API hasSpecialCategory */ +const MARRIED_HOUSEHOLD_FAMILY_TO_SPECIAL: Record = { + "1": "나 또는 배우자가 노부모를 1년 이상 부양", + "2": "조손가족", +}; + +/** Store 라벨(또는 id) → API hasSpecialCategory (라벨로 저장된 경우 대응) */ +const SPECIAL_LABEL_TO_API: Record = { + "국가 유공자 본인/가구": "국가 유공자 본인/가구", + "위안부 피해자 본인/가구": "위안부 피해자 본인/가구", + "북한이탈주민 본인": "북한이탈주민 본인", + "장애인 등록자/장애인 가구": "장애인 등록자/장애인 가구", + "교통사고 유자녀 가정": "교통사고 유자녀 가정", + "부도 공공임대 퇴거자": "영구임대 퇴거자", + "영구임대 퇴거자": "영구임대 퇴거자", + "주거 취약계층/긴급 주거지원 대상자": "주거 취약계층/긴급 주거지원 대상자", + "산단 근로자": "산단근로자", + 산단근로자: "산단근로자", + 보증거절자: "보증 거절자", + "보증 거절자": "보증 거절자", +}; + +function mapHasSpecialCategory(data: EligibilityData): DiagnosisSpecialCategory[] { + const set = new Set(); + + (data.benefitTypes ?? []).forEach(id => { + const api = BENEFIT_TO_SPECIAL[id]; + if (api) set.add(api); + }); + (data.familyTypes ?? []).forEach(id => { + const api = FAMILY_TO_SPECIAL[id]; + if (api) set.add(api); + }); + (data.specialEligibilityTypes ?? []).forEach(val => { + const api = SPECIAL_ELIGIBILITY_TO_API[val] ?? SPECIAL_LABEL_TO_API[val]; + if (api) set.add(api); + }); + (data.marriedHouseholdFamilyTypes ?? []).forEach(id => { + const api = MARRIED_HOUSEHOLD_FAMILY_TO_SPECIAL[id]; + if (api) set.add(api); + }); + + return Array.from(set); +} + +/** + * EligibilityData(store) → POST /v2/diagnosis 요청 body 로 변환 (API DTO Enum 준수) + */ +export function mapEligibilityToDiagnosisRequest(data: EligibilityData): DiagnosisPostRequest { + const hasAccount = data.hasHousingSubscriptionSavings === "1"; + const maritalStatus = data.isNewlyMarried === true; + const isSingle = !maritalStatus && data.marriageStatus !== "1"; + + const spouse = data.spouseChildrenInfo ?? data.marriedHouseholdChildrenInfo; + const unbornChildrenCount = spouse?.expectedBirth ?? 0; + const under6ChildrenCount = data.childrenInfo?.under6 ?? spouse?.under6 ?? 0; + const over7MinorChildrenCount = data.childrenInfo?.over7 ?? spouse?.over7 ?? 0; + const minorCount = under6ChildrenCount + over7MinorChildrenCount; + const adultCount = maritalStatus ? 2 : 1; + + const housingStatus = + HOUSING_STATUS_MAP[data.householdHousingOwnershipStatus ?? ""] ?? + "우리집 가구원 모두 주택을 소유하고 있지 않아요"; + + const monthPay = monthlyIncomeToWon(data.monthlyIncome); + + const gender = data.gender === "1" ? "남성" : data.gender === "2" ? "여성" : "미정"; + + return { + gender, + birthday: formatBirthday(data.birthDate), + monthPay, + hasAccount, + accountYears: ACCOUNT_YEARS_MAP[data.housingSubscriptionPeriod ?? ""] ?? "2년 이상", + accountDeposit: ACCOUNT_DEPOSIT_MAP[data.housingSubscriptionPaymentCount ?? ""] ?? "0회 ~ 5회", + account: ACCOUNT_MAP[data.totalPaymentAmount ?? ""] ?? "600만원 이하", + maritalStatus, + marriageYears: toNum(data.marriagePeriod), + unbornChildrenCount, + under6ChildrenCount, + over7MinorChildrenCount, + educationStatus: EDUCATION_STATUS_MAP[data.youngSingleStudentStatus ?? ""] ?? "해당 사항 없음", + hasCar: data.hasCar === "1", + carValue: toNum(data.carAssetValue ?? data.householdCarAssetValue), + isHouseholdHead: data.householdRole === "1", + isSingle, + fetusCount: unbornChildrenCount, + minorCount, + adultCount, + incomeLevel: toIncomeLevel(data.monthlyIncome, data.benefitTypes ?? []), + housingStatus, + housingYears: toNum(data.housingDisposalYears), + propertyAsset: toNum(data.landAssetValue ?? data.householdLandAssetValue), + carAsset: toNum(data.carAssetValue ?? data.householdCarAssetValue), + financialAsset: toNum(data.financialAssetValue ?? data.householdFinancialAssetValue), + hasSpecialCategory: mapHasSpecialCategory(data), + }; +} diff --git a/src/features/eligibility/hooks/useDiagnosisLatest.ts b/src/features/eligibility/hooks/useDiagnosisLatest.ts new file mode 100644 index 0000000..a2e58c7 --- /dev/null +++ b/src/features/eligibility/hooks/useDiagnosisLatest.ts @@ -0,0 +1,26 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { getDiagnosisLatest } from "../api/diagnosisApi"; +import type { DiagnosisResultData } from "../api/diagnosisTypes"; + +const QUERY_KEY = ["diagnosis", "latest"]; + +/** 마이페이지 등에서 자격진단 최신 결과 조회 (GET /v2/diagnosis/latest) */ +export function useDiagnosisLatest() { + const query = useQuery({ + queryKey: QUERY_KEY, + queryFn: async () => { + const res = await getDiagnosisLatest(); + return (res?.data ?? null) as DiagnosisResultData | null; + }, + retry: false, + }); + + return { + data: query.data ?? null, + isLoading: query.isLoading, + isError: query.isError, + refetch: query.refetch, + }; +} diff --git a/src/features/eligibility/hooks/useDiagnosisResult.ts b/src/features/eligibility/hooks/useDiagnosisResult.ts new file mode 100644 index 0000000..9b5abcc --- /dev/null +++ b/src/features/eligibility/hooks/useDiagnosisResult.ts @@ -0,0 +1,39 @@ +"use client"; + +import { useCallback, useState } from "react"; +import { useEligibilityStore } from "../model/eligibilityStore"; +import { postDiagnosis } from "../api/diagnosisApi"; +import { mapEligibilityToDiagnosisRequest } from "../api/mapEligibilityToDiagnosisRequest"; +import type { DiagnosisResultData } from "../api/diagnosisTypes"; + +export function useDiagnosisResult() { + const data = useEligibilityStore(); + const [result, setResult] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const submit = useCallback(async () => { + setIsLoading(true); + setError(null); + try { + const body = mapEligibilityToDiagnosisRequest(data); + const response = await postDiagnosis(body); + const resultData = response.data as DiagnosisResultData | undefined; + if (resultData != null) { + setResult(resultData); + return resultData; + } + setResult(null); + return null; + } catch (err) { + const e = err instanceof Error ? err : new Error(String(err)); + setError(e); + setResult(null); + throw e; + } finally { + setIsLoading(false); + } + }, [data]); + + return { result, isLoading, error, submit }; +} diff --git a/src/features/eligibility/model/diagnosisResultStore.ts b/src/features/eligibility/model/diagnosisResultStore.ts new file mode 100644 index 0000000..67bee26 --- /dev/null +++ b/src/features/eligibility/model/diagnosisResultStore.ts @@ -0,0 +1,22 @@ +import { create } from "zustand"; +import type { DiagnosisResultData } from "../api/diagnosisTypes"; + +export interface DiagnosisResultMeta { + incomeLevel?: string; +} + +interface DiagnosisResultState { + result: DiagnosisResultData | null; + incomeLevel: string | null; + setResult: (result: DiagnosisResultData | null, meta?: DiagnosisResultMeta) => void; +} + +export const useDiagnosisResultStore = create(set => ({ + result: null, + incomeLevel: null, + setResult: (result, meta) => + set({ + result, + incomeLevel: result ? (meta?.incomeLevel ?? null) : null, + }), +})); diff --git a/src/features/eligibility/ui/result/diagnosisResultBanner.tsx b/src/features/eligibility/ui/result/diagnosisResultBanner.tsx new file mode 100644 index 0000000..d159eb5 --- /dev/null +++ b/src/features/eligibility/ui/result/diagnosisResultBanner.tsx @@ -0,0 +1,65 @@ +"use client"; + +import ResultBannerImg from "@/src/assets/images/eligibility/resultBannerImg"; +import { cn } from "@/lib/utils"; + +export interface DiagnosisResultBannerProps { + /** 사용자 이름 */ + userName: string; + /** 소득 구간 (예: "4구간" → "4분위"로 표시) */ + incomeLevel: string | null; + /** 추천 유형 요약 (예: ["청년 특별공급", "신혼부부 특별공급"] → "청년 특별공급, 신혼부부 특별공급 으로 신청이 가능합니다") */ + applicationTypeSummary: string[]; + className?: string; +} + +/** 소득 구간 문자열을 분위 표시로 변환 (예: "4구간" → "4분위") */ +function toIncomeBunwi(incomeLevel: string | null): string { + if (!incomeLevel) return "0분위"; + const match = incomeLevel.replace("구간", "").trim(); + return /^\d+$/.test(match) ? `${match}분위` : incomeLevel; +} + +export const DiagnosisResultBanner = ({ + userName, + incomeLevel, + applicationTypeSummary, + className, +}: DiagnosisResultBannerProps) => { + const bunwi = toIncomeBunwi(incomeLevel); + const summaryText = + applicationTypeSummary.length > 0 + ? `${applicationTypeSummary.join(", ")} 으로 신청이 가능합니다` + : "추천 매물이 있습니다"; + + return ( +
+
+ +
+
+

+ + {userName} + + 님은 소득{" "} + + {bunwi} + + 이고, +

+

+ + {summaryText} + +

+
+
+ ); +}; diff --git a/src/features/eligibility/ui/result/diagnosisResultConstants.ts b/src/features/eligibility/ui/result/diagnosisResultConstants.ts new file mode 100644 index 0000000..10259fb --- /dev/null +++ b/src/features/eligibility/ui/result/diagnosisResultConstants.ts @@ -0,0 +1,31 @@ +/** 진단결과 임대주택 유형별 설명 (API recommended 파싱용 라벨과 매칭) */ +export const HOUSING_TYPE_DESCRIPTIONS: Record = { + 통합공공임대: + "저소득층, 젊은 층 및 장애인·국가유공자 등 사회 취약 계층 등의 주거안정을 목적으로 공급하는 공공임대주택", + 국민임대: + "저소득 무주택 세대의 주거 안정을 위해 국가와 지자체가 공급하는 국민임대주택", + 행복주택: + "청년·신혼부부·노인 등 주거 취약계층을 위한 공공 주거지원 주택", + 공공임대: + "국가·지자체 등이 건설하거나 매입하여 저소득층 등에 임대하는 공공임대주택", + 영구임대: + "저소득 무주택자에게 평생 거주권을 부여하는 영구임대주택", + 장기전세: + "전세금을 지원하여 장기간 거주할 수 있도록 하는 주택", + 매입임대: + "국가·지자체가 기존 주택을 매입하여 저소득층 등에 임대하는 주택", + 전세임대: + "전세 계약을 통해 저소득층 등에 임대하는 주택", +}; + +/** 진단결과 임대주택 유형별 태그 배경색 (tailwind 또는 임의 클래스) */ +export const HOUSING_TYPE_TAG_CLASS: Record = { + 통합공공임대: "bg-teal-100 text-teal-800", + 국민임대: "bg-amber-100 text-amber-800", + 행복주택: "bg-emerald-100 text-emerald-800", + 공공임대: "bg-amber-100 text-amber-800", + 영구임대: "bg-violet-100 text-violet-800", + 장기전세: "bg-rose-100 text-rose-800", + 매입임대: "bg-violet-100 text-violet-800", + 전세임대: "bg-orange-100 text-orange-800", +}; diff --git a/src/features/eligibility/ui/result/diagnosisResultItem.tsx b/src/features/eligibility/ui/result/diagnosisResultItem.tsx new file mode 100644 index 0000000..d9d21f2 --- /dev/null +++ b/src/features/eligibility/ui/result/diagnosisResultItem.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { HOUSING_TYPE_TAG_CLASS } from "./diagnosisResultConstants"; + +export interface DiagnosisResultItemProps { + /** 임대주택 유형 (예: "통합공공임대") */ + housingType: string; + /** 유형 설명 문단 */ + description: string; + /** 신청 가능 유형 목록 (예: ["일반 공급", "청년 특별공급"]) */ + applicationTypes: string[]; + className?: string; +} + +const DEFAULT_TAG_CLASS = "bg-greyscale-grey-100 text-greyscale-grey-700"; + +export const DiagnosisResultItem = ({ + housingType, + description, + applicationTypes, + className, +}: DiagnosisResultItemProps) => { + const tagClass = HOUSING_TYPE_TAG_CLASS[housingType] ?? DEFAULT_TAG_CLASS; + + return ( +
+
+ + {housingType} + +

+ {description} +

+
+
+
+ + 신청 가능 유형 + +
+ {applicationTypes.map((type, index) => ( + + {type} + + ))} +
+
+
+ ); +}; diff --git a/src/features/eligibility/ui/result/diagnosisResultList.tsx b/src/features/eligibility/ui/result/diagnosisResultList.tsx new file mode 100644 index 0000000..57836f2 --- /dev/null +++ b/src/features/eligibility/ui/result/diagnosisResultList.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { useMemo } from "react"; +import { DiagnosisResultItem } from "./diagnosisResultItem"; +import { HOUSING_TYPE_DESCRIPTIONS } from "./diagnosisResultConstants"; + +export interface DiagnosisResultListProps { + /** API recommended 배열 (예: ["통합공공임대 : 청년 특별공급", "통합공공임대 : 신혼부부 특별공급"]) */ + recommended: string[]; + className?: string; +} + +const SECTION_TITLE = "신청 가능한 임대주택"; + +/** "통합공공임대 : 청년 특별공급" 형태를 { housingType, applicationType } 로 파싱 */ +function parseRecommendedItem(item: string): { housingType: string; applicationType: string } { + const sep = " : "; + const idx = item.indexOf(sep); + if (idx === -1) { + return { housingType: item.trim(), applicationType: "" }; + } + return { + housingType: item.slice(0, idx).trim(), + applicationType: item.slice(idx + sep.length).trim(), + }; +} + +/** recommended 배열을 housingType 기준으로 묶어서, 유형별 신청 가능 유형 목록으로 변환 */ +function groupByHousingType( + recommended: string[] +): Array<{ housingType: string; applicationTypes: string[] }> { + const map = new Map>(); + for (const raw of recommended) { + const { housingType, applicationType } = parseRecommendedItem(raw); + if (!housingType) continue; + if (!map.has(housingType)) map.set(housingType, new Set()); + if (applicationType) map.get(housingType)!.add(applicationType); + } + return Array.from(map.entries()).map(([housingType, set]) => ({ + housingType, + applicationTypes: Array.from(set), + })); +} + +export const DiagnosisResultList = ({ + recommended, + className, +}: DiagnosisResultListProps) => { + const items = useMemo(() => groupByHousingType(recommended), [recommended]); + + if (items.length === 0) return null; + + return ( +
+

+ {SECTION_TITLE} +

+
    + {items.map(({ housingType, applicationTypes }) => ( +
  • + +
  • + ))} +
+
+ ); +}; diff --git a/src/features/eligibility/ui/result/index.ts b/src/features/eligibility/ui/result/index.ts index b79c9d2..98561a7 100644 --- a/src/features/eligibility/ui/result/index.ts +++ b/src/features/eligibility/ui/result/index.ts @@ -2,3 +2,10 @@ export { EligibilityResultBanner } from "./eligibilityResultBanner"; export type { EligibilityResultBannerProps } from "./eligibilityResultBanner"; export { EligibilityResultList } from "./eligibilityResultList"; export type { EligibilityResultListProps } from "./eligibilityResultList"; + +export { DiagnosisResultBanner } from "./diagnosisResultBanner"; +export type { DiagnosisResultBannerProps } from "./diagnosisResultBanner"; +export { DiagnosisResultItem } from "./diagnosisResultItem"; +export type { DiagnosisResultItemProps } from "./diagnosisResultItem"; +export { DiagnosisResultList } from "./diagnosisResultList"; +export type { DiagnosisResultListProps } from "./diagnosisResultList"; diff --git a/src/features/home/ui/homeActionCardList.tsx b/src/features/home/ui/homeActionCardList.tsx index 510cbc6..f97d534 100644 --- a/src/features/home/ui/homeActionCardList.tsx +++ b/src/features/home/ui/homeActionCardList.tsx @@ -1,12 +1,13 @@ "use client"; import { ArrowUpRight } from "@/src/assets/icons/button/arrowUpRight"; import { useNoticeCount, useRecommendedNotice } from "@/src/entities/home/hooks/homeHooks"; -import { useRouter } from "next/navigation"; import { useHomeActionCard } from "@/src/features/home/ui/homeUseHooks/homeUseHooks"; +import { useDiagnosisResultStore } from "@/src/features/eligibility/model/diagnosisResultStore"; export const ActionCardList = () => { const { data } = useNoticeCount(); const { data: recommend } = useRecommendedNotice(); + const hasDiagnosisResult = useDiagnosisResultStore(state => state.result != null); const count = data?.count; const { onListingsPageMove, onEligibilityPageMove } = useHomeActionCard(); @@ -46,13 +47,13 @@ export const ActionCardList = () => {

- {recommend?.pages?.length ? recommend?.pages?.length : "0"}건 + {recommend?.pages?.[0]?.totalElements ?? 0}건

-

0% 완료

+

{hasDiagnosisResult ? "100% 완료" : "0% 완료"}

diff --git a/src/features/mypage/model/mypageConstants.ts b/src/features/mypage/model/mypageConstants.ts index 6b83e51..591a875 100644 --- a/src/features/mypage/model/mypageConstants.ts +++ b/src/features/mypage/model/mypageConstants.ts @@ -35,6 +35,11 @@ export const MYPAGE_PIN_REPORT_DESCRIPTION_LINES = [ "맞춤 보고서를 받아보세요", ] as const; export const MYPAGE_PIN_REPORT_BUTTON = "자격진단 하러가기"; +export const MYPAGE_PIN_REPORT_REDIAGNOSIS = "재진단"; +export const MYPAGE_PIN_REPORT_VIEW_DETAIL = "자세히 보기"; +export const MYPAGE_PIN_REPORT_INCOME_LABEL = "내 소득분위"; +export const MYPAGE_PIN_REPORT_TARGET_LABEL = "나의 지원 가능 대상"; +export const MYPAGE_PIN_REPORT_HOUSING_LABEL = "신청 가능한 임대주택"; /** 설정 화면 (settings) */ export const MYPAGE_SETTINGS_TITLE = "설정"; diff --git a/src/features/mypage/ui/pinReportSection.tsx b/src/features/mypage/ui/pinReportSection.tsx index 79ac1fe..b3d5ab6 100644 --- a/src/features/mypage/ui/pinReportSection.tsx +++ b/src/features/mypage/ui/pinReportSection.tsx @@ -1,21 +1,145 @@ "use client"; +import { useMemo } from "react"; +import { ChevronRight, RefreshCw } from "lucide-react"; import { Button } from "@/src/shared/lib/headlessUi"; import { MYPAGE_PIN_REPORT_BUTTON, MYPAGE_PIN_REPORT_DESCRIPTION_LINES, MYPAGE_PIN_REPORT_TITLE, + MYPAGE_PIN_REPORT_REDIAGNOSIS, + MYPAGE_PIN_REPORT_VIEW_DETAIL, + MYPAGE_PIN_REPORT_INCOME_LABEL, + MYPAGE_PIN_REPORT_TARGET_LABEL, + MYPAGE_PIN_REPORT_HOUSING_LABEL, } from "@/src/features/mypage/model/mypageConstants"; +import type { DiagnosisResultData } from "@/src/features/eligibility/api/diagnosisTypes"; +import { useDiagnosisResultStore } from "@/src/features/eligibility/model/diagnosisResultStore"; interface PinReportSectionProps { + /** 자격진단 최신 결과. 있으면 요약 카드, 없으면 '자격진단 하러가기' 빈 상태 표시 */ + diagnosisResult: DiagnosisResultData | null; onDiagnosisClick?: () => void; + onRediagnosisClick?: () => void; + onViewDetailClick?: () => void; } const PIN_REPORT_HEADING_ID = "pin-report-heading"; +const TAG_CLASS = + "inline-flex items-center rounded-full bg-primary-blue-100 px-2.5 py-1 text-xs font-medium text-primary-blue-700"; + +/** recommended 항목에서 housingType만 추출 (예: "통합공공임대 : 청년 특별공급" → "통합공공임대") */ +function getUniqueHousingTypes(recommended: string[]): string[] { + const set = new Set(); + const sep = " : "; + for (const raw of recommended) { + const idx = raw.indexOf(sep); + const housingType = (idx === -1 ? raw : raw.slice(0, idx)).trim(); + if (housingType) set.add(housingType); + } + return Array.from(set); +} export const PinReportSection = ({ + diagnosisResult, onDiagnosisClick, + onRediagnosisClick, + onViewDetailClick, }: PinReportSectionProps) => { + const storeIncomeLevel = useDiagnosisResultStore(s => s.incomeLevel); + + const hasResult = diagnosisResult != null && Array.isArray(diagnosisResult.recommended); + const incomeLevel = + diagnosisResult?.incomeLevel ?? storeIncomeLevel ?? null; + const targetGroups = diagnosisResult?.targetGroups ?? []; + const housingTypes = useMemo( + () => + hasResult && diagnosisResult?.recommended?.length + ? getUniqueHousingTypes(diagnosisResult.recommended) + : [], + [hasResult, diagnosisResult?.recommended] + ); + + if (hasResult) { + return ( +
+
+

+ {MYPAGE_PIN_REPORT_TITLE} +

+ +
+ +
+ {incomeLevel != null && incomeLevel !== "" && ( +
+

+ {MYPAGE_PIN_REPORT_INCOME_LABEL} +

+ {incomeLevel} +
+ )} + + {targetGroups.length > 0 && ( +
+

+ {MYPAGE_PIN_REPORT_TARGET_LABEL} +

+
+ {targetGroups.map(label => ( + + {label} + + ))} +
+
+ )} + + {housingTypes.length > 0 && ( +
+

+ {MYPAGE_PIN_REPORT_HOUSING_LABEL} +

+
+ {housingTypes.map(name => ( + + {name} + + ))} +
+
+ )} + + {onViewDetailClick && ( +
+ +
+ )} +
+
+ ); + } + return (
{ const BASE_URL = process.env.NEXT_PUBLIC_API_URL; +/** v1 base URL을 v2로 치환 (특정 API만 v2 사용 시 baseURL 옵션으로 전달) */ +export const API_BASE_URL_V2 = + typeof process.env.NEXT_PUBLIC_API_URL === "string" + ? process.env.NEXT_PUBLIC_API_URL.replace(/\/v1\/?$/, "/v2") + : ""; + const api: AxiosInstance = axios.create({ baseURL: BASE_URL, withCredentials: true, diff --git a/src/shared/lib/headlessUi/modal/dialog.tsx b/src/shared/lib/headlessUi/modal/dialog.tsx index 6c34511..ed429f0 100644 --- a/src/shared/lib/headlessUi/modal/dialog.tsx +++ b/src/shared/lib/headlessUi/modal/dialog.tsx @@ -32,32 +32,45 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; type DialogContentProps = React.ComponentPropsWithoutRef & { overlayClassName?: string; showCloseButton?: boolean; + /** When set, portal and overlay/content are rendered inside this element with absolute positioning (e.g. mobile frame). */ + container?: HTMLElement | null; }; const DialogContent = React.forwardRef< React.ElementRef, DialogContentProps ->(({ className, children, overlayClassName, showCloseButton = true, ...props }, ref) => ( - - - - {children} - {showCloseButton && ( - - - Close - - )} - - -)); +>(({ className, children, overlayClassName, showCloseButton = true, container, ...props }, ref) => { + const isInsideContainer = Boolean(container); + const contentPositionClass = isInsideContainer + ? "absolute left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%]" + : "fixed left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%]"; + const overlayMergedClassName = isInsideContainer + ? cn("!absolute inset-0", overlayClassName?.replace(/\bfixed\b/g, "absolute")) + : overlayClassName; + + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ); +}); DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( diff --git a/src/shared/ui/errorState/ErrorState.tsx b/src/shared/ui/errorState/ErrorState.tsx index a375d6c..c22a0e3 100644 --- a/src/shared/ui/errorState/ErrorState.tsx +++ b/src/shared/ui/errorState/ErrorState.tsx @@ -29,10 +29,7 @@ export const ErrorState = ({ return (
- home으로 돌아가기 + 홈으로 돌아가기 diff --git a/src/shared/ui/header/header/rightIconDefaultHeader/rightIconDefaultHeader.tsx b/src/shared/ui/header/header/rightIconDefaultHeader/rightIconDefaultHeader.tsx new file mode 100644 index 0000000..e749e08 --- /dev/null +++ b/src/shared/ui/header/header/rightIconDefaultHeader/rightIconDefaultHeader.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { CloseButton, LeftButton } from "@/src/assets/icons/button"; + +type RightIconDefaultHeaderProps = { + title: string; + onRightClick: () => void; +}; + +export const RightIconDefaultHeader = ({ title, onRightClick }: RightIconDefaultHeaderProps) => { + return ( +
+

+ {title} +

+ +
+ ); +}; diff --git a/src/shared/ui/header/index.ts b/src/shared/ui/header/index.ts index a47c509..350bc80 100644 --- a/src/shared/ui/header/index.ts +++ b/src/shared/ui/header/index.ts @@ -1 +1,2 @@ export * from "@/src/shared/ui/header/header/defaultHeader/defaultHeader"; +export * from "@/src/shared/ui/header/header/rightIconDefaultHeader/rightIconDefaultHeader"; diff --git a/src/shared/ui/modal/default/modal.tsx b/src/shared/ui/modal/default/modal.tsx index ceb8746..a3ec896 100644 --- a/src/shared/ui/modal/default/modal.tsx +++ b/src/shared/ui/modal/default/modal.tsx @@ -1,5 +1,9 @@ +"use client"; + +import { useState, useLayoutEffect } from "react"; import { cn } from "@/src/shared/lib/utils"; import { Dialog, DialogContent, DialogTitle } from "@/src/shared/lib/headlessUi/modal/dialog"; +import { useMobileSheetPortal } from "@/src/shared/context/mobileSheetPortalContext"; import { discription, modalContainerPreset, modalOverlayPreset } from "../preset"; import { ModalProps } from "./type"; @@ -13,17 +17,32 @@ export const Modal = ({ overlayClassName, onButtonClick, confirmButtonDisabled = false, + showCloseButton = false, + onClose, }: ModalProps) => { + const portalRef = useMobileSheetPortal(); + const [container, setContainer] = useState(null); + + useLayoutEffect(() => { + if (portalRef?.current) setContainer(portalRef.current); + }, [portalRef]); + if (!open) return null; const modalScript = discription[type]; return ( - + { + if (!value) onClose?.(); + }} + >
@@ -31,20 +50,25 @@ export const Modal = ({
- {modalScript?.btnlabel?.map((item, index) => ( - - ))} + {modalScript?.btnlabel?.map((item, index) => { + const isSinglePrimary = (modalScript?.btnlabel?.length ?? 0) === 1; + const variant = isSinglePrimary ? "solid" : index === 0 ? "outline" : "solid"; + const theme = isSinglePrimary ? "mainBlue" : index === 0 ? "black" : "mainBlue"; + return ( + + ); + })}
diff --git a/src/shared/ui/modal/default/type.ts b/src/shared/ui/modal/default/type.ts index d60802c..fefc7a0 100644 --- a/src/shared/ui/modal/default/type.ts +++ b/src/shared/ui/modal/default/type.ts @@ -20,4 +20,8 @@ export interface ModalProps { onButtonClick?: (buttonIndex: number, buttonLabel: string) => void; /** 확인(두 번째) 버튼 비활성화 (예: 제출 중) */ confirmButtonDisabled?: boolean; + /** 우측 상단 X 버튼 노출 여부. true일 때만 표시 */ + showCloseButton?: boolean; + /** X 버튼 또는 외부 클릭으로 닫을 때 호출 (showCloseButton 사용 시 상태 정리용) */ + onClose?: () => void; } diff --git a/src/shared/ui/modal/preset.ts b/src/shared/ui/modal/preset.ts index 2175cdb..72bf802 100644 --- a/src/shared/ui/modal/preset.ts +++ b/src/shared/ui/modal/preset.ts @@ -5,7 +5,7 @@ export const modalOverlayPreset = export const modalContainerPreset = [ "rounded-xl sm:rounded-xl bg-white p-6 shadow-lg transition-all", - "w-[90%] min-w-[322px] sm:w-[322px] md:w-[400px] lg:w-[500px]", + "w-[90%] min-w-[280px] sm:w-[300px] md:w-[320px]", ] as const; export const filterScript: ModalDescriptProps = { @@ -33,10 +33,22 @@ export const withdrawConfirmScript: ModalDescriptProps = { btnlabel: ["취소", "탈퇴하기"], }; +export const eligibilityPreviousDiagnosisScript: ModalDescriptProps = { + descript: "이전에 진행한 진단 이력이 있어요!\n 새로 시작할까요?", + btnlabel: ["새로 시작하기", "결과보기"], +}; + +export const eligibilityDiagnosisGoScript: ModalDescriptProps = { + descript: "자격진단으로\n나에게 맞는 공고를 확인해볼까요?", + btnlabel: ["자격진단 하러가기"], +}; + export const discription: ModalDescriptMap = { filterSearch: filterScript, quickSearchEnterCheck: quickSearchEnterCheckScript, quickSearchSaveCheck: quickSearchSaveCheckScript, quickSearchResetAlert: quickSearchResetAlertScript, withdrawConfirm: withdrawConfirmScript, + eligibilityPreviousDiagnosis: eligibilityPreviousDiagnosisScript, + eligibilityDiagnosisGo: eligibilityDiagnosisGoScript, }; diff --git a/src/widgets/eligibilitySection/index.ts b/src/widgets/eligibilitySection/index.ts index 26e470b..49468a0 100644 --- a/src/widgets/eligibilitySection/index.ts +++ b/src/widgets/eligibilitySection/index.ts @@ -1,2 +1,3 @@ export { EligibilitySection } from "./ui/eligibilitySection"; export { EligibilityResultSection } from "./ui/eligibilityResultSection"; +export { EligibilityFinalResultSection } from "./ui/eligibilityFinalResultSection"; diff --git a/src/widgets/eligibilitySection/ui/eligibilityFinalResultSection.tsx b/src/widgets/eligibilitySection/ui/eligibilityFinalResultSection.tsx new file mode 100644 index 0000000..2d6b24e --- /dev/null +++ b/src/widgets/eligibilitySection/ui/eligibilityFinalResultSection.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { useMemo } from "react"; +import { useRouter } from "next/navigation"; +import { useDiagnosisResultStore } from "@/src/features/eligibility/model/diagnosisResultStore"; +import { DiagnosisResultBanner, DiagnosisResultList } from "@/src/features/eligibility/ui/result"; +import { RightIconDefaultHeader } from "@/src/shared/ui/header"; +import { ErrorState } from "@/src/shared/ui/errorState"; +import { useOAuthStore } from "@/src/features/login/model/authStore"; +import { PageTransition } from "@/src/shared/ui/animation/pageTransition"; + +const FINAL_RESULT_PAGE_TITLE = "진단 결과"; +const INELIGIBLE_MESSAGE = "조건 미충족으로 인해 자격미달입니다."; +const NULL_RESULT_MESSAGE = "결과가 없습니다."; + +/** "통합공공임대 : 청년 특별공급" 에서 신청 유형만 추출 (배너 요약용) */ +function getApplicationTypeSummary(recommended: string[], maxCount = 3): string[] { + const sep = " : "; + const set = new Set(); + for (const raw of recommended) { + const idx = raw.indexOf(sep); + const applicationType = idx === -1 ? raw.trim() : raw.slice(idx + sep.length).trim(); + if (applicationType) set.add(applicationType); + if (set.size >= maxCount) break; + } + return Array.from(set); +} + +export const EligibilityFinalResultSection = () => { + const router = useRouter(); + const result = useDiagnosisResultStore(s => s.result); + const incomeLevel = useDiagnosisResultStore(s => s.incomeLevel); + const { userName } = useOAuthStore(); + + const applicationTypeSummary = useMemo( + () => (result?.recommended ? getApplicationTypeSummary(result.recommended) : []), + [result?.recommended] + ); + + const showErrorState = result === null || !result.eligible; + + if (showErrorState) { + return ( +
+ +
+ router.push("/home")} + /> +
+
+ +
+
+
+ ); + } + + return ( +
+ +
+ router.push("/home")} + /> +
+
+ + {result.recommended.length > 0 && ( + + )} +
+
+
+ ); +}; diff --git a/src/widgets/eligibilitySection/ui/eligibilityResultSection.tsx b/src/widgets/eligibilitySection/ui/eligibilityResultSection.tsx index aefab08..e67a7dc 100644 --- a/src/widgets/eligibilitySection/ui/eligibilityResultSection.tsx +++ b/src/widgets/eligibilitySection/ui/eligibilityResultSection.tsx @@ -2,6 +2,9 @@ import { useRouter } from "next/navigation"; import { useEligibilityStore } from "@/src/features/eligibility/model/eligibilityStore"; +import { useDiagnosisResult } from "@/src/features/eligibility/hooks/useDiagnosisResult"; +import { useDiagnosisResultStore } from "@/src/features/eligibility/model/diagnosisResultStore"; +import { mapEligibilityToDiagnosisRequest } from "@/src/features/eligibility/api/mapEligibilityToDiagnosisRequest"; import { ELIGIBILITY_RESULT_BUTTON, ELIGIBILITY_RESULT_PAGE_TITLE, @@ -10,39 +13,74 @@ import { EligibilityResultBanner, EligibilityResultList, } from "@/src/features/eligibility/ui/result"; +import EligibilityLoadingState from "@/src/features/eligibility/ui/common/eligibilityLoadingState"; import { DefaultHeader } from "@/src/shared/ui/header"; import { Button } from "@/src/shared/lib/headlessUi/button/button"; import { useOAuthStore } from "@/src/features/login/model/authStore"; +import { toast } from "sonner"; +import { PageTransition } from "@/src/shared/ui/animation/pageTransition"; export const EligibilityResultSection = () => { const router = useRouter(); const data = useEligibilityStore(); const { userName } = useOAuthStore(); + const { submit, isLoading, error } = useDiagnosisResult(); + const setDiagnosisResult = useDiagnosisResultStore(s => s.setResult); + + const handleResultClick = async () => { + try { + const resultData = await submit(); + if (resultData) { + const body = mapEligibilityToDiagnosisRequest(data); + setDiagnosisResult(resultData, { incomeLevel: body.incomeLevel }); + router.push("/eligibility/result/final"); + } else { + toast.error("진단 결과를 받지 못했어요. 다시 시도해 주세요."); + } + } catch { + toast.error("진단 요청에 실패했어요. 다시 시도해 주세요."); + } + }; return (
-
- -
-
- - -
-
- -
+ + + {isLoading ? ( +
+ +
+ ) : ( + <> +
+ + +
+
+ + {error && ( +

+ {error.message} +

+ )} +
+ + )} +
); }; diff --git a/src/widgets/eligibilitySection/ui/eligibilitySection.tsx b/src/widgets/eligibilitySection/ui/eligibilitySection.tsx index 320ef27..935c2a4 100644 --- a/src/widgets/eligibilitySection/ui/eligibilitySection.tsx +++ b/src/widgets/eligibilitySection/ui/eligibilitySection.tsx @@ -32,22 +32,24 @@ export const EligibilitySection = () => { return (
- {/* Stepper */} - {ELIGIBILITY_STEPS.length > 0 && currentGroup && ( -
- -
- )} - - {/* 단계별 폼 컴포넌트 */} - + {/* Stepper */} + {ELIGIBILITY_STEPS.length > 0 && currentGroup && ( +
+ +
+ )} + + + {/* 단계별 폼 컴포넌트 */} + + + + {/* 다음 버튼 */} +
+ +
- - {/* 다음 버튼 */} -
- -
); }; diff --git a/src/widgets/mypageSection/ui/MypageSection.tsx b/src/widgets/mypageSection/ui/MypageSection.tsx index f7e57cc..795e371 100644 --- a/src/widgets/mypageSection/ui/MypageSection.tsx +++ b/src/widgets/mypageSection/ui/MypageSection.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import FootPrintIcon from "@/src/assets/icons/mypage/footPrintIcon"; import MapPinIcon from "@/src/assets/icons/mypage/mapPinIcon"; @@ -23,8 +24,12 @@ import { PinReportSection, UserInfoCard, } from "@/src/features/mypage/ui"; +import { useDiagnosisLatest } from "@/src/features/eligibility/hooks/useDiagnosisLatest"; +import { useDiagnosisResultStore } from "@/src/features/eligibility/model/diagnosisResultStore"; import { ErrorState } from "@/src/shared/ui/errorState"; import { LoadingState } from "@/src/shared/ui/loadingState"; +import { Modal } from "@/src/shared/ui/modal/default/modal"; +import { PageTransition } from "@/src/shared/ui/animation/pageTransition"; /** * 마이페이지 메인 화면 위젯 @@ -33,7 +38,28 @@ import { LoadingState } from "@/src/shared/ui/loadingState"; */ export const MypageSection = () => { const { data, isLoading, isError } = useMypageUser(); + const { data: diagnosisLatest } = useDiagnosisLatest(); + const setDiagnosisResult = useDiagnosisResultStore(s => s.setResult); const router = useRouter(); + const [eligibilityModalOpen, setEligibilityModalOpen] = useState(false); + + const openEligibilityModal = () => setEligibilityModalOpen(true); + const handleModalConfirm = () => { + setEligibilityModalOpen(false); + router.push("/eligibility"); + }; + const handleRediagnosis = () => { + router.push("/eligibility"); + }; + const handleViewDetail = () => router.push("/eligibility/result/final"); + + useEffect(() => { + if (diagnosisLatest) { + setDiagnosisResult(diagnosisLatest, { + incomeLevel: diagnosisLatest.incomeLevel, + }); + } + }, [diagnosisLatest, setDiagnosisResult]); if (isLoading) { return ( @@ -56,55 +82,65 @@ export const MypageSection = () => { return (
- -
- router.push("/mypage/settings")} - /> + + +
+ router.push("/mypage/settings")} /> - router.push("/eligibility")} - /> + - , - label: MYPAGE_LABEL_INTEREST_ENV, - onClick: () => { - alert("관심 주변 환경 설정 미구현 상태"); - }, - }, - { - icon: , - label: MYPAGE_LABEL_PINPOINTS, - onClick: () => router.push("/mypage/pinpoints"), - }, - ]} - /> + setEligibilityModalOpen(false)} + onButtonClick={handleModalConfirm} + /> - , - label: MYPAGE_LABEL_SAVED_LIST, - onClick: () => { - alert("저장 목록 이동 미구현 상태"); - }, - }, - { - icon: , - label: MYPAGE_LABEL_RECENT_ADS, - onClick: () => { - alert("최근 본 공고 이동 미구현 상태"); - }, - }, - ]} - /> -
+ , + label: MYPAGE_LABEL_INTEREST_ENV, + onClick: () => { + alert("관심 주변 환경 설정 미구현 상태"); + }, + }, + { + icon: , + label: MYPAGE_LABEL_PINPOINTS, + onClick: () => router.push("/mypage/pinpoints"), + }, + ]} + /> + + , + label: MYPAGE_LABEL_SAVED_LIST, + onClick: () => { + alert("저장 목록 이동 미구현 상태"); + }, + }, + { + icon: , + label: MYPAGE_LABEL_RECENT_ADS, + onClick: () => { + alert("최근 본 공고 이동 미구현 상태"); + }, + }, + ]} + /> +
+
); }; diff --git a/src/widgets/mypageSection/ui/PinpointsSection.tsx b/src/widgets/mypageSection/ui/PinpointsSection.tsx index 95b3e3e..22bb868 100644 --- a/src/widgets/mypageSection/ui/PinpointsSection.tsx +++ b/src/widgets/mypageSection/ui/PinpointsSection.tsx @@ -15,6 +15,7 @@ import { } from "@/src/features/mypage/model/mypageConstants"; import { Button } from "@/src/shared/lib/headlessUi"; import { DefaultHeader } from "@/src/shared/ui/header"; +import { PageTransition } from "@/src/shared/ui/animation/pageTransition"; /** * 마이페이지 핀포인트 설정 화면 위젯 @@ -42,37 +43,40 @@ export const PinpointsSection = () => { return (
-
- + +
+
-
-
+
+
-
-

- {MYPAGE_LABEL_PINPOINTS} -

-

+

+

{MYPAGE_LABEL_PINPOINTS}

+

{MYPAGE_PINPOINTS_DESCRIPTION} -

-
+

+
-
+
{address ? ( -
+
-
+
) : null} +
); }; diff --git a/src/widgets/mypageSection/ui/ProfileSection.tsx b/src/widgets/mypageSection/ui/ProfileSection.tsx index 4cb5040..5198ba2 100644 --- a/src/widgets/mypageSection/ui/ProfileSection.tsx +++ b/src/widgets/mypageSection/ui/ProfileSection.tsx @@ -8,6 +8,7 @@ import { MYPAGE_PROFILE_LOADING_TITLE, } from "@/src/features/mypage/model/mypageConstants"; import { ProfileForm } from "@/src/features/mypage/ui"; +import { PageTransition } from "@/src/shared/ui/animation/pageTransition"; import { ErrorState } from "@/src/shared/ui/errorState"; import { DefaultHeader } from "@/src/shared/ui/header"; import { LoadingState } from "@/src/shared/ui/loadingState"; @@ -41,15 +42,18 @@ export const ProfileSection = () => { return (
-
- + +
+
- +
+
); }; diff --git a/src/widgets/mypageSection/ui/SettingsSection.tsx b/src/widgets/mypageSection/ui/SettingsSection.tsx index ff19a9c..3eab101 100644 --- a/src/widgets/mypageSection/ui/SettingsSection.tsx +++ b/src/widgets/mypageSection/ui/SettingsSection.tsx @@ -7,10 +7,8 @@ import { MYPAGE_SETTINGS_TITLE, MYPAGE_SETTINGS_WITHDRAW, } from "@/src/features/mypage/model/mypageConstants"; -import { - MypageSettingsMenu, - type MypageSettingsMenuItem, -} from "@/src/features/mypage/ui"; +import { MypageSettingsMenu, type MypageSettingsMenuItem } from "@/src/features/mypage/ui"; +import { PageTransition } from "@/src/shared/ui/animation/pageTransition"; import { DefaultHeader } from "@/src/shared/ui/header"; /** @@ -26,13 +24,15 @@ export const SettingsSection = () => { return (
-
- -
-
-
- -
+ +
+ +
+
+
+ +
+
); }; diff --git a/src/widgets/mypageSection/ui/WithdrawSection.tsx b/src/widgets/mypageSection/ui/WithdrawSection.tsx index df11155..0d6b127 100644 --- a/src/widgets/mypageSection/ui/WithdrawSection.tsx +++ b/src/widgets/mypageSection/ui/WithdrawSection.tsx @@ -2,11 +2,12 @@ import { WithdrawBanner, WithdrawForm } from "@/src/features/mypage/ui"; import { - MYPAGE_WITHDRAW_HEADER_TITLE, + MYPAGE_WITHDRAW_HEADER_TITLE, WITHDRAW_BANNER_DESCRIPTION, WITHDRAW_BANNER_TITLE, } from "@/src/features/mypage/model/mypageConstants"; import { DefaultHeader } from "@/src/shared/ui/header"; +import { PageTransition } from "@/src/shared/ui/animation/pageTransition"; /** * 마이페이지 회원 탈퇴 화면 위젯 @@ -14,19 +15,24 @@ import { DefaultHeader } from "@/src/shared/ui/header"; export const WithdrawSection = () => { return (
-
- + +
+
- -
-
- -
+ +
+
+ +
+
); }; From 66c1a17cccb9df8f87513051479aa1c520ce3439 Mon Sep 17 00:00:00 2001 From: JW_Ahn0 Date: Thu, 19 Feb 2026 15:21:24 +0900 Subject: [PATCH 16/18] =?UTF-8?q?feat:=20=EC=9E=90=EA=B2=A9=EC=A7=84?= =?UTF-8?q?=EB=8B=A8=20=ED=95=80=20=EB=B3=B4=EA=B3=A0=EC=84=9C=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=ED=99=94=EB=A9=B4(=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80,=EC=9E=90=EA=B2=A9=EC=A7=84=EB=8B=A8)=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/eligibility/page.tsx | 15 +++- .../images/eligibility/resultBannerImg.tsx | 18 ++++- src/features/eligibility/api/diagnosisApi.ts | 6 +- .../eligibility/api/diagnosisTypes.ts | 17 ++++- .../eligibility/hooks/useDiagnosisLatest.ts | 6 +- .../ui/result/diagnosisResultBanner.tsx | 10 +-- .../ui/result/diagnosisResultItem.tsx | 19 +++-- .../ui/result/diagnosisResultList.tsx | 10 +-- src/features/mypage/ui/pinReportSection.tsx | 73 ++++++------------- .../ui/eligibilityFinalResultSection.tsx | 2 +- .../mypageSection/ui/MypageSection.tsx | 11 ++- 11 files changed, 98 insertions(+), 89 deletions(-) diff --git a/app/eligibility/page.tsx b/app/eligibility/page.tsx index 9544940..b6a01dc 100644 --- a/app/eligibility/page.tsx +++ b/app/eligibility/page.tsx @@ -6,7 +6,7 @@ import { EligibilitySection } from "@/src/widgets/eligibilitySection"; import { useEligibilityStore } from "@/src/features/eligibility/model/eligibilityStore"; import { useDiagnosisResultStore } from "@/src/features/eligibility/model/diagnosisResultStore"; import { getDiagnosisLatest } from "@/src/features/eligibility/api/diagnosisApi"; -import type { DiagnosisResultData } from "@/src/features/eligibility/api/diagnosisTypes"; +import type { DiagnosisLatestData } from "@/src/features/eligibility/api/diagnosisTypes"; import { Modal } from "@/src/shared/ui/modal/default/modal"; import { Spinner } from "@/src/shared/ui/spinner/default"; @@ -21,11 +21,18 @@ export default function EligibilityPage() { const checkLatest = async () => { try { - const response = await getDiagnosisLatest(); + const response = await getDiagnosisLatest(); if (!mounted) return; - const data = response?.data; + const data = response?.data as DiagnosisLatestData | undefined; if (data != null && typeof data === "object" && "eligible" in data) { - setDiagnosisResult(data as DiagnosisResultData); + setDiagnosisResult( + { + eligible: data.eligible, + decisionMessage: data.diagnosisResult, + recommended: data.recommended, + }, + { incomeLevel: data.myIncomeLevel } + ); setIsModalOpen(true); } else { reset(); diff --git a/src/assets/images/eligibility/resultBannerImg.tsx b/src/assets/images/eligibility/resultBannerImg.tsx index c4b89dc..ae78623 100644 --- a/src/assets/images/eligibility/resultBannerImg.tsx +++ b/src/assets/images/eligibility/resultBannerImg.tsx @@ -1,6 +1,20 @@ -const ResultBannerImg = () => { +import { SVGProps } from "react"; + +interface ResultBannerImgProps extends SVGProps { + width?: number | string; + height?: number | string; +} + +const ResultBannerImg = ({ width = 96, height = 96, ...props }: ResultBannerImgProps) => { return ( - + () { +/** GET /v2/diagnosis/latest - 청약 진단 최신 결과 조회 (POST 응답과 구조 상이) */ +export function getDiagnosisLatest() { return http.get>(DIAGNOSIS_LATEST_ENDPOINT, undefined, v2Options); } diff --git a/src/features/eligibility/api/diagnosisTypes.ts b/src/features/eligibility/api/diagnosisTypes.ts index 0c728d7..3adbde6 100644 --- a/src/features/eligibility/api/diagnosisTypes.ts +++ b/src/features/eligibility/api/diagnosisTypes.ts @@ -109,7 +109,7 @@ export interface DiagnosisPostRequest { hasSpecialCategory: DiagnosisSpecialCategory[]; } -/** POST /v2/diagnosis 응답 data (공통 제외). GET /diagnosis/latest 응답에 소득분위·지원대상이 포함 */ +/** POST /v2/diagnosis 응답 data (공통 제외) */ export interface DiagnosisResultData { eligible: boolean; decisionMessage: string; @@ -119,3 +119,18 @@ export interface DiagnosisResultData { /** 나의 지원 가능 대상 */ targetGroups?: string[]; } + +/** GET /v2/diagnosis/latest 응답 data (POST 응답과 별도 구조) */ +export interface DiagnosisLatestData { + age: number; + availableRentalTypes: string[]; + availableSupplyTypes: string[]; + diagnosedAt: string; + diagnosisId: number; + diagnosisResult: string; + eligible: boolean; + gender: string; + myIncomeLevel: string; + nickname: string; + recommended: string[]; +} diff --git a/src/features/eligibility/hooks/useDiagnosisLatest.ts b/src/features/eligibility/hooks/useDiagnosisLatest.ts index a2e58c7..716d46f 100644 --- a/src/features/eligibility/hooks/useDiagnosisLatest.ts +++ b/src/features/eligibility/hooks/useDiagnosisLatest.ts @@ -2,7 +2,7 @@ import { useQuery } from "@tanstack/react-query"; import { getDiagnosisLatest } from "../api/diagnosisApi"; -import type { DiagnosisResultData } from "../api/diagnosisTypes"; +import type { DiagnosisLatestData } from "../api/diagnosisTypes"; const QUERY_KEY = ["diagnosis", "latest"]; @@ -11,8 +11,8 @@ export function useDiagnosisLatest() { const query = useQuery({ queryKey: QUERY_KEY, queryFn: async () => { - const res = await getDiagnosisLatest(); - return (res?.data ?? null) as DiagnosisResultData | null; + const res = await getDiagnosisLatest(); + return (res?.data ?? null) as DiagnosisLatestData | null; }, retry: false, }); diff --git a/src/features/eligibility/ui/result/diagnosisResultBanner.tsx b/src/features/eligibility/ui/result/diagnosisResultBanner.tsx index d159eb5..567ba94 100644 --- a/src/features/eligibility/ui/result/diagnosisResultBanner.tsx +++ b/src/features/eligibility/ui/result/diagnosisResultBanner.tsx @@ -35,24 +35,22 @@ export const DiagnosisResultBanner = ({ return (
- +

- {userName} + {userName}님은
- 님은 소득{" "} - {bunwi} + 소득 {bunwi} - 이고,

diff --git a/src/features/eligibility/ui/result/diagnosisResultItem.tsx b/src/features/eligibility/ui/result/diagnosisResultItem.tsx index d9d21f2..83906a4 100644 --- a/src/features/eligibility/ui/result/diagnosisResultItem.tsx +++ b/src/features/eligibility/ui/result/diagnosisResultItem.tsx @@ -26,33 +26,32 @@ export const DiagnosisResultItem = ({ return (

-
+
{housingType} -

+

{description}

-
-
- +
+ 신청 가능 유형 -
+
{applicationTypes.map((type, index) => ( {type} diff --git a/src/features/eligibility/ui/result/diagnosisResultList.tsx b/src/features/eligibility/ui/result/diagnosisResultList.tsx index 57836f2..b35779d 100644 --- a/src/features/eligibility/ui/result/diagnosisResultList.tsx +++ b/src/features/eligibility/ui/result/diagnosisResultList.tsx @@ -42,10 +42,7 @@ function groupByHousingType( })); } -export const DiagnosisResultList = ({ - recommended, - className, -}: DiagnosisResultListProps) => { +export const DiagnosisResultList = ({ recommended, className }: DiagnosisResultListProps) => { const items = useMemo(() => groupByHousingType(recommended), [recommended]); if (items.length === 0) return null; @@ -54,7 +51,7 @@ export const DiagnosisResultList = ({

{SECTION_TITLE}

@@ -64,8 +61,7 @@ export const DiagnosisResultList = ({ diff --git a/src/features/mypage/ui/pinReportSection.tsx b/src/features/mypage/ui/pinReportSection.tsx index b3d5ab6..ea78cc3 100644 --- a/src/features/mypage/ui/pinReportSection.tsx +++ b/src/features/mypage/ui/pinReportSection.tsx @@ -13,12 +13,11 @@ import { MYPAGE_PIN_REPORT_TARGET_LABEL, MYPAGE_PIN_REPORT_HOUSING_LABEL, } from "@/src/features/mypage/model/mypageConstants"; -import type { DiagnosisResultData } from "@/src/features/eligibility/api/diagnosisTypes"; -import { useDiagnosisResultStore } from "@/src/features/eligibility/model/diagnosisResultStore"; +import type { DiagnosisLatestData } from "@/src/features/eligibility/api/diagnosisTypes"; interface PinReportSectionProps { - /** 자격진단 최신 결과. 있으면 요약 카드, 없으면 '자격진단 하러가기' 빈 상태 표시 */ - diagnosisResult: DiagnosisResultData | null; + /** GET /diagnosis/latest 결과. 있으면 요약 카드, 없으면 '자격진단 하러가기' 빈 상태 표시 */ + diagnosisResult: DiagnosisLatestData | null; onDiagnosisClick?: () => void; onRediagnosisClick?: () => void; onViewDetailClick?: () => void; @@ -26,18 +25,16 @@ interface PinReportSectionProps { const PIN_REPORT_HEADING_ID = "pin-report-heading"; const TAG_CLASS = - "inline-flex items-center rounded-full bg-primary-blue-100 px-2.5 py-1 text-xs font-medium text-primary-blue-700"; + "inline-flex items-center rounded-full bg-primary-blue-25 px-2.5 py-1 text-xs font-medium text-primary-blue-400"; -/** recommended 항목에서 housingType만 추출 (예: "통합공공임대 : 청년 특별공급" → "통합공공임대") */ -function getUniqueHousingTypes(recommended: string[]): string[] { - const set = new Set(); - const sep = " : "; - for (const raw of recommended) { - const idx = raw.indexOf(sep); - const housingType = (idx === -1 ? raw : raw.slice(0, idx)).trim(); - if (housingType) set.add(housingType); +/** 나의 지원 가능 대상: gender + availableSupplyTypes */ +function getTargetGroupLabels(data: DiagnosisLatestData): string[] { + const list: string[] = []; + if (data.gender) list.push(data.gender); + if (Array.isArray(data.availableSupplyTypes)) { + list.push(...data.availableSupplyTypes); } - return Array.from(set); + return list; } export const PinReportSection = ({ @@ -46,19 +43,16 @@ export const PinReportSection = ({ onRediagnosisClick, onViewDetailClick, }: PinReportSectionProps) => { - const storeIncomeLevel = useDiagnosisResultStore(s => s.incomeLevel); - - const hasResult = diagnosisResult != null && Array.isArray(diagnosisResult.recommended); - const incomeLevel = - diagnosisResult?.incomeLevel ?? storeIncomeLevel ?? null; - const targetGroups = diagnosisResult?.targetGroups ?? []; - const housingTypes = useMemo( - () => - hasResult && diagnosisResult?.recommended?.length - ? getUniqueHousingTypes(diagnosisResult.recommended) - : [], - [hasResult, diagnosisResult?.recommended] + const hasResult = + diagnosisResult != null && + Array.isArray(diagnosisResult.recommended) && + diagnosisResult.recommended.length > 0; + const incomeLevel = diagnosisResult?.myIncomeLevel ?? null; + const targetGroups = useMemo( + () => (diagnosisResult ? getTargetGroupLabels(diagnosisResult) : []), + [diagnosisResult] ); + const housingTypes = diagnosisResult?.availableRentalTypes ?? []; if (hasResult) { return ( @@ -86,31 +80,15 @@ export const PinReportSection = ({
{incomeLevel != null && incomeLevel !== "" && (
-

+

{MYPAGE_PIN_REPORT_INCOME_LABEL}

{incomeLevel}
)} - - {targetGroups.length > 0 && ( -
-

- {MYPAGE_PIN_REPORT_TARGET_LABEL} -

-
- {targetGroups.map(label => ( - - {label} - - ))} -
-
- )} - {housingTypes.length > 0 && (
-

+

{MYPAGE_PIN_REPORT_HOUSING_LABEL}

@@ -128,7 +106,7 @@ export const PinReportSection = ({
-
+
{ useEffect(() => { if (diagnosisLatest) { - setDiagnosisResult(diagnosisLatest, { - incomeLevel: diagnosisLatest.incomeLevel, - }); + setDiagnosisResult( + { + eligible: diagnosisLatest.eligible, + decisionMessage: diagnosisLatest.diagnosisResult, + recommended: diagnosisLatest.recommended, + }, + { incomeLevel: diagnosisLatest.myIncomeLevel } + ); } }, [diagnosisLatest, setDiagnosisResult]); From 15d757697a5a3690dc4571a066af19a8d51e958e Mon Sep 17 00:00:00 2001 From: chan Date: Fri, 20 Feb 2026 09:29:56 +0900 Subject: [PATCH 17/18] fix: --- next.config.ts | 10 +++ src/entities/home/hooks/homeHooks.ts | 39 ++++++------ src/features/home/index.ts | 2 +- .../home/ui/homeAction/homeActionCardList.tsx | 15 +++++ .../home/ui/homeAction/pinpointStandard.tsx | 35 +++++++++++ .../ui/homeAction/qualificationdiagnosis.tsx | 38 ++++++++++++ src/features/home/ui/homeActionCardList.tsx | 61 ------------------- 7 files changed, 121 insertions(+), 79 deletions(-) create mode 100644 src/features/home/ui/homeAction/homeActionCardList.tsx create mode 100644 src/features/home/ui/homeAction/pinpointStandard.tsx create mode 100644 src/features/home/ui/homeAction/qualificationdiagnosis.tsx delete mode 100644 src/features/home/ui/homeActionCardList.tsx diff --git a/next.config.ts b/next.config.ts index 46abc39..0f94eca 100644 --- a/next.config.ts +++ b/next.config.ts @@ -25,6 +25,16 @@ const nextConfig: NextConfig = { hostname: "t1.kakaocdn.net", pathname: "/**", }, + { + protocol: "https", + hostname: "k.kakaocdn.net", + pathname: "/**", + }, + { + protocol: "http", + hostname: "k.kakaocdn.net", + pathname: "/**", + }, ], }, webpack(config: Configuration) { diff --git a/src/entities/home/hooks/homeHooks.ts b/src/entities/home/hooks/homeHooks.ts index d792ca1..85364ed 100644 --- a/src/entities/home/hooks/homeHooks.ts +++ b/src/entities/home/hooks/homeHooks.ts @@ -100,28 +100,33 @@ export const useGlobalPageNation = ({ }); }; +const recommendedFetchedKey = (userId: string) => `home-recommended-fetched:${userId ?? "anon"}`; + export const useRecommendedNotice = () => { + const { userName } = useOAuthStore(); + const isBrowser = typeof window !== "undefined"; + + const fetched = + isBrowser && !!userName + ? sessionStorage.getItem(recommendedFetchedKey(userName)) === "query" + : false; + return useInfiniteQuery, Error>({ - queryKey: ["HOME_RECOMMENDED"], + queryKey: ["HOME_RECOMMENDED", userName], initialPageParam: 1, retry: false, + enabled: isBrowser && !!userName && !fetched, + staleTime: Infinity, + gcTime: Infinity, queryFn: async ({ pageParam }) => { - try { - return await getNoticeByPinPoint>({ - url: HOME_RECOMMENDED_ENDPOINT, - params: { page: Number(pageParam), offSet: 10 }, - }); - } catch (e) { - if (axios.isAxiosError(e)) { - const message = e.response?.data?.message ?? e.response?.data?.error ?? e.message; - toast.error(message); - throw new Error(message); - } - throw e instanceof Error ? e : new Error("Unknown error"); - } - }, - getNextPageParam: lastPage => { - return lastPage.hasNext ? lastPage.pages + 1 : undefined; + const data = await getNoticeByPinPoint>({ + url: HOME_RECOMMENDED_ENDPOINT, + params: { page: Number(pageParam), offSet: 10 }, + }); + + sessionStorage.setItem(recommendedFetchedKey(userName), "query"); + return data; }, + getNextPageParam: lastPage => (lastPage.hasNext ? lastPage.pages + 1 : undefined), }); }; diff --git a/src/features/home/index.ts b/src/features/home/index.ts index feecdcb..97d2772 100644 --- a/src/features/home/index.ts +++ b/src/features/home/index.ts @@ -1,5 +1,5 @@ export * from "./ui/homeContentsCard"; -export * from "./ui/homeActionCardList"; +export * from "./ui/homeAction/homeActionCardList"; export * from "./ui/homeContentsCard"; export * from "./ui/homeHeader"; export * from "./ui/homeHero"; diff --git a/src/features/home/ui/homeAction/homeActionCardList.tsx b/src/features/home/ui/homeAction/homeActionCardList.tsx new file mode 100644 index 0000000..2e5acba --- /dev/null +++ b/src/features/home/ui/homeAction/homeActionCardList.tsx @@ -0,0 +1,15 @@ +"use client"; +import { useHomeActionCard } from "@/src/features/home/ui/homeUseHooks/homeUseHooks"; +import { PinpointStandard } from "@/src/features/home/ui/homeAction/pinpointStandard"; +import { QualificationDiagnosis } from "@/src/features/home/ui/homeAction/qualificationdiagnosis"; + +export const ActionCardList = () => { + const { onListingsPageMove, onEligibilityPageMove } = useHomeActionCard(); + + return ( +
+ + +
+ ); +}; diff --git a/src/features/home/ui/homeAction/pinpointStandard.tsx b/src/features/home/ui/homeAction/pinpointStandard.tsx new file mode 100644 index 0000000..7af9f87 --- /dev/null +++ b/src/features/home/ui/homeAction/pinpointStandard.tsx @@ -0,0 +1,35 @@ +import { ArrowUpRight } from "@/src/assets/icons/button/arrowUpRight"; +import { useNoticeCount } from "@/src/entities/home/hooks/homeHooks"; +import { useOAuthStore } from "@/src/features/login/model"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { SliceResponse } from "@/src/entities/home/model/type"; +import { ListingItem } from "@/src/entities/listings/model/type"; +import { getNoticeByPinPoint } from "@/src/entities/home/interface/homeInterface"; +import { HOME_RECOMMENDED_ENDPOINT } from "@/src/shared/api"; + +type PinpointStandardProps = { + onListingsPageMove: () => void; +}; + +export const PinpointStandard = ({ onListingsPageMove }: PinpointStandardProps) => { + const { data } = useNoticeCount(); + const count = data?.count; + console.log(count); + return ( +
+
+

+ 핀포인트 기준 +

+
+ +
+
+ +

{count}건

+
+ ); +}; diff --git a/src/features/home/ui/homeAction/qualificationdiagnosis.tsx b/src/features/home/ui/homeAction/qualificationdiagnosis.tsx new file mode 100644 index 0000000..a103141 --- /dev/null +++ b/src/features/home/ui/homeAction/qualificationdiagnosis.tsx @@ -0,0 +1,38 @@ +import { ArrowUpRight } from "@/src/assets/icons/button/arrowUpRight"; +import { useRecommendedNotice } from "@/src/entities/home/hooks/homeHooks"; + +type QualificationDiagnosisProps = { + onEligibilityPageMove: () => void; +}; +export const QualificationDiagnosis = ({ onEligibilityPageMove }: QualificationDiagnosisProps) => { + const { data: recommend } = useRecommendedNotice(); + const count = recommend?.pages[0]?.totalCount; + + return ( +
+
+

+ 자격진단 기준 +

+ +
+ +
+
+ +
+

{count ? count : "0"}건

+ +

0% 완료

+
+
+
+ ); +}; diff --git a/src/features/home/ui/homeActionCardList.tsx b/src/features/home/ui/homeActionCardList.tsx deleted file mode 100644 index 510cbc6..0000000 --- a/src/features/home/ui/homeActionCardList.tsx +++ /dev/null @@ -1,61 +0,0 @@ -"use client"; -import { ArrowUpRight } from "@/src/assets/icons/button/arrowUpRight"; -import { useNoticeCount, useRecommendedNotice } from "@/src/entities/home/hooks/homeHooks"; -import { useRouter } from "next/navigation"; -import { useHomeActionCard } from "@/src/features/home/ui/homeUseHooks/homeUseHooks"; - -export const ActionCardList = () => { - const { data } = useNoticeCount(); - const { data: recommend } = useRecommendedNotice(); - - const count = data?.count; - const { onListingsPageMove, onEligibilityPageMove } = useHomeActionCard(); - - return ( -
-
-
-

- 핀포인트 기준 -

-
- -
-
- -

{count}건

-
- -
-
-

- 자격진단 기준 -

- -
- -
-
- -
-

- {recommend?.pages?.length ? recommend?.pages?.length : "0"}건 -

- -

0% 완료

-
-
-
-
- ); -}; From b40bc8c1d9aae7fb7d8e70099ad05aabd49c1c6b Mon Sep 17 00:00:00 2001 From: chan Date: Fri, 20 Feb 2026 10:04:07 +0900 Subject: [PATCH 18/18] =?UTF-8?q?refactor:homeActionCardList=20=EC=9D=B4?= =?UTF-8?q?=EC=A0=84=20=EC=BB=A4=EB=B0=8B=EC=9C=BC=EB=A1=9C=20=EB=A1=A4?= =?UTF-8?q?=EB=B0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/ui/homeAction/pinpointStandard.tsx | 1 - .../ui/homeAction/qualificationdiagnosis.tsx | 6 +- src/features/home/ui/homeActionCardList.tsx | 62 ------------------- 3 files changed, 4 insertions(+), 65 deletions(-) delete mode 100644 src/features/home/ui/homeActionCardList.tsx diff --git a/src/features/home/ui/homeAction/pinpointStandard.tsx b/src/features/home/ui/homeAction/pinpointStandard.tsx index 7af9f87..a010d51 100644 --- a/src/features/home/ui/homeAction/pinpointStandard.tsx +++ b/src/features/home/ui/homeAction/pinpointStandard.tsx @@ -14,7 +14,6 @@ type PinpointStandardProps = { export const PinpointStandard = ({ onListingsPageMove }: PinpointStandardProps) => { const { data } = useNoticeCount(); const count = data?.count; - console.log(count); return (
void; @@ -7,6 +8,7 @@ type QualificationDiagnosisProps = { export const QualificationDiagnosis = ({ onEligibilityPageMove }: QualificationDiagnosisProps) => { const { data: recommend } = useRecommendedNotice(); const count = recommend?.pages[0]?.totalCount; + const hasDiagnosisResult = useDiagnosisResultStore(state => state.result != null); return (
-

{count ? count : "0"}건

+

{count ?? 0}건

-

0% 완료

+

{hasDiagnosisResult ? "100% 완료" : "0% 완료"}

diff --git a/src/features/home/ui/homeActionCardList.tsx b/src/features/home/ui/homeActionCardList.tsx deleted file mode 100644 index f97d534..0000000 --- a/src/features/home/ui/homeActionCardList.tsx +++ /dev/null @@ -1,62 +0,0 @@ -"use client"; -import { ArrowUpRight } from "@/src/assets/icons/button/arrowUpRight"; -import { useNoticeCount, useRecommendedNotice } from "@/src/entities/home/hooks/homeHooks"; -import { useHomeActionCard } from "@/src/features/home/ui/homeUseHooks/homeUseHooks"; -import { useDiagnosisResultStore } from "@/src/features/eligibility/model/diagnosisResultStore"; - -export const ActionCardList = () => { - const { data } = useNoticeCount(); - const { data: recommend } = useRecommendedNotice(); - const hasDiagnosisResult = useDiagnosisResultStore(state => state.result != null); - - const count = data?.count; - const { onListingsPageMove, onEligibilityPageMove } = useHomeActionCard(); - - return ( -
-
-
-

- 핀포인트 기준 -

-
- -
-
- -

{count}건

-
- -
-
-

- 자격진단 기준 -

- -
- -
-
- -
-

- {recommend?.pages?.[0]?.totalElements ?? 0}건 -

- -

{hasDiagnosisResult ? "100% 완료" : "0% 완료"}

-
-
-
-
- ); -};