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 ? (
+
+ 저장한 멤버가 없습니다
+
+ ) : (
+
+ )}
+
+ ) : 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 ? (
-
+
+
+
+
+
+