diff --git a/uniro_frontend/package-lock.json b/uniro_frontend/package-lock.json index 9fb6f47..8e59f44 100644 --- a/uniro_frontend/package-lock.json +++ b/uniro_frontend/package-lock.json @@ -9,10 +9,13 @@ "version": "0.0.0", "dependencies": { "@googlemaps/js-api-loader": "^1.16.8", + "@react-spring/web": "^9.7.5", "@tailwindcss/vite": "^4.0.0", + "framer-motion": "^12.0.6", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7.1.3", + "react-sheet-slide": "^1.5.0", "tailwindcss": "^4.0.0" }, "devDependencies": { @@ -1048,6 +1051,78 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@react-spring/animated": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", + "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/core": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", + "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/rafz": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", + "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", + "license": "MIT" + }, + "node_modules/@react-spring/shared": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", + "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@react-spring/types": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", + "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", + "license": "MIT" + }, + "node_modules/@react-spring/web": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", + "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/core": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@rollup/pluginutils": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", @@ -2098,6 +2173,26 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@use-gesture/core": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", + "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==", + "license": "MIT", + "peer": true + }, + "node_modules/@use-gesture/react": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz", + "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@use-gesture/core": "10.3.1" + }, + "peerDependencies": { + "react": ">= 16.8.0" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", @@ -2552,6 +2647,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", @@ -2579,15 +2683,6 @@ } } }, - "node_modules/cookie": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3462,6 +3557,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "12.0.6", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.0.6.tgz", + "integrity": "sha512-LmrXbXF6Vv5WCNmb+O/zn891VPZrH7XbsZgRLBROw6kFiP+iTK49gxTv2Ur3F0Tbw6+sy9BVtSqnWfMUpH+6nA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.0.0", + "motion-utils": "^12.0.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4675,6 +4797,21 @@ "node": "*" } }, + "node_modules/motion-dom": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.0.0.tgz", + "integrity": "sha512-CvYd15OeIR6kHgMdonCc1ihsaUG4MYh/wrkz8gZ3hBX/uamyZCXN9S9qJoYF03GqfTt7thTV/dxnHYX4+55vDg==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.0.0" + } + }, + "node_modules/motion-utils": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.0.0.tgz", + "integrity": "sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5174,6 +5311,18 @@ } } }, + "node_modules/react-sheet-slide": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/react-sheet-slide/-/react-sheet-slide-1.5.0.tgz", + "integrity": "sha512-mCq+/e0HQJp1QoVfpugWAlSEgJyQ/Vit0hJYNa7zrUSCB2VXmQKIubJEg2CJQtokzi1BwLaMU4tqEJbw4XwJlw==", + "license": "MIT", + "peerDependencies": { + "@react-spring/web": "^9.0.0", + "@use-gesture/react": "^10.0.0", + "react": "^16.8.0 || >=17", + "react-dom": "^16.8.0 || >=17" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5773,7 +5922,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/turbo-stream": { diff --git a/uniro_frontend/package.json b/uniro_frontend/package.json index 6d9da40..d854e29 100644 --- a/uniro_frontend/package.json +++ b/uniro_frontend/package.json @@ -11,10 +11,13 @@ }, "dependencies": { "@googlemaps/js-api-loader": "^1.16.8", + "@react-spring/web": "^9.7.5", "@tailwindcss/vite": "^4.0.0", + "framer-motion": "^12.0.6", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7.1.3", + "react-sheet-slide": "^1.5.0", "tailwindcss": "^4.0.0" }, "devDependencies": { diff --git a/uniro_frontend/src/App.tsx b/uniro_frontend/src/App.tsx index 26e9e4d..41eb866 100644 --- a/uniro_frontend/src/App.tsx +++ b/uniro_frontend/src/App.tsx @@ -3,6 +3,7 @@ import "./App.css"; import Demo from "./pages/demo"; import LandingPage from "./pages/landing"; import UniversitySearchPage from "./pages/search"; +import NavigationResultPage from "./pages/navigationResult"; function App() { return ( @@ -10,6 +11,7 @@ function App() { } /> } /> } /> + } /> ); } diff --git a/uniro_frontend/src/assets/icon/cautionText.svg b/uniro_frontend/src/assets/icon/cautionText.svg new file mode 100644 index 0000000..1862757 --- /dev/null +++ b/uniro_frontend/src/assets/icon/cautionText.svg @@ -0,0 +1,4 @@ + + + + diff --git a/uniro_frontend/src/assets/icon/close.svg b/uniro_frontend/src/assets/icon/close.svg new file mode 100644 index 0000000..2a1889d --- /dev/null +++ b/uniro_frontend/src/assets/icon/close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/uniro_frontend/src/assets/icon/destination.svg b/uniro_frontend/src/assets/icon/destination.svg new file mode 100644 index 0000000..b356c81 --- /dev/null +++ b/uniro_frontend/src/assets/icon/destination.svg @@ -0,0 +1,4 @@ + + + + diff --git a/uniro_frontend/src/assets/icon/goBack.svg b/uniro_frontend/src/assets/icon/goBack.svg new file mode 100644 index 0000000..3a3e03b --- /dev/null +++ b/uniro_frontend/src/assets/icon/goBack.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/uniro_frontend/src/assets/icon/resultDivider.svg b/uniro_frontend/src/assets/icon/resultDivider.svg new file mode 100644 index 0000000..d8576d1 --- /dev/null +++ b/uniro_frontend/src/assets/icon/resultDivider.svg @@ -0,0 +1,3 @@ + + + diff --git a/uniro_frontend/src/assets/icon/safeText.svg b/uniro_frontend/src/assets/icon/safeText.svg new file mode 100644 index 0000000..20e1ea5 --- /dev/null +++ b/uniro_frontend/src/assets/icon/safeText.svg @@ -0,0 +1,4 @@ + + + + diff --git a/uniro_frontend/src/assets/icon/start.svg b/uniro_frontend/src/assets/icon/start.svg new file mode 100644 index 0000000..e1fb5c7 --- /dev/null +++ b/uniro_frontend/src/assets/icon/start.svg @@ -0,0 +1,4 @@ + + + + diff --git a/uniro_frontend/src/assets/markers/building.svg b/uniro_frontend/src/assets/markers/building.svg new file mode 100644 index 0000000..4fc91f3 --- /dev/null +++ b/uniro_frontend/src/assets/markers/building.svg @@ -0,0 +1,4 @@ + + + + diff --git a/uniro_frontend/src/assets/markers/caution.svg b/uniro_frontend/src/assets/markers/caution.svg new file mode 100644 index 0000000..556f369 --- /dev/null +++ b/uniro_frontend/src/assets/markers/caution.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/uniro_frontend/src/assets/markers/danger.svg b/uniro_frontend/src/assets/markers/danger.svg new file mode 100644 index 0000000..5508d10 --- /dev/null +++ b/uniro_frontend/src/assets/markers/danger.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/uniro_frontend/src/assets/markers/destination.svg b/uniro_frontend/src/assets/markers/destination.svg new file mode 100644 index 0000000..80cbe8d --- /dev/null +++ b/uniro_frontend/src/assets/markers/destination.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/uniro_frontend/src/assets/markers/origin.svg b/uniro_frontend/src/assets/markers/origin.svg new file mode 100644 index 0000000..d633fcd --- /dev/null +++ b/uniro_frontend/src/assets/markers/origin.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/uniro_frontend/src/assets/markers/selectedBuilding.svg b/uniro_frontend/src/assets/markers/selectedBuilding.svg new file mode 100644 index 0000000..1187dc7 --- /dev/null +++ b/uniro_frontend/src/assets/markers/selectedBuilding.svg @@ -0,0 +1,4 @@ + + + + diff --git a/uniro_frontend/src/assets/markers/waypoint.svg b/uniro_frontend/src/assets/markers/waypoint.svg new file mode 100644 index 0000000..5107e6a --- /dev/null +++ b/uniro_frontend/src/assets/markers/waypoint.svg @@ -0,0 +1,3 @@ + + + diff --git a/uniro_frontend/src/assets/route/dest.svg b/uniro_frontend/src/assets/route/dest.svg new file mode 100644 index 0000000..90b6d7d --- /dev/null +++ b/uniro_frontend/src/assets/route/dest.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/uniro_frontend/src/assets/route/left.svg b/uniro_frontend/src/assets/route/left.svg new file mode 100644 index 0000000..8ae9bd7 --- /dev/null +++ b/uniro_frontend/src/assets/route/left.svg @@ -0,0 +1,3 @@ + + + diff --git a/uniro_frontend/src/assets/route/right.svg b/uniro_frontend/src/assets/route/right.svg new file mode 100644 index 0000000..427acf4 --- /dev/null +++ b/uniro_frontend/src/assets/route/right.svg @@ -0,0 +1,3 @@ + + + diff --git a/uniro_frontend/src/assets/route/start.svg b/uniro_frontend/src/assets/route/start.svg new file mode 100644 index 0000000..afaa65c --- /dev/null +++ b/uniro_frontend/src/assets/route/start.svg @@ -0,0 +1,3 @@ + + + diff --git a/uniro_frontend/src/assets/route/straight.svg b/uniro_frontend/src/assets/route/straight.svg new file mode 100644 index 0000000..e485217 --- /dev/null +++ b/uniro_frontend/src/assets/route/straight.svg @@ -0,0 +1,3 @@ + + + diff --git "a/uniro_frontend/src/assets/\352\263\240\353\240\244\353\214\200\355\225\231\352\265\220.svg" "b/uniro_frontend/src/assets/university/\352\263\240\353\240\244\353\214\200\355\225\231\352\265\220.svg" similarity index 100% rename from "uniro_frontend/src/assets/\352\263\240\353\240\244\353\214\200\355\225\231\352\265\220.svg" rename to "uniro_frontend/src/assets/university/\352\263\240\353\240\244\353\214\200\355\225\231\352\265\220.svg" diff --git "a/uniro_frontend/src/assets/\354\204\234\354\232\270\354\213\234\353\246\275\353\214\200\355\225\231\352\265\220.svg" "b/uniro_frontend/src/assets/university/\354\204\234\354\232\270\354\213\234\353\246\275\353\214\200\355\225\231\352\265\220.svg" similarity index 100% rename from "uniro_frontend/src/assets/\354\204\234\354\232\270\354\213\234\353\246\275\353\214\200\355\225\231\352\265\220.svg" rename to "uniro_frontend/src/assets/university/\354\204\234\354\232\270\354\213\234\353\246\275\353\214\200\355\225\231\352\265\220.svg" diff --git "a/uniro_frontend/src/assets/\354\235\264\355\231\224\354\227\254\354\236\220\353\214\200\355\225\231\352\265\220.svg" "b/uniro_frontend/src/assets/university/\354\235\264\355\231\224\354\227\254\354\236\220\353\214\200\355\225\231\352\265\220.svg" similarity index 100% rename from "uniro_frontend/src/assets/\354\235\264\355\231\224\354\227\254\354\236\220\353\214\200\355\225\231\352\265\220.svg" rename to "uniro_frontend/src/assets/university/\354\235\264\355\231\224\354\227\254\354\236\220\353\214\200\355\225\231\352\265\220.svg" diff --git "a/uniro_frontend/src/assets/\354\235\270\355\225\230\353\214\200\355\225\231\352\265\220.svg" "b/uniro_frontend/src/assets/university/\354\235\270\355\225\230\353\214\200\355\225\231\352\265\220.svg" similarity index 100% rename from "uniro_frontend/src/assets/\354\235\270\355\225\230\353\214\200\355\225\231\352\265\220.svg" rename to "uniro_frontend/src/assets/university/\354\235\270\355\225\230\353\214\200\355\225\231\352\265\220.svg" diff --git "a/uniro_frontend/src/assets/\355\225\234\354\226\221\353\214\200\355\225\231\352\265\220.svg" "b/uniro_frontend/src/assets/university/\355\225\234\354\226\221\353\214\200\355\225\231\352\265\220.svg" similarity index 100% rename from "uniro_frontend/src/assets/\355\225\234\354\226\221\353\214\200\355\225\231\352\265\220.svg" rename to "uniro_frontend/src/assets/university/\355\225\234\354\226\221\353\214\200\355\225\231\352\265\220.svg" diff --git a/uniro_frontend/src/component/Map.tsx b/uniro_frontend/src/component/Map.tsx index 1b1ad43..14517f5 100644 --- a/uniro_frontend/src/component/Map.tsx +++ b/uniro_frontend/src/component/Map.tsx @@ -1,9 +1,19 @@ +import { useEffect } from "react"; import useMap from "../hooks/useMap"; -const Map = () => { - const { mapRef } = useMap(); +type MapProps = { + style?: React.CSSProperties; +}; +const Map = ({ style }: MapProps) => { + const { mapRef, map, overlay, AdvancedMarker, Polyline, mapLoaded } = useMap(); + + if (!style) { + style = { height: "100%", width: "100%" }; + } + + useEffect(() => {}, []); - return
; + return
; }; export default Map; diff --git a/uniro_frontend/src/component/NavgationMap.tsx b/uniro_frontend/src/component/NavgationMap.tsx new file mode 100644 index 0000000..1756403 --- /dev/null +++ b/uniro_frontend/src/component/NavgationMap.tsx @@ -0,0 +1,104 @@ +import { useEffect, useRef } from "react"; +import useMap from "../hooks/useMap"; +import { NavigationRoute } from "../data/types/route"; +import createAdvancedMarker from "../utils/markers/createAdvanedMarker"; +import createMarkerElement from "../components/map/mapMarkers"; +import { Markers } from "../constant/enums"; + +type MapProps = { + style?: React.CSSProperties; + routes: NavigationRoute; + /** 바텀시트나 상단 UI에 의해 가려지는 영역이 있을 경우, 지도 fitBounds에 추가할 패딩값 */ + topPadding?: number; + bottomPadding?: number; +}; + +const NavigationMap = ({ style, routes, topPadding = 0, bottomPadding = 0 }: MapProps) => { + const { mapRef, map, AdvancedMarker, Polyline } = useMap(); + + const boundsRef = useRef(null); + + if (!style) { + style = { height: "100%", width: "100%" }; + } + + useEffect(() => { + if (!map || !AdvancedMarker || !routes || !Polyline) return; + + const { route: routeList } = routes; + if (!routeList || routeList.length === 0) return; + const bounds = new google.maps.LatLngBounds(); + + new Polyline({ + path: [...routeList.map((edge) => edge.startNode), routeList[routeList.length - 1].endNode], + map, + strokeColor: "#000000", + strokeWeight: 2.0, + }); + + routeList.forEach((edge, index) => { + const { startNode, endNode } = edge; + const startCoordinate = new google.maps.LatLng(startNode.lat, startNode.lng); + const endCoordinate = new google.maps.LatLng(endNode.lat, endNode.lng); + bounds.extend(startCoordinate); + bounds.extend(endCoordinate); + if (index !== 0 && index !== routeList.length - 1) { + const markerElement = createMarkerElement({ + type: Markers.WAYPOINT, + className: "translate-waypoint", + }); + if (index === 1) { + createAdvancedMarker(AdvancedMarker, map, startCoordinate, markerElement); + } + createAdvancedMarker(AdvancedMarker, map, endCoordinate, markerElement); + } + }); + + const edgeRoutes = [routeList[0], routeList[routeList.length - 1]]; + + edgeRoutes.forEach((edge, index) => { + const { startNode, endNode } = edge; + if (index === 0) { + const startCoordinate = new google.maps.LatLng(startNode.lat, startNode.lng); + const markerElement = createMarkerElement({ + type: Markers.ORIGIN, + title: routes.originBuilding.buildingName, + className: "translate-routemarker", + }); + createAdvancedMarker(AdvancedMarker, map, startCoordinate, markerElement); + bounds.extend(startCoordinate); + } else { + const endCoordinate = new google.maps.LatLng(endNode.lat, endNode.lng); + const markerElement = createMarkerElement({ + type: Markers.DESTINATION, + title: routes.destinationBuilding.buildingName, + className: "translate-routemarker", + }); + createAdvancedMarker(AdvancedMarker, map, endCoordinate, markerElement); + bounds.extend(endCoordinate); + } + }); + + boundsRef.current = bounds; + map.fitBounds(bounds, { + top: topPadding, + right: 50, + bottom: bottomPadding, + left: 50, + }); + }, [map, AdvancedMarker, Polyline, routes]); + + useEffect(() => { + if (!map || !boundsRef.current) return; + map.fitBounds(boundsRef.current, { + top: topPadding, + right: 50, + bottom: bottomPadding, + left: 50, + }); + }, [map, bottomPadding, topPadding]); + + return
; +}; + +export default NavigationMap; diff --git a/uniro_frontend/src/components/map/mapMarkers.tsx b/uniro_frontend/src/components/map/mapMarkers.tsx new file mode 100644 index 0000000..9e4eaf9 --- /dev/null +++ b/uniro_frontend/src/components/map/mapMarkers.tsx @@ -0,0 +1,83 @@ +import { MarkerTypes } from "../../data/types/marker"; +import { Markers } from "../../constant/enums"; + +function createTextElement(type: MarkerTypes, title: string): HTMLElement { + const markerTitle = document.createElement("p"); + markerTitle.innerText = title; + + switch (type) { + case Markers.CAUTION: + markerTitle.className = + "h-[38px] py-2 px-4 mb-2 text-kor-body3 font-semibold text-gray-100 bg-system-orange text-center rounded-200"; + return markerTitle; + case Markers.DANGER: + markerTitle.className = + "h-[38px] py-2 px-4 mb-2 text-kor-body3 font-semibold text-gray-100 bg-system-red text-center rounded-200"; + return markerTitle; + case Markers.BUILDING: + markerTitle.className = + "py-1 px-3 text-kor-caption font-medium text-gray-100 bg-gray-900 text-center rounded-200"; + return markerTitle; + case Markers.SELECTED_BUILDING: + markerTitle.className = + "py-1 px-3 text-kor-caption font-medium text-gray-100 bg-primary-500 text-center rounded-200"; + return markerTitle; + case Markers.ORIGIN: + markerTitle.className = + "py-1 px-3 text-kor-caption font-medium text-gray-100 bg-primary-500 text-center rounded-200"; + return markerTitle; + case Markers.DESTINATION: + markerTitle.className = + "py-1 px-3 text-kor-caption font-medium text-gray-100 bg-primary-500 text-center rounded-200"; + return markerTitle; + default: + return markerTitle; + } +} + +function createImageElement(type: MarkerTypes): HTMLElement { + const markerImage = document.createElement("img"); + + markerImage.src = `/src/assets/markers/${type}.svg`; + return markerImage; +} + +function createContainerElement(className?: string) { + const container = document.createElement("div"); + console.log(className); + container.className = `flex flex-col items-center space-y-[7px] ${className}`; + + return container; +} + +export default function createMarkerElement({ + type, + title, + className, + hasTopContent = false, +}: { + type: MarkerTypes; + className?: string; + title?: string; + hasTopContent?: boolean; +}): HTMLElement { + const container = createContainerElement(className); + + const markerImage = createImageElement(type); + + if (title) { + const markerTitle = createTextElement(type, title); + if (hasTopContent) { + container.appendChild(markerTitle); + container.appendChild(markerImage); + return container; + } + + container.appendChild(markerImage); + container.appendChild(markerTitle); + return container; + } + + container.appendChild(markerImage); + return container; +} diff --git a/uniro_frontend/src/components/navigation/bottomSheet/bottomSheetHandle.tsx b/uniro_frontend/src/components/navigation/bottomSheet/bottomSheetHandle.tsx new file mode 100644 index 0000000..9fdb66e --- /dev/null +++ b/uniro_frontend/src/components/navigation/bottomSheet/bottomSheetHandle.tsx @@ -0,0 +1,20 @@ +import { DragControls } from "framer-motion"; +import React from "react"; + +type HandleProps = { + dragControls: DragControls; +}; + +const BottomSheetHandle = ({ dragControls }: HandleProps) => { + return ( +
dragControls.start(e)} + > +
+
+ ); +}; + +export default BottomSheetHandle; diff --git a/uniro_frontend/src/components/navigation/navigationDescription.tsx b/uniro_frontend/src/components/navigation/navigationDescription.tsx new file mode 100644 index 0000000..e3ab1e0 --- /dev/null +++ b/uniro_frontend/src/components/navigation/navigationDescription.tsx @@ -0,0 +1,60 @@ +import React, { useState } from "react"; +import Cancel from "../../assets/icon/close.svg?react"; +import CautionIcon from "../../assets/icon/cautionText.svg?react"; +import SafeIcon from "../../assets/icon/safeText.svg?react"; +import DestinationIcon from "../../assets/icon/destination.svg?react"; +import OriginIcon from "../../assets/icon/start.svg?react"; +import ResultDivider from "../../assets/icon/resultDivider.svg?react"; +import { NavigationRoute } from "../../data/types/route"; +import { mockNavigationRoute } from "../../data/mock/hanyangRoute"; +const TITLE = "전동휠체어 예상소요시간"; + +type TopBarProps = { + isDetailView: boolean; +}; + +const NavigationDescription = ({ isDetailView }: TopBarProps) => { + const [route, _] = useState(mockNavigationRoute); + + return ( +
+
+ {TITLE} + {!isDetailView && } +
+
+
+
+
+ {route.totalCost} +
+
+
+
+
+ {`${route.totalDistance}m`} +
+
+
+ {route.hasCaution ? : } + + 가는 길에 주의 요소가 {route.hasCaution ? "있어요" : "없어요"} + +
+
+
+
+ + {route.originBuilding.buildingName} +
+ +
+ + {route.destinationBuilding.buildingName} +
+
+
+ ); +}; + +export default NavigationDescription; diff --git a/uniro_frontend/src/components/navigation/route/routeCard.tsx b/uniro_frontend/src/components/navigation/route/routeCard.tsx new file mode 100644 index 0000000..d00f175 --- /dev/null +++ b/uniro_frontend/src/components/navigation/route/routeCard.tsx @@ -0,0 +1,128 @@ +import OriginIcon from "../../../assets/route/start.svg?react"; +import DestinationIcon from "../../../assets/route/dest.svg?react"; +import StraightIcon from "../../../assets/route/straight.svg?react"; +import RightIcon from "../../../assets/route/right.svg?react"; +import LeftIcon from "../../../assets/route/left.svg?react"; +import CautionText from "../../../assets/icon/cautionText.svg?react"; +import { RouteEdge } from "../../../data/types/edge"; +import { Building } from "../../../data/types/node"; + +const NumberIcon = ({ index }: { index: number }) => { + return ( +
+
{index}
+
+ ); +}; + +export const RouteCard = ({ + index, + route, + originBuilding, + destinationBuilding, +}: { + index: number; + route: RouteEdge; + originBuilding: Building; + destinationBuilding: Building; +}) => { + switch (route.direction) { + case "straight": + return ( +
+
+ +
{route.distance}m
+
+
+ +
직진
+
+
+ ); + case "right": + return ( +
+
+ +
{route.distance}m
+
+
+ +
우회전
+
+
+ ); + case "left": + return ( +
+
+ +
{route.distance}m
+
+
+ +
좌회전
+
+
+ ); + case "uturn": + return ( +
+
+ +
{route.distance}m
+
+
+ +
U턴
+
+
+ ); + case "origin": + return ( +
+
+ +
출발
+
+
+
{originBuilding.buildingName}
+
{originBuilding.address}
+
+
+ ); + case "destination": + return ( +
+
+ +
도착
+
+
+
{destinationBuilding.buildingName}
+
{destinationBuilding.address}
+
+
+ ); + case "caution": + return ( +
+
+ +
{route.distance}m
+
+
+ +
턱이 있어요
+
+
+ ); + default: + return ( +
+ 알 수 없음 +
+ ); + } +}; diff --git a/uniro_frontend/src/components/navigation/route/routeList.tsx b/uniro_frontend/src/components/navigation/route/routeList.tsx new file mode 100644 index 0000000..6c27124 --- /dev/null +++ b/uniro_frontend/src/components/navigation/route/routeList.tsx @@ -0,0 +1,34 @@ +import { Fragment } from "react"; +import { RouteEdge } from "../../../data/types/edge"; +import { Building } from "../../../data/types/node"; +import { RouteCard } from "./routeCard"; + +type RouteListProps = { + routes: RouteEdge[]; + originBuilding: Building; + destinationBuilding: Building; +}; + +const Divider = () =>
; + +const RouteList = ({ routes, originBuilding, destinationBuilding }: RouteListProps) => { + return ( +
+ {routes.map((route, index) => ( + + +
+ +
+
+ ))} +
+ ); +}; + +export default RouteList; diff --git a/uniro_frontend/src/components/universityButton.tsx b/uniro_frontend/src/components/universityButton.tsx index dbc0dca..2dd99bf 100644 --- a/uniro_frontend/src/components/universityButton.tsx +++ b/uniro_frontend/src/components/universityButton.tsx @@ -7,19 +7,23 @@ interface UniversityButtonProps { onClick: () => void; } +const svgModules = import.meta.glob("/src/assets/university/*.svg", { eager: true }); + export default function UniversityButton({ name, img, selected, onClick }: UniversityButtonProps) { const handleClick = (e: MouseEvent) => { e.stopPropagation(); onClick(); }; + const svgPath = (svgModules[`/src/assets/university/${img}`] as { default: string })?.default; + return (
  • diff --git a/uniro_frontend/src/constant/enums.ts b/uniro_frontend/src/constant/enums.ts new file mode 100644 index 0000000..e816ddb --- /dev/null +++ b/uniro_frontend/src/constant/enums.ts @@ -0,0 +1,15 @@ +export const enum Markers { + CAUTION = "caution", + DANGER = "danger", + BUILDING = "building", + ORIGIN = "origin", + DESTINATION = "destination", + SELECTED_BUILDING = "selectedBuilding", + WAYPOINT = "waypoint", + NUMBERED_WAYPOINT = "numberedWayPoint", +} + +export const enum RoutePoint { + ORIGIN = "origin", + DESTINATION = "destination", +} diff --git a/uniro_frontend/src/container/animatedContainer.tsx b/uniro_frontend/src/container/animatedContainer.tsx new file mode 100644 index 0000000..657afde --- /dev/null +++ b/uniro_frontend/src/container/animatedContainer.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { AnimatePresence, motion, MotionProps } from "framer-motion"; + +type Props = { + isVisible: boolean; + children: React.ReactNode; + positionDelta: number; + className: string; + isTop?: boolean; + transition?: MotionProps["transition"]; + motionProps?: MotionProps; +}; + +// default는 바텀에서 시작 +const AnimatedContainer = ({ + isVisible, + children, + positionDelta, + className, + isTop = false, + transition = { duration: 0.3, type: "tween" }, + motionProps = {}, +}: Props) => { + return ( + + {isVisible && ( + + {children} + + )} + + ); +}; + +export default AnimatedContainer; diff --git a/uniro_frontend/src/data/factory/edgeFactory.ts b/uniro_frontend/src/data/factory/edgeFactory.ts index 445d21f..77ca341 100644 --- a/uniro_frontend/src/data/factory/edgeFactory.ts +++ b/uniro_frontend/src/data/factory/edgeFactory.ts @@ -1,4 +1,4 @@ -import { HazardEdge } from "../types/edge"; +import { Direction, HazardEdge, RouteEdge } from "../types/edge"; import { CautionFactor, DangerFactor } from "../types/factor"; import { CustomNode } from "../types/node"; @@ -15,3 +15,20 @@ export const createHazardEdge = ( cautionFactors, dangerFactors, }); + +export const createRouteEdges = (edges: HazardEdge[]): RouteEdge[] => { + const routeEdges: RouteEdge[] = []; + edges.forEach((edge, index) => { + const routeEdge: RouteEdge = { + ...edge, + distance: 100, + direction: ["right", "straight", "left"][index % 3] as Direction, + }; + routeEdges.push(routeEdge); + }); + + routeEdges[0].direction = "origin"; + routeEdges[routeEdges.length - 1].direction = "destination"; + + return routeEdges; +}; diff --git a/uniro_frontend/src/data/factory/navigationFactory.ts b/uniro_frontend/src/data/factory/navigationFactory.ts index 900ae92..d62f342 100644 --- a/uniro_frontend/src/data/factory/navigationFactory.ts +++ b/uniro_frontend/src/data/factory/navigationFactory.ts @@ -1,11 +1,15 @@ -import { HazardEdge } from "../types/edge"; +import { hanyangBuildings } from "../mock/hanyangBuildings"; +import { RouteEdge } from "../types/edge"; import { NavigationRoute } from "../types/route"; -export const createNavigationRoute = (edges: HazardEdge[]): NavigationRoute => { +// TODO: Distance를 m-> km로 자동 변환해주는 util +export const createNavigationRoute = (edges: RouteEdge[]): NavigationRoute => { return { route: edges, hasCaution: edges.some((edge) => edge.cautionFactors !== undefined), - totalDistance: 1.5, + totalDistance: 635, totalCost: 10, + originBuilding: hanyangBuildings[0], + destinationBuilding: hanyangBuildings[1], }; }; diff --git a/uniro_frontend/src/data/mock/hanyangBuildings.ts b/uniro_frontend/src/data/mock/hanyangBuildings.ts index a75dcd5..38f6d64 100644 --- a/uniro_frontend/src/data/mock/hanyangBuildings.ts +++ b/uniro_frontend/src/data/mock/hanyangBuildings.ts @@ -1,12 +1,12 @@ import { Building } from "../types/node"; -export const buildings: Building[] = [ +export const hanyangBuildings: Building[] = [ { id: "101", lng: 127.044755, lat: 37.555994, isCore: true, - buildingName: "역사관", + buildingName: "한양대학교 법학관", buildingImageUrl: "https://upload.wikimedia.org/wikipedia/commons/6/69/Hanyang_University_008.JPG", phoneNumber: "02-2220-0114", address: "서울특별시 성동구 왕십리로 222", @@ -16,7 +16,7 @@ export const buildings: Building[] = [ lng: 127.0455, lat: 37.5565, isCore: true, - buildingName: "본관", + buildingName: "한양대학교 제2공학관", buildingImageUrl: "https://upload.wikimedia.org/wikipedia/commons/6/69/Hanyang_University_008.JPG", phoneNumber: "02-2220-0114", address: "서울특별시 성동구 왕십리로 222", diff --git a/uniro_frontend/src/data/mock/hanyangRoute.ts b/uniro_frontend/src/data/mock/hanyangRoute.ts index 544b166..40c9594 100644 --- a/uniro_frontend/src/data/mock/hanyangRoute.ts +++ b/uniro_frontend/src/data/mock/hanyangRoute.ts @@ -1,4 +1,4 @@ -import { createHazardEdge } from "../factory/edgeFactory"; +import { createHazardEdge, createRouteEdges } from "../factory/edgeFactory"; import { createNavigationRoute } from "../factory/navigationFactory"; import { createNode } from "../factory/nodeFactory"; import { HazardEdge } from "../types/edge"; @@ -17,6 +17,8 @@ const edges: HazardEdge[] = [ createHazardEdge("route2", nodes[1], nodes[2], ["도로에 균열이 있어요"]), createHazardEdge("route3", nodes[2], nodes[3]), createHazardEdge("route4", nodes[3], nodes[4]), + createHazardEdge("route5", nodes[3], nodes[4]), + createHazardEdge("route6", nodes[3], nodes[4]), ]; -export const mockNavigationRoute = createNavigationRoute(edges); +export const mockNavigationRoute = createNavigationRoute(createRouteEdges(edges)); diff --git a/uniro_frontend/src/data/types/edge.d.ts b/uniro_frontend/src/data/types/edge.d.ts index 585186e..bf2271f 100644 --- a/uniro_frontend/src/data/types/edge.d.ts +++ b/uniro_frontend/src/data/types/edge.d.ts @@ -7,9 +7,16 @@ export interface Edge { endNode: CustomNode; } +export type Direction = "origin" | "right" | "straight" | "left" | "uturn" | "destination" | "caution"; + // 위험 요소 & 주의 요소 // 마커를 표시하거나, 길 찾기 결과의 경로를 그릴 때 사용 export interface HazardEdge extends Edge { dangerFactors?: DangerFactor[]; cautionFactors?: CautionFactor[]; } + +export interface RouteEdge extends HazardEdge { + distance: number; + direction: Direction; +} diff --git a/uniro_frontend/src/data/types/marker.d.ts b/uniro_frontend/src/data/types/marker.d.ts new file mode 100644 index 0000000..866d06c --- /dev/null +++ b/uniro_frontend/src/data/types/marker.d.ts @@ -0,0 +1,13 @@ +import { Markers } from "../../constant/enums"; + +export type AdvancedMarker = google.maps.marker.AdvancedMarkerElement; + +export type MarkerTypes = + | Markers.BUILDING + | Markers.CAUTION + | Markers.DANGER + | Markers.DESTINATION + | Markers.ORIGIN + | Markers.NUMBERED_WAYPOINT + | Markers.WAYPOINT + | Markers.SELECTED_BUILDING; diff --git a/uniro_frontend/src/data/types/route.d.ts b/uniro_frontend/src/data/types/route.d.ts index a335dc5..e8f0532 100644 --- a/uniro_frontend/src/data/types/route.d.ts +++ b/uniro_frontend/src/data/types/route.d.ts @@ -1,11 +1,14 @@ -import { HazardEdge } from "./edge"; +import { RouteEdge } from "./edge"; +import { Building } from "./node"; export interface Route { - route: HazardEdge[]; + route: RouteEdge[]; } export interface NavigationRoute extends Route { hasCaution: boolean; totalDistance: number; totalCost: number; + originBuilding: Building; + destinationBuilding: Building; } diff --git a/uniro_frontend/src/hooks/useScrollControl.tsx b/uniro_frontend/src/hooks/useScrollControl.tsx new file mode 100644 index 0000000..271854a --- /dev/null +++ b/uniro_frontend/src/hooks/useScrollControl.tsx @@ -0,0 +1,31 @@ +import React, { useEffect, useState } from "react"; + +// 스크롤 비활성화, 맵을 움직일 때, 다른 부분들도 같이 움직이는 것들을 제어함. +// 전역 적용이 필요하다면 다른 방식을 사용할 예정. +const useScrollControl = () => { + const [scrollState, setScrollState] = useState(false); + + const enableScroll = () => { + setScrollState(true); + }; + + const disableScroll = () => { + setScrollState(false); + }; + + useEffect(() => { + if (scrollState) { + document.body.style.overflow = "auto"; + } else { + document.body.style.overflow = "hidden"; + } + + return () => { + document.body.style.overflow = "hidden"; + }; + }, [scrollState]); + + return { enableScroll, disableScroll }; +}; + +export default useScrollControl; diff --git a/uniro_frontend/src/index.css b/uniro_frontend/src/index.css index 0dadffa..914f9ac 100644 --- a/uniro_frontend/src/index.css +++ b/uniro_frontend/src/index.css @@ -40,7 +40,7 @@ } * { - font-family: "SF Pro Display", "AppleSDGothicNeo"; + font-family: "SF Pro Display", "AppleSDGothicNeo" !important; outline: none; --webkit-scrollbar-width: none; } @@ -123,3 +123,13 @@ --text-eng-caption: 12px; --text-eng-caption--line-height: 160%; } + +@utility translate-marker { + transform: translateY(calc(100% - 10px)); +} +@utility translate-routemarker { + transform: translateY(calc(100% - 40px)); +} +@utility translate-waypoint { + transform: translateY(+4px); +} diff --git a/uniro_frontend/src/map/initializer/googleMapInitializer.ts b/uniro_frontend/src/map/initializer/googleMapInitializer.ts index edfb317..2bcdc23 100644 --- a/uniro_frontend/src/map/initializer/googleMapInitializer.ts +++ b/uniro_frontend/src/map/initializer/googleMapInitializer.ts @@ -1,5 +1,4 @@ import { HanyangUniversity } from "../../constant/university"; -import { HanyangUniversityBounds } from "../../constant/bounds"; import loadGoogleMapsLibraries from "../loader/googleMapLoader"; @@ -25,10 +24,12 @@ export const initializeMap = async (mapElement: HTMLElement | null): Promise { + const [isDetailView, setIsDetailView] = useState(false); + + const [sheetHeight, setSheetHeight] = useState(CLOSED_SHEET_HEIGHT); + const [topBarHeight, setTopBarHeight] = useState(INITIAL_TOP_BAR_HEIGHT); + + const [route, setRoute] = useState(mockNavigationRoute); + + useScrollControl(); + + const dragControls = useDragControls(); + + const showDetailView = () => { + setSheetHeight(MAX_SHEET_HEIGHT); + setTopBarHeight(PADDING_FOR_MAP_BOUNDARY); + setIsDetailView(true); + }; + const hideDetailView = () => { + setSheetHeight(CLOSED_SHEET_HEIGHT); + setTopBarHeight(INITIAL_TOP_BAR_HEIGHT); + setIsDetailView(false); + }; + + const handleDrag = useCallback( + (event: Event, info: PanInfo) => { + setSheetHeight((prev) => { + const newHeight = prev - info.delta.y; + return Math.min(Math.max(newHeight, MIN_SHEET_HEIGHT), MAX_SHEET_HEIGHT); + }); + }, + [setSheetHeight, MAX_SHEET_HEIGHT, MIN_SHEET_HEIGHT], + ); + + return ( +
    + + + + + + + + + + + + + + +
    + + +
    +
    +
    + ); +}; + +export default NavigationResultPage; diff --git a/uniro_frontend/src/utils/markers/createAdvanedMarker.ts b/uniro_frontend/src/utils/markers/createAdvanedMarker.ts new file mode 100644 index 0000000..bdbcbd7 --- /dev/null +++ b/uniro_frontend/src/utils/markers/createAdvanedMarker.ts @@ -0,0 +1,17 @@ +export default function createAdvancedMarker( + AdvancedMarker: typeof google.maps.marker.AdvancedMarkerElement, + map: google.maps.Map, + position: google.maps.LatLng, + content: HTMLElement, + onClick?: () => void, +) { + const newMarker = new AdvancedMarker({ + map: map, + position: position, + content: content, + }); + + if (onClick) newMarker.addListener("click", onClick); + + return newMarker; +}