diff --git a/app.config.ts b/app.config.ts index 27ba1b1..83307fd 100644 --- a/app.config.ts +++ b/app.config.ts @@ -11,17 +11,17 @@ export default { userInterfaceStyle: "automatic", newArchEnabled: true, updates: { - url: "https://u.expo.dev/51f8ef22-b7f8-40ed-92d1-201c776e3b87" + url: "https://u.expo.dev/51f8ef22-b7f8-40ed-92d1-201c776e3b87", }, runtimeVersion: { - policy: "appVersion" + policy: "appVersion", }, extra: { APP_ENV_API_BASE_URL: process.env.APP_ENV_API_BASE_URL, - APP_ENV_GEOFENCE_RADIUS: process.env.APP_ENV_GEOFENCE_RADIUS || '100', + APP_ENV_GEOFENCE_RADIUS: process.env.APP_ENV_GEOFENCE_RADIUS || "100", eas: { - projectId: "51f8ef22-b7f8-40ed-92d1-201c776e3b87" - } + projectId: "51f8ef22-b7f8-40ed-92d1-201c776e3b87", + }, }, ios: { bundleIdentifier: "com.earseo.earseo", @@ -32,7 +32,7 @@ export default { UIBackgroundModes: ["audio", "location"], }, config: { - usesNonExemptEncryption: false + usesNonExemptEncryption: false, }, icon: "./src/assets/earseo-ios.icon", displayName: "이어서", @@ -84,4 +84,4 @@ export default { reactCompiler: true, }, }, -}; \ No newline at end of file +}; diff --git a/app/(tabs)/(index,story,route)/[mapType].tsx b/app/(tabs)/(index,story,route)/[mapType].tsx new file mode 100644 index 0000000..a3410ab --- /dev/null +++ b/app/(tabs)/(index,story,route)/[mapType].tsx @@ -0,0 +1,174 @@ +import React, { useEffect, useRef } from "react"; + +import { InteractionManager } from "react-native"; +import { useSharedValue } from "react-native-reanimated"; + +import { BottomSheetMethods } from "@gorhom/bottom-sheet/src/types"; +import { useGlobalSearchParams } from "expo-router"; + +import CurationBottomSheet from "@/components/bottomSheet/CurationBottomSheet"; +import CustomBottomSheet from "@/components/bottomSheet/CustomBottomSheet"; +import RouteBottomSheet from "@/components/bottomSheet/RouteBottomSheet"; +import StoryBottomSheet from "@/components/bottomSheet/StoryBottomSheet"; +import BaseMap from "@/components/map/BaseMap"; +import MapHeader from "@/components/map/header/MapHeader"; +import MapSearchBar from "@/components/map/header/MapSearchBar"; + +import { useBaseMap } from "@/hooks/map/useBaseMap"; +import { useSightMap } from "@/hooks/sight/useSightMap"; +import { useStorySpotMap } from "@/hooks/story/useStorySpotMap"; + +import { useBottomSheetStore } from "@/store/common/useBottomSheetStore"; +import { useBaseMapStore } from "@/store/map/useBaseMapStore"; +import { useMapHeaderStore } from "@/store/map/useMapHeaderStore"; +import { useRouteStore } from "@/store/route/useRouteStore"; +import { useSightStore } from "@/store/sight/useSightStore"; +import { useStoryAddStore } from "@/store/story/useStoryAddStore"; +import { useStoryStore } from "@/store/story/useStoryStore"; + +function MapTabScreen() { + const { mapType } = useGlobalSearchParams(); + const bottomSheetRef = useRef(null); + const bottomSheetPosition = useSharedValue(0); + const { setBottomSheetContent, setBottomSheetLayout, setIsBottomSheetDragg } = + useBottomSheetStore(); + const { setMapHeaderContent } = useMapHeaderStore(); + const { + setMapMovable, + setEnableCluster, + setCenterPinVisibility, + setRegionChangeCompleteMethod, + } = useBaseMapStore(); + const { loadMarkerFromStore } = useBaseMap(); + //sight + const { fetchSightInBoundingBox } = useSightMap(); + const { setSights } = useSightStore(); + //story + const { fetchStorySpotInBoundingBox } = useStorySpotMap(); + const { selectedStorySpot, setSpotListInMap } = useStoryStore(); + const { setNewSpotName } = useStoryAddStore(); + const storyAddStep = useStoryAddStore((state) => state.storyAddStep); + //route + const { setPathVisibility } = useRouteStore(); + + useEffect(() => { + /**TODO + * 화면 별 요소 추가 + * - 초기 마커 컴포넌트 O + * - regionChange 시 마커 로드 함수 O + * - 검색바 컴포넌트 O + * - 바텀시트 내부 컴포넌트 O + * - 바텀시트 위치 조정 X + * + * TODO issue: param이 변경될때마다 세번씩 호출됨 O + */ + if (!mapType) return; + setRegionChangeCompleteMethod(undefined); + if (mapType === "route") { + //TODO 경로 지도용 요소 추가 + setBottomSheetLayout({ + snapPoints: ["15%", "45%", "75%", "100%"], + index: 1, + }); + setSights([]); + setEnableCluster(false); + setPathVisibility(true); + setSpotListInMap([]); + setBottomSheetContent(); + } else if (mapType === "sight") { + //TODO 관광지 지도용 요소 추가 + setBottomSheetLayout({ + snapPoints: ["15%", "45%", "75%", "100%"], + index: 1, + }); + setMapHeaderContent( + { + //TODO 관광지 검색 모달 + console.log("sight map search bar pressed"); + }} + /> + ); + setBottomSheetContent(); + } + + if (mapType === "story") { + setBottomSheetContent(); + if (storyAddStep === "none") { + setBottomSheetLayout({ + snapPoints: ["15%", "45%", "75%", "100%"], + index: 1, + }); + setCenterPinVisibility(false); + setMapHeaderContent( + { + console.log("story map search bar pressed"); + }} + /> + ); + } + if (storyAddStep === "location") { + setCenterPinVisibility(true); + setIsBottomSheetDragg(false); + setBottomSheetLayout({ + snapPoints: ["30%"], + index: 0, + }); + } + + if (storyAddStep === "storyAdd") { + setCenterPinVisibility(false); + setIsBottomSheetDragg(true); + setBottomSheetLayout({ + snapPoints: ["100%"], + index: 0, + }); + } + } + + const task = InteractionManager.runAfterInteractions(() => { + if (mapType === "sight") { + setRegionChangeCompleteMethod(fetchSightInBoundingBox); + loadMarkerFromStore(); + } else if (mapType === "story" && storyAddStep === "none") { + setRegionChangeCompleteMethod(fetchStorySpotInBoundingBox); + loadMarkerFromStore(); + } + }); + + return () => { + // store 기반 공유 컴포넌트 데이터 초기화 + task.cancel(); + setBottomSheetLayout({ + snapPoints: ["15%", "45%", "75%", "100%"], + index: 1, + }); + setMapHeaderContent(undefined); + setMapMovable(true); + setEnableCluster(true); + setPathVisibility(false); + setSights([]); + setBottomSheetContent(undefined); + selectedStorySpot([]); + setIsBottomSheetDragg(true); + setCenterPinVisibility(false); + setNewSpotName(undefined); + }; + }, [mapType, storyAddStep]); + + return ( + <> + + + + + ); +} + +export default React.memo(MapTabScreen); diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 2a5c0ca..3068374 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,27 +1,39 @@ import { Ionicons } from "@expo/vector-icons"; -import { Tabs } from "expo-router"; +import { Tabs, usePathname } from "expo-router"; import CourseIcon from "@/components/icons/tabs/CourseIcon"; import MyRouteIcon from "@/components/icons/tabs/MyRouteIcon"; import { theme } from "@/styles/theme"; +import { useNavigationBarStore } from "@/store/common/useNavigationBarStore"; + export default function TabLayout() { + const pathname = usePathname(); + const currentMapType = pathname.startsWith("/sight") + ? "sight" + : pathname.startsWith("/route") + ? "route" + : pathname.startsWith("/story") + ? "story" + : null; + const { isNavigationBarHidden } = useNavigationBarStore(); return ( + + {/*@deprecated*/} + null, - tabBarIcon: ({ focused, color, size }) => ( - - ), + tabBarIcon: ({ color }) => { + color = + currentMapType === "route" + ? theme.colors.main.primary + : theme.colors.black; + return ; + }, }} /> null, - tabBarIcon: ({ focused, color, size }) => ( - - ), + tabBarIcon: ({ color }) => { + color = + currentMapType === "sight" + ? theme.colors.main.primary + : theme.colors.black; + return ; + }, }} /> null, - tabBarIcon: ({ focused, color, size }) => ( - - ), + tabBarIcon: ({ color }) => { + color = + currentMapType === "story" + ? theme.colors.main.primary + : theme.colors.black; + return ( + + ); + }, }} /> ; - } - return ; + return ( + + {routeItems === undefined ? : } + + ); } diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx deleted file mode 100644 index 1d225bd..0000000 --- a/app/(tabs)/index.tsx +++ /dev/null @@ -1,340 +0,0 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; - -import { FlatList, Keyboard, StyleSheet, TouchableOpacity } from "react-native"; -import { GestureHandlerRootView } from "react-native-gesture-handler"; -import { useSharedValue } from "react-native-reanimated"; - -import { Ionicons } from "@expo/vector-icons"; -import styled from "styled-components/native"; - -import CustomBottomSheet from "@/components/bottomSheet/CustomBottomSheet"; -import Button from "@/components/common/Button"; -import CurationDetail from "@/components/curation/CurationDetail"; -import CurationList from "@/components/curation/CurationList"; -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"; - -import { MapRef } from "@/types/map"; -import { SightInfo } from "@/types/sight"; - -import { theme } from "@/styles/theme"; - -import { RouteCartItem, useRouteCartStore } from "@/store/useRouteCartStore"; - -export default function Index() { - const bottomSheetRef = useRef(null); - const animatedPosition = useSharedValue(0); - const mapRef = useRef(null); - - const { location } = useLocation(); - const { insertRouteCartItem, removeRouteCartItem, routeCartItems } = - useRouteCartStore(); - - const [searchText, setSearchText] = useState(""); - const [showResults, setShowResults] = useState(false); - - const { - sights, - selectedSight, - sightDetail, - isDetailLoading, - fetchSightsDebounced, - fetchSightDetail, - deselectSight, - searchSightsInBounds, - searchResults, - } = useSightMap(); - - const { - curations, - isCurationLoading, - fetchCurations, - fetchCurationDetail, - curationSightList, - selectedCurationTitle, - selectedCurationDescription, - handleAddSightListToMy, - setCurationSightList, - } = useCurationDetail(); - - useEffect(() => { - 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; - - // 지도 bounds 가져오기 - const boundaries = await mapRef.current?.getBoundaries(); - if (!boundaries) return; - - const bounds = { - minLongitude: boundaries.southWest.longitude, - minLatitude: boundaries.southWest.latitude, - maxLongitude: boundaries.northEast.longitude, - maxLatitude: boundaries.northEast.latitude, - }; - - // 검색 API 호출 - await searchSightsInBounds( - searchText, - { - longitude: location.longitude, - latitude: location.latitude, - }, - bounds - ); - - setShowResults(true); - Keyboard.dismiss(); - }, [searchText, location, searchSightsInBounds]); - - // 검색 결과 선택 시 - const handleSelectResult = (sight: SightInfo) => { - setShowResults(false); - setSearchText(""); - - mapRef.current?.moveToLocation({ - latitude: sight.latitude, - longitude: sight.longitude, - latitudeDelta: 0.01, - longitudeDelta: 0.01, - }); - - fetchSightDetail(sight, { - longitude: location.longitude, - latitude: location.latitude, - }); - }; - - const isInCart = routeCartItems.some( - (item) => item.sightId === selectedSight?.id - ); - - const handleToggleRoute = () => { - if (!sightDetail || !selectedSight) return; - - if (isInCart) { - removeRouteCartItem(selectedSight.id); - } else { - const cartItem: RouteCartItem = { - sightId: selectedSight.id, - theme: sightDetail.theme, - title: sightDetail.title, - address: sightDetail.address, - point: { - longitude: sightDetail.longitude, - latitude: sightDetail.latitude, - }, - imageUrl: sightDetail.imgUrl, - }; - insertRouteCartItem(cartItem); - } - }; - - // 지도 영역 변경 시 관광지 조회 - const handleRegionChangeComplete = (bounds: { - minLongitude: number; - minLatitude: number; - maxLongitude: number; - maxLatitude: number; - }) => { - fetchSightsDebounced(bounds); - }; - - // 마커 클릭 시 - const handleMarkerPress = (sight: SightInfo) => { - fetchSightDetail(sight, { - longitude: location.longitude, - latitude: location.latitude, - }); - }; - - return ( - - - - - - - - - - {searchText.length > 0 && ( - setSearchText("")}> - - - )} - - - - {showResults && searchResults.length > 0 && ( - - item.id} - renderItem={({ item }) => ( - handleSelectResult(item)}> - {item.title} - - )} - keyboardShouldPersistTaps="handled" - /> - - )} - - - {selectedSight ? ( - - ) : curationSightList !== undefined ? ( - <> - setCurationSightList(undefined)} - /> - - ) : ( - - )} - - - {curationSightList && !selectedSight ? ( - -