diff --git a/src/app/router/index.tsx b/src/app/router/index.tsx index eb7c8e3..30403d3 100644 --- a/src/app/router/index.tsx +++ b/src/app/router/index.tsx @@ -11,7 +11,6 @@ import EditPlacePage from "@/pages/EditPlacePage"; import EntryPage from "@/pages/EntryPage"; import PrivacyPolicyPage from "@/pages/legal/PrivacyPolicyPage"; import TermsOfServicePage from "@/pages/legal/TermsOfServicePage"; -import LinkPlaceSelectPage from "@/pages/LinkPlaceSelectPage"; import LoginPage from "@/pages/LoginPage"; import { mapHomeLoader } from "@/pages/map/map-home-loader"; import NicknamePage from "@/pages/onboarding/NicknamePage"; @@ -19,7 +18,6 @@ import TermsAgreementPage from "@/pages/onboarding/TermsAgreementPage"; import RoomLinkCandidatesPage from "@/pages/rooms/RoomLinkCandidatesPage"; import RoomPlaceFromLinkPage from "@/pages/rooms/RoomPlaceFromLinkPage"; import RoomPlaceSearchPage from "@/pages/rooms/RoomPlaceSearchPage"; -import CoursePlannerPagePreview from "@/pages/tabs/CoursePlannerPage"; const MapHomePage = lazy(() => import("@/pages/MapHomePage")); const RoomMainPage = lazy(() => import("@/pages/room/RoomMainPage")); @@ -33,12 +31,10 @@ export const router = createBrowserRouter([ element: , children: [ { index: true, element: }, - { path: "places/register/from-link", element: }, { path: "places/edit", element: }, { path: "login", element: }, { path: "privacys", element: }, { path: "terms", element: }, - { path: "dev/course", element: }, { path: "auth/callback", element: , diff --git a/src/components/mypage/MyPlaceSummaryCard.tsx b/src/components/mypage/MyPlaceSummaryCard.tsx index 3b87082..88b8779 100644 --- a/src/components/mypage/MyPlaceSummaryCard.tsx +++ b/src/components/mypage/MyPlaceSummaryCard.tsx @@ -59,10 +59,7 @@ export function MyPlaceSummaryCard({ ) : hasPlaces ? ( recentPlaces.slice(0, 2).map((place) => ( -
+
- - + +
diff --git a/src/components/mypage/SavedCourseCard.tsx b/src/components/mypage/SavedCourseCard.tsx index 0c66f92..1d99488 100644 --- a/src/components/mypage/SavedCourseCard.tsx +++ b/src/components/mypage/SavedCourseCard.tsx @@ -1,5 +1,7 @@ -import { ChevronRight, Heart, UsersRound } from "lucide-react"; +import { ChevronRight, Heart, User, UsersRound } from "lucide-react"; +import { useCallback, useState } from "react"; +import { RoomAvatar } from "@/components/room/RoomAvatar"; import { cn } from "@/lib/utils"; import type { SavedCourse } from "@/shared/types/course"; @@ -12,6 +14,15 @@ type SavedCourseCardProps = { export function SavedCourseCard({ course, onSelect, className }: SavedCourseCardProps) { const isFriendCourse = course.badgeLabel === "친구"; const Icon = isFriendCourse ? UsersRound : Heart; + const saverNickname = course.savedByNickname?.trim() ?? ""; + const roomName = course.savedFromRoomName?.trim() ?? ""; + const roomAvatarSeed = course.savedFromRoomAvatarSeed?.trim() ?? ""; + const hasSaver = saverNickname.length > 0 || Boolean(course.savedByProfileImageUrl?.trim()); + const metadata = saverNickname + ? `${saverNickname}님이 저장 · ${course.executedAtLabel}` + : roomName + ? `${roomName}에서 저장 · ${course.executedAtLabel}` + : course.executedAtLabel; return ( ); } + +function SaverAvatar({ imageUrl }: { imageUrl?: string | null }) { + const url = imageUrl?.trim() ?? ""; + const [failedUrl, setFailedUrl] = useState(null); + const showImage = Boolean(url) && failedUrl !== url; + + const handleImageError = useCallback(() => { + setFailedUrl(url); + }, [url]); + + if (showImage) { + return ( + + ); + } + + return ( + + + + ); +} diff --git a/src/components/mypage/map-places-from-my-saved.ts b/src/components/mypage/map-places-from-my-saved.ts index 2887703..a615ea6 100644 --- a/src/components/mypage/map-places-from-my-saved.ts +++ b/src/components/mypage/map-places-from-my-saved.ts @@ -1,4 +1,4 @@ -import { MAP_INITIAL_CENTER } from "@/shared/mocks/place-mocks"; +import { MAP_INITIAL_CENTER } from "@/shared/config/map"; import type { MapCoordinate, SavedPlace as MapSavedPlace } from "@/shared/types/map-home"; import type { SavedPlace as MySavedPlace } from "@/shared/types/my-page"; diff --git a/src/components/mypage/saved-course-planner-map.ts b/src/components/mypage/saved-course-planner-map.ts index 3877049..21caa91 100644 --- a/src/components/mypage/saved-course-planner-map.ts +++ b/src/components/mypage/saved-course-planner-map.ts @@ -1,5 +1,3 @@ -import { MOCK_LEGACY_PLACE_ID_TO_ROOM_PLACE_ID } from "@/shared/mocks/course-mocks"; -import { SAVED_PLACE_MOCKS } from "@/shared/mocks/place-mocks"; import type { CourseStop as PlannerCourseStop, SavedCourse } from "@/shared/types/course"; import type { SavedPlace as MapSavedPlace } from "@/shared/types/map-home"; import type { SavedPlace } from "@/shared/types/my-page"; @@ -12,11 +10,6 @@ function resolveRoomPlaceId( return stop.roomPlaceId; } - const mapped = MOCK_LEGACY_PLACE_ID_TO_ROOM_PLACE_ID[stop.id]; - if (mapped != null) { - return mapped; - } - const fromMy = savedPlaces.find((p) => p.name === stop.name || p.address === stop.address); if (fromMy?.roomPlaceId != null) { return fromMy.roomPlaceId; @@ -27,11 +20,6 @@ function resolveRoomPlaceId( return parsedFromMy; } - const fromMap = SAVED_PLACE_MOCKS.find((p) => p.name === stop.name || p.address === stop.address); - if (fromMap?.roomPlaceId != null) { - return fromMap.roomPlaceId; - } - const parsedStopId = Number(stop.id); return Number.isInteger(parsedStopId) ? parsedStopId : null; } @@ -111,20 +99,6 @@ export function mapPlacesFromSavedCourses( result.push(fromApi); continue; } - - const mock = SAVED_PLACE_MOCKS.find( - (place) => - place.roomPlaceId === roomPlaceId || - MOCK_LEGACY_PLACE_ID_TO_ROOM_PLACE_ID[place.id] === roomPlaceId, - ); - if (mock) { - seen.add(roomPlaceId); - result.push({ - ...mock, - id: String(roomPlaceId), - roomPlaceId, - }); - } } } diff --git a/src/components/place-flow/PlaceSearchMapSheet.tsx b/src/components/place-flow/PlaceSearchMapSheet.tsx index 031ca9c..d768d53 100644 --- a/src/components/place-flow/PlaceSearchMapSheet.tsx +++ b/src/components/place-flow/PlaceSearchMapSheet.tsx @@ -18,8 +18,8 @@ import { PROMPT_FLOW_LIST_TOP_BORDER_CLASS, } from "@/features/place-flow/prompt-flow-layout"; import { cn } from "@/lib/utils"; +import { MAP_INITIAL_CENTER } from "@/shared/config/map"; import type { LinkSourceType } from "@/shared/lib/link-source-type"; -import { MAP_INITIAL_CENTER } from "@/shared/mocks/place-mocks"; import type { MapCoordinate, SavedPlace } from "@/shared/types/map-home"; const KAKAO_MAP_APP_KEY = import.meta.env.VITE_KAKAO_MAP_APP_KEY; diff --git a/src/components/place-list/PlaceListSavedCoursesPage.tsx b/src/components/place-list/PlaceListSavedCoursesPage.tsx new file mode 100644 index 0000000..dcec794 --- /dev/null +++ b/src/components/place-list/PlaceListSavedCoursesPage.tsx @@ -0,0 +1,562 @@ +import { AlertCircle, Check, ChevronDown, User } from "lucide-react"; +import { lazy, Suspense, useCallback, useMemo, useRef, useState } from "react"; + +import { type BottomNavId, BottomNavigationBar } from "@/components/common/BottomNavigationBar"; +import { BottomNavToast } from "@/components/common/BottomNavToast"; +import { EmptyState } from "@/components/common/EmptyState"; +import { LIST_TOP_BAR_AFTER_TITLE_CLASS, ListTopBar } from "@/components/common/ListTopBar"; +import { MapBackdropLayer } from "@/components/common/MapBackdropLayer"; +import { CoursePlaceInfoPanel } from "@/components/course-planner/CoursePlaceInfoPanel"; +import { CoursePlannerBottomSheet } from "@/components/course-planner/CoursePlannerBottomSheet"; +import { DateCalendarPanel } from "@/components/course-planner/DateTimeSelectionPanel"; +import { + MAP_CHIP_BASE_CLASS, + MAP_CHIP_SELECTED_CLASS, + MAP_CHIP_UNSELECTED_CLASS, + MAP_FILTER_PANEL_BASE_CLASS, +} from "@/components/map/chip-style"; +import { weightedMapCenter } from "@/components/mypage/map-places-from-my-saved"; +import { + mapPlacesFromSavedCourses, + savedCourseToPlannerStops, +} from "@/components/mypage/saved-course-planner-map"; +import { SavedCourseCard } from "@/components/mypage/SavedCourseCard"; +import { PlaceDetailSheet } from "@/components/place/PlaceDetailSheet"; +import { useDateCourseDetailQuery } from "@/features/course-planner/hooks/use-date-course-detail-query"; +import { useRoomDateCoursesQuery } from "@/features/course-planner/hooks/use-room-date-courses-query"; +import { mapRoomSavedDateCourseToSavedCourse } from "@/features/course-planner/lib/map-room-saved-date-course"; +import type { BottomNavToastPlacement } from "@/hooks/use-bottom-nav-controller"; +import { usePointerDownOutside } from "@/hooks/use-pointer-down-outside"; +import { cn } from "@/lib/utils"; +import { MAP_INITIAL_CENTER } from "@/shared/config/map"; +import type { CourseSavePayload, SavedCourse } from "@/shared/types/course"; +import type { SavedPlace } from "@/shared/types/my-page"; +import { usePlaceDetailStore } from "@/store/place-detail-store"; + +const KAKAO_MAP_APP_KEY = import.meta.env.VITE_KAKAO_MAP_APP_KEY; +const KakaoMapView = lazy(() => + import("@/components/map/KakaoMapView").then((module) => ({ default: module.KakaoMapView })), +); + +type CourseFilter = "all" | "member" | "date"; +type FilterPopup = Exclude | null; + +type MemberFilterOption = { + id: string; + nickname: string; + profileImageUrl: string | null; +}; + +type PlaceListSavedCoursesPageProps = { + roomId?: string | null; + roomName?: string; + savedPlaces: SavedPlace[]; + toastMessage: string; + toastPlacement?: BottomNavToastPlacement; + onSelectBottomNav: (id: BottomNavId) => void; + onBackToMap: () => void; + onSwitchTab: (tab: "places" | "courses") => void; +}; + +function formatCount(count: number) { + return count > 999 ? "999+" : String(count); +} + +function filterChipClass(active: boolean) { + return cn(MAP_CHIP_BASE_CLASS, active ? MAP_CHIP_SELECTED_CLASS : MAP_CHIP_UNSELECTED_CLASS); +} + +function formatDateLabel(date: string | null) { + return date ?? "날짜"; +} + +export function PlaceListSavedCoursesPage({ + roomId = null, + roomName, + savedPlaces, + toastMessage, + toastPlacement = "bottom", + onSelectBottomNav, + onBackToMap, + onSwitchTab, +}: PlaceListSavedCoursesPageProps) { + const [selectedCourse, setSelectedCourse] = useState(null); + const [courseOverrides, setCourseOverrides] = useState>({}); + const [, setSelectedFilter] = useState("all"); + const [openPopup, setOpenPopup] = useState(null); + const [selectedMemberIds, setSelectedMemberIds] = useState([]); + const [selectedDate, setSelectedDate] = useState(null); + const roomDateCoursesQuery = useRoomDateCoursesQuery({ + roomId, + enabled: Boolean(roomId), + }); + const dateCourseDetailQuery = useDateCourseDetailQuery({ + roomId, + dateCourseId: selectedCourse?.id ?? null, + enabled: Boolean(selectedCourse), + }); + + const detailOpen = usePlaceDetailStore((s) => s.isOpen); + const selectedPlaceId = usePlaceDetailStore((s) => s.selectedPlaceId); + const closeDetail = usePlaceDetailStore((s) => s.closeDetail); + const filterChromeRef = useRef(null); + + const memberChipApplied = selectedMemberIds.length > 0; + const dateChipApplied = selectedDate !== null; + const allChipActive = !memberChipApplied && !dateChipApplied; + const overlayMapOpen = Boolean(selectedCourse) || detailOpen; + + const closeFilterPopups = useCallback(() => { + setOpenPopup(null); + setSelectedFilter((prev) => { + if (prev === "date" && selectedDate === null) return "all"; + if (prev === "member" && selectedMemberIds.length === 0) return "all"; + return prev; + }); + }, [selectedDate, selectedMemberIds.length]); + + usePointerDownOutside(filterChromeRef, openPopup !== null && !overlayMapOpen, closeFilterPopups); + + const apiCourses = useMemo( + () => + (roomDateCoursesQuery.data?.items ?? []).map((course) => + mapRoomSavedDateCourseToSavedCourse(course, roomId), + ), + [roomDateCoursesQuery.data?.items, roomId], + ); + + const savedCourses = useMemo( + () => apiCourses.map((course) => courseOverrides[course.id] ?? course), + [apiCourses, courseOverrides], + ); + + const memberOptions = useMemo((): MemberFilterOption[] => { + const byId = new Map(); + + for (const course of savedCourses) { + if (course.savedByUserId == null) { + continue; + } + + const nickname = course.savedByNickname?.trim(); + if (!nickname) { + continue; + } + + const id = String(course.savedByUserId); + if (!byId.has(id)) { + byId.set(id, { + id, + nickname, + profileImageUrl: course.savedByProfileImageUrl ?? null, + }); + } + } + + return [...byId.values()]; + }, [savedCourses]); + + const selectedCourseWithDetail = useMemo(() => { + if (!selectedCourse) { + return null; + } + + const detailCourse = dateCourseDetailQuery.data + ? mapRoomSavedDateCourseToSavedCourse(dateCourseDetailQuery.data, roomId) + : selectedCourse; + + return courseOverrides[detailCourse.id] ?? detailCourse; + }, [courseOverrides, dateCourseDetailQuery.data, roomId, selectedCourse]); + + const visibleCourses = useMemo(() => { + return savedCourses.filter((course) => { + const matchesMember = + selectedMemberIds.length === 0 || + (course.savedByUserId != null && selectedMemberIds.includes(String(course.savedByUserId))); + const matchesDate = selectedDate == null || course.courseDateKey === selectedDate; + + return matchesMember && matchesDate; + }); + }, [savedCourses, selectedDate, selectedMemberIds]); + + const mapPins = useMemo(() => { + return selectedCourseWithDetail + ? mapPlacesFromSavedCourses([selectedCourseWithDetail], savedPlaces) + : mapPlacesFromSavedCourses(visibleCourses, savedPlaces); + }, [savedPlaces, selectedCourseWithDetail, visibleCourses]); + + const mapCenter = useMemo(() => { + if (detailOpen && selectedPlaceId) { + const pin = mapPins.find((place) => place.id === selectedPlaceId); + if (pin) { + return { latitude: pin.latitude, longitude: pin.longitude }; + } + } + + if (selectedCourseWithDetail) { + const focusedPins = mapPlacesFromSavedCourses([selectedCourseWithDetail], savedPlaces); + if (focusedPins.length > 0) { + return weightedMapCenter(focusedPins); + } + } + + return weightedMapCenter(mapPins); + }, [detailOpen, mapPins, savedPlaces, selectedCourseWithDetail, selectedPlaceId]); + + const handleSelectAll = () => { + setSelectedFilter("all"); + setOpenPopup(null); + setSelectedMemberIds([]); + setSelectedDate(null); + }; + + const handleToggleMember = (memberId: string) => { + setSelectedMemberIds((current) => { + const nextIds = current.includes(memberId) + ? current.filter((item) => item !== memberId) + : [...current, memberId]; + setSelectedFilter(nextIds.length === 0 && selectedDate == null ? "all" : "member"); + return nextIds; + }); + }; + + const handlePickCalendarDate = (date: string) => { + if (selectedDate === date) { + setSelectedDate(null); + setSelectedFilter("all"); + setOpenPopup(null); + return; + } + + setSelectedFilter("date"); + setSelectedDate(date); + setOpenPopup(null); + }; + + const handlePersistCourse = (prevCourseId: string, payload: CourseSavePayload) => { + if (payload.kind !== "edit") { + return; + } + + const source = savedCourses.find((course) => course.id === prevCourseId) ?? selectedCourse; + if (!source) { + return; + } + + const nextCourse: SavedCourse = { + ...source, + title: payload.title, + stops: payload.stops.map((stop) => ({ + id: stop.id, + roomPlaceId: stop.roomPlaceId, + name: stop.name, + address: stop.address, + category: stop.category, + categoryName: stop.categoryName, + tagCode: stop.tagCode, + tagName: stop.tagName, + latitude: stop.latitude, + longitude: stop.longitude, + walkingTime: stop.walkingTime, + hours: stop.hours, + })), + }; + + setCourseOverrides((current) => ({ ...current, [prevCourseId]: nextCourse })); + setSelectedCourse(nextCourse); + }; + + return ( +
+ {overlayMapOpen ? ( + + }> + 0 ? mapCenter : MAP_INITIAL_CENTER} + className="h-full w-full" + /> + + + ) : null} + + { + if (detailOpen) { + closeDetail(); + return; + } + if (selectedCourse) { + setSelectedCourse(null); + return; + } + onBackToMap(); + }} + > + {!overlayMapOpen ? ( +
+
+ + +
+ +
+ + +
+ + + {openPopup === "member" ? ( +
+ {roomDateCoursesQuery.isLoading ? ( +
+ {Array.from({ length: 3 }, (_, index) => ( +
+
+
+
+
+ ))} +
+ ) : memberOptions.length === 0 ? ( +
+ 저장한 멤버가 없습니다 +
+ ) : ( +
    + {memberOptions.map((member) => { + const checked = selectedMemberIds.includes(member.id); + return ( +
  • + +
  • + ); + })} +
+ )} +
+ ) : null} +
+ +
+ + + {openPopup === "date" ? ( +
+ +
+ ) : null} +
+
+
+ ) : null} + + + {!overlayMapOpen ? ( +
+ {roomDateCoursesQuery.isLoading ? ( + } + message="저장된 데이트 코스를 불러오는 중이에요." + /> + ) : visibleCourses.length > 0 ? ( +
+ {visibleCourses.map((course) => ( + + ))} +
+ ) : ( + } + message={ + roomDateCoursesQuery.isError + ? "저장된 데이트 코스를 불러오지 못했어요." + : "해당하는 데이트 코스가 없습니다." + } + /> + )} +
+ ) : null} + + setSelectedCourse(null)} + > + {selectedCourseWithDetail ? ( + setSelectedCourse(null)} + onSave={(payload) => handlePersistCourse(selectedCourseWithDetail.id, payload)} + hideNewCourseSaveButton + /> + ) : null} + + +
+ + +
+ + +
+ ); +} + +function MemberAvatar({ imageUrl, className }: { imageUrl?: string | null; className?: string }) { + const url = imageUrl?.trim() ?? ""; + const [failedUrl, setFailedUrl] = useState(null); + const showImage = Boolean(url) && failedUrl !== url; + + const handleImageError = useCallback(() => { + setFailedUrl(url); + }, [url]); + + if (showImage) { + return ( + + ); + } + + return ( + + + + ); +} diff --git a/src/components/place/PlaceDetailSheet.tsx b/src/components/place/PlaceDetailSheet.tsx index 6d4ddf8..d5f5ce7 100644 --- a/src/components/place/PlaceDetailSheet.tsx +++ b/src/components/place/PlaceDetailSheet.tsx @@ -17,7 +17,6 @@ import { roomPlaceToSavedPlace, useRoomPlace } from "@/features/room-places"; import { usePointerDownOutside } from "@/hooks/use-pointer-down-outside"; import { cn } from "@/lib/utils"; import { sharePlace } from "@/shared/lib/share-place"; -import { SAVED_PLACE_MOCKS } from "@/shared/mocks/place-mocks"; import type { SavedPlace as MapSavedPlace } from "@/shared/types/map-home"; import type { SavedPlace as MySavedPlace } from "@/shared/types/my-page"; import type { RoomPlaceMemo } from "@/shared/types/place-memo"; @@ -146,13 +145,7 @@ export function PlaceDetailSheet({ } else if (shouldFetchRoomPlaceDetail) { sourcePlaces = []; } else { - sourcePlaces = SAVED_PLACE_MOCKS.filter((place) => !localRemoved.has(place.id)).map( - (place) => ({ - ...place, - memo: localMemos[place.id], - memos: localMemos[place.id] ? createLocalMemoList(localMemos[place.id]) : undefined, - }), - ); + sourcePlaces = []; } return sourcePlaces; diff --git a/src/components/room/RoomMainHeader.tsx b/src/components/room/RoomMainHeader.tsx index 63ee66b..958847d 100644 --- a/src/components/room/RoomMainHeader.tsx +++ b/src/components/room/RoomMainHeader.tsx @@ -1,11 +1,10 @@ import { SearchField } from "@/components/common/SearchField"; import { cn } from "@/lib/utils"; -const ROOM_SEARCH_DEFAULT_PLACEHOLDER = "친구 이름 또는 장소 검색"; +const ROOM_SEARCH_DEFAULT_PLACEHOLDER = "방 이름 또는 장소 검색"; export type RoomMainHeaderProps = { title: string; - /** 검색창 placeholder (기본: 친구 이름 또는 장소 검색) */ searchPlaceholder?: string; searchValue?: string; onSearchValueChange?: (value: string) => void; diff --git a/src/features/course-planner/api/date-course-api.ts b/src/features/course-planner/api/date-course-api.ts index 380b8d2..e738c20 100644 --- a/src/features/course-planner/api/date-course-api.ts +++ b/src/features/course-planner/api/date-course-api.ts @@ -72,15 +72,17 @@ export type GenerateDateCoursesResponse = { export type SavedRoomDateCourseItemResponse = { dateCourseId: string; - courseName: string; - mode: DateCourseMode; - startDateTime: string; - endDateTime: string; - savedByUserId: string; - savedByNickname: string; - savedAt: string; - places: DateCoursePlaceResponse[]; - orderedCoordinates: DateCourseCoordinateResponse[]; + courseName: string | null; + mode?: DateCourseMode | null; + startDateTime?: string | null; + endDateTime?: string | null; + savedByUserId: number | string | null; + savedByNickname: string | null; + savedByProfileImageUrl: string | null; + savedAt: string | null; + roomPublicId?: string | null; + places?: DateCoursePlaceResponse[]; + orderedCoordinates?: DateCourseCoordinateResponse[]; }; export type DateCourseListResponse = { @@ -97,6 +99,11 @@ export type DateCourseDetailResponse = { mode: DateCourseMode; startDateTime: string; endDateTime: string; + savedByUserId?: number | string | null; + savedByNickname?: string | null; + savedByProfileImageUrl?: string | null; + savedAt?: string | null; + roomPublicId?: string | null; places: DateCoursePlaceResponse[]; orderedCoordinates: DateCourseCoordinateResponse[]; }; diff --git a/src/features/course-planner/components/CourseDevMapBackground.tsx b/src/features/course-planner/components/CourseDevMapBackground.tsx deleted file mode 100644 index 93bb5c6..0000000 --- a/src/features/course-planner/components/CourseDevMapBackground.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { lazy, Suspense } from "react"; - -import { BottomNavigationBar } from "@/components/common/BottomNavigationBar"; -import { CoursePlannerMapPreview } from "@/components/course-planner/CoursePlannerMapPreview"; -import { MapHeader } from "@/components/map/MapHeader"; -import { COURSE_DEV_MAP_TITLE } from "@/features/course-planner/constants"; -import type { BottomNavId } from "@/shared/config/navigation"; -import { MAP_INITIAL_CENTER, SAVED_PLACE_MOCKS } from "@/shared/mocks/place-mocks"; - -const KAKAO_MAP_APP_KEY = import.meta.env.VITE_KAKAO_MAP_APP_KEY; -const KakaoMapView = lazy(() => - import("@/components/map/KakaoMapView").then((module) => ({ default: module.KakaoMapView })), -); - -type CourseDevMapBackgroundProps = { - onSelectBottomNav: (id: BottomNavId) => void; -}; - -export function CourseDevMapBackground({ onSelectBottomNav }: CourseDevMapBackgroundProps) { - return ( -
- - -
- }> - - -
- -
- -
-
- ); -} diff --git a/src/features/course-planner/constants.ts b/src/features/course-planner/constants.ts index 21c5b58..7fb3523 100644 --- a/src/features/course-planner/constants.ts +++ b/src/features/course-planner/constants.ts @@ -22,7 +22,6 @@ export const COURSE_DISTRICTS_BY_CITY: Record = { export const COURSE_GENERATION_DELAY_MS = 900; export const COURSE_TOAST_DURATION_MS = 3200; export const COURSE_FALLBACK_TITLE = "코스 1"; -export const COURSE_DEV_MAP_TITLE = "데이트 지도"; export const COURSE_LOADING_ROOM_FALLBACK = "방"; export const DATE_COURSE_MAX_NAME_LENGTH = 20; diff --git a/src/features/course-planner/hooks/use-date-course-detail-query.ts b/src/features/course-planner/hooks/use-date-course-detail-query.ts new file mode 100644 index 0000000..f8aff9a --- /dev/null +++ b/src/features/course-planner/hooks/use-date-course-detail-query.ts @@ -0,0 +1,41 @@ +import { useQuery, type UseQueryOptions } from "@tanstack/react-query"; + +import { + dateCourseApi, + type DateCourseDetailResponse, +} from "@/features/course-planner/api/date-course-api"; +import { dateCourseQueryKeys } from "@/features/course-planner/query-keys"; + +type UseDateCourseDetailQueryOptions = { + roomId?: string | null; + dateCourseId?: string | null; + enabled?: boolean; + queryOptions?: Omit< + UseQueryOptions< + DateCourseDetailResponse, + Error, + DateCourseDetailResponse, + ReturnType + >, + "queryKey" | "queryFn" | "enabled" + >; +}; + +export function useDateCourseDetailQuery({ + roomId, + dateCourseId, + enabled = true, + queryOptions, +}: UseDateCourseDetailQueryOptions) { + const resolvedRoomId = roomId ?? ""; + const resolvedDateCourseId = dateCourseId ?? ""; + + return useQuery({ + queryKey: dateCourseQueryKeys.detail(resolvedRoomId, resolvedDateCourseId), + queryFn: () => dateCourseApi.getDateCourseDetail(resolvedRoomId, resolvedDateCourseId), + staleTime: 1000 * 60, + retry: 1, + enabled: enabled && Boolean(roomId) && Boolean(dateCourseId), + ...(queryOptions ?? {}), + }); +} diff --git a/src/features/course-planner/hooks/use-room-date-courses-query.ts b/src/features/course-planner/hooks/use-room-date-courses-query.ts new file mode 100644 index 0000000..2c81500 --- /dev/null +++ b/src/features/course-planner/hooks/use-room-date-courses-query.ts @@ -0,0 +1,46 @@ +import { useQuery, type UseQueryOptions } from "@tanstack/react-query"; + +import { + dateCourseApi, + type DateCourseListResponse, + type SavedRoomDateCourseItemResponse, +} from "@/features/course-planner/api/date-course-api"; +import { dateCourseQueryKeys } from "@/features/course-planner/query-keys"; + +const DEFAULT_PAGE = 0; +const DEFAULT_LIMIT = 100; + +type UseRoomDateCoursesQueryOptions = { + roomId?: string | null; + page?: number; + limit?: number; + enabled?: boolean; + queryOptions?: Omit< + UseQueryOptions< + DateCourseListResponse, + Error, + DateCourseListResponse, + ReturnType + >, + "queryKey" | "queryFn" | "enabled" + >; +}; + +export function useRoomDateCoursesQuery({ + roomId, + page = DEFAULT_PAGE, + limit = DEFAULT_LIMIT, + enabled = true, + queryOptions, +}: UseRoomDateCoursesQueryOptions) { + const resolvedRoomId = roomId ?? ""; + + return useQuery({ + queryKey: dateCourseQueryKeys.roomList(resolvedRoomId, page, limit), + queryFn: () => dateCourseApi.listRoomDateCourses(resolvedRoomId, { page, limit }), + staleTime: 1000 * 60, + retry: 1, + enabled: enabled && Boolean(roomId), + ...(queryOptions ?? {}), + }); +} diff --git a/src/features/course-planner/lib/map-my-saved-date-course.ts b/src/features/course-planner/lib/map-my-saved-date-course.ts index 74af311..d9a32e1 100644 --- a/src/features/course-planner/lib/map-my-saved-date-course.ts +++ b/src/features/course-planner/lib/map-my-saved-date-course.ts @@ -11,7 +11,11 @@ function toCoordinateNumber(value: string | number | null | undefined) { } /** 캘린더 필터(`yyyy.MM.dd`)와 맞추기 위한 키 */ -export function toCourseDateKey(isoInstant: string) { +export function toCourseDateKey(isoInstant: string | null | undefined) { + if (!isoInstant) { + return null; + } + const date = new Date(isoInstant); if (Number.isNaN(date.getTime())) { return null; @@ -68,10 +72,11 @@ export function mapMySavedDateCourseToSavedCourse( ): SavedCourse { return { id: item.dateCourseId, - title: `${item.roomName.trim()} | ${item.courseName.trim()}`, + title: item.courseName.trim(), executedAtLabel: formatSavedAtLabel(item.savedAt), badgeLabel: "하트", savedFromRoomId: item.roomPublicId, + savedFromRoomName: item.roomName.trim(), courseDateKey: toCourseDateKey(item.startDateTime) ?? toCourseDateKey(item.savedAt), stops: item.places .slice() diff --git a/src/features/course-planner/lib/map-room-saved-date-course.ts b/src/features/course-planner/lib/map-room-saved-date-course.ts new file mode 100644 index 0000000..c0eb7d4 --- /dev/null +++ b/src/features/course-planner/lib/map-room-saved-date-course.ts @@ -0,0 +1,62 @@ +import type { + DateCourseDetailResponse, + DateCoursePlaceResponse, + SavedRoomDateCourseItemResponse, +} from "@/features/course-planner/api/date-course-api"; +import { + formatSavedAtLabel, + toCourseDateKey, +} from "@/features/course-planner/lib/map-my-saved-date-course"; +import type { SavedCourse, SavedCourseStop } from "@/shared/types/course"; + +function toCoordinateNumber(value: string | number | null | undefined) { + if (typeof value === "number") { + return Number.isFinite(value) ? value : null; + } + + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; +} + +function mapPlaceToSavedCourseStop(place: DateCoursePlaceResponse): SavedCourseStop { + return { + id: String(place.roomPlaceId), + roomPlaceId: place.roomPlaceId, + name: place.name.trim(), + address: place.roadAddress?.trim() || place.address.trim(), + category: place.categoryCode, + categoryName: place.categoryName ?? null, + tagCode: place.tagCode, + tagName: place.tagName ?? null, + latitude: toCoordinateNumber(place.latitude), + longitude: toCoordinateNumber(place.longitude), + }; +} + +export function mapRoomSavedDateCourseToSavedCourse( + item: SavedRoomDateCourseItemResponse | DateCourseDetailResponse, + roomId?: string | null, +): SavedCourse { + const courseName = item.courseName?.trim() || "저장된 데이트 코스"; + const savedAt = item.savedAt ?? null; + const places = item.places ?? []; + + return { + id: item.dateCourseId, + title: courseName, + executedAtLabel: savedAt ? formatSavedAtLabel(savedAt) : "저장된 코스", + badgeLabel: item.savedByNickname ? "친구" : "하트", + savedByUserId: item.savedByUserId ?? null, + savedByNickname: item.savedByNickname ?? null, + savedByProfileImageUrl: item.savedByProfileImageUrl ?? null, + savedAt, + savedFromRoomName: null, + savedFromRoomId: item.roomPublicId ?? roomId ?? null, + courseDateKey: + toCourseDateKey(item.startDateTime) ?? (savedAt ? toCourseDateKey(savedAt) : null), + stops: places + .slice() + .sort((left, right) => left.sequenceOrder - right.sequenceOrder) + .map(mapPlaceToSavedCourseStop), + }; +} diff --git a/src/features/place-flow/edit-place-navigation.ts b/src/features/place-flow/edit-place-navigation.ts index d65c5ee..18a8844 100644 --- a/src/features/place-flow/edit-place-navigation.ts +++ b/src/features/place-flow/edit-place-navigation.ts @@ -1,4 +1,4 @@ -export type EditPlaceReturnTo = "link-add" | "register-place" | "back"; +export type EditPlaceReturnTo = "link-add" | "back"; export type EditPlaceLocationState = { placeId?: string; @@ -44,11 +44,8 @@ export function resolveEditPlaceReturnTo(state: EditPlaceLocationState): EditPla if (state.onConfirmNavigate === "link-add-back") { return "link-add"; } - if (state.onConfirmNavigate === "register_place") { - return "register-place"; - } if (state.onConfirmNavigate === "back") { return "back"; } - return "register-place"; + return "back"; } diff --git a/src/features/place-link/constants.ts b/src/features/place-link/constants.ts deleted file mode 100644 index 4fe3717..0000000 --- a/src/features/place-link/constants.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** 미연동 구간: 공유 링크 미리보기·복사용 목 URL */ -export const LINK_PREVIEW_MOCK = - "https://www.instagram.com/reel/DNp9tqSz6rT/?igsh=MW4yOGd6aGNzMmRsYw=="; - -export const PLACE_RENDER_ORDER = [ - "restaurant-1", - "restaurant-2", - "restaurant-3", - "restaurant-4", - "restaurant-5", - "cafe-1", -] as const; - -export const SAVED_PLACE_ID = "restaurant-3"; diff --git a/src/pages/EditPlacePage.tsx b/src/pages/EditPlacePage.tsx index 3522162..7c37cb5 100644 --- a/src/pages/EditPlacePage.tsx +++ b/src/pages/EditPlacePage.tsx @@ -16,9 +16,8 @@ import { resolveEditPlaceReturnTo, } from "@/features/place-flow/edit-place-navigation"; import { PLACE_FLOW_COPY } from "@/features/place-flow/place-flow-copy"; -import { LINK_PREVIEW_MOCK } from "@/features/place-link/constants"; import { resolveGeneralApiErrorMessage } from "@/shared/api/error"; -import { APP_ROUTES, ROOM_APP_PATHS } from "@/shared/config/routes"; +import { ROOM_APP_PATHS } from "@/shared/config/routes"; import { useEditPlaceStore } from "@/store/edit-place-store"; const EMPTY_PLACE_CANDIDATES: PlaceCandidate[] = []; @@ -85,7 +84,7 @@ export default function EditPlacePage() { const linkPreviewUrl = typeof routeState.linkAddOriginalUrl === "string" && routeState.linkAddOriginalUrl.length > 0 ? routeState.linkAddOriginalUrl - : LINK_PREVIEW_MOCK; + : null; const searchResults = useMemo(() => { if (!trimmedKeyword) { return []; @@ -208,7 +207,7 @@ export default function EditPlacePage() { return; } - navigate(APP_ROUTES.placeRegisterFromLink); + navigate(-1); }; return ( diff --git a/src/pages/LinkPlaceSelectPage.tsx b/src/pages/LinkPlaceSelectPage.tsx deleted file mode 100644 index cab3fce..0000000 --- a/src/pages/LinkPlaceSelectPage.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { useMemo } from "react"; -import { useNavigate } from "react-router-dom"; - -import { CopyableLinkBar } from "@/components/common/CopyableLinkBar"; -import { TwoButtonFooter } from "@/components/common/TwoButtonFooter"; -import { FullscreenFlowRouteMount } from "@/components/layout/FullscreenFlowRouteMount"; -import { PlaceSelectCard } from "@/components/link-place/PlaceSelectCard"; -import { PlaceFlowCancelPillButton } from "@/components/place-flow/PlaceFlowCancelPillButton"; -import { PlaceFlowHeadlines } from "@/components/place-flow/PlaceFlowHeadlines"; -import { PillButton } from "@/components/ui/PillButton"; -import { PLACE_FLOW_COPY } from "@/features/place-flow/place-flow-copy"; -import { - PROMPT_FLOW_BELOW_HEADLINES_CLASS, - PROMPT_FLOW_HEADER_CLASS, - PROMPT_FLOW_LIST_TOP_BORDER_CLASS, - PROMPT_FLOW_SCROLL_BODY_CLASS, -} from "@/features/place-flow/prompt-flow-layout"; -import { - LINK_PREVIEW_MOCK, - PLACE_RENDER_ORDER, - SAVED_PLACE_ID, -} from "@/features/place-link/constants"; -import { APP_ROUTES } from "@/shared/config/routes"; -import { SAVED_PLACE_MOCKS } from "@/shared/mocks/place-mocks"; -import { useEditPlaceStore } from "@/store/edit-place-store"; -import { useLinkPlaceSelectStore } from "@/store/link-place-select-store"; -import { useRegisterRoomStore } from "@/store/register-room-store"; -import { useRoomSelectionStore } from "@/store/room-selection-store"; - -export default function LinkPlaceSelectPage() { - const navigate = useNavigate(); - const selectedPlaceIds = useLinkPlaceSelectStore((state) => state.selectedPlaceIds); - const togglePlace = useLinkPlaceSelectStore((state) => state.togglePlace); - const clearSelection = useLinkPlaceSelectStore((state) => state.clearSelection); - const editingPlaceId = useEditPlaceStore((state) => state.editingPlaceId); - const selectedResultId = useEditPlaceStore((state) => state.selectedResultId); - const setSelectedPlacesForRegister = useRegisterRoomStore((state) => state.setSelectedPlaces); - const completeRegisterToRoom = useRegisterRoomStore((state) => state.completeRegisterToRoom); - const selectedRoom = useRoomSelectionStore((state) => state.selectedRoom); - - const placeRows = useMemo( - () => - PLACE_RENDER_ORDER.map((placeId) => { - const originalPlace = SAVED_PLACE_MOCKS.find((place) => place.id === placeId); - const editedPlace = - editingPlaceId === placeId && selectedResultId - ? SAVED_PLACE_MOCKS.find((place) => place.id === selectedResultId) - : null; - - const place = editedPlace ?? originalPlace; - - return place ? { slotId: placeId, place } : null; - }).filter((row) => row != null), - [editingPlaceId, selectedResultId], - ); - const canConfirm = selectedPlaceIds.length > 0 && selectedRoom != null; - - return ( - -
- - -
- -
-
- -
-
    - {placeRows.map(({ slotId, place }) => { - const disabled = slotId === SAVED_PLACE_ID; - return ( - togglePlace(slotId)} - onEdit={() => - navigate(APP_ROUTES.editPlace, { - state: { - placeId: slotId, - placeName: place.name, - returnTo: "register-place", - }, - }) - } - /> - ); - })} -
-
- - - {PLACE_FLOW_COPY.cancel} - - } - right={ - { - if (!selectedRoom) { - return; - } - setSelectedPlacesForRegister(selectedPlaceIds); - if (completeRegisterToRoom(selectedRoom.id)) { - clearSelection(); - navigate(APP_ROUTES.room, { - replace: true, - state: { showPlacesRegisteredToast: true }, - }); - } - }} - > - 확인 - - } - /> -
- ); -} diff --git a/src/pages/map/MapHomePage.tsx b/src/pages/map/MapHomePage.tsx index e77b4d3..97c4ba4 100644 --- a/src/pages/map/MapHomePage.tsx +++ b/src/pages/map/MapHomePage.tsx @@ -19,12 +19,8 @@ import { import { useRoomMembersQuery } from "@/features/room"; import { roomPlaceToSavedPlace, useRoomPlaces } from "@/features/room-places"; import { useBottomNavController } from "@/hooks/use-bottom-nav-controller"; +import { MAP_INITIAL_CENTER, MAP_KOREA_BOUNDS, MAP_SEARCH_PLACEHOLDER } from "@/shared/config/map"; import { APP_ROUTES, ROOM_APP_PATHS } from "@/shared/config/routes"; -import { - MAP_INITIAL_CENTER, - MAP_KOREA_BOUNDS, - MAP_SEARCH_PLACEHOLDER, -} from "@/shared/mocks/place-mocks"; import type { MapCoordinate, RoomFriend, SavedPlace } from "@/shared/types/map-home"; import { PLACE_DETAIL_OPEN_EVENT } from "@/store/place-detail-store"; import { useRoomSelectionStore } from "@/store/room-selection-store"; diff --git a/src/pages/room/room-list-mock.ts b/src/pages/room/room-list-mock.ts deleted file mode 100644 index 89d62b9..0000000 --- a/src/pages/room/room-list-mock.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { RoomListRow } from "@/shared/types/room"; - -export type { RoomListRow } from "@/shared/types/room"; - -export const ROOM_LIST_MOCK_ROWS: RoomListRow[] = [ - { - id: "1", - avatarSeed: "mock-room-1", - displayName: "내꺼♥", - memberCount: 2, - placeCount: 5, - isPinned: true, - pinnedAt: 1_700_000_000_000, - inviteCode: "28194", - }, - { - id: "2", - avatarSeed: "mock-room-2", - displayName: "가족방", - memberCount: 4, - placeCount: 6, - inviteCode: "44102", - }, - { - id: "3", - avatarSeed: "mock-room-3", - displayName: "재수팟", - memberCount: 6, - placeCount: 6, - inviteCode: "90351", - }, -]; diff --git a/src/pages/rooms/RoomPlaceSearchPage.tsx b/src/pages/rooms/RoomPlaceSearchPage.tsx index ed775b8..6bb493c 100644 --- a/src/pages/rooms/RoomPlaceSearchPage.tsx +++ b/src/pages/rooms/RoomPlaceSearchPage.tsx @@ -24,7 +24,6 @@ import { PROMPT_FLOW_LIST_TOP_BORDER_CLASS, PROMPT_FLOW_SCROLL_BODY_CLASS, } from "@/features/place-flow/prompt-flow-layout"; -import { LINK_PREVIEW_MOCK } from "@/features/place-link/constants"; import { resolveGeneralApiErrorMessage } from "@/shared/api/error"; import { APP_ROUTES } from "@/shared/config/routes"; import { useInpersonPlaceStore } from "@/store/inperson-place-store"; @@ -56,7 +55,7 @@ export default function RoomPlaceSearchPage() { const linkPreviewUrl = typeof routeState?.linkAddOriginalUrl === "string" && routeState.linkAddOriginalUrl.length > 0 ? routeState.linkAddOriginalUrl - : LINK_PREVIEW_MOCK; + : null; const trimmedKeyword = keyword.trim(); const canSearch = trimmedKeyword.length > 0; @@ -165,7 +164,7 @@ export default function RoomPlaceSearchPage() { />
- + {linkPreviewUrl ? : null} import("@/components/map/KakaoMapView").then((module) => ({ default: module.KakaoMapView })), @@ -117,7 +113,7 @@ function resolveCoursePlannerApiErrorMessage(error: unknown) { return firstFieldError ?? formError.formError ?? resolveGeneralApiErrorMessage(error); } -export default function CoursePlannerPage({ skipRoomGuard = false }: CoursePlannerPageProps) { +export default function CoursePlannerPage() { const queryClient = useQueryClient(); const selectedRoom = useRoomSelectionStore((s) => s.selectedRoom); const { toastMessage, toastPlacement, handleSelectBottomNav, showToast } = @@ -507,7 +503,7 @@ export default function CoursePlannerPage({ skipRoomGuard = false }: CoursePlann [handleSaveCourse, queryClient, saveCourse, selectedCourseId, showToast], ); - if (!skipRoomGuard && !selectedRoom) { + if (!selectedRoom) { return ; } diff --git a/src/pages/tabs/MyPage.tsx b/src/pages/tabs/MyPage.tsx index 583859c..87f0681 100644 --- a/src/pages/tabs/MyPage.tsx +++ b/src/pages/tabs/MyPage.tsx @@ -15,6 +15,7 @@ import { PlaceDetailSheet } from "@/components/place/PlaceDetailSheet"; import { useLogout } from "@/features/auth/hooks/use-logout"; import { useMyDateCoursesQuery } from "@/features/course-planner/hooks/use-my-date-courses-query"; import { mapMySavedDateCourseToSavedCourse } from "@/features/course-planner/lib/map-my-saved-date-course"; +import { useRoomsQuery } from "@/features/room"; import { roomPlaceApi, roomPlaceQueryKeys } from "@/features/room-places"; import { useMyPlacesQuery, @@ -74,14 +75,29 @@ export default function MyPage() { const myDateCoursesQuery = useMyDateCoursesQuery({ enabled: view === "main" || view === "courses", }); + const roomsQuery = useRoomsQuery({ + enabled: view === "main" || view === "courses", + }); const apiCourses = useMemo( () => (myDateCoursesQuery.data?.items ?? []).map(mapMySavedDateCourseToSavedCourse), [myDateCoursesQuery.data?.items], ); - const coursesList = useMemo( - () => apiCourses.map((course) => courseOverrides[course.id] ?? course), - [apiCourses, courseOverrides], - ); + const coursesList = useMemo(() => { + const roomById = new Map((roomsQuery.data ?? []).map((room) => [room.roomId, room])); + + return apiCourses.map((course) => { + const overridden = courseOverrides[course.id] ?? course; + const room = overridden.savedFromRoomId + ? roomById.get(overridden.savedFromRoomId) + : undefined; + + return { + ...overridden, + savedFromRoomName: overridden.savedFromRoomName ?? room?.roomName ?? null, + savedFromRoomAvatarSeed: room?.avatarSeed ?? overridden.savedFromRoomAvatarSeed ?? null, + }; + }); + }, [apiCourses, courseOverrides, roomsQuery.data]); const apiPlaces = useMemo( () => (myPlacesQuery.data?.items ?? []).map(userPlaceToSavedPlace), [myPlacesQuery.data?.items], diff --git a/src/pages/tabs/PlaceListPage.tsx b/src/pages/tabs/PlaceListPage.tsx index 8484d7f..62b071d 100644 --- a/src/pages/tabs/PlaceListPage.tsx +++ b/src/pages/tabs/PlaceListPage.tsx @@ -15,6 +15,7 @@ import { FilterBar } from "@/components/map/FilterBar"; import { weightedMapCenter } from "@/components/mypage/map-places-from-my-saved"; import { SavedPlaceItem } from "@/components/mypage/SavedPlaceItem"; import { PlaceDetailSheet } from "@/components/place/PlaceDetailSheet"; +import { PlaceListSavedCoursesPage } from "@/components/place-list/PlaceListSavedCoursesPage"; import { RoomConfirmModal } from "@/components/room/RoomConfirmModal"; import { useMapSearchFilters } from "@/features/map/hooks/use-map-search-filters"; import { usePlaceFilterViewModel } from "@/features/map/hooks/use-place-filter-view-model"; @@ -70,6 +71,7 @@ export default function PlaceListPage() { const [draftSigungu, setDraftSigungu] = useState(REGION_ALL_OPTION); const [regionSearchKeyword, setRegionSearchKeyword] = useState(""); const [isRegionPanelOpen, setIsRegionPanelOpen] = useState(false); + const [activeTab, setActiveTab] = useState<"places" | "courses">("places"); const roomPlaceListParams = useMemo(() => { const hasSido = selectedSido.code !== REGION_ALL_CODE; const hasSigungu = hasSido && selectedSigungu.code !== REGION_ALL_CODE; @@ -217,6 +219,21 @@ export default function PlaceListPage() { })); }, [categoryFilteredPlaces]); + const savedPlacesForCourses = useMemo( + () => + listPlacesBase.map((place) => ({ + id: place.id, + name: place.name, + address: place.address, + category: place.category, + shareLinkUrl: place.shareLinkUrl, + roomId: effectiveRoomId ?? undefined, + memo: place.memo, + memos: place.memos, + })), + [effectiveRoomId, listPlacesBase], + ); + const [openMenuId, setOpenMenuId] = useState(null); const [editingPlaceId, setEditingPlaceId] = useState(null); const [memoDraft, setMemoDraft] = useState(""); @@ -296,11 +313,26 @@ export default function PlaceListPage() { shownCount === regionTotal && regionTotal === categoryTotal ? `${formatCount(shownCount)}개` : shownCount === regionTotal - ? `${formatCount(shownCount)}개 · 전체 ${formatCount(categoryTotal)}` - : `${formatCount(shownCount)}개 · 전체 ${formatCount(regionTotal)}`; + ? `${formatCount(shownCount)}개 / 전체 ${formatCount(categoryTotal)}` + : `${formatCount(shownCount)}개 / 전체 ${formatCount(regionTotal)}`; const roomName = selectedRoom?.id === effectiveRoomId ? selectedRoom.name : roomDetailQuery.data?.roomName; - const pageTitle = roomName ? `${roomName} 목록` : "목록"; + const pageTitle = roomName; + + if (activeTab === "courses") { + return ( + navigate(APP_ROUTES.map)} + onSwitchTab={setActiveTab} + /> + ); + } const hasRegionFilter = selectedSido.code !== REGION_ALL_CODE; const emptyMessage = roomPlacesQuery.isError @@ -410,7 +442,23 @@ export default function PlaceListPage() { onBack={handleHeaderBack} > {!detailOpen ? ( -
+
+
+ + +
+