diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 5328ca7..1d225bd 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -15,6 +15,7 @@ import Map from "@/components/map/Map"; import SightDetailCard from "@/components/sight/SightDetailCard"; import { useCurationDetail } from "@/hooks/sight/useCurationDetail"; +import { useSightNavigation } from "@/hooks/sight/useSightNavigation"; import { useLocation } from "@/hooks/useLocation"; import { useSightMap } from "@/hooks/useSightMap"; @@ -65,6 +66,17 @@ export default function Index() { fetchCurations(); }, [fetchCurations]); + const { navigateSightId, navigateToSight } = useSightNavigation({ + mapRef, + location, + }); + + useEffect(() => { + if (navigateSightId && location) { + navigateToSight(navigateSightId); + } + }, [navigateSightId, location.latitude, location.longitude, navigateToSight]); + const handleSearch = useCallback(async () => { if (!searchText.trim()) return; diff --git a/app/(tabs)/myPage/_layout.tsx b/app/(tabs)/myPage/_layout.tsx index cb85f2e..8c0a25b 100644 --- a/app/(tabs)/myPage/_layout.tsx +++ b/app/(tabs)/myPage/_layout.tsx @@ -23,6 +23,8 @@ export default function MyPageLayout() { /> + + ); } diff --git a/app/(tabs)/myPage/index.tsx b/app/(tabs)/myPage/index.tsx index a20fc47..fbbd506 100644 --- a/app/(tabs)/myPage/index.tsx +++ b/app/(tabs)/myPage/index.tsx @@ -114,7 +114,7 @@ function LoggedInView({ 북마크 - console.log("지난 여행")}> + router.push("/myPage/pastTrip" as any)}> 지난 여행 diff --git a/app/(tabs)/myPage/pastTrip.tsx b/app/(tabs)/myPage/pastTrip.tsx new file mode 100644 index 0000000..56210ee --- /dev/null +++ b/app/(tabs)/myPage/pastTrip.tsx @@ -0,0 +1,288 @@ +import { useCallback, useEffect, useState } from "react"; + +import { ActivityIndicator, Alert, FlatList, Modal } from "react-native"; + +import { useRouter } from "expo-router"; +import { Pencil, Trash2 } from "lucide-react-native"; +import styled from "styled-components/native"; + +import { useCompletedRoute } from "@/hooks/route/useCompletedRoute"; + +import { CompletedRouteSummary } from "@/types/completedRoute"; + +import { theme } from "@/styles/theme"; + +export default function PastTrip() { + const router = useRouter(); + const { + routes, + hasNext, + isLoading, + fetchRoutes, + modifyRouteName, + deleteRoute, + } = useCompletedRoute(); + + const [editModalVisible, setEditModalVisible] = useState(false); + const [editingRoute, setEditingRoute] = useState(null); + const [editName, setEditName] = useState(""); + + useEffect(() => { + fetchRoutes(true); + }, []); + + const handleItemPress = (routeId: number) => { + router.push({ + pathname: "/myPage/pastTripDetail", + params: { routeId }, + }); + }; + + const handleEditPress = (route: CompletedRouteSummary) => { + setEditingRoute(route); + setEditName(route.name); + setEditModalVisible(true); + }; + + const handleEditSubmit = async () => { + if (!editingRoute || !editName.trim()) return; + + try { + await modifyRouteName(editingRoute.routeId, editName.trim()); + setEditModalVisible(false); + setEditingRoute(null); + setEditName(""); + } catch (error) { + Alert.alert("오류", "이름 수정에 실패했습니다."); + } + }; + + const handleDeletePress = (route: CompletedRouteSummary) => { + Alert.alert( + "여행 삭제", + `"${route.name}"기록을 정말 삭제하시겠습니까?\n삭제된 데이터는 복구할 수 없습니다.`, + [ + { text: "취소", style: "cancel" }, + { + text: "삭제", + style: "destructive", + onPress: async () => { + try { + await deleteRoute(route.routeId); + } catch (error) { + Alert.alert("오류", "삭제에 실패했습니다."); + } + }, + }, + ] + ); + }; + + const renderItem = useCallback( + ({ item }: { item: CompletedRouteSummary }) => ( + handleItemPress(item.routeId)}> + + {item.name} + {item.completedAt} + + + handleEditPress(item)}> + + + handleDeletePress(item)}> + + + + + ), + [] + ); + + return ( + +
+ 지난 여행 +
+ + item.routeId.toString()} + renderItem={renderItem} + refreshing={isLoading && routes.length === 0} + onRefresh={() => fetchRoutes(true)} + onEndReached={() => { + if (hasNext && !isLoading) { + fetchRoutes(false); + } + }} + onEndReachedThreshold={0.5} + ListFooterComponent={ + isLoading && routes.length > 0 ? ( + + ) : null + } + ListEmptyComponent={ + !isLoading ? ( + + 지난 여행이 없습니다 + + ) : null + } + contentContainerStyle={{ flexGrow: 1 }} + /> + + setEditModalVisible(false)} + > + + + 여행 이름 수정 + + + setEditModalVisible(false)}> + 취소 + + + 확인 + + + + + +
+ ); +} + +const Container = styled.View` + flex: 1; + background-color: ${theme.colors.white}; +`; + +const Header = styled.View` + height: 50px; + justify-content: center; + align-items: center; + border-bottom-width: 1px; + border-bottom-color: ${theme.colors.grey.neutral200}; +`; + +const HeaderTitle = styled.Text` + font-family: ${theme.typography.fontFamily.semiBold}; + font-size: ${theme.typography.fontSize.lg}px; + color: ${theme.colors.text.textPrimary}; +`; + +const RouteItem = styled.TouchableOpacity` + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom-width: 1px; + border-bottom-color: ${theme.colors.grey.neutral200}; +`; + +const RouteInfo = styled.View` + flex: 1; +`; + +const RouteName = styled.Text` + font-family: ${theme.typography.fontFamily.medium}; + font-size: ${theme.typography.fontSize.md}px; + color: ${theme.colors.text.textPrimary}; + margin-bottom: 4px; +`; + +const RouteDate = styled.Text` + font-family: ${theme.typography.fontFamily.regular}; + font-size: ${theme.typography.fontSize.xs}px; + color: ${theme.colors.text.textTertiary}; +`; + +const ButtonGroup = styled.View` + flex-direction: row; + gap: 8px; +`; + +const ActionButton = styled.TouchableOpacity` + padding: 8px 12px; +`; + +const ActionButtonText = styled.Text` + font-family: ${theme.typography.fontFamily.medium}; + font-size: ${theme.typography.fontSize.xs}px; + color: ${theme.colors.text.textSecondary}; +`; + +const EmptyContainer = styled.View` + flex: 1; + justify-content: center; + align-items: center; +`; + +const EmptyText = styled.Text` + font-family: ${theme.typography.fontFamily.regular}; + font-size: ${theme.typography.fontSize.sm}px; + color: ${theme.colors.text.textTertiary}; +`; + +const ModalOverlay = styled.View` + flex: 1; + background-color: ${theme.colors.background.modalBackground}; + justify-content: center; + align-items: center; + padding: 20px; +`; + +const ModalContent = styled.View` + width: 100%; + background-color: ${theme.colors.white}; + border-radius: ${theme.borderRadius.md}px; + padding: 20px; +`; + +const ModalTitle = styled.Text` + font-family: ${theme.typography.fontFamily.semiBold}; + font-size: ${theme.typography.fontSize.md}px; + color: ${theme.colors.text.textPrimary}; + margin-bottom: 16px; + text-align: center; +`; + +const ModalInput = styled.TextInput` + background-color: ${theme.colors.background.background50}; + border-radius: ${theme.borderRadius.md}px; + padding: 12px 16px; + font-size: ${theme.typography.fontSize.md}px; + color: ${theme.colors.text.textPrimary}; + margin-bottom: 16px; +`; + +const ModalButtonGroup = styled.View` + flex-direction: row; + gap: 12px; +`; + +const ModalButton = styled.TouchableOpacity<{ primary?: boolean }>` + flex: 1; + padding: 12px; + border-radius: ${theme.borderRadius.md}px; + background-color: ${(props) => + props.primary ? theme.colors.main.primary : theme.colors.grey.neutral200}; + align-items: center; +`; + +const ModalButtonText = styled.Text<{ primary?: boolean }>` + font-family: ${theme.typography.fontFamily.medium}; + font-size: ${theme.typography.fontSize.sm}px; + color: ${(props) => + props.primary ? theme.colors.white : theme.colors.text.textPrimary}; +`; \ No newline at end of file diff --git a/app/(tabs)/myPage/pastTripDetail.tsx b/app/(tabs)/myPage/pastTripDetail.tsx new file mode 100644 index 0000000..36d5618 --- /dev/null +++ b/app/(tabs)/myPage/pastTripDetail.tsx @@ -0,0 +1,175 @@ +import { useEffect } from "react"; + +import { ActivityIndicator, FlatList } from "react-native"; + +import { useLocalSearchParams, useRouter } from "expo-router"; +import { ChevronLeft, MessageSquareHeart } from "lucide-react-native"; +import styled from "styled-components/native"; + +import SightCard from "@/components/common/SightCard"; + +import { useCompletedRoute } from "@/hooks/route/useCompletedRoute"; + +import { CompletedRouteItem } from "@/types/completedRoute"; + +import { theme } from "@/styles/theme"; + +import { useStoryStore } from "@/store/story/useStoryStore"; +import { useSightStore } from "@/store/useSightStore"; + +export default function PastTripDetail() { + const router = useRouter(); + const { routeId } = useLocalSearchParams<{ routeId: string }>(); + const { + selectedRoute, + selectedRouteItems, + isDetailLoading, + fetchRouteDetail, + clearDetail, + } = useCompletedRoute(); + + const handleCardPress = (item: CompletedRouteItem) => { + if (item.itemType === "SIGHT") { + useSightStore.getState().setNavigateSightId(item.itemId); + router.push("/(tabs)"); + } else { + useStoryStore.getState().setNavigateStorySpotId(Number(item.itemId)); + router.push("/(tabs)/story"); + } + }; + + useEffect(() => { + if (routeId) { + fetchRouteDetail(Number(routeId)); + } + + return () => { + clearDetail(); + }; + }, [routeId]); + + const renderItem = ({ item }: { item: CompletedRouteItem }) => { + if (item.itemType === "SIGHT") { + return ( + + handleCardPress(item)} + /> + + ); + } + + return ( + handleCardPress(item)}> + + {item.itemName} + + ); + }; + + if (isDetailLoading) { + return ( + + + + ); + } + + return ( + +
+ router.back()}> + + + {selectedRoute?.name || "여행 상세"} + +
+ + `${item.itemType}-${item.itemId}-${index}`} + renderItem={renderItem} + contentContainerStyle={{ padding: 20, gap: 12 }} + ListEmptyComponent={ + + 방문한 장소가 없습니다 + + } + /> +
+ ); +} + +const Container = styled.View` + flex: 1; + background-color: ${theme.colors.white}; +`; + +const LoadingContainer = styled.View` + flex: 1; + justify-content: center; + align-items: center; + background-color: ${theme.colors.white}; +`; + +const Header = styled.View` + height: 50px; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding-horizontal: 15px; + border-bottom-width: 1px; + border-bottom-color: ${theme.colors.grey.neutral200}; +`; + +const BackButton = styled.TouchableOpacity` + width: 40px; + height: 40px; + justify-content: center; + align-items: flex-start; +`; + +const HeaderTitle = styled.Text` + font-family: ${theme.typography.fontFamily.semiBold}; + font-size: ${theme.typography.fontSize.lg}px; + color: ${theme.colors.text.textPrimary}; + flex: 1; + text-align: center; +`; + +const HeaderSpacer = styled.View` + width: 40px; +`; + +const CardWrapper = styled.View` + align-items: center; +`; + +const EmptyContainer = styled.View` + padding: 40px; + align-items: center; +`; + +const EmptyText = styled.Text` + font-family: ${theme.typography.fontFamily.regular}; + font-size: ${theme.typography.fontSize.sm}px; + color: ${theme.colors.text.textTertiary}; +`; + +const StorySpotItem = styled.TouchableOpacity` + flex-direction: row; + align-items: center; + padding: 12px 16px; + gap: 10px; +`; + +const StorySpotText = styled.Text` + font-family: ${theme.typography.fontFamily.regular}; + font-size: ${theme.typography.fontSize.sm}px; + color: ${theme.colors.text.textPrimary}; +`; \ No newline at end of file diff --git a/app/(tabs)/story/index.tsx b/app/(tabs)/story/index.tsx index 16dd3a1..890bb79 100644 --- a/app/(tabs)/story/index.tsx +++ b/app/(tabs)/story/index.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from "react"; +import React, { useEffect, useRef } from "react"; import { StyleSheet } from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; @@ -12,7 +12,9 @@ import MainStoryHeader from "@/components/story/MainStoryHeader"; import { StoryAddButton } from "@/components/story/StoryAddButton"; import StorySpotHeader from "@/components/story/StorySpotHeader"; +import { useStoryNavigation } from "@/hooks/story/useStoryNavigation"; import { useStorySpotMap } from "@/hooks/story/useStorySpotMap"; +import { useLocation } from "@/hooks/useLocation"; import { useStoryStore } from "@/store/story/useStoryStore"; @@ -23,6 +25,7 @@ export default function Index() { const { mapRef, selectedMarker, + setSelectedMarker, handleMapPress, handleRegionChange, handleStoryMarkerPress, @@ -30,6 +33,20 @@ export default function Index() { const { spotLocationInMap } = useStoryStore(); + const { location } = useLocation(); + + const { navigateStorySpotId, navigateToStorySpot } = useStoryNavigation({ + mapRef, + location, + setSelectedMarker, + }); + + useEffect(() => { + if (navigateStorySpotId && location) { + navigateToStorySpot(navigateStorySpotId); + } + }, [navigateStorySpotId, location.latitude, location.longitude, navigateToStorySpot]); + return ( diff --git a/src/api/route/completedRouteApi.ts b/src/api/route/completedRouteApi.ts new file mode 100644 index 0000000..af4e1ca --- /dev/null +++ b/src/api/route/completedRouteApi.ts @@ -0,0 +1,55 @@ +import { BaseResponse } from "@/types/auth"; +import { + CompletedRouteDetailResponse, + CompletedRouteListRequest, + CompletedRouteListResponse, + CompletedRouteSummary, + ModifyCompletedRouteRequest, +} from "@/types/completedRoute"; + +import API_ENDPOINTS from "@/constants/endpoints"; + +import api from "@/api/axios"; + +// 완료된 경로 리스트 조회 +export const getCompletedRoutes = async ( + params?: CompletedRouteListRequest +): Promise => { + const response = await api.get>( + API_ENDPOINTS.ROUTE.COMPLETED_LIST, + { + params: { + page: params?.page ?? 0, + size: params?.size ?? 10, + }, + } + ); + return response.data.data; +}; + +// 완료된 경로 상세 조회 +export const getCompletedRouteDetail = async ( + routeId: number +): Promise => { + const response = await api.get>( + API_ENDPOINTS.ROUTE.COMPLETED_DETAIL(routeId) + ); + return response.data.data; +}; + +// 완료된 경로 이름 수정 +export const modifyCompletedRouteName = async ( + routeId: number, + data: ModifyCompletedRouteRequest +): Promise => { + const response = await api.put>( + API_ENDPOINTS.ROUTE.COMPLETED_MODIFY(routeId), + data + ); + return response.data.data; +}; + +// 완료된 경로 삭제 +export const deleteCompletedRoute = async (routeId: number): Promise => { + await api.delete(API_ENDPOINTS.ROUTE.COMPLETED_MODIFY(routeId)); +}; \ No newline at end of file diff --git a/src/constants/endpoints.ts b/src/constants/endpoints.ts index db31bfd..dce7864 100644 --- a/src/constants/endpoints.ts +++ b/src/constants/endpoints.ts @@ -48,6 +48,9 @@ export const API_ENDPOINTS = { ROUTE: { CREATE_IN_PROGRESS_ROUTE: "/api/user/route/in-progress", COMPLETE_ROUTE: (routeId: number) => `/api/user/route/${routeId}/complete`, + COMPLETED_LIST: "/api/user/route/completed", + COMPLETED_DETAIL: (routeId: number) => `/api/user/route/completed/detail/${routeId}`, + COMPLETED_MODIFY: (routeId: number) => `/api/user/route/completed/${routeId}`, }, MEMBER: { GET_PROFILE: "/api/user/member/profile", diff --git a/src/hooks/route/useCompletedRoute.ts b/src/hooks/route/useCompletedRoute.ts new file mode 100644 index 0000000..b27a557 --- /dev/null +++ b/src/hooks/route/useCompletedRoute.ts @@ -0,0 +1,122 @@ +import { create } from "zustand"; + +import { + CompletedRouteItem, + CompletedRouteSummary, +} from "@/types/completedRoute"; + +import { + deleteCompletedRoute, + getCompletedRouteDetail, + getCompletedRoutes, + modifyCompletedRouteName, +} from "@/api/route/completedRouteApi"; + +type CompletedRouteState = { + routes: CompletedRouteSummary[]; + hasNext: boolean; + currentPage: number; + isLoading: boolean; + + selectedRoute: CompletedRouteSummary | null; + selectedRouteItems: CompletedRouteItem[]; + isDetailLoading: boolean; + + fetchRoutes: (isRefresh?: boolean) => Promise; + fetchRouteDetail: (routeId: number) => Promise; + modifyRouteName: (routeId: number, name: string) => Promise; + deleteRoute: (routeId: number) => Promise; + clearDetail: () => void; +}; + +export const useCompletedRoute = create((set, get) => ({ + routes: [], + hasNext: false, + currentPage: 0, + isLoading: false, + + selectedRoute: null, + selectedRouteItems: [], + isDetailLoading: false, + + fetchRoutes: async (isRefresh = false) => { + const { isLoading, currentPage, hasNext } = get(); + + if (isLoading) return; + if (!isRefresh && !hasNext && currentPage > 0) return; + + set({ isLoading: true }); + + try { + const page = isRefresh ? 0 : currentPage; + const response = await getCompletedRoutes({ page, size: 10 }); + + set((state) => ({ + routes: isRefresh + ? response.routes + : [...state.routes, ...response.routes], + hasNext: response.hasNext, + currentPage: page + 1, + })); + } catch (error) { + console.error("완료된 경로 조회 실패:", error); + } finally { + set({ isLoading: false }); + } + }, + + fetchRouteDetail: async (routeId: number) => { + set({ isDetailLoading: true }); + + try { + const response = await getCompletedRouteDetail(routeId); + + set({ + selectedRoute: response.route, + selectedRouteItems: response.items, + }); + } catch (error) { + console.error("경로 상세 조회 실패:", error); + } finally { + set({ isDetailLoading: false }); + } + }, + + modifyRouteName: async (routeId: number, name: string) => { + try { + const updated = await modifyCompletedRouteName(routeId, { name }); + + set((state) => ({ + routes: state.routes.map((route) => + route.routeId === routeId ? { ...route, name: updated.name } : route + ), + selectedRoute: state.selectedRoute?.routeId === routeId + ? { ...state.selectedRoute, name: updated.name } + : state.selectedRoute, + })); + } catch (error) { + console.error("경로 이름 수정 실패:", error); + throw error; + } + }, + + deleteRoute: async (routeId: number) => { + try { + await deleteCompletedRoute(routeId); + + set((state) => ({ + routes: state.routes.filter((route) => route.routeId !== routeId), + })); + } catch (error) { + console.error("경로 삭제 실패:", error); + throw error; + } + }, + + clearDetail: () => { + set({ + selectedRoute: null, + selectedRouteItems: [], + }); + }, +})); \ No newline at end of file diff --git a/src/hooks/sight/useSightNavigation.ts b/src/hooks/sight/useSightNavigation.ts new file mode 100644 index 0000000..b366b1a --- /dev/null +++ b/src/hooks/sight/useSightNavigation.ts @@ -0,0 +1,61 @@ +import { useCallback } from "react"; + +import { MapRef } from "@/types/map"; +import { SightInfo } from "@/types/sight"; + +import { getSightDetail } from "@/api/sight/getSight"; +import { useSightStore } from "@/store/useSightStore"; + +interface UseSightNavigationParams { + mapRef: React.RefObject; + location: { + latitude: number; + longitude: number; + }; +} + +export const useSightNavigation = ({ mapRef, location }: UseSightNavigationParams) => { + const navigateSightId = useSightStore((state) => state.navigateSightId); + + const navigateToSight = useCallback(async (sightId: string) => { + try { + useSightStore.getState().setDetailLoading(true); + + const detail = await getSightDetail({ + id: sightId, + longitude: location.longitude, + latitude: location.latitude, + }); + + const sight: SightInfo = { + id: sightId, + title: detail.title, + longitude: detail.longitude, + latitude: detail.latitude, + geoHash: "", + }; + + // 지도 이동 + mapRef.current?.moveToLocation({ + latitude: detail.latitude, + longitude: detail.longitude, + latitudeDelta: 0.01, + longitudeDelta: 0.01, + }); + + useSightStore.getState().selectSight(sight); + useSightStore.getState().setSightDetail(detail); + useSightStore.getState().clearNavigateSightId(); + } catch (error) { + console.error("관광지 조회 실패:", error); + useSightStore.getState().clearNavigateSightId(); + } finally { + useSightStore.getState().setDetailLoading(false); + } + }, [location.latitude, location.longitude, mapRef]); + + return { + navigateSightId, + navigateToSight, + }; +}; \ No newline at end of file diff --git a/src/hooks/story/useStoryNavigation.ts b/src/hooks/story/useStoryNavigation.ts new file mode 100644 index 0000000..f25341c --- /dev/null +++ b/src/hooks/story/useStoryNavigation.ts @@ -0,0 +1,71 @@ +import { useCallback } from "react"; + +import { MapRef } from "@/types/map"; + +import { useStoryStore } from "@/store/story/useStoryStore"; + +interface UseStoryNavigationParams { + mapRef: React.RefObject; + location: { + latitude: number; + longitude: number; + }; + setSelectedMarker: (id: number | undefined) => void; +} + +export const useStoryNavigation = ({ mapRef, location, setSelectedMarker }: UseStoryNavigationParams) => { + const navigateStorySpotId = useStoryStore((state) => state.navigateStorySpotId); + const { setStoryInfo, setStorySpotBriefInfo } = useStoryStore(); + + const navigateToStorySpot = useCallback(async (storySpotId: number) => { + try { + const storyRequest = { + storySpotId, + query: { + query: { + longitude: location.longitude, + latitude: location.latitude, + locale: "KO" as const, + page: 0, + size: 1000, + sort: "createdAt,desc" as const, + }, + }, + }; + + // 스토어 업데이트 (바텀시트 데이터) + const response = await setStoryInfo(storyRequest); + + const briefSpotInfo = response?.briefSpotInfo; + + if (briefSpotInfo?.latitude && briefSpotInfo?.longitude) { + // 스토어 업데이트 + setStorySpotBriefInfo({ + latitude: briefSpotInfo.latitude, + longitude: briefSpotInfo.longitude, + }); + + // 지도 이동 + mapRef.current?.moveToLocation({ + latitude: briefSpotInfo.latitude, + longitude: briefSpotInfo.longitude, + latitudeDelta: 0.01, + longitudeDelta: 0.01, + }); + + // 바텀시트 전환 + setSelectedMarker(storySpotId); + } + + useStoryStore.getState().clearNavigateStorySpotId(); + } catch (error) { + console.error("이야기 스팟 조회 실패:", error); + useStoryStore.getState().clearNavigateStorySpotId(); + } + }, [location.latitude, location.longitude, mapRef, setSelectedMarker, setStoryInfo, setStorySpotBriefInfo]); + + return { + navigateStorySpotId, + navigateToStorySpot, + }; +}; \ No newline at end of file diff --git a/src/hooks/story/useStorySpotMap.ts b/src/hooks/story/useStorySpotMap.ts index b97cb35..fd3d06a 100644 --- a/src/hooks/story/useStorySpotMap.ts +++ b/src/hooks/story/useStorySpotMap.ts @@ -133,6 +133,7 @@ export const useStorySpotMap = () => { return { mapRef, selectedMarker: selectedMarkerId, + setSelectedMarker: setSelectedMarkerId, handleMapPress, handleRegionChange, handleStoryMarkerPress, diff --git a/src/store/story/useStoryStore.ts b/src/store/story/useStoryStore.ts index a88105c..4172488 100644 --- a/src/store/story/useStoryStore.ts +++ b/src/store/story/useStoryStore.ts @@ -42,6 +42,10 @@ interface StoryStore { isLoading: boolean; loading: boolean; + navigateStorySpotId: number | null; + setNavigateStorySpotId: (storySpotId: number) => void; + clearNavigateStorySpotId: () => void; + setSearchStory: ( param: GetSearchTitleRequest ) => Promise; @@ -191,6 +195,11 @@ export const useStoryStore = create((set, get) => ({ } }, + navigateStorySpotId: null, + + setNavigateStorySpotId: (storySpotId: number) => set({ navigateStorySpotId: storySpotId }), + + clearNavigateStorySpotId: () => set({ navigateStorySpotId: null }), setStoryLoading: (loading: boolean) => { set({ isLoading: loading }); }, diff --git a/src/store/useSightStore.ts b/src/store/useSightStore.ts index 45ed90a..7325d16 100644 --- a/src/store/useSightStore.ts +++ b/src/store/useSightStore.ts @@ -10,6 +10,8 @@ interface SightState { isDetailLoading: boolean; error: string | null; + navigateSightId: string | null; + setSights: (sights: SightInfo[]) => void; selectSight: (sight: SightInfo | null) => void; setSightDetail: (detail: SightDetailInfo | null) => void; @@ -18,6 +20,9 @@ interface SightState { setError: (error: string | null) => void; clearSelection: () => void; reset: () => void; + + setNavigateSightId: (sightId: string) => void; + clearNavigateSightId: () => void; } const initialState = { @@ -27,6 +32,7 @@ const initialState = { isLoading: false, isDetailLoading: false, error: null, + navigateSightId: null, }; export const useSightStore = create((set) => ({ @@ -55,4 +61,8 @@ export const useSightStore = create((set) => ({ }), reset: () => set(initialState), + + setNavigateSightId: (sightId) => set({ navigateSightId: sightId }), + + clearNavigateSightId: () => set({ navigateSightId: null }), })); diff --git a/src/types/completedRoute.ts b/src/types/completedRoute.ts new file mode 100644 index 0000000..b14f9c8 --- /dev/null +++ b/src/types/completedRoute.ts @@ -0,0 +1,39 @@ +import { RouteItemType } from "./route"; + +// 완료된 경로 요약 정보 +export interface CompletedRouteSummary { + routeId: number; + name: string; + completedAt: string; // yyyy.MM.dd 형식 +} + +// 완료된 경로 리스트 응답 +export interface CompletedRouteListResponse { + routes: CompletedRouteSummary[]; + hasNext: boolean; +} + +// 완료된 경로 상세 아이템 +export interface CompletedRouteItem { + itemType: RouteItemType; + itemId: string; + itemName: string; + itemImageUrl: string; +} + +// 완료된 경로 상세 응답 +export interface CompletedRouteDetailResponse { + route: CompletedRouteSummary; + items: CompletedRouteItem[]; +} + +// 경로 이름 수정 요청 +export interface ModifyCompletedRouteRequest { + name: string; +} + +// 페이징 요청 파라미터 +export interface CompletedRouteListRequest { + page?: number; + size?: number; +} \ No newline at end of file