diff --git a/.github/workflows/be-ci.yml b/.github/workflows/be-ci.yml index c724bfc..b5b8eb0 100644 --- a/.github/workflows/be-ci.yml +++ b/.github/workflows/be-ci.yml @@ -39,4 +39,4 @@ jobs: - name: Build and analyze (SpringBoot) run: | cd uniro_backend - ./gradlew clean build -x test \ No newline at end of file + ./gradlew clean build \ No newline at end of file diff --git a/uniro_admin_frontend/package-lock.json b/uniro_admin_frontend/package-lock.json index ceaafc1..0a997a1 100644 --- a/uniro_admin_frontend/package-lock.json +++ b/uniro_admin_frontend/package-lock.json @@ -11,6 +11,7 @@ "@googlemaps/js-api-loader": "^1.16.8", "@tailwindcss/vite": "^4.0.0", "@tanstack/react-query": "^5.66.0", + "@tanstack/react-query-devtools": "^5.66.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7.1.3", @@ -1812,6 +1813,16 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/query-devtools": { + "version": "5.65.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.65.0.tgz", + "integrity": "sha512-g5y7zc07U9D3esMdqUfTEVu9kMHoIaVBsD0+M3LPdAdD710RpTcLiNvJY1JkYXqkq9+NV+CQoemVNpQPBXVsJg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/react-query": { "version": "5.66.0", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.66.0.tgz", @@ -1828,6 +1839,23 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.66.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.66.0.tgz", + "integrity": "sha512-uB57wA2YZaQ2fPcFW0E9O1zAGDGSbRKRx84uMk/86VyU9jWVxvJ3Uzp+zNm+nZJYsuekCIo2opTdgNuvM3cKgA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.65.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.66.0", + "react": "^18 || ^19" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", diff --git a/uniro_admin_frontend/package.json b/uniro_admin_frontend/package.json index 70f430a..b40818c 100644 --- a/uniro_admin_frontend/package.json +++ b/uniro_admin_frontend/package.json @@ -13,6 +13,7 @@ "@googlemaps/js-api-loader": "^1.16.8", "@tailwindcss/vite": "^4.0.0", "@tanstack/react-query": "^5.66.0", + "@tanstack/react-query-devtools": "^5.66.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7.1.3", diff --git a/uniro_admin_frontend/src/App.tsx b/uniro_admin_frontend/src/App.tsx index 416b32f..cd4ba76 100644 --- a/uniro_admin_frontend/src/App.tsx +++ b/uniro_admin_frontend/src/App.tsx @@ -1,17 +1,14 @@ +import { Outlet } from "react-router"; import "./App.css"; import NavBar from "./components/navBar"; -import LogListContainer from "./container/logListContainer"; -import MainContainer from "./container/mainContainer"; -import MapContainer from "./container/mapContainer"; +import SubNavBar from "./components/subNavBar"; function App() { return (
- - - - + +
); } diff --git a/uniro_admin_frontend/src/AppRouter.tsx b/uniro_admin_frontend/src/AppRouter.tsx new file mode 100644 index 0000000..61ef199 --- /dev/null +++ b/uniro_admin_frontend/src/AppRouter.tsx @@ -0,0 +1,29 @@ +import { BrowserRouter, Route, Routes } from "react-router"; +import App from "./App"; +import LogPage from "./page/logPage"; +import BuildingPage from "./page/buildingPage"; +import SimulationPage from "./page/simulationPage"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; + +const queryClient = new QueryClient(); + +function AppRouter() { + return ( + + + + }> + } /> + } /> + } /> + } /> + + + + + + ); +} + +export default AppRouter; diff --git a/uniro_admin_frontend/src/api/nodes.ts b/uniro_admin_frontend/src/api/nodes.ts new file mode 100644 index 0000000..7340192 --- /dev/null +++ b/uniro_admin_frontend/src/api/nodes.ts @@ -0,0 +1,34 @@ +import { Building } from "../data/types/node"; +import { getFetch, postFetch } from "../utils/fetch/fetch"; + +export const getAllBuildings = ( + univId: number, + params: { + leftUpLng: number; + leftUpLat: number; + rightDownLng: number; + rightDownLat: number; + } +): Promise => { + return getFetch(`/${univId}/nodes/buildings`, { + "left-up-lng": params.leftUpLng, + "left-up-lat": params.leftUpLat, + "right-down-lng": params.rightDownLng, + "right-down-lat": params.rightDownLat, + }); +}; + +export const postBuilding = ( + univId: number, + body: { + buildingName: string; + buildingImageUrl: string; + phoneNumber: string; + address: string; + lat: number; + lng: number; + level: number; + } +): Promise => { + return postFetch(`/${univId}/nodes/building`, body); +}; diff --git a/uniro_admin_frontend/src/api/route.ts b/uniro_admin_frontend/src/api/route.ts new file mode 100644 index 0000000..12cb4a5 --- /dev/null +++ b/uniro_admin_frontend/src/api/route.ts @@ -0,0 +1,74 @@ +import { IssueTypeKey } from "../constant/enum/reportEnum"; +import { Coord } from "../data/types/coord"; +import { CautionIssueType, DangerIssueType } from "../data/types/enum"; +import { NodeId } from "../data/types/node"; + +import { + CoreRoutesList, + NavigationRouteList, + RouteId, +} from "../data/types/route"; +import { getFetch, postFetch } from "../utils/fetch/fetch"; +import { transformAllRoutes } from "./transformer/route"; +import { GetAllRouteRepsonse } from "./type/response/route"; + +export const getNavigationResult = ( + univId: number, + startNodeId: NodeId, + endNodeId: NodeId +): Promise => { + return getFetch(`/${univId}/routes/fastest`, { + "start-node-id": startNodeId, + "end-node-id": endNodeId, + }); +}; + +export const getAllRoutes = (univId: number): Promise => { + return getFetch(`/${univId}/routes`).then((data) => + transformAllRoutes(data) + ); +}; + +export const getSingleRouteRisk = ( + univId: number, + routeId: RouteId +): Promise<{ + routeId: NodeId; + dangerTypes: IssueTypeKey[]; + cautionTypes: IssueTypeKey[]; +}> => { + return getFetch<{ + routeId: NodeId; + dangerTypes: IssueTypeKey[]; + cautionTypes: IssueTypeKey[]; + }>(`/${univId}/routes/${routeId}/risk`); +}; + +export const postReport = ( + univId: number, + routeId: RouteId, + body: { dangerTypes: DangerIssueType[]; cautionTypes: CautionIssueType[] } +): Promise => { + return postFetch(`/${univId}/route/risk/${routeId}`, body); +}; + +export const postReportRoute = ( + univId: number, + body: { + startNodeId: NodeId; + endNodeId: NodeId | null; + coordinates: Coord[]; + } +): Promise => { + return postFetch(`/${univId}/route`, body); +}; + +export const postBuildingRoute = ( + univId: number, + body: { + buildingNodeId: NodeId; + nodeId: NodeId; + } +): Promise => { + return postFetch(`/${univId}/routes/building`, body); +}; diff --git a/uniro_admin_frontend/src/api/routes.ts b/uniro_admin_frontend/src/api/routes.ts new file mode 100644 index 0000000..6ac4143 --- /dev/null +++ b/uniro_admin_frontend/src/api/routes.ts @@ -0,0 +1,8 @@ +import { CautionRoute, DangerRoute } from "../data/types/route"; +import { getFetch } from "../utils/fetch/fetch"; + +export const getAllRisks = ( + univId: number, +): Promise<{ dangerRoutes: DangerRoute[]; cautionRoutes: CautionRoute[] }> => { + return getFetch<{ dangerRoutes: DangerRoute[]; cautionRoutes: CautionRoute[] }>(`/${univId}/routes/risks`); +}; diff --git a/uniro_admin_frontend/src/api/search.ts b/uniro_admin_frontend/src/api/search.ts new file mode 100644 index 0000000..d7c47e9 --- /dev/null +++ b/uniro_admin_frontend/src/api/search.ts @@ -0,0 +1,8 @@ +import { University } from "../data/types/university"; +import { getFetch } from "../utils/fetch/fetch"; +import { transformGetUniversityList } from "./transformer/search"; +import { GetUniversityListResponse } from "./type/response/search"; + +export const getUniversityList = (): Promise => { + return getFetch("/univ/search").then(transformGetUniversityList); +}; diff --git a/uniro_admin_frontend/src/api/transformer/route.ts b/uniro_admin_frontend/src/api/transformer/route.ts new file mode 100644 index 0000000..fbf6508 --- /dev/null +++ b/uniro_admin_frontend/src/api/transformer/route.ts @@ -0,0 +1,31 @@ +import { DangerIssueType } from "../../constant/enum/reportEnum"; +import { CoreRoutesList } from "../../data/types/route"; +import { GetAllRouteRepsonse, GetSingleRouteRiskResponse } from "../type/response/route"; + +export const transformAllRoutes = (data: GetAllRouteRepsonse): CoreRoutesList => { + const { nodeInfos, coreRoutes } = data; + const nodeInfoMap = new Map(nodeInfos.map((node) => [node.nodeId, node])); + + return coreRoutes.map((coreRoute) => { + return { + ...coreRoute, + routes: coreRoute.routes.map((route) => { + const node1 = nodeInfoMap.get(route.startNodeId); + const node2 = nodeInfoMap.get(route.endNodeId); + + if (!node1) { + throw new Error(`Node not found: ${route.startNodeId}`); + } + if (!node2) { + throw new Error(`Node not found: ${route.endNodeId}`); + } + + return { + routeId: route.routeId, + node1, + node2, + }; + }), + }; + }); +}; diff --git a/uniro_admin_frontend/src/api/transformer/search.ts b/uniro_admin_frontend/src/api/transformer/search.ts new file mode 100644 index 0000000..3d5fc76 --- /dev/null +++ b/uniro_admin_frontend/src/api/transformer/search.ts @@ -0,0 +1,6 @@ +import { University } from "../../data/types/university"; +import { GetUniversityListResponse } from "../type/response/search"; + +export const transformGetUniversityList = (res: GetUniversityListResponse): University[] => { + return res.data; +}; diff --git a/uniro_admin_frontend/src/api/type/request/route.d.ts b/uniro_admin_frontend/src/api/type/request/route.d.ts new file mode 100644 index 0000000..d42feee --- /dev/null +++ b/uniro_admin_frontend/src/api/type/request/route.d.ts @@ -0,0 +1,3 @@ +export type getAllRouteRequest = { + univId: number; +}; diff --git a/uniro_admin_frontend/src/api/type/response/route.d.ts b/uniro_admin_frontend/src/api/type/response/route.d.ts new file mode 100644 index 0000000..5120f82 --- /dev/null +++ b/uniro_admin_frontend/src/api/type/response/route.d.ts @@ -0,0 +1,23 @@ +import { IssueTypeKey } from "../../../constant/enum/reportEnum"; +import { Node, NodeId } from "../../../data/types/node"; + +type CoreRoutesResponse = { + coreNode1Id: NodeId; + coreNode2Id: NodeId; + routes: { + routeId: NodeId; + startNodeId: NodeId; + endNodeId: NodeId; + }[]; +}; + +export type GetAllRouteRepsonse = { + nodeInfos: Node[]; + coreRoutes: CoreRoutesResponse[]; +}; + +export type GetSingleRouteRiskResponse = { + routeId: NodeId; + dangerTypes?: IssueTypeKey[]; + cautionTypes?: IssueTypeKey[]; +}; diff --git a/uniro_admin_frontend/src/api/type/response/search.d.ts b/uniro_admin_frontend/src/api/type/response/search.d.ts new file mode 100644 index 0000000..0b7acde --- /dev/null +++ b/uniro_admin_frontend/src/api/type/response/search.d.ts @@ -0,0 +1,5 @@ +export type GetUniversityListResponse = { + data: University[]; + nextCursor: number | null; + hasNext: boolean; +}; diff --git a/uniro_admin_frontend/src/assets/markers/building.svg b/uniro_admin_frontend/src/assets/markers/building.svg new file mode 100644 index 0000000..4fc91f3 --- /dev/null +++ b/uniro_admin_frontend/src/assets/markers/building.svg @@ -0,0 +1,4 @@ + + + + diff --git a/uniro_admin_frontend/src/assets/markers/caution.svg b/uniro_admin_frontend/src/assets/markers/caution.svg new file mode 100644 index 0000000..556f369 --- /dev/null +++ b/uniro_admin_frontend/src/assets/markers/caution.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/uniro_admin_frontend/src/assets/markers/danger.svg b/uniro_admin_frontend/src/assets/markers/danger.svg new file mode 100644 index 0000000..5508d10 --- /dev/null +++ b/uniro_admin_frontend/src/assets/markers/danger.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/uniro_admin_frontend/src/assets/markers/destination.svg b/uniro_admin_frontend/src/assets/markers/destination.svg new file mode 100644 index 0000000..80cbe8d --- /dev/null +++ b/uniro_admin_frontend/src/assets/markers/destination.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/uniro_admin_frontend/src/assets/markers/origin.svg b/uniro_admin_frontend/src/assets/markers/origin.svg new file mode 100644 index 0000000..d633fcd --- /dev/null +++ b/uniro_admin_frontend/src/assets/markers/origin.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/uniro_admin_frontend/src/assets/markers/report.svg b/uniro_admin_frontend/src/assets/markers/report.svg new file mode 100644 index 0000000..6fd82e6 --- /dev/null +++ b/uniro_admin_frontend/src/assets/markers/report.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/uniro_admin_frontend/src/assets/markers/selectedBuilding.svg b/uniro_admin_frontend/src/assets/markers/selectedBuilding.svg new file mode 100644 index 0000000..1187dc7 --- /dev/null +++ b/uniro_admin_frontend/src/assets/markers/selectedBuilding.svg @@ -0,0 +1,4 @@ + + + + diff --git a/uniro_admin_frontend/src/assets/markers/university.svg b/uniro_admin_frontend/src/assets/markers/university.svg new file mode 100644 index 0000000..9fc7685 --- /dev/null +++ b/uniro_admin_frontend/src/assets/markers/university.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/uniro_admin_frontend/src/assets/markers/waypoint.svg b/uniro_admin_frontend/src/assets/markers/waypoint.svg new file mode 100644 index 0000000..5107e6a --- /dev/null +++ b/uniro_admin_frontend/src/assets/markers/waypoint.svg @@ -0,0 +1,3 @@ + + + diff --git a/uniro_admin_frontend/src/assets/markers/waypoint_blue.svg b/uniro_admin_frontend/src/assets/markers/waypoint_blue.svg new file mode 100644 index 0000000..8405f23 --- /dev/null +++ b/uniro_admin_frontend/src/assets/markers/waypoint_blue.svg @@ -0,0 +1,3 @@ + + + diff --git a/uniro_admin_frontend/src/assets/markers/waypoint_red.svg b/uniro_admin_frontend/src/assets/markers/waypoint_red.svg new file mode 100644 index 0000000..d3ec652 --- /dev/null +++ b/uniro_admin_frontend/src/assets/markers/waypoint_red.svg @@ -0,0 +1,3 @@ + + + diff --git a/uniro_admin_frontend/src/component/BuildingMap.tsx b/uniro_admin_frontend/src/component/BuildingMap.tsx new file mode 100644 index 0000000..e8080b5 --- /dev/null +++ b/uniro_admin_frontend/src/component/BuildingMap.tsx @@ -0,0 +1,28 @@ +import { useEffect } from "react"; +import useMap from "../hooks/useMap"; +import useSearchBuilding from "../hooks/useUniversityRecord"; + +type MapProps = { + style?: React.CSSProperties; +}; +const BuildingMap = ({ style }: MapProps) => { + const { mapRef, map, mapLoaded } = useMap(); + + const { getCurrentUniversityLngLat, currentUniversity } = useSearchBuilding(); + + if (!style) { + style = { height: "100%", width: "100%" }; + } + + useEffect(() => { + if (!map || !mapLoaded) return; + const universityLatLng = getCurrentUniversityLngLat(); + map.setCenter(universityLatLng); + }, [currentUniversity, mapLoaded, getCurrentUniversityLngLat, map]); + + return ( +
+ ); +}; + +export default BuildingMap; diff --git a/uniro_admin_frontend/src/component/Map.tsx b/uniro_admin_frontend/src/component/Map.tsx index 35e798d..05b1fdb 100644 --- a/uniro_admin_frontend/src/component/Map.tsx +++ b/uniro_admin_frontend/src/component/Map.tsx @@ -17,9 +17,8 @@ const Map = ({ style }: MapProps) => { useEffect(() => { if (!map || !mapLoaded) return; const universityLatLng = getCurrentUniversityLngLat(); - console.log("Setting center to:", universityLatLng); map.setCenter(universityLatLng); - }, [currentUniversity, mapLoaded]); + }, [currentUniversity, mapLoaded, getCurrentUniversityLngLat, map]); return (
diff --git a/uniro_admin_frontend/src/components/buildings/buildingCard.tsx b/uniro_admin_frontend/src/components/buildings/buildingCard.tsx new file mode 100644 index 0000000..468755c --- /dev/null +++ b/uniro_admin_frontend/src/components/buildings/buildingCard.tsx @@ -0,0 +1,42 @@ +import React from "react"; + +type BuildingCardProps = { + buildingName: string; + nodeId: number; + isSelected: boolean; + onDelete?: () => void; + onClick: () => void; +}; + +const BuildingCard = ({ + buildingName, + nodeId, + isSelected = false, + onDelete, + onClick, +}: BuildingCardProps) => { + return ( +
onClick()} + > +
+

건물명 : {buildingName}

+

node ID : {nodeId}

+
+ +
+ +
+
+ ); +}; + +export default BuildingCard; diff --git a/uniro_admin_frontend/src/components/buildings/buildingList.tsx b/uniro_admin_frontend/src/components/buildings/buildingList.tsx new file mode 100644 index 0000000..bfe6a1a --- /dev/null +++ b/uniro_admin_frontend/src/components/buildings/buildingList.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { Building } from "../../data/types/node"; +import BuildingCard from "./buildingCard"; +import { Coord } from "../../data/types/coord"; + +// export interface Node extends Coord { +// nodeId: NodeId; +// } + +// export interface Building extends Node { +// buildingName: string; +// buildingImageUrl: string; +// phoneNumber: string; +// address: string; +// } + +type BuildingListProps = { + buildings: Building[]; + selectedBuildingId: number | null; + setCenterToCoordinate: (nodeId: number, coord: Coord) => void; +}; + +const BuildingList = ({ + selectedBuildingId, + buildings, + setCenterToCoordinate, +}: BuildingListProps) => { + return ( +
+ {buildings && + buildings.map((building) => { + const { nodeId, buildingName } = building; + return ( + + setCenterToCoordinate(nodeId, { + lat: building.lat, + lng: building.lng, + }) + } + isSelected={selectedBuildingId === building.nodeId} + nodeId={nodeId} + buildingName={buildingName} + /> + ); + })} +
+ ); +}; + +export default BuildingList; diff --git a/uniro_admin_frontend/src/components/buildings/buildingListTitle.tsx b/uniro_admin_frontend/src/components/buildings/buildingListTitle.tsx new file mode 100644 index 0000000..15e2e49 --- /dev/null +++ b/uniro_admin_frontend/src/components/buildings/buildingListTitle.tsx @@ -0,0 +1,25 @@ +import React from "react"; + +type BuildingListTitleProps = { + refreshBuildings: () => void; + refetching: boolean; +}; + +const BuildingListTitle = ({ + refreshBuildings, + refetching, +}: BuildingListTitleProps) => { + return ( +
+
건물 목록
+ +
+ ); +}; + +export default BuildingListTitle; diff --git a/uniro_admin_frontend/src/components/map/mapMarkers.tsx b/uniro_admin_frontend/src/components/map/mapMarkers.tsx new file mode 100644 index 0000000..1977507 --- /dev/null +++ b/uniro_admin_frontend/src/components/map/mapMarkers.tsx @@ -0,0 +1,38 @@ +const markerImages = import.meta.glob("/src/assets/markers/*.svg", { + eager: true, +}); + +function getImage(type: string): string { + return ( + markerImages[`/src/assets/markers/${type}.svg`] as { default: string } + )?.default; +} + +function createImageElement(type: string): HTMLElement { + const markerImage = document.createElement("img"); + markerImage.src = getImage(type); + return markerImage; +} + +function createContainerElement(className?: string) { + const container = document.createElement("div"); + container.className = `flex flex-col items-center space-y-[7px] ${className}`; + + return container; +} + +export default function createMarkerElement({ + type, + className, +}: { + type: string; + className?: string; +}): HTMLElement { + const container = createContainerElement(className); + + const markerImage = createImageElement(type); + + container.appendChild(markerImage); + + return container; +} diff --git a/uniro_admin_frontend/src/components/navBar.tsx b/uniro_admin_frontend/src/components/navBar.tsx index df7c9bd..0f14b11 100644 --- a/uniro_admin_frontend/src/components/navBar.tsx +++ b/uniro_admin_frontend/src/components/navBar.tsx @@ -3,9 +3,7 @@ import UNIROLOGO from "../assets/navbar/UNIRO_ADMIN.svg?react"; import DropDownArrow from "../assets/navbar/dropDownArrow.svg?react"; import useSearchBuilding from "../hooks/useUniversityRecord"; -interface Props {} - -const NavBar: React.FC = () => { +const NavBar: React.FC = () => { const { currentUniversity, getUniversityNameList, setCurrentUniversity } = useSearchBuilding(); const [selectedUniversity, setSelectedUniversity] = diff --git a/uniro_admin_frontend/src/components/subNavBar.tsx b/uniro_admin_frontend/src/components/subNavBar.tsx new file mode 100644 index 0000000..64f1ba6 --- /dev/null +++ b/uniro_admin_frontend/src/components/subNavBar.tsx @@ -0,0 +1,32 @@ +import { Link, useLocation } from "react-router"; + +const SubNavBar = () => { + const location = useLocation(); // 📌 현재 경로 감지 + + const getLinkStyle = (path: string) => + location.pathname === path + ? "text-blue-700" + : "text-gray-700 hover:text-black"; + + return ( + + ); +}; + +export default SubNavBar; diff --git a/uniro_admin_frontend/src/constant/bounds.ts b/uniro_admin_frontend/src/constant/bounds.ts new file mode 100644 index 0000000..7b34585 --- /dev/null +++ b/uniro_admin_frontend/src/constant/bounds.ts @@ -0,0 +1,13 @@ +interface Bounds { + north: number; + south: number; + east: number; + west: number; +} + +export const HanyangUniversityBounds: Bounds = { + north: 37.560645, + south: 37.552997, + east: 127.051049, + west: 127.04111, +}; diff --git a/uniro_admin_frontend/src/constant/edge.ts b/uniro_admin_frontend/src/constant/edge.ts new file mode 100644 index 0000000..d55001d --- /dev/null +++ b/uniro_admin_frontend/src/constant/edge.ts @@ -0,0 +1 @@ +export const EDGE_LENGTH = 3; diff --git a/uniro_admin_frontend/src/constant/enum/markerEnum.ts b/uniro_admin_frontend/src/constant/enum/markerEnum.ts new file mode 100644 index 0000000..9487ca9 --- /dev/null +++ b/uniro_admin_frontend/src/constant/enum/markerEnum.ts @@ -0,0 +1,11 @@ +export const enum Markers { + CAUTION = "caution", + DANGER = "danger", + BUILDING = "building", + ORIGIN = "origin", + DESTINATION = "destination", + SELECTED_BUILDING = "selectedBuilding", + WAYPOINT = "waypoint", + NUMBERED_WAYPOINT = "numberedWayPoint", + REPORT = "report", +} diff --git a/uniro_admin_frontend/src/constant/enum/messageEnum.ts b/uniro_admin_frontend/src/constant/enum/messageEnum.ts new file mode 100644 index 0000000..c2f249d --- /dev/null +++ b/uniro_admin_frontend/src/constant/enum/messageEnum.ts @@ -0,0 +1,6 @@ +export enum ReportRiskMessage { + DEFAULT = "선 위를 눌러 제보할 지점을 선택하세요", + CREATE = "이 지점으로 새로운 제보를 진행할까요?", + UPDATE = "이 지점에 제보된 기존 정보를 바꿀까요?", + ERROR = "선 위에서만 선택 가능해요", +} diff --git a/uniro_admin_frontend/src/constant/enum/reportEnum.ts b/uniro_admin_frontend/src/constant/enum/reportEnum.ts new file mode 100644 index 0000000..04ccce6 --- /dev/null +++ b/uniro_admin_frontend/src/constant/enum/reportEnum.ts @@ -0,0 +1,28 @@ +export enum PassableStatus { + DANGER = "DANGER", + CAUTION = "CAUTION", + RESTORED = "RESTORED", + INITIAL = "INITIAL", +} + +export enum CautionIssue { + CURB = "낮은 턱이 있어요", + CRACK = "도로에 균열이 있어요", + SLOPE = "낮은 비탈길이 있어요", + ETC = "그 외 요소", +} + +export enum DangerIssue { + CURB = "높은 턱이 있어요", + STAIRS = "계단이 있어요", + SLOPE = "경사가 매우 높아요", + ETC = "그 외 요소", +} + +export enum IssueTypeKey { + CURB = "CURB", + CRACK = "CRACK", + SLOPE = "SLOPE", + ETC = "ETC", + STAIRS = "STAIRS", +} diff --git a/uniro_admin_frontend/src/constant/enum/routeEnum.ts b/uniro_admin_frontend/src/constant/enum/routeEnum.ts new file mode 100644 index 0000000..aeff459 --- /dev/null +++ b/uniro_admin_frontend/src/constant/enum/routeEnum.ts @@ -0,0 +1,4 @@ +export const enum RoutePoint { + ORIGIN = "origin", + DESTINATION = "destination", +} diff --git a/uniro_admin_frontend/src/constant/error.ts b/uniro_admin_frontend/src/constant/error.ts new file mode 100644 index 0000000..8f89370 --- /dev/null +++ b/uniro_admin_frontend/src/constant/error.ts @@ -0,0 +1,19 @@ +export class NotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = "Not Found"; + } +} + +export class BadRequestError extends Error { + constructor(message: string) { + super(message); + this.name = "Bad Request"; + } +} + +export enum ERROR_STATUS { + NOT_FOUND = 404, + BAD_REQUEST = 400, + INTERNAL_ERROR = 500, +} diff --git a/uniro_admin_frontend/src/constant/fallback.tsx b/uniro_admin_frontend/src/constant/fallback.tsx new file mode 100644 index 0000000..adf8c73 --- /dev/null +++ b/uniro_admin_frontend/src/constant/fallback.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import Loading from "../components/loading/loading"; + +export const fallbackConfig: Record = { + "/": , + "/map": , + "/result": , + "/report/route": , + "/report/hazard": , + "/university": , + "/form": , +}; diff --git a/uniro_admin_frontend/src/constant/reportTheme.ts b/uniro_admin_frontend/src/constant/reportTheme.ts new file mode 100644 index 0000000..8e8d6ac --- /dev/null +++ b/uniro_admin_frontend/src/constant/reportTheme.ts @@ -0,0 +1,8 @@ +import { PassableStatus } from "./enum/reportEnum"; + +export const THEME_MAP: Record = { + [PassableStatus.DANGER]: "border-system-red text-system-red bg-[#FFF5F7]", + [PassableStatus.CAUTION]: "border-system-orange text-system-orange bg-[#FFF7EF]", + [PassableStatus.RESTORED]: "border-primary-400 text-primary-400 bg-[#F0F5FE]", + [PassableStatus.INITIAL]: "bg-gray-100 border-gray-400", +}; diff --git a/uniro_admin_frontend/src/container/building/buildingAddContainer.tsx b/uniro_admin_frontend/src/container/building/buildingAddContainer.tsx new file mode 100644 index 0000000..d8e3174 --- /dev/null +++ b/uniro_admin_frontend/src/container/building/buildingAddContainer.tsx @@ -0,0 +1,337 @@ +import React, { useEffect, useState } from "react"; +import { Building, Node, NodeId } from "../../data/types/node"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { postBuilding } from "../../api/nodes"; +import { postBuildingRoute } from "../../api/route"; + +type Coord = { + lat: number; + lng: number; +}; + +type BuildingAddContainerProps = { + selectedCoord?: Coord; + setSelectedCoord?: React.Dispatch>; + markerRef: React.MutableRefObject; + selectedBuilding: Building | null; + drawSingleRoute: (start: Node, end: Node) => void; + mode: "add" | "connect" | "view"; + selectedNode: Node[]; + resetConnectMode: () => void; +}; + +const BuildingAddContainer: React.FC = ({ + selectedCoord, + setSelectedCoord, + markerRef, + selectedBuilding, + mode, + selectedNode, + drawSingleRoute, + resetConnectMode, +}) => { + const queryClient = useQueryClient(); + const [buildingName, setBuildingName] = useState(""); + const [buildingPhoto, setBuildingPhoto] = useState(""); + const [phone, setPhone] = useState(""); + const [address, setAddress] = useState(""); + + const [isSelectedNode1, setIsSelectedNode1] = useState(true); + + const addBuilding = useMutation({ + mutationFn: (body: { + buildingName: string; + buildingImageUrl: string; + phoneNumber: string; + address: string; + lat: number; + lng: number; + level: number; + }) => postBuilding(1001, body), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [1001, "buildings"] }); + }, + onError: (error) => { + alert("건물 추가에 실패했습니다."); + }, + }); + + const addBuildingRoute = useMutation({ + mutationFn: (body: { buildingNodeId: NodeId; nodeId: NodeId }) => + postBuildingRoute(1001, body), + onSuccess: () => { + alert("경로가 추가되었습니다."); + queryClient.invalidateQueries({ queryKey: [1001, "routes"] }); + }, + onError: (error) => { + alert("경로 추가에 실패했습니다."); + }, + }); + + useEffect(() => { + if (selectedBuilding) { + setBuildingName(selectedBuilding?.buildingName); + setPhone(selectedBuilding?.phoneNumber || ""); + setBuildingPhoto(selectedBuilding?.buildingImageUrl || ""); + setAddress(selectedBuilding.address || ""); + } else { + setBuildingName(""); + setPhone(""); + setBuildingPhoto(""); + setAddress(""); + } + }, [selectedBuilding]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!selectedCoord && !selectedBuilding) { + alert("먼저 지도에서 위치를 선택하거나, 기존 건물을 선택해주세요."); + return; + } + + if (!buildingName || !buildingPhoto || !phone || !address) { + alert("모든 필드를 입력해주세요."); + return; + } + + const payload = { + buildingName, + buildingImageUrl: buildingPhoto, + phoneNumber: phone, + address, + lat: selectedCoord?.lat ?? selectedBuilding?.lat ?? 0, + lng: selectedCoord?.lng ?? selectedBuilding?.lng ?? 0, + level: 10, + }; + if (mode === "add") { + if (payload.lat && payload.lng) { + addBuilding.mutate(payload); + } + } + + alert( + selectedBuilding ? "건물이 수정되었습니다." : "건물이 추가되었습니다." + ); + // 폼 초기화 + if (setSelectedCoord) setSelectedCoord(undefined); + if (markerRef.current) markerRef.current.map = null; + setBuildingName(""); + setBuildingPhoto(""); + setPhone(""); + setAddress(""); + }; + + const handleConnectSubmit = () => { + if (selectedNode.length === 2 && selectedBuilding) { + if (isSelectedNode1) { + addBuildingRoute.mutate({ + buildingNodeId: selectedBuilding.nodeId, + nodeId: selectedNode[0].nodeId, + }); + + resetConnectMode(); + return; + } + addBuildingRoute.mutate({ + buildingNodeId: selectedBuilding.nodeId, + nodeId: selectedNode[1].nodeId, + }); + resetConnectMode(); + return; + } + alert("노드를 2개 선택해주세요."); + return; + }; + + return ( +
+ {mode === "add" || mode === "view" ? ( + selectedCoord || selectedBuilding ? ( + <> + {selectedCoord && ( +
+

선택한 위치:

+

lat: {selectedCoord.lat}

+

lng: {selectedCoord.lng}

+
+ )} + +
+

+ {selectedBuilding ? "건물 수정하기" : "건물 추가하기"} +

+ +
건물명
+ setBuildingName(e.target.value)} + required + /> + +
건물 사진
+ setBuildingPhoto(e.target.value)} + required + /> + +
전화번호
+ setPhone(e.target.value)} + required + /> + +
주소
+ setAddress(e.target.value)} + required + /> + +
+ +
+
+ + ) : ( +
+
+ 지도에서 위치를 선택해주세요 +
+
+ ) + ) : ( +
+

+ 선택된 정보 +

+ + + {selectedBuilding ? ( +
+

+ 🏢 건물명:{" "} + {selectedBuilding.buildingName} +

+
+ ) : ( +

🏢 선택된 건물이 없습니다.

+ )} + + {selectedNode.length > 0 ? ( + <> +
+

+ 📍 선택한 노드 정보 1 +

+

+ NODE ID: {selectedNode[0].nodeId} +

+

+ 위도: {selectedNode[0].lat} +

+

+ 경도: {selectedNode[0].lng} +

+
+ +
+
+
+

+ 📍 선택한 노드 정보 2 +

+

+ NODE ID: {selectedNode[1].nodeId} +

+

+ 위도: {selectedNode[1].lat} +

+

+ 경도: {selectedNode[1].lng} +

+
+ +
+
+ + + ) : ( +

+ 📍 선택된 길 좌표가 없습니다. +

+ )} +
+ )} +
+ ); +}; + +export default BuildingAddContainer; diff --git a/uniro_admin_frontend/src/container/building/buildingListContainer.tsx b/uniro_admin_frontend/src/container/building/buildingListContainer.tsx new file mode 100644 index 0000000..63eac90 --- /dev/null +++ b/uniro_admin_frontend/src/container/building/buildingListContainer.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import BuildingListTitle from "../../components/buildings/buildingListTitle"; +import BuildingList from "../../components/buildings/buildingList"; +import { Building } from "../../data/types/node"; +import { Coord } from "../../data/types/coord"; + +type BuildingListContainerProps = { + selectedBuildingId: number | null; + buildings: Building[]; + setCenterToCoordinate: (nodeId: number, coord: Coord) => void; + refreshBuildings: () => void; + refetching: boolean; +}; + +const BuildingListContainer = ({ + selectedBuildingId, + buildings, + setCenterToCoordinate, + refreshBuildings, + refetching, +}: BuildingListContainerProps) => { + return ( +
+ + {buildings ? ( + + ) : ( +
+

데이터가 없습니다, undefined or null

+
+ )} +
+ ); +}; + +export default BuildingListContainer; diff --git a/uniro_admin_frontend/src/container/building/buildingMapContainer.tsx b/uniro_admin_frontend/src/container/building/buildingMapContainer.tsx new file mode 100644 index 0000000..5a4b576 --- /dev/null +++ b/uniro_admin_frontend/src/container/building/buildingMapContainer.tsx @@ -0,0 +1,59 @@ +import React, { forwardRef } from "react"; +type BuildingMapContainerProps = { + mode: "add" | "connect" | "view"; + setMode: React.Dispatch>; + resetToAddMode: () => void; + changeToConnectMode: () => void; +}; + +const BuildingMapContainer = forwardRef< + HTMLDivElement, + BuildingMapContainerProps +>( + ( + { mode, resetToAddMode, changeToConnectMode }: BuildingMapContainerProps, + ref + ) => { + return ( +
+
+ + + + + +
+
+
+ ); + } +); + +export default BuildingMapContainer; diff --git a/uniro_admin_frontend/src/container/mapContainer.tsx b/uniro_admin_frontend/src/container/mapContainer.tsx index e20f603..8dd0696 100644 --- a/uniro_admin_frontend/src/container/mapContainer.tsx +++ b/uniro_admin_frontend/src/container/mapContainer.tsx @@ -1,9 +1,7 @@ import React from "react"; import Map from "../component/Map"; -type Props = {}; - -const MapContainer = (props: Props) => { +const MapContainer = () => { return (
diff --git a/uniro_admin_frontend/src/data/types/coord.d.ts b/uniro_admin_frontend/src/data/types/coord.d.ts new file mode 100644 index 0000000..625eadd --- /dev/null +++ b/uniro_admin_frontend/src/data/types/coord.d.ts @@ -0,0 +1 @@ +export type Coord = google.maps.LatLngLiteral; diff --git a/uniro_admin_frontend/src/data/types/node.d.ts b/uniro_admin_frontend/src/data/types/node.d.ts new file mode 100644 index 0000000..5e653b1 --- /dev/null +++ b/uniro_admin_frontend/src/data/types/node.d.ts @@ -0,0 +1,14 @@ +import { Coord } from "./coord"; + +export type NodeId = number; + +export interface Node extends Coord { + nodeId: NodeId; +} + +export interface Building extends Node { + buildingName: string; + buildingImageUrl: string; + phoneNumber: string; + address: string; +} diff --git a/uniro_admin_frontend/src/data/types/route.d.ts b/uniro_admin_frontend/src/data/types/route.d.ts new file mode 100644 index 0000000..ca0df11 --- /dev/null +++ b/uniro_admin_frontend/src/data/types/route.d.ts @@ -0,0 +1,54 @@ +import { + CautionIssueType, + DangerIssueType, +} from "../../constant/enum/reportEnum"; +import { RoutePoint } from "../../constant/enum/routeEnum"; +import { Coord } from "./coord"; +import { Node } from "./node"; + +export type RouteId = number; + +export type Route = { + routeId: RouteId; + node1: Coord; + node2: Coord; +}; + +export interface CautionRoute extends Route { + cautionTypes: CautionIssueType[]; +} + +export interface DangerRoute extends Route { + dangerTypes: DangerIssueType[]; +} + +export interface CoreRoute { + routeId: RouteId; + node1: Node; + node2: Node; +} + +export interface CoreRoutes { + coreNode1Id: NodeId; + coreNode2Id: NodeId; + routes: CoreRoute[]; +} + +export type CoreRoutesList = CoreRoutes[]; + +export type RoutePointType = RoutePoint.ORIGIN | RoutePoint.DESTINATION; + +export type RouteDetail = { + dist: number; + directionType: Direction; + coordinates: Coord; + cautionFactors: CautionIssueType[]; +}; + +export type NavigationRouteList = { + hasCaution: boolean; + totalDistance: number; + totalCost: number; + routes: Route[]; + routeDetails: RouteDetail[]; +}; diff --git a/uniro_admin_frontend/src/main.tsx b/uniro_admin_frontend/src/main.tsx index bef5202..5a81f29 100644 --- a/uniro_admin_frontend/src/main.tsx +++ b/uniro_admin_frontend/src/main.tsx @@ -1,10 +1,10 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import AppRouter from "./AppRouter.tsx"; -createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById("root")!).render( - - , -) + + +); diff --git a/uniro_admin_frontend/src/page/buildingPage.tsx b/uniro_admin_frontend/src/page/buildingPage.tsx new file mode 100644 index 0000000..7af7627 --- /dev/null +++ b/uniro_admin_frontend/src/page/buildingPage.tsx @@ -0,0 +1,476 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import MainContainer from "../container/mainContainer"; +import BuildingListContainer from "../container/building/buildingListContainer"; +import BuildingMapContainer from "../container/building/buildingMapContainer"; +import BuildingAddContainer from "../container/building/buildingAddContainer"; +import useMap from "../hooks/useMap"; +import { Coord } from "../data/types/coord"; +import useSearchBuilding from "../hooks/useUniversityRecord"; + +import createMarkerElement from "../components/map/mapMarkers"; +import { QueryClient, useQueries } from "@tanstack/react-query"; +import { getAllRoutes } from "../api/route"; +import { CoreRoute, CoreRoutesList } from "../data/types/route"; +import { getAllBuildings } from "../api/nodes"; +import createAdvancedMarker from "../utils/markers/createAdvanedMarker"; +import { Node, NodeId } from "../data/types/node"; +import { Markers } from "../constant/enum/markerEnum"; +import findNearestSubEdge from "../utils/polylines/findNearestEdge"; + +const BuildingPage = () => { + const queryClient = new QueryClient(); + const result = useQueries({ + queries: [ + { queryKey: ["1001", "routes"], queryFn: () => getAllRoutes(1001) }, + { + queryKey: [1001, "buildings"], + queryFn: () => + getAllBuildings(1001, { + leftUpLat: 38, + leftUpLng: 127, + rightDownLat: 37, + rightDownLng: 128, + }), + }, + ], + }); + + // 선택한 지점은 가장 높은 레벨의 컴포넌트에서 관리 + const [routes, buildings] = result; + + // mode는 두개로 분류 (건물 추가 / 수정 모드 , 건물과 주변 길 잇는 모드(출입문 연결) + const [mode, setMode] = useState<"add" | "connect" | "view">("add"); + + const { map, mapLoaded, mapRef, AdvancedMarker, Polyline } = useMap(); + + const [selectedBuildingId, setSelectedBuildingId] = useState( + 0 + ); + const { getCurrentUniversityLngLat } = useSearchBuilding(); + + // mode = add or view일 때 선택한 빌딩 좌표 + const [selectedCoord, setSelectedCoord] = useState( + undefined + ); + + // mode = connect일 때 선택한 좌표들 + const [selectedNode, setSelectedNode] = useState([]); + // mode = connect일 때 연결한 하나의 좌표 + const [_, setSelectedSingleNode] = useState(null); + + // mode = connect일 때 선택한 Edge + const [selectedEdge, setSelectedEdge] = useState<{ + info: CoreRoute; + marker1: google.maps.marker.AdvancedMarkerElement; + marker2: google.maps.marker.AdvancedMarkerElement; + polyline: google.maps.Polyline; + }>(); + + /// mode = connect일 때 그려진 작은 하나의 Edge(선택한 Edge) + const [singleRoute, setSingleRoute] = useState<{ + start: Coord; + end: Coord; + polyline: google.maps.Polyline; + }>(); + + // event의 mode가 참조가 안되어서 만든 State + const [buildingMarkers, setBuildingMarkers] = useState< + google.maps.marker.AdvancedMarkerElement[] + >([]); + + const [polylineList, setPolylineList] = useState([]); + + // 새로 선택한 건물 마커를 관리하기 위한 ref + const markerRef = useRef( + null + ); + + // 마커 생성 + const createMarker = useCallback( + (coord: Coord) => { + if (!map || !AdvancedMarker) return null; + return new AdvancedMarker({ position: coord, map }); + }, + [map, AdvancedMarker] + ); + + const handleMapClick = (e: google.maps.MapMouseEvent) => { + if (!e.latLng) return; + + setSelectedBuildingId(0); + setMode("add"); + setSelectedCoord({ + lat: e.latLng.lat(), + lng: e.latLng.lng(), + }); + }; + + const drawRoute = ( + coreRouteList: CoreRoutesList, + mode: "add" | "connect" | "view" + ) => { + if (!Polyline || !AdvancedMarker || !map) return; + + for (const coreRoutes of coreRouteList) { + const { routes: subRoutes } = coreRoutes; + + // 가장 끝쪽 Core Node 그리기 + const endNode = subRoutes[subRoutes.length - 1].node2; + + const endMarker = new AdvancedMarker({ + map: map, + position: endNode, + content: createMarkerElement({ + type: "waypoint", + className: "translate-waypoint", + }), + }); + + endMarker.addListener("click", () => { + if (mode === "view" || mode === "add") setSelectedBuildingId(0); + }); + + const subNodes = [subRoutes[0].node1, ...subRoutes.map((el) => el.node2)]; + + const polyline = new Polyline({ + map: map, + path: subNodes.map((el) => { + return { lat: el.lat, lng: el.lng }; + }), + strokeColor: "#808080", + }); + + //polyline을 선택 방지 + google.maps.event.addListener( + polyline, + "click", + (e: { latLng: google.maps.LatLng }) => { + if (mode === "view" || mode === "add") setSelectedBuildingId(0); + if (mode === "connect") { + const edges: CoreRoute[] = subRoutes.map( + ({ routeId, node1, node2 }) => { + return { routeId, node1, node2 }; + } + ); + + const { edge: nearestEdge } = findNearestSubEdge(edges, { + lat: e.latLng.lat(), + lng: e.latLng.lng(), + }); + const { node1, node2 } = nearestEdge; + + const polyline = new Polyline({ + map: map, + path: [ + { lat: node1.lat, lng: node1.lng }, + { lat: node2.lat, lng: node2.lng }, + ], + strokeColor: "#000000", + zIndex: 100, + }); + + const marker1 = new AdvancedMarker({ + map: map, + position: { lat: node1.lat, lng: node1.lng }, + content: createMarkerElement({ + type: "waypoint_red", + className: "translate-waypoint", + }), + zIndex: 100, + }); + + const marker2 = new AdvancedMarker({ + map: map, + position: { lat: node2.lat, lng: node2.lng }, + content: createMarkerElement({ + type: "waypoint_blue", + className: "translate-waypoint", + }), + zIndex: 100, + }); + setSelectedEdge({ + info: nearestEdge, + marker1, + marker2, + polyline, + }); + + setSelectedNode([node1, node2]); + } + } + ); + setPolylineList((prev) => [...prev, polyline]); + + const startNode = subRoutes[0].node1; + + const startMarker = new AdvancedMarker({ + map: map, + position: startNode, + content: createMarkerElement({ + type: "waypoint", + className: "translate-waypoint", + }), + }); + + startMarker.addListener("click", () => { + if (mode === "view" || mode === "add") setSelectedBuildingId(0); + }); + } + }; + + const drawSingleRoute = (start: Coord, end: Coord) => { + if (!Polyline || !AdvancedMarker || !map) return; + + if (singleRoute) { + singleRoute.polyline.setMap(null); + } + + if (selectedBuildingId === 0) { + return; + } + + const polyline = new Polyline({ + map: map, + path: [start, end], + strokeColor: "#808080", + }); + + setSingleRoute({ start, end, polyline }); + }; + + const eraseRoute = () => { + if (singleRoute) { + singleRoute.polyline.setMap(null); + } + }; + + const erasePolylineList = () => { + for (const polyline of polylineList) { + polyline.setMap(null); + } + setPolylineList([]); + }; + + const addBuildings = () => { + if (google.maps.marker.AdvancedMarkerElement === null || map === null) + return; + + const buildingList = buildings.data; + + if (buildingList === undefined) return; + console.log(buildingList); + for (const building of buildingList) { + const { nodeId, lat, lng } = building; + + const buildingMarker = createAdvancedMarker( + google.maps.marker.AdvancedMarkerElement, + map, + new google.maps.LatLng(lat, lng), + createMarkerElement({ + type: Markers.BUILDING, + className: "translate-marker", + }) + ); + + buildingMarker.addListener("click", () => { + setSelectedBuildingId(nodeId); + map.setCenter({ lat, lng }); + map.setZoom(18); + console.log("click", mode); + if (mode === "view" || mode === "add") { + setMode("view"); + } + }); + + setBuildingMarkers((prev) => [...prev, buildingMarker]); + } + }; + + const clearBuildingMarkers = () => { + for (const marker of buildingMarkers) { + marker.map = null; + } + setBuildingMarkers([]); + }; + + // 지도가 로드되면 클릭 이벤트를 추가 + useEffect(() => { + if (!map || !mapLoaded) return; + + if (routes.data) { + if (routes.data) { + drawRoute(routes.data, mode); + } + } + if (buildings.data) { + addBuildings(); + } + + if (mode === "add") { + google.maps.event.clearListeners(map, "click"); + google.maps.event.clearListeners(map, "rightclick"); + map.addListener("click", handleMapClick); + map.addListener("rightclick", () => { + setSelectedCoord(undefined); + setSelectedBuildingId(0); + + if (markerRef.current) markerRef.current.map = null; + }); + } + + if (mode === "connect") { + setSelectedCoord(undefined); + setSelectedBuildingId(0); + google.maps.event.clearListeners(map, "click"); + google.maps.event.clearListeners(map, "rightclick"); + } + + map.setCenter(getCurrentUniversityLngLat()); + eraseRoute(); + + return () => { + google.maps.event.clearListeners(map, "click"); + google.maps.event.clearListeners(map, "rightclick"); + clearBuildingMarkers(); + }; + }, [map, mapLoaded, getCurrentUniversityLngLat, routes.data, buildings.data]); + + useEffect(() => { + if (!map || !mapLoaded) return; + + if (routes.data) { + clearBuildingMarkers(); + erasePolylineList(); + drawRoute(routes.data, mode); + } + + if (mode === "add") { + addBuildings(); + google.maps.event.clearListeners(map, "click"); + google.maps.event.clearListeners(map, "rightclick"); + map.addListener("click", handleMapClick); + map.addListener("rightclick", () => { + setSelectedCoord(undefined); + setSelectedBuildingId(0); + + if (markerRef.current) markerRef.current.map = null; + }); + } + + if (mode === "connect") { + addBuildings(); + setSelectedCoord(undefined); + setSelectedBuildingId(0); + google.maps.event.clearListeners(map, "click"); + google.maps.event.clearListeners(map, "rightclick"); + } + return () => { + google.maps.event.clearListeners(map, "click"); + google.maps.event.clearListeners(map, "rightclick"); + }; + }, [mode]); + + // 선택한 좌표가 바뀌면 마커를 생성하거나 제거 + useEffect(() => { + if (!selectedCoord || !map) return; + + if (markerRef.current) markerRef.current.map = null; + markerRef.current = createMarker(selectedCoord); + + return () => { + if (markerRef.current) markerRef.current.map = null; + }; + }, [selectedCoord, map, AdvancedMarker, createMarker]); + + const setCenterToCoordinate = (nodeId: number, coord: Coord) => { + if (map) { + map.setZoom(18); + map.setCenter(coord); + setSelectedBuildingId(nodeId); + setMode("view"); + } + }; + + // 새로고침 기능 + const refreshBuildings = () => { + queryClient.invalidateQueries({ queryKey: [1001, "buildings"] }); + buildings.refetch(); + }; + + const changeToConnectMode = () => { + setMode("connect"); + setSelectedBuildingId(0); + if (map) { + map.setZoom(17); + map.setCenter(getCurrentUniversityLngLat()); + } + }; + + const resetConnectMode = () => { + setSelectedBuildingId(0); + setSelectedEdge(undefined); + setSelectedNode([]); + setSelectedSingleNode(null); + eraseRoute(); + }; + + const resetToAddMode = () => { + setMode("add"); + setSelectedBuildingId(0); + setSelectedEdge(undefined); + setSelectedNode([]); + setSelectedSingleNode(null); + if (map) { + map.setZoom(17); + map.setCenter(getCurrentUniversityLngLat()); + } + }; + + useEffect(() => { + return () => { + if (!selectedEdge) return; + const { marker1, marker2, polyline } = selectedEdge; + marker1.map = null; + marker2.map = null; + polyline.setMap(null); + if (singleRoute) { + eraseRoute(); + } + }; + }, [selectedEdge]); + + return ( + + + + building.nodeId === selectedBuildingId + ) || null + } + /> + + ); +}; + +export default BuildingPage; diff --git a/uniro_admin_frontend/src/page/logPage.tsx b/uniro_admin_frontend/src/page/logPage.tsx new file mode 100644 index 0000000..d0f7365 --- /dev/null +++ b/uniro_admin_frontend/src/page/logPage.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import MainContainer from "../container/mainContainer"; +import LogListContainer from "../container/logListContainer"; +import MapContainer from "../container/mapContainer"; + +const LogPage = () => { + return ( + + + + + ); +}; + +export default LogPage; diff --git a/uniro_admin_frontend/src/page/simulationPage.tsx b/uniro_admin_frontend/src/page/simulationPage.tsx new file mode 100644 index 0000000..36431ed --- /dev/null +++ b/uniro_admin_frontend/src/page/simulationPage.tsx @@ -0,0 +1,186 @@ +import React, { useEffect, useState } from "react"; +import MainContainer from "../container/mainContainer"; +import { useQueries } from "@tanstack/react-query"; +import { transformAllRoutes } from "../utils/route"; +import useMap from "../hooks/useMap"; +import useSearchBuilding from "../hooks/useUniversityRecord"; +import { CoreRoute, CoreRoutesList } from "../data/types/route"; +import createMarkerElement from "../components/map/mapMarkers"; +import findNearestSubEdge from "../utils/polylines/findNearestEdge"; + +type AdvancedMarker = google.maps.marker.AdvancedMarkerElement; +type Polyline = google.maps.Polyline; + +const getRoutes = () => { + return fetch(`${import.meta.env.VITE_REACT_SERVER_BASE_URL}/1001/routes`) + .then((res) => res.json()) + .then(transformAllRoutes); +}; + +const SimulationPage = () => { + const result = useQueries({ + queries: [{ queryKey: ["1001", "routes"], queryFn: getRoutes }], + }); + const { currentUniversity, getCurrentUniversityLngLat } = useSearchBuilding(); + + const { mapRef, map, mapLoaded, AdvancedMarker, Polyline } = useMap(); + const [routes] = result; + const [selectedEdge, setSelectedEdge] = useState<{ + info: CoreRoute; + marker1: AdvancedMarker; + marker2: AdvancedMarker; + polyline: Polyline; + }>(); + + const drawRoute = (coreRouteList: CoreRoutesList) => { + if (!Polyline || !AdvancedMarker || !map) return; + + for (const coreRoutes of coreRouteList) { + const { coreNode1Id, coreNode2Id, routes: subRoutes } = coreRoutes; + + // 가장 끝쪽 Core Node 그리기 + const endNode = subRoutes[subRoutes.length - 1].node2; + + new AdvancedMarker({ + map: map, + position: endNode, + content: createMarkerElement({ + type: "waypoint", + className: "translate-waypoint", + }), + }); + + const subNodes = [subRoutes[0].node1, ...subRoutes.map((el) => el.node2)]; + + const routePolyLine = new Polyline({ + map: map, + path: subNodes.map((el) => { + return { lat: el.lat, lng: el.lng }; + }), + strokeColor: "#808080", + }); + + routePolyLine.addListener( + "click", + (e: { latLng: google.maps.LatLng }) => { + const edges: CoreRoute[] = subRoutes.map( + ({ routeId, node1, node2 }) => { + return { routeId, node1, node2 }; + } + ); + + const { edge: nearestEdge } = findNearestSubEdge(edges, { + lat: e.latLng.lat(), + lng: e.latLng.lng(), + }); + + const { node1, node2 } = nearestEdge; + + const newPolyline = new Polyline({ + map: map, + path: [ + { lat: node1.lat, lng: node1.lng }, + { lat: node2.lat, lng: node2.lng }, + ], + strokeColor: "#000000", + zIndex: 100, + }); + + const marker1 = new AdvancedMarker({ + map: map, + position: { lat: node1.lat, lng: node1.lng }, + content: createMarkerElement({ + type: "waypoint_red", + className: "translate-waypoint", + }), + zIndex: 100, + }); + + const marker2 = new AdvancedMarker({ + map: map, + position: { lat: node2.lat, lng: node2.lng }, + content: createMarkerElement({ + type: "waypoint_blue", + className: "translate-waypoint", + }), + zIndex: 100, + }); + + setSelectedEdge({ + info: nearestEdge, + marker1: marker1, + marker2: marker2, + polyline: newPolyline, + }); + } + ); + + const startNode = subRoutes[0].node1; + + new AdvancedMarker({ + map: map, + position: startNode, + content: createMarkerElement({ + type: "waypoint", + className: "translate-waypoint", + }), + }); + } + }; + + useEffect(() => { + if (routes.data) { + drawRoute(routes.data); + } + }, [routes]); + + useEffect(() => { + if (!map || !mapLoaded) return; + const universityLatLng = getCurrentUniversityLngLat(); + console.log("Setting center to:", universityLatLng); + map.setCenter(universityLatLng); + }, [currentUniversity, mapLoaded]); + + useEffect(() => { + return () => { + if (!selectedEdge) return; + const { marker1, marker2, polyline } = selectedEdge; + marker1.map = null; + marker2.map = null; + polyline.setMap(null); + }; + }, [selectedEdge]); + + return ( + +
+
+ + + {" "} + + + + + + + + + + + +
선택된 간선 정보ID
node1 (red) {selectedEdge?.info.node1.nodeId}
node2 (red) {selectedEdge?.info.node2.nodeId}
route (black) {selectedEdge?.info.routeId}
+
+
+
+
+
+
+ ); +}; + +export default SimulationPage; diff --git a/uniro_admin_frontend/src/utils/coordinates/centerCoordinate.ts b/uniro_admin_frontend/src/utils/coordinates/centerCoordinate.ts new file mode 100644 index 0000000..eb06c12 --- /dev/null +++ b/uniro_admin_frontend/src/utils/coordinates/centerCoordinate.ts @@ -0,0 +1,8 @@ +import { Coord } from "../../data/types/coord"; + +export default function centerCoordinate(point1: Coord, point2: Coord): Coord { + return { + lat: (point1.lat + point2.lat) / 2, + lng: (point1.lng + point2.lng) / 2, + }; +} diff --git a/uniro_admin_frontend/src/utils/coordinates/coordinateTransform.ts b/uniro_admin_frontend/src/utils/coordinates/coordinateTransform.ts new file mode 100644 index 0000000..088eab8 --- /dev/null +++ b/uniro_admin_frontend/src/utils/coordinates/coordinateTransform.ts @@ -0,0 +1,6 @@ +export function LatLngToLiteral(coordinate: google.maps.LatLng): google.maps.LatLngLiteral { + return { + lat: coordinate.lat(), + lng: coordinate.lng(), + }; +} diff --git a/uniro_admin_frontend/src/utils/coordinates/distance.ts b/uniro_admin_frontend/src/utils/coordinates/distance.ts new file mode 100644 index 0000000..44bad78 --- /dev/null +++ b/uniro_admin_frontend/src/utils/coordinates/distance.ts @@ -0,0 +1,16 @@ +/** 하버사인 공식 */ +export default function distance(point1: google.maps.LatLngLiteral, point2: google.maps.LatLngLiteral): number { + const { lat: lat1, lng: lng1 } = point1; + const { lat: lat2, lng: lng2 } = point2; + + const R = 6371000; + const toRad = (angle: number) => (angle * Math.PI) / 180; + + const dLat = toRad(lat2 - lat1); + const dLon = toRad(lng2 - lng1); + + const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return R * c; +} diff --git a/uniro_admin_frontend/src/utils/fetch/fetch.ts b/uniro_admin_frontend/src/utils/fetch/fetch.ts new file mode 100644 index 0000000..b31bbf4 --- /dev/null +++ b/uniro_admin_frontend/src/utils/fetch/fetch.ts @@ -0,0 +1,57 @@ +export default function Fetch() { + const baseURL = import.meta.env.VITE_REACT_SERVER_BASE_URL; + + const get = async (url: string, params?: Record): Promise => { + const paramsURL = new URLSearchParams( + Object.entries(params || {}).map(([key, value]) => [key, String(value)]), + ).toString(); + + const response = await fetch(`${baseURL}${url}?${paramsURL}`, { + method: "GET", + }); + + if (!response.ok) { + throw new Error(`${response.status}-${response.statusText}`); + } + + return response.json(); + }; + + const post = async (url: string, body?: Record): Promise => { + const response = await fetch(`${baseURL}${url}`, { + method: "POST", + body: JSON.stringify(body), + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`${response.status}-${response.statusText}`); + } + + return response.ok; + }; + + const put = async (url: string, body?: Record): Promise => { + const response = await fetch(`${baseURL}${url}`, { + method: "PUT", + body: JSON.stringify(body), + }); + + if (!response.ok) { + throw new Error(`${response.status}-${response.statusText}`); + } + + return response.json(); + }; + + return { + get, + post, + put, + }; +} + +const { get, post, put } = Fetch(); +export { get as getFetch, post as postFetch, put as putFetch }; diff --git a/uniro_admin_frontend/src/utils/fetch/mockFetch.ts b/uniro_admin_frontend/src/utils/fetch/mockFetch.ts new file mode 100644 index 0000000..d6780aa --- /dev/null +++ b/uniro_admin_frontend/src/utils/fetch/mockFetch.ts @@ -0,0 +1,27 @@ +export function mockRealisticFetch( + data: T, + minDelay: number = 1000, + maxDelay: number = 4000, + failRate: number = 0.2, +): Promise { + const delay = Math.floor(Math.random() * (maxDelay - minDelay + 1)) + minDelay; + const shouldFail = Math.random() < failRate; + + return new Promise((resolve, reject) => { + console.log(`${delay / 1000}초 동안 로딩 중...`); + + setTimeout(() => { + if (shouldFail) { + console.error("네트워크 오류 발생!"); + reject(new Error("네트워크 오류 발생")); + } else { + console.log("데이터 로드 완료:", data); + resolve(data); + } + }, delay); + }); +} + +export const getMockTest = async () => { + return mockRealisticFetch({ message: "Hello from Mock API" }); +}; diff --git a/uniro_admin_frontend/src/utils/markers/createAdvanedMarker.ts b/uniro_admin_frontend/src/utils/markers/createAdvanedMarker.ts new file mode 100644 index 0000000..28fb887 --- /dev/null +++ b/uniro_admin_frontend/src/utils/markers/createAdvanedMarker.ts @@ -0,0 +1,47 @@ +export default function createAdvancedMarker( + AdvancedMarker: typeof google.maps.marker.AdvancedMarkerElement, + map: google.maps.Map | null, + position: google.maps.LatLng | google.maps.LatLngLiteral, + content: HTMLElement, + onClick?: () => void, +) { + const newMarker = new AdvancedMarker({ + map: map, + position: position, + content: content, + }); + + if (onClick) newMarker.addListener("click", onClick); + + return newMarker; +} + +export function createUniversityMarker( + AdvancedMarker: typeof google.maps.marker.AdvancedMarkerElement, + map: google.maps.Map | null, + position: google.maps.LatLng | google.maps.LatLngLiteral, + university: string, +) { + const container = document.createElement("div"); + container.className = `flex flex-col items-center`; + const markerTitle = document.createElement("p"); + markerTitle.innerText = university ? university : ""; + markerTitle.className = + "py-1 px-3 text-kor-caption font-medium text-gray-100 bg-primary-500 text-center rounded-200"; + container.appendChild(markerTitle); + const markerImage = document.createElement("img"); + + const markerImages = import.meta.glob("/src/assets/markers/*.svg", { eager: true }); + + markerImage.className = "border-0 translate-y-[-1px]"; + markerImage.src = (markerImages[`/src/assets/markers/university.svg`] as { default: string })?.default; + container.appendChild(markerImage); + + const newMarker = new AdvancedMarker({ + map: map, + position: position, + content: container, + }); + + return newMarker; +} diff --git a/uniro_admin_frontend/src/utils/markers/toggleMarkers.ts b/uniro_admin_frontend/src/utils/markers/toggleMarkers.ts new file mode 100644 index 0000000..bcfc52a --- /dev/null +++ b/uniro_admin_frontend/src/utils/markers/toggleMarkers.ts @@ -0,0 +1,14 @@ +import { AdvancedMarker } from "../../data/types/marker"; + +/** Marker 보이기 안보이기 토글 */ +export default function toggleMarkers(isActive: boolean, markers: AdvancedMarker[], map: google.maps.Map) { + if (isActive) { + for (const marker of markers) { + marker.map = map; + } + } else { + for (const marker of markers) { + marker.map = null; + } + } +} diff --git a/uniro_admin_frontend/src/utils/navigation/formatDistance.ts b/uniro_admin_frontend/src/utils/navigation/formatDistance.ts new file mode 100644 index 0000000..f2e81c1 --- /dev/null +++ b/uniro_admin_frontend/src/utils/navigation/formatDistance.ts @@ -0,0 +1,9 @@ +export const formatDistance = (distance: number) => { + if (distance < 1) { + return `${Math.ceil(distance * 1000) / 1000}m`; + } + + return distance >= 1000 + ? `${(distance / 1000).toFixed(1)} km` // 1000m 이상이면 km 단위로 변환 + : `${distance} m`; // 1000m 미만이면 m 단위 유지 +}; diff --git a/uniro_admin_frontend/src/utils/polylines/createSubnodes.ts b/uniro_admin_frontend/src/utils/polylines/createSubnodes.ts new file mode 100644 index 0000000..4560fd3 --- /dev/null +++ b/uniro_admin_frontend/src/utils/polylines/createSubnodes.ts @@ -0,0 +1,33 @@ +import { EDGE_LENGTH } from "../../constant/edge"; +import { Coord } from "../../data/types/coord"; +import { LatLngToLiteral } from "../coordinates/coordinateTransform"; +import distance from "../coordinates/distance"; + +/** 구면 보간 없이 계산한 결과 */ +export default function createSubNodes(polyLine: google.maps.Polyline): Coord[] { + const paths = polyLine.getPath(); + const [startNode, endNode] = paths.getArray().map((el) => LatLngToLiteral(el)); + + const length = distance(startNode, endNode); + + const subEdgesCount = Math.ceil(length / EDGE_LENGTH); + + const interval = { + lat: (endNode.lat - startNode.lat) / subEdgesCount, + lng: (endNode.lng - startNode.lng) / subEdgesCount, + }; + + const subNodes = []; + + for (let i = 0; i < subEdgesCount; i++) { + const subNode: Coord = { + lat: startNode.lat + interval.lat * i, + lng: startNode.lng + interval.lng * i, + }; + subNodes.push(subNode); + } + + subNodes.push(endNode); + + return subNodes; +} diff --git a/uniro_admin_frontend/src/utils/polylines/findNearestEdge.ts b/uniro_admin_frontend/src/utils/polylines/findNearestEdge.ts new file mode 100644 index 0000000..a479280 --- /dev/null +++ b/uniro_admin_frontend/src/utils/polylines/findNearestEdge.ts @@ -0,0 +1,36 @@ +import { Coord } from "../../data/types/coord"; +import { Node } from "../../data/types/node"; +import { CoreRoute } from "../../data/types/route"; +import centerCoordinate from "../coordinates/centerCoordinate"; +import distance from "../coordinates/distance"; + +export default function findNearestSubEdge( + edges: CoreRoute[], + point: Coord +): { + edge: CoreRoute; + point: Node; +} { + const edgesWithDistance = edges + .map((edge) => { + return { + edge, + distance: distance(point, centerCoordinate(edge.node1, edge.node2)), + }; + }) + .sort((a, b) => { + return a.distance - b.distance; + }); + + const nearestEdge = edgesWithDistance[0].edge; + + const { node1, node2 } = nearestEdge; + const distance0 = distance(node1, point); + const distance1 = distance(node2, point); + const nearestPoint = distance0 > distance1 ? node2 : node1; + + return { + edge: nearestEdge, + point: nearestPoint, + }; +} diff --git a/uniro_admin_frontend/src/utils/report/getThemeByPassableStatus.ts b/uniro_admin_frontend/src/utils/report/getThemeByPassableStatus.ts new file mode 100644 index 0000000..f58b320 --- /dev/null +++ b/uniro_admin_frontend/src/utils/report/getThemeByPassableStatus.ts @@ -0,0 +1,6 @@ +import { PassableStatus } from "../../constant/enum/reportEnum"; +import { THEME_MAP } from "../../constant/reportTheme"; + +export const getThemeByPassableStatus = (status: PassableStatus): string => { + return THEME_MAP[status]; +}; diff --git a/uniro_admin_frontend/src/utils/route.ts b/uniro_admin_frontend/src/utils/route.ts new file mode 100644 index 0000000..fead5dd --- /dev/null +++ b/uniro_admin_frontend/src/utils/route.ts @@ -0,0 +1,43 @@ +import { Node, NodeId } from "../data/types/node"; +import { CoreRoutesList } from "../data/types/route"; + +type CoreRoutesResponse = { + coreNode1Id: NodeId; + coreNode2Id: NodeId; + routes: { + routeId: NodeId; + startNodeId: NodeId; + endNodeId: NodeId; + }[]; +}; + +export const transformAllRoutes = (data: { + nodeInfos: Node[]; + coreRoutes: CoreRoutesResponse[]; +}): CoreRoutesList => { + const { nodeInfos, coreRoutes } = data; + const nodeInfoMap = new Map(nodeInfos.map((node) => [node.nodeId, node])); + + return coreRoutes.map((coreRoute) => { + return { + ...coreRoute, + routes: coreRoute.routes.map((route) => { + const node1 = nodeInfoMap.get(route.startNodeId); + const node2 = nodeInfoMap.get(route.endNodeId); + + if (!node1) { + throw new Error(`Node not found: ${route.startNodeId}`); + } + if (!node2) { + throw new Error(`Node not found: ${route.endNodeId}`); + } + + return { + routeId: route.routeId, + node1, + node2, + }; + }), + }; + }); +}; diff --git a/uniro_backend/.gitignore b/uniro_backend/.gitignore index 39864c4..9c3a336 100644 --- a/uniro_backend/.gitignore +++ b/uniro_backend/.gitignore @@ -38,3 +38,5 @@ out/ ### properties *.properties +*.sql +!src/test/resources/sql/*.sql \ No newline at end of file diff --git a/uniro_backend/build.gradle b/uniro_backend/build.gradle index ea48f8e..d859c80 100644 --- a/uniro_backend/build.gradle +++ b/uniro_backend/build.gradle @@ -71,6 +71,19 @@ dependencies { // actuator implementation 'org.springframework.boot:spring-boot-starter-actuator' + + // test container + testImplementation 'org.testcontainers:testcontainers:1.19.5' + testImplementation 'org.testcontainers:junit-jupiter:1.19.5' + testImplementation 'org.testcontainers:jdbc:1.19.5' + testImplementation 'org.testcontainers:mysql:1.19.5' + + // jwt + implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5' + implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5' + implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5' + implementation 'com.auth0:java-jwt:4.4.0' + } tasks.named('test') { diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/UniroBackendApplication.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/UniroBackendApplication.java index b42e9b4..c95e7ab 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/UniroBackendApplication.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/UniroBackendApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing public class UniroBackendApplication { public static void main(String[] args) { diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/annotation/DisableAudit.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/annotation/DisableAudit.java new file mode 100644 index 0000000..7298f5e --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/annotation/DisableAudit.java @@ -0,0 +1,11 @@ +package com.softeer5.uniro_backend.admin.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface DisableAudit { +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/annotation/RevisionOperation.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/annotation/RevisionOperation.java index a767ece..b84dced 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/annotation/RevisionOperation.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/annotation/RevisionOperation.java @@ -1,6 +1,6 @@ package com.softeer5.uniro_backend.admin.annotation; -import com.softeer5.uniro_backend.admin.entity.RevisionOperationType; +import com.softeer5.uniro_backend.admin.enums.RevisionOperationType; import java.lang.annotation.*; diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/aspect/CustomPostUpdateListener.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/aspect/CustomPostUpdateListener.java new file mode 100644 index 0000000..6186582 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/aspect/CustomPostUpdateListener.java @@ -0,0 +1,25 @@ +package com.softeer5.uniro_backend.admin.aspect; + +import org.hibernate.envers.boot.internal.EnversService; +import org.hibernate.envers.event.spi.EnversPostUpdateEventListenerImpl; +import org.hibernate.event.spi.PostUpdateEvent; + +import com.softeer5.uniro_backend.admin.setting.RevisionContext; +import com.softeer5.uniro_backend.admin.setting.RevisionType; + +public class CustomPostUpdateListener extends EnversPostUpdateEventListenerImpl { + public CustomPostUpdateListener(EnversService enversService) { + super(enversService); + } + + @Override + public void onPostUpdate(PostUpdateEvent event) { + RevisionType revisionType = RevisionContext.getRevisionType(); + + if (revisionType == RevisionType.IGNORE) { + // Ignore this entity + return; + } + super.onPostUpdate(event); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/aspect/DisableEnversAspect.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/aspect/DisableEnversAspect.java new file mode 100644 index 0000000..437f594 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/aspect/DisableEnversAspect.java @@ -0,0 +1,30 @@ +package com.softeer5.uniro_backend.admin.aspect; + +import static com.softeer5.uniro_backend.common.constant.UniroConst.*; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import com.softeer5.uniro_backend.admin.setting.RevisionContext; +import com.softeer5.uniro_backend.admin.setting.RevisionType; + +@Component +@Aspect +@Order(BEFORE_DEFAULT_ORDER) +public class DisableEnversAspect { + @Around("@annotation(com.softeer5.uniro_backend.admin.annotation.DisableAudit)") + public Object disableAudit(ProceedingJoinPoint joinPoint) throws Throwable { + // 현재 스레드에서 Envers 감사 비활성화 + RevisionContext.setRevisionType(RevisionType.IGNORE); + try { + return joinPoint.proceed(); // 트랜잭션 실행 + } finally { + // 트랜잭션 종료 후 다시 감사 활성화 + RevisionContext.clear(); + } + } +} + diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/aspect/RevisionOperationAspect.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/aspect/RevisionOperationAspect.java index 86e304f..2fd0a0b 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/aspect/RevisionOperationAspect.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/aspect/RevisionOperationAspect.java @@ -1,10 +1,9 @@ package com.softeer5.uniro_backend.admin.aspect; import com.softeer5.uniro_backend.admin.annotation.RevisionOperation; -import com.softeer5.uniro_backend.admin.entity.RevisionOperationType; +import com.softeer5.uniro_backend.admin.enums.RevisionOperationType; import com.softeer5.uniro_backend.admin.setting.RevisionContext; -import com.softeer5.uniro_backend.route.dto.request.CreateRoutesReqDTO; -import com.softeer5.uniro_backend.route.dto.request.PostRiskReqDTO; +import com.softeer5.uniro_backend.map.dto.request.PostRiskReqDTO; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; @@ -28,6 +27,8 @@ public Object around(ProceedingJoinPoint joinPoint, RevisionOperation revisionOp switch (opType) { case UPDATE_RISK -> result = updateRiskHandler(joinPoint); case CREATE_ROUTE -> result = updateRouteHandler(joinPoint); + case CREATE_BUILDING_NODE -> result = createBuildingNodeHandler(joinPoint); + case CREATE_BUILDING_ROUTE -> result = createBuildingRouteHandler(joinPoint); default -> result = joinPoint.proceed(); } @@ -46,8 +47,8 @@ private Object updateRiskHandler(ProceedingJoinPoint joinPoint) throws Throwable univId = (Long) args[i]; } else if(args[i] instanceof PostRiskReqDTO postRiskReqDTO){ - int cautionSize = postRiskReqDTO.getCautionTypes().size(); - int dangerSize = postRiskReqDTO.getDangerTypes().size(); + int cautionSize = postRiskReqDTO.getCautionFactors().size(); + int dangerSize = postRiskReqDTO.getDangerFactors().size(); if (cautionSize > 0) { action = "주의요소 업데이트"; @@ -90,4 +91,51 @@ private Object updateRouteHandler(ProceedingJoinPoint joinPoint) throws Throwabl RevisionContext.clear(); } } + + private Object createBuildingNodeHandler(ProceedingJoinPoint joinPoint) throws Throwable { + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + String[] parameterNames = signature.getParameterNames(); + Object[] args = joinPoint.getArgs(); + + Long univId = null; + String action = "빌딩 노드 추가"; + + for (int i = 0; i < args.length; i++) { + if (args[i] instanceof Long && "univId".equals(parameterNames[i])) { + univId = (Long) args[i]; + } + } + RevisionContext.setUnivId(univId); + RevisionContext.setAction(action); + try{ + return joinPoint.proceed(); + } + finally { + RevisionContext.clear(); + } + } + + private Object createBuildingRouteHandler(ProceedingJoinPoint joinPoint) throws Throwable{ + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + String[] parameterNames = signature.getParameterNames(); + Object[] args = joinPoint.getArgs(); + + Long univId = null; + String action = "빌딩 route 추가"; + + for (int i = 0; i < args.length; i++) { + if (args[i] instanceof Long && "univId".equals(parameterNames[i])) { + univId = (Long) args[i]; + } + } + RevisionContext.setUnivId(univId); + RevisionContext.setAction(action); + try{ + return joinPoint.proceed(); + } + finally { + RevisionContext.clear(); + } + } + } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/controller/AdminApi.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/controller/AdminApi.java index e28d4d1..53e1066 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/controller/AdminApi.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/controller/AdminApi.java @@ -1,6 +1,7 @@ package com.softeer5.uniro_backend.admin.controller; -import com.softeer5.uniro_backend.admin.dto.RevInfoDTO; +import com.softeer5.uniro_backend.admin.dto.response.GetAllRoutesByRevisionResDTO; +import com.softeer5.uniro_backend.admin.dto.response.RevInfoDTO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -21,4 +22,21 @@ public interface AdminApi { }) ResponseEntity> getAllRev(@PathVariable("univId") Long univId); + @Operation(summary = "특정 버전으로 롤백") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "특정 버전으로 롤백 성공"), + @ApiResponse(responseCode = "400", description = "EXCEPTION(임시)", content = @Content), + }) + ResponseEntity rollbackRev( + @PathVariable("univId") Long univId, + @PathVariable("versionId") Long versionId); + + @Operation(summary = "특정 버전 지도 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "특정 버전 지도 조회 성공"), + @ApiResponse(responseCode = "400", description = "EXCEPTION(임시)", content = @Content), + }) + ResponseEntity getAllRoutesByRevision( + @PathVariable("univId") Long univId, + @PathVariable("versionId") Long versionId); } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/controller/AdminController.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/controller/AdminController.java index 1ed101a..a59bc03 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/controller/AdminController.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/controller/AdminController.java @@ -1,10 +1,13 @@ package com.softeer5.uniro_backend.admin.controller; -import com.softeer5.uniro_backend.admin.dto.RevInfoDTO; +import com.softeer5.uniro_backend.admin.dto.response.GetAllRoutesByRevisionResDTO; +import com.softeer5.uniro_backend.admin.dto.response.RevInfoDTO; import com.softeer5.uniro_backend.admin.service.AdminService; import lombok.RequiredArgsConstructor; + import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @@ -16,8 +19,29 @@ public class AdminController implements AdminApi { private final AdminService adminService; @Override - @GetMapping("/admin/revision/{univId}") + @GetMapping("/admin/{univId}/revisions") public ResponseEntity> getAllRev(@PathVariable("univId") Long univId) { return ResponseEntity.ok().body(adminService.getAllRevInfo(univId)); } + + @Override + @PatchMapping("/admin/{univId}/revisions/{versionId}") + public ResponseEntity rollbackRev( + @PathVariable("univId") Long univId, + @PathVariable("versionId") Long versionId) { + + adminService.rollbackRev(univId, versionId); + + return ResponseEntity.ok().build(); + } + + @Override + @GetMapping("/admin/{univId}/revisions/{versionId}") + public ResponseEntity getAllRoutesByRevision( + @PathVariable("univId") Long univId, + @PathVariable("versionId") Long versionId) { + + GetAllRoutesByRevisionResDTO resDTO = adminService.getAllRoutesByRevision(univId, versionId); + return ResponseEntity.ok().body(resDTO); + } } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/controller/AuthApi.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/controller/AuthApi.java new file mode 100644 index 0000000..ec3d93d --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/controller/AuthApi.java @@ -0,0 +1,24 @@ +package com.softeer5.uniro_backend.admin.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; + +import com.softeer5.uniro_backend.admin.dto.request.LoginReqDTO; +import com.softeer5.uniro_backend.admin.dto.response.LoginResDTO; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; + +@Tag(name = "admin 로그인 페이지 API") +public interface AuthApi { + @Operation(summary = "로그인") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인 성공"), + @ApiResponse(responseCode = "400", description = "EXCEPTION(임시)", content = @Content), + }) + ResponseEntity login(@RequestBody @Valid LoginReqDTO loginReqDTO); +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/controller/AuthController.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/controller/AuthController.java new file mode 100644 index 0000000..4e16dfe --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/controller/AuthController.java @@ -0,0 +1,25 @@ +package com.softeer5.uniro_backend.admin.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import com.softeer5.uniro_backend.admin.dto.request.LoginReqDTO; +import com.softeer5.uniro_backend.admin.dto.response.LoginResDTO; +import com.softeer5.uniro_backend.admin.service.AuthService; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +public class AuthController implements AuthApi { + private final AuthService authService; + @Override + @PostMapping("/admin/auth/login") + public ResponseEntity login(@RequestBody @Valid LoginReqDTO loginReqDTO) { + LoginResDTO loginResDTO = authService.login(loginReqDTO); + return ResponseEntity.ok(loginResDTO); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/request/LoginReqDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/request/LoginReqDTO.java new file mode 100644 index 0000000..4077e87 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/request/LoginReqDTO.java @@ -0,0 +1,20 @@ +package com.softeer5.uniro_backend.admin.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Schema(name = "LoginReqDTO", description = "로그인 요청 DTO") +public class LoginReqDTO { + @Schema(description = "대학교 id", example = "1001") + @NotNull + private final Long univId; + + @Schema(description = "인증 코드", example = "abcd123") + @NotNull + private final String code; +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/ChangedRouteDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/ChangedRouteDTO.java new file mode 100644 index 0000000..670809d --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/ChangedRouteDTO.java @@ -0,0 +1,22 @@ +package com.softeer5.uniro_backend.admin.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Schema(name = "ChangedRouteDTO", description = "위험요소, 주의요소, cost가 변경된 길의 현재&과거 정보 DTO") +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class ChangedRouteDTO { + @Schema(description = "route ID", example = "17") + private final Long routeId; + @Schema(description = "현재", example = "") + private final RouteDifferInfo current; + @Schema(description = "해당 버전 (과거)", example = "") + private final RouteDifferInfo revision; + + public static ChangedRouteDTO of(Long id, RouteDifferInfo current, RouteDifferInfo revision) { + return new ChangedRouteDTO(id, current, revision); + } +} \ No newline at end of file diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/GetAllRoutesByRevisionResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/GetAllRoutesByRevisionResDTO.java new file mode 100644 index 0000000..4f8c383 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/GetAllRoutesByRevisionResDTO.java @@ -0,0 +1,29 @@ +package com.softeer5.uniro_backend.admin.dto.response; + +import com.softeer5.uniro_backend.map.dto.response.GetAllRoutesResDTO; +import com.softeer5.uniro_backend.map.dto.response.GetRiskRoutesResDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@Schema(name = "GetAllRoutesByRevisionResDTO", description = "특정 버전 map 조회 응답 DTO") +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class GetAllRoutesByRevisionResDTO { + @Schema(description = "특정 버전에 존재하는 길&노드 스냅샷 정보", example = "") + private final GetAllRoutesResDTO routesInfo; + @Schema(description = "특정 버전에 존재하는 위험 요소 스냅샷 정보", example = "") + private final GetRiskRoutesResDTO getRiskRoutesResDTO; + @Schema(description = "삭제된 길&노드 정보 정보", example = "") + private final LostRoutesDTO lostRoutes; + @Schema(description = "현재 버전과 비교하여 변경된 주의/위험 요소 정보", example = "") + private final List changedList; + + public static GetAllRoutesByRevisionResDTO of(GetAllRoutesResDTO routesInfo, GetRiskRoutesResDTO getRiskRoutesResDTO, + LostRoutesDTO lostRoutes, List changedList) { + return new GetAllRoutesByRevisionResDTO(routesInfo, getRiskRoutesResDTO, lostRoutes, changedList); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/LoginResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/LoginResDTO.java new file mode 100644 index 0000000..d01157f --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/LoginResDTO.java @@ -0,0 +1,17 @@ +package com.softeer5.uniro_backend.admin.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@Schema(name = "LoginResDTO", description = "로그인 응답 DTO") +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class LoginResDTO { + private final String accessToken; + + public static LoginResDTO of(String accessToken) { + return new LoginResDTO(accessToken); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/LostRoutesDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/LostRoutesDTO.java new file mode 100644 index 0000000..82fe21a --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/LostRoutesDTO.java @@ -0,0 +1,24 @@ +package com.softeer5.uniro_backend.admin.dto.response; + +import com.softeer5.uniro_backend.map.dto.response.CoreRouteResDTO; +import com.softeer5.uniro_backend.map.dto.response.NodeInfoResDTO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@Schema(name = "LostRoutesDTO", description = "삭제된 길의 코어 route DTO") +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class LostRoutesDTO { + @Schema(description = "노드 정보 (id, 좌표)", example = "") + private final List nodeInfos; + @Schema(description = "루트 정보 (id, startNodeId, endNodeId)", example = "") + private final List coreRoutes; + + public static LostRoutesDTO of(List nodeInfos, List coreRoutes){ + return new LostRoutesDTO(nodeInfos, coreRoutes); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/RevInfoDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/RevInfoDTO.java similarity index 90% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/RevInfoDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/RevInfoDTO.java index 40f287c..b550ae6 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/RevInfoDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/RevInfoDTO.java @@ -1,6 +1,5 @@ -package com.softeer5.uniro_backend.admin.dto; +package com.softeer5.uniro_backend.admin.dto.response; -import com.softeer5.uniro_backend.admin.entity.RevInfo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/RouteDifferInfo.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/RouteDifferInfo.java new file mode 100644 index 0000000..47dc779 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/response/RouteDifferInfo.java @@ -0,0 +1,27 @@ +package com.softeer5.uniro_backend.admin.dto.response; + +import com.softeer5.uniro_backend.map.enums.CautionFactor; +import com.softeer5.uniro_backend.map.enums.DangerFactor; +import com.softeer5.uniro_backend.map.entity.Route; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@Schema(name = "RouteDifferInfo", description = "cost, 위험요소, 주의요소 정보 DTO") +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class RouteDifferInfo { + @Schema(description = "주의요소 타입 리스트", example = "[\"SLOPE\", \"STAIRS\"]") + private final List cautionFactors; + @Schema(description = "위험 요소 타입 리스트", example = "[\"SLOPE\", \"STAIRS\"]") + private final List dangerFactors; + @Schema(description = "가중치", example = "3.021742") + private final double cost; + + public static RouteDifferInfo of(Route route) { + return new RouteDifferInfo(route.getCautionFactorsByList(), route.getDangerFactorsByList(), route.getDistance()); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/entity/Admin.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/entity/Admin.java new file mode 100644 index 0000000..9f52483 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/entity/Admin.java @@ -0,0 +1,33 @@ +package com.softeer5.uniro_backend.admin.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Admin { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + private String code; + + @NotNull + private Long univId; + + @Builder + private Admin(Long id, String code, Long univId) { + this.id = id; + this.code = code; + this.univId = univId; + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/entity/RevisionOperationType.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/entity/RevisionOperationType.java deleted file mode 100644 index 070e6ab..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/entity/RevisionOperationType.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.softeer5.uniro_backend.admin.entity; - -public enum RevisionOperationType { - UPDATE_RISK, - CREATE_ROUTE, - DEFAULT; -} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/enums/RevisionOperationType.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/enums/RevisionOperationType.java new file mode 100644 index 0000000..09bced8 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/enums/RevisionOperationType.java @@ -0,0 +1,9 @@ +package com.softeer5.uniro_backend.admin.enums; + +public enum RevisionOperationType { + UPDATE_RISK, + CREATE_ROUTE, + CREATE_BUILDING_NODE, + CREATE_BUILDING_ROUTE, + DEFAULT; +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/interceptor/AdminInterceptor.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/interceptor/AdminInterceptor.java new file mode 100644 index 0000000..93d5f5c --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/interceptor/AdminInterceptor.java @@ -0,0 +1,38 @@ +package com.softeer5.uniro_backend.admin.interceptor; + +import static com.softeer5.uniro_backend.common.error.ErrorCode.*; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import com.softeer5.uniro_backend.admin.jwt.SecurityContext; +import com.softeer5.uniro_backend.common.exception.custom.AdminException; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class AdminInterceptor implements HandlerInterceptor { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String requestURI = request.getRequestURI(); + + // "/admin/{univId}/" 패턴만 검증 + Pattern pattern = Pattern.compile("^/admin/(\\d+)/.*$"); + Matcher matcher = pattern.matcher(requestURI); + + if (matcher.matches()) { + Long univId = Long.valueOf(matcher.group(1)); // URL에서 univId 추출 + Long adminUnivId = SecurityContext.getUnivId(); // JWT에서 가져온 univId + + if (!univId.equals(adminUnivId)) { + throw new AdminException("Unauthorized university access", UNAUTHORIZED_UNIV); + } + } + return true; + } +} + diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/interceptor/JwtInterceptor.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/interceptor/JwtInterceptor.java new file mode 100644 index 0000000..16a44d8 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/interceptor/JwtInterceptor.java @@ -0,0 +1,49 @@ +package com.softeer5.uniro_backend.admin.interceptor; + +import com.softeer5.uniro_backend.admin.jwt.JwtTokenProvider; +import com.softeer5.uniro_backend.admin.jwt.SecurityContext; +import com.softeer5.uniro_backend.common.exception.custom.AdminException; +import com.softeer5.uniro_backend.common.error.ErrorCode; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.stereotype.Component; + +@Component +public class JwtInterceptor implements HandlerInterceptor { + private final JwtTokenProvider jwtTokenProvider; + + public JwtInterceptor(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // Authorization 헤더에서 JWT 토큰 추출 + + String authHeader = request.getHeader("Authorization"); + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + throw new AdminException("Invalid token", ErrorCode.INVALID_TOKEN); + } + + String token = authHeader.substring(7); // "Bearer " 제거 + + if (!jwtTokenProvider.validateToken(token)) { + throw new AdminException("Invalid token", ErrorCode.INVALID_TOKEN); + } + + Long univId = jwtTokenProvider.getUnivId(token); + + // ✅ SecurityContext에 저장 (ThreadLocal 방식) + SecurityContext.setUnivId(univId); + + return true; // 컨트롤러 실행 허용 + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + // 요청 처리 후 SecurityContext 비우기 + SecurityContext.clear(); + } +} + diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/jwt/JwtTokenProvider.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..dc31562 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/jwt/JwtTokenProvider.java @@ -0,0 +1,50 @@ +package com.softeer5.uniro_backend.admin.jwt; + +import java.util.Date; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; + +@Component +public class JwtTokenProvider { + + @Value("${jwt.secret}") + private String secretKey; // 실제로는 안전한 방법으로 관리해야 함 + private final long validityInMilliseconds = 36000000000L; // 10000 hour + + public String createToken(Long univId) { + final Claims claims = Jwts.claims().setSubject(String.valueOf(univId)); + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + } + + public Long getUnivId(String token) { + return Long.parseLong(Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody() + .getSubject()); + } + + public boolean validateToken(String token) { + try { + Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } +} + diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/jwt/SecurityContext.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/jwt/SecurityContext.java new file mode 100644 index 0000000..b910518 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/jwt/SecurityContext.java @@ -0,0 +1,18 @@ +package com.softeer5.uniro_backend.admin.jwt; + +public class SecurityContext { + private static final ThreadLocal univIdHolder = new ThreadLocal<>(); + + public static void setUnivId(Long univId) { + univIdHolder.set(univId); + } + + public static Long getUnivId() { + return univIdHolder.get(); + } + + public static void clear() { + univIdHolder.remove(); + } +} + diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/repository/AdminRepository.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/repository/AdminRepository.java new file mode 100644 index 0000000..800bf10 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/repository/AdminRepository.java @@ -0,0 +1,12 @@ +package com.softeer5.uniro_backend.admin.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.softeer5.uniro_backend.admin.entity.Admin; + +public interface AdminRepository extends JpaRepository { + + Optional findByUnivId(Long univId); +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/repository/NodeAuditRepository.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/repository/NodeAuditRepository.java new file mode 100644 index 0000000..86d9fb1 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/repository/NodeAuditRepository.java @@ -0,0 +1,36 @@ +package com.softeer5.uniro_backend.admin.repository; + +import java.util.List; + +import org.hibernate.envers.AuditReader; +import org.hibernate.envers.AuditReaderFactory; +import org.hibernate.envers.query.AuditEntity; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import com.softeer5.uniro_backend.map.entity.Node; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +@Transactional +public class NodeAuditRepository { + private final EntityManager entityManager; + + public List getAllNodesAtRevision(Long univId, Long versionId) { + AuditReader auditReader = AuditReaderFactory.get(entityManager); + return auditReader.createQuery() + .forEntitiesAtRevision(Node.class, versionId) + .add(AuditEntity.property("univId").eq(univId)) + .getResultList(); + } + + public void deleteAllAfterVersionId(Long univId, Long versionId) { + entityManager.createNativeQuery("DELETE FROM node_aud WHERE univ_id = :univId AND rev > :versionId") + .setParameter("univId", univId) + .setParameter("versionId", versionId) + .executeUpdate(); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/repository/RevInfoRepository.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/repository/RevInfoRepository.java index a7ab85c..fbb1b7e 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/repository/RevInfoRepository.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/repository/RevInfoRepository.java @@ -2,10 +2,18 @@ import com.softeer5.uniro_backend.admin.entity.RevInfo; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.transaction.annotation.Transactional; import java.util.List; public interface RevInfoRepository extends JpaRepository { List findAllByUnivId(Long univId); + + @Modifying(clearAutomatically = true) + @Transactional + @Query("DELETE FROM RevInfo r WHERE r.univId = :univId AND r.rev > :versionId") + void deleteAllAfterVersionId(Long univId, Long versionId); } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/repository/RouteAuditRepository.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/repository/RouteAuditRepository.java new file mode 100644 index 0000000..ce0c486 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/repository/RouteAuditRepository.java @@ -0,0 +1,38 @@ +package com.softeer5.uniro_backend.admin.repository; + +import java.util.List; + +import org.hibernate.envers.AuditReader; +import org.hibernate.envers.AuditReaderFactory; +import org.hibernate.envers.query.AuditEntity; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import com.softeer5.uniro_backend.map.entity.Route; + +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +@Transactional +public class RouteAuditRepository { + + private final EntityManager entityManager; + + public List getAllRoutesAtRevision(Long univId, Long versionId) { + AuditReader auditReader = AuditReaderFactory.get(entityManager); + return auditReader.createQuery() + .forEntitiesAtRevision(Route.class, versionId) + .add(AuditEntity.property("univId").eq(univId)) + .getResultList(); + } + + public void deleteAllAfterVersionId(Long univId, Long versionId) { + entityManager.createNativeQuery("DELETE FROM route_aud WHERE univ_id = :univId AND rev > :versionId") + .setParameter("univId", univId) + .setParameter("versionId", versionId) + .executeUpdate(); + } + +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/service/AdminService.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/service/AdminService.java index 8dbd32a..7d43ccb 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/service/AdminService.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/service/AdminService.java @@ -1,18 +1,47 @@ package com.softeer5.uniro_backend.admin.service; -import com.softeer5.uniro_backend.admin.dto.RevInfoDTO; +import static com.softeer5.uniro_backend.common.error.ErrorCode.*; + +import com.softeer5.uniro_backend.admin.annotation.DisableAudit; +import com.softeer5.uniro_backend.admin.dto.response.*; +import com.softeer5.uniro_backend.admin.entity.RevInfo; +import com.softeer5.uniro_backend.admin.repository.NodeAuditRepository; import com.softeer5.uniro_backend.admin.repository.RevInfoRepository; +import com.softeer5.uniro_backend.admin.repository.RouteAuditRepository; +import com.softeer5.uniro_backend.building.repository.BuildingRepository; +import com.softeer5.uniro_backend.common.exception.custom.AdminException; +import com.softeer5.uniro_backend.common.exception.custom.RouteException; +import com.softeer5.uniro_backend.map.dto.response.GetAllRoutesResDTO; +import com.softeer5.uniro_backend.map.dto.response.GetRiskRoutesResDTO; +import com.softeer5.uniro_backend.map.dto.response.NodeInfoResDTO; +import com.softeer5.uniro_backend.map.entity.Node; +import com.softeer5.uniro_backend.map.entity.Route; +import com.softeer5.uniro_backend.map.repository.NodeRepository; +import com.softeer5.uniro_backend.map.repository.RouteRepository; +import com.softeer5.uniro_backend.map.service.RouteCalculator; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class AdminService { private final RevInfoRepository revInfoRepository; + private final RouteRepository routeRepository; + private final NodeRepository nodeRepository; + private final BuildingRepository buildingRepository; + + private final RouteAuditRepository routeAuditRepository; + private final NodeAuditRepository nodeAuditRepository; + + private final RouteCalculator routeCalculator; public List getAllRevInfo(Long univId){ return revInfoRepository.findAllByUnivId(univId).stream().map(r -> RevInfoDTO.of(r.getRev(), @@ -20,4 +49,129 @@ public List getAllRevInfo(Long univId){ r.getUnivId(), r.getAction())).toList(); } + + @Transactional + @DisableAudit + public void rollbackRev(Long univId, Long versionId){ + RevInfo revInfo = revInfoRepository.findById(versionId) + .orElseThrow(() -> new AdminException("invalid version id", INVALID_VERSION_ID)); + + List revRoutes = routeAuditRepository.getAllRoutesAtRevision(univId, versionId); + List revNodes = nodeAuditRepository.getAllNodesAtRevision(univId, versionId); + + routeRepository.deleteAllByCreatedAt(univId, revInfo.getRevTimeStamp()); + nodeRepository.deleteAllByCreatedAt(univId, revInfo.getRevTimeStamp()); + buildingRepository.deleteAllByCreatedAt(univId, revInfo.getRevTimeStamp()); + + routeAuditRepository.deleteAllAfterVersionId(univId, versionId); + nodeAuditRepository.deleteAllAfterVersionId(univId, versionId); + revInfoRepository.deleteAllAfterVersionId(univId, versionId); + + List routes = routeRepository.findAllRouteByUnivIdWithNodes(univId); + + Map routeMap = new HashMap<>(); + Map nodeMap = new HashMap<>(); + + for(Route route : routes){ + routeMap.put(route.getId(), route); + nodeMap.put(route.getNode1().getId(), route.getNode1()); + nodeMap.put(route.getNode2().getId(), route.getNode2()); + } + + // 매핑하여 업데이트 또는 새로 저장 + for (Route revRoute : revRoutes) { + Route currentRoute = routeMap.get(revRoute.getId()); + + if (currentRoute != null) { + currentRoute.updateFromRevision(revRoute); + } + } + + for (Node revNode : revNodes) { + Node currentNode = nodeMap.get(revNode.getId()); + + if (currentNode != null) { + currentNode.updateFromRevision(revNode.isCore()); + } + } + + } + + public GetAllRoutesByRevisionResDTO getAllRoutesByRevision(Long univId, Long versionId){ + revInfoRepository.findById(versionId) + .orElseThrow(() -> new AdminException("invalid version id", INVALID_VERSION_ID)); + + List revRoutes = routeAuditRepository.getAllRoutesAtRevision(univId, versionId); + + if(revRoutes.isEmpty()) { + throw new RouteException("Route Not Found", ROUTE_NOT_FOUND); + } + GetAllRoutesResDTO routesInfo = routeCalculator.assembleRoutes(revRoutes); + + Map revRouteMap = new HashMap<>(); + for(Route revRoute : revRoutes){ + revRouteMap.put(revRoute.getId(), revRoute); + } + + List routes = routeRepository.findAllRouteByUnivIdWithNodes(univId); + + Map> lostAdjMap = new HashMap<>(); + Map lostNodeMap = new HashMap<>(); + List changedRoutes = new ArrayList<>(); + List riskRoutes = new ArrayList<>(); + + for(Route route : routes){ + if(revRouteMap.containsKey(route.getId())){ + Route revRoute = revRouteMap.get(route.getId()); + handleRevisionRoute(revRoute, route, changedRoutes, riskRoutes); + continue; + } + //해당 시점 이후에 생성된 루트들 (과거 시점엔 보이지 않는 루트) + handleLostRoute(route, lostAdjMap, lostNodeMap); + } + + GetRiskRoutesResDTO getRiskRoutesResDTO = routeCalculator.mapRisks(riskRoutes); + + //시작점이 1개인 nodeList 생성 + List endNodes = determineEndNodes(lostAdjMap, lostNodeMap); + + List lostNodeInfos = lostNodeMap.entrySet().stream() + .map(entry -> { + Node node = entry.getValue(); + return NodeInfoResDTO.of(entry.getKey(), node.getX(), node.getY()); + }) + .toList(); + + LostRoutesDTO lostRouteDTO = LostRoutesDTO.of(lostNodeInfos, routeCalculator.getCoreRoutes(lostAdjMap, endNodes)); + + return GetAllRoutesByRevisionResDTO.of(routesInfo, getRiskRoutesResDTO, lostRouteDTO, changedRoutes); + } + + private void handleRevisionRoute(Route revRoute, Route route, List changedRoutes, List riskRoutes) { + //변경사항이 있는 경우 + if(!route.isEqualRoute(revRoute)){ + changedRoutes.add(ChangedRouteDTO.of(route.getId(), RouteDifferInfo.of(route), RouteDifferInfo.of(revRoute))); + } + //변경사항이 없으면서 risk가 존재하는 route의 경우 riskRoutes에 추가 + else if(!route.getCautionFactors().isEmpty() || !route.getDangerFactors().isEmpty()){ + riskRoutes.add(route); + } + } + + private void handleLostRoute(Route route, Map> lostAdjMap, Map lostNodeMap) { + lostAdjMap.computeIfAbsent(route.getNode1().getId(), k -> new ArrayList<>()).add(route); + lostAdjMap.computeIfAbsent(route.getNode2().getId(), k -> new ArrayList<>()).add(route); + lostNodeMap.put(route.getNode1().getId(), route.getNode1()); + lostNodeMap.put(route.getNode2().getId(), route.getNode2()); + } + + private List determineEndNodes(Map> lostAdjMap, Map lostNodeMap) { + return lostAdjMap.entrySet() + .stream() + .filter(entry -> (entry.getValue().size() == 1) || lostNodeMap.get(entry.getKey()).isCore()) + .map(Map.Entry::getKey) + .map(lostNodeMap::get) + .collect(Collectors.toList()); + } + } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/service/AuthService.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/service/AuthService.java new file mode 100644 index 0000000..75b3cd3 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/service/AuthService.java @@ -0,0 +1,36 @@ +package com.softeer5.uniro_backend.admin.service; + +import static com.softeer5.uniro_backend.common.error.ErrorCode.*; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.softeer5.uniro_backend.admin.dto.request.LoginReqDTO; +import com.softeer5.uniro_backend.admin.dto.response.LoginResDTO; +import com.softeer5.uniro_backend.admin.entity.Admin; +import com.softeer5.uniro_backend.admin.jwt.JwtTokenProvider; +import com.softeer5.uniro_backend.admin.repository.AdminRepository; +import com.softeer5.uniro_backend.common.exception.custom.AdminException; + +import lombok.RequiredArgsConstructor; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class AuthService { + + private final AdminRepository adminRepository; + private final JwtTokenProvider jwtTokenProvider; + + public LoginResDTO login(LoginReqDTO loginReqDTO) { + + Admin admin = adminRepository.findByUnivId(loginReqDTO.getUnivId()) + .orElseThrow(() -> new AdminException("invalid univ id", INVALID_UNIV_ID)); + + if (!admin.getCode().equals(loginReqDTO.getCode())) { + throw new AdminException("invalid code", INVALID_ADMIN_CODE); + } + + return LoginResDTO.of(jwtTokenProvider.createToken(admin.getUnivId())); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/setting/CustomReversionListener.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/setting/CustomReversionListener.java index 9af5ca4..d6cbdd0 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/setting/CustomReversionListener.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/setting/CustomReversionListener.java @@ -7,6 +7,7 @@ public class CustomReversionListener implements RevisionListener { @Override public void newRevision(Object revisionEntity) { RevInfo revinfo = (RevInfo) revisionEntity; + revinfo.setUnivId(RevisionContext.getUnivId()); revinfo.setAction(RevisionContext.getAction()); } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/setting/RevisionContext.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/setting/RevisionContext.java index 7bc21ee..abb05a2 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/setting/RevisionContext.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/setting/RevisionContext.java @@ -3,6 +3,7 @@ public class RevisionContext { private static final ThreadLocal univIdHolder = new ThreadLocal<>(); private static final ThreadLocal actionHolder = new ThreadLocal<>(); + private static final ThreadLocal REVISION_TYPE_THREAD_LOCAL = ThreadLocal.withInitial(() -> RevisionType.DEFAULT); public static void setAction(String action) { actionHolder.set(action); @@ -20,6 +21,14 @@ public static Long getUnivId() { return univIdHolder.get(); } + public static void setRevisionType(RevisionType revisionType) { + REVISION_TYPE_THREAD_LOCAL.set(revisionType); + } + + public static RevisionType getRevisionType() { + return REVISION_TYPE_THREAD_LOCAL.get(); + } + public static void clear() { univIdHolder.remove(); } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/setting/RevisionType.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/setting/RevisionType.java new file mode 100644 index 0000000..a5d987e --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/setting/RevisionType.java @@ -0,0 +1,5 @@ +package com.softeer5.uniro_backend.admin.setting; + +public enum RevisionType { + DEFAULT, IGNORE +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/controller/NodeApi.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/controller/BuildingApi.java similarity index 73% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/node/controller/NodeApi.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/building/controller/BuildingApi.java index df098fe..bf4d724 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/controller/NodeApi.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/controller/BuildingApi.java @@ -1,13 +1,15 @@ -package com.softeer5.uniro_backend.node.controller; +package com.softeer5.uniro_backend.building.controller; import java.util.List; +import com.softeer5.uniro_backend.building.dto.request.CreateBuildingNodeReqDTO; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; -import com.softeer5.uniro_backend.node.dto.GetBuildingResDTO; -import com.softeer5.uniro_backend.node.dto.SearchBuildingResDTO; +import com.softeer5.uniro_backend.building.dto.response.GetBuildingResDTO; +import com.softeer5.uniro_backend.building.dto.response.SearchBuildingResDTO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -16,7 +18,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; @Tag(name = "노드(코어노드, 서브노드, 건물노드) 관련 Api") -public interface NodeApi { +public interface BuildingApi { @Operation(summary = "건물 노드 조회") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "건물 노드 조회 성공"), @@ -52,4 +54,13 @@ ResponseEntity getBuilding( @PathVariable("univId") Long univId, @PathVariable("nodeId") Long nodeId ); + + @Operation(summary = "건물 노드 생성") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "건물 노드 생성 성공"), + @ApiResponse(responseCode = "400", description = "EXCEPTION(임시)", content = @Content), + }) + ResponseEntity createBuildingNode(@PathVariable("univId") Long univId, + @RequestBody CreateBuildingNodeReqDTO createBuildingNodeReqDTO); + } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/controller/NodeController.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/controller/BuildingController.java similarity index 53% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/node/controller/NodeController.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/building/controller/BuildingController.java index 47344bc..aaa6a57 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/controller/NodeController.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/controller/BuildingController.java @@ -1,23 +1,23 @@ -package com.softeer5.uniro_backend.node.controller; +package com.softeer5.uniro_backend.building.controller; import java.util.List; +import com.softeer5.uniro_backend.building.dto.request.CreateBuildingNodeReqDTO; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; -import com.softeer5.uniro_backend.node.dto.GetBuildingResDTO; -import com.softeer5.uniro_backend.node.dto.SearchBuildingResDTO; -import com.softeer5.uniro_backend.node.service.NodeService; +import com.softeer5.uniro_backend.building.dto.response.GetBuildingResDTO; +import com.softeer5.uniro_backend.building.dto.response.SearchBuildingResDTO; +import com.softeer5.uniro_backend.building.service.BuildingService; import lombok.RequiredArgsConstructor; @RestController @RequiredArgsConstructor -public class NodeController implements NodeApi { - private final NodeService nodeService; +public class BuildingController implements BuildingApi { + private final BuildingService buildingService; @Override @GetMapping("/{univId}/nodes/buildings") @@ -29,7 +29,7 @@ public ResponseEntity> getBuildings( @RequestParam(value = "right-down-lat") double rightDownLat, @RequestParam(value = "right-down-lng") double rightDownLng ) { - List buildingResDTOS = nodeService.getBuildings(univId, level, leftUpLat, leftUpLng, + List buildingResDTOS = buildingService.getBuildings(univId, level, leftUpLat, leftUpLng, rightDownLat, rightDownLng); return ResponseEntity.ok().body(buildingResDTOS); } @@ -42,7 +42,7 @@ public ResponseEntity searchBuildings( @RequestParam(value = "cursor-id", required = false) Long cursorId, @RequestParam(value = "page-size", required = false, defaultValue = "6") Integer pageSize ) { - SearchBuildingResDTO searchBuildingResDTO = nodeService.searchBuildings(univId, name, cursorId, pageSize); + SearchBuildingResDTO searchBuildingResDTO = buildingService.searchBuildings(univId, name, cursorId, pageSize); return ResponseEntity.ok().body(searchBuildingResDTO); } @@ -52,8 +52,17 @@ public ResponseEntity getBuilding( @PathVariable("univId") Long univId, @PathVariable("nodeId") Long nodeId) { - GetBuildingResDTO buildingResDTO = nodeService.getBuilding(nodeId); + GetBuildingResDTO buildingResDTO = buildingService.getBuilding(nodeId); return ResponseEntity.ok().body(buildingResDTO); } + @Override + @PostMapping("{univId}/nodes/building") + public ResponseEntity createBuildingNode(@PathVariable("univId") Long univId, + @RequestBody @Valid CreateBuildingNodeReqDTO createBuildingNodeReqDTO){ + buildingService.createBuildingNode(univId, createBuildingNodeReqDTO); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/dto/request/CreateBuildingNodeReqDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/dto/request/CreateBuildingNodeReqDTO.java new file mode 100644 index 0000000..7047c26 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/dto/request/CreateBuildingNodeReqDTO.java @@ -0,0 +1,29 @@ +package com.softeer5.uniro_backend.building.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +@Schema(name = "CreateBuildingNodeReqDTO", description = "건물 노드 생성 요청 DTO") +public class CreateBuildingNodeReqDTO { + @Schema(description = "x 좌표", example = "127.123456") + @NotNull + private final double lng; + @Schema(description = "y 좌표", example = "37.123456") + @NotNull + private final double lat; + @Schema(description = "건물명", example = "공학관") + @NotNull + private final String buildingName; + @Schema(description = "전화번호", example = "02-1234-1234") + private final String phoneNumber; + @Schema(description = "주소", example = "한양로 123번길123") + private final String address; + @Schema(description = "이미지", example = "") + private final String buildingImageUrl; // 추후 S3학습 후 변경예정 + @Schema(description = "레벨(지도 축척에 따른 노출정도, 1~10)", example = "3") + private final int level; +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/dto/GetBuildingResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/dto/response/GetBuildingResDTO.java similarity index 88% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/node/dto/GetBuildingResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/building/dto/response/GetBuildingResDTO.java index e6a92b5..79d9b3b 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/dto/GetBuildingResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/dto/response/GetBuildingResDTO.java @@ -1,7 +1,7 @@ -package com.softeer5.uniro_backend.node.dto; +package com.softeer5.uniro_backend.building.dto.response; -import com.softeer5.uniro_backend.node.entity.Building; -import com.softeer5.uniro_backend.node.entity.Node; +import com.softeer5.uniro_backend.building.entity.Building; +import com.softeer5.uniro_backend.map.entity.Node; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AccessLevel; diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/dto/SearchBuildingResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/dto/response/SearchBuildingResDTO.java similarity index 93% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/node/dto/SearchBuildingResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/building/dto/response/SearchBuildingResDTO.java index f3b12b6..c2f6b70 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/dto/SearchBuildingResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/dto/response/SearchBuildingResDTO.java @@ -1,4 +1,4 @@ -package com.softeer5.uniro_backend.node.dto; +package com.softeer5.uniro_backend.building.dto.response; import java.util.List; diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/entity/Building.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/entity/Building.java similarity index 63% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/node/entity/Building.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/building/entity/Building.java index f6703a8..bf0ede4 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/entity/Building.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/entity/Building.java @@ -1,18 +1,25 @@ -package com.softeer5.uniro_backend.node.entity; +package com.softeer5.uniro_backend.building.entity; + +import java.time.LocalDateTime; + +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.validation.constraints.NotNull; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +@EntityListeners(AuditingEntityListener.class) public class Building { @Id @@ -41,4 +48,6 @@ public class Building { @NotNull private Long univId; + @CreatedDate + private LocalDateTime createdAt; } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/repository/BuildingCustomRepository.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/repository/BuildingCustomRepository.java similarity index 65% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/node/repository/BuildingCustomRepository.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/building/repository/BuildingCustomRepository.java index c364277..a357414 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/repository/BuildingCustomRepository.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/repository/BuildingCustomRepository.java @@ -1,9 +1,9 @@ -package com.softeer5.uniro_backend.node.repository; +package com.softeer5.uniro_backend.building.repository; import java.util.List; import com.softeer5.uniro_backend.common.CursorPage; -import com.softeer5.uniro_backend.node.dto.BuildingNode; +import com.softeer5.uniro_backend.building.service.vo.BuildingNode; public interface BuildingCustomRepository { CursorPage> searchBuildings(Long univId, String name, Long cursorId, Integer pageSize); diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/repository/BuildingCustomRepositoryImpl.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/repository/BuildingCustomRepositoryImpl.java similarity index 83% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/node/repository/BuildingCustomRepositoryImpl.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/building/repository/BuildingCustomRepositoryImpl.java index 83541ed..a4b53a3 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/repository/BuildingCustomRepositoryImpl.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/repository/BuildingCustomRepositoryImpl.java @@ -1,20 +1,22 @@ -package com.softeer5.uniro_backend.node.repository; +package com.softeer5.uniro_backend.building.repository; -import static com.softeer5.uniro_backend.node.entity.QBuilding.*; -import static com.softeer5.uniro_backend.node.entity.QNode.*; +import static com.softeer5.uniro_backend.map.entity.QNode.node; import java.util.List; +import com.softeer5.uniro_backend.building.service.vo.QBuildingNode; import org.springframework.stereotype.Repository; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; + import com.softeer5.uniro_backend.common.CursorPage; -import com.softeer5.uniro_backend.node.dto.BuildingNode; -import com.softeer5.uniro_backend.node.dto.QBuildingNode; +import com.softeer5.uniro_backend.building.service.vo.BuildingNode; import lombok.RequiredArgsConstructor; +import static com.softeer5.uniro_backend.building.entity.QBuilding.building; + @Repository @RequiredArgsConstructor public class BuildingCustomRepositoryImpl implements BuildingCustomRepository { diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/repository/BuildingRepository.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/repository/BuildingRepository.java new file mode 100644 index 0000000..521d1d6 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/repository/BuildingRepository.java @@ -0,0 +1,44 @@ +package com.softeer5.uniro_backend.building.repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.transaction.annotation.Transactional; + +import com.softeer5.uniro_backend.building.service.vo.BuildingNode; +import com.softeer5.uniro_backend.building.entity.Building; + +public interface BuildingRepository extends JpaRepository, BuildingCustomRepository { + + // 추후에 인덱싱 작업 필요. + @Query(""" + SELECT new com.softeer5.uniro_backend.building.service.vo.BuildingNode(b, n) + FROM Building b + JOIN FETCH Node n ON b.nodeId = n.id + WHERE b.univId = :univId + AND b.level >= :level + AND ST_Within(n.coordinates, ST_PolygonFromText((:polygon),4326)) + """) + List findByUnivIdAndLevelWithNode(Long univId, int level, String polygon); + + @Query(""" + SELECT new com.softeer5.uniro_backend.building.service.vo.BuildingNode(b, n) + FROM Building b + JOIN FETCH Node n ON b.nodeId = n.id + WHERE b.nodeId = :nodeId + """) + Optional findByNodeIdWithNode(Long nodeId); + + List findAllByNodeIdIn(List nodeIds); + + boolean existsByNodeIdAndUnivId(Long nodeId, Long univId); + + @Modifying(clearAutomatically = true) + @Transactional + @Query("DELETE FROM Building b WHERE b.univId = :univId AND b.createdAt > :versionTimeStamp") + void deleteAllByCreatedAt(Long univId, LocalDateTime versionTimeStamp); +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/service/BuildingService.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/service/BuildingService.java new file mode 100644 index 0000000..bcbc609 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/service/BuildingService.java @@ -0,0 +1,90 @@ +package com.softeer5.uniro_backend.building.service; + +import java.util.List; +import java.util.Optional; + +import com.softeer5.uniro_backend.admin.annotation.RevisionOperation; +import com.softeer5.uniro_backend.admin.enums.RevisionOperationType; +import com.softeer5.uniro_backend.external.MapClient; +import com.softeer5.uniro_backend.building.dto.request.CreateBuildingNodeReqDTO; +import com.softeer5.uniro_backend.building.entity.Building; +import com.softeer5.uniro_backend.map.entity.Node; +import com.softeer5.uniro_backend.map.repository.NodeRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.softeer5.uniro_backend.common.CursorPage; +import com.softeer5.uniro_backend.common.error.ErrorCode; +import com.softeer5.uniro_backend.common.exception.custom.BuildingException; +import com.softeer5.uniro_backend.common.utils.GeoUtils; +import com.softeer5.uniro_backend.building.service.vo.BuildingNode; +import com.softeer5.uniro_backend.building.dto.response.GetBuildingResDTO; +import com.softeer5.uniro_backend.building.dto.response.SearchBuildingResDTO; +import com.softeer5.uniro_backend.building.repository.BuildingRepository; + +import lombok.RequiredArgsConstructor; + +import static com.softeer5.uniro_backend.common.utils.GeoUtils.convertDoubleToPoint; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BuildingService { + private final BuildingRepository buildingRepository; + private final NodeRepository nodeRepository; + private final MapClient mapClient; + + public List getBuildings( + Long univId, int level, + double leftUpLat, double leftUpLng, double rightDownLat, double rightDownLng) { + + String polygon = GeoUtils.makeSquarePolygonString(leftUpLat, leftUpLng, rightDownLat, rightDownLng); + List buildingNodes = buildingRepository.findByUnivIdAndLevelWithNode(univId, level, polygon); + + return buildingNodes.stream() + .map(buildingNode -> GetBuildingResDTO.of(buildingNode.getBuilding(), buildingNode.getNode())) + .toList(); + } + + public SearchBuildingResDTO searchBuildings(Long univId, String name, Long cursorId, Integer pageSize){ + + CursorPage> buildingNodes = buildingRepository.searchBuildings(univId, name, cursorId, pageSize); + + List data = buildingNodes.getData().stream() + .map(buildingNode -> GetBuildingResDTO.of(buildingNode.getBuilding(), buildingNode.getNode())) + .toList(); + + return SearchBuildingResDTO.of(data, buildingNodes.getNextCursor(), buildingNodes.isHasNext()); + } + + public GetBuildingResDTO getBuilding(Long nodeId){ + Optional buildingNode = buildingRepository.findByNodeIdWithNode(nodeId); + if(buildingNode.isEmpty()){ + throw new BuildingException("Building Not Found", ErrorCode.BUILDING_NOT_FOUND); + } + + return GetBuildingResDTO.of(buildingNode.get().getBuilding(), buildingNode.get().getNode()); + } + + @RevisionOperation(RevisionOperationType.CREATE_BUILDING_NODE) + @Transactional + public void createBuildingNode(Long univId, CreateBuildingNodeReqDTO createBuildingNodeReqDTO) { + Node node = Node.builder() + .coordinates(convertDoubleToPoint(createBuildingNodeReqDTO.getLng(), createBuildingNodeReqDTO.getLat())) + .isCore(false) + .univId(univId).build(); + mapClient.fetchHeights(List.of(node)); + nodeRepository.save(node); + + Building building = Building.builder() + .phoneNumber(createBuildingNodeReqDTO.getPhoneNumber()) + .address(createBuildingNodeReqDTO.getAddress()) + .name(createBuildingNodeReqDTO.getBuildingName()) + .imageUrl(createBuildingNodeReqDTO.getBuildingImageUrl()) + .level(createBuildingNodeReqDTO.getLevel()) + .nodeId(node.getId()) + .univId(univId).build(); + buildingRepository.save(building); + } + +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/dto/BuildingNode.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/service/vo/BuildingNode.java similarity index 64% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/node/dto/BuildingNode.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/building/service/vo/BuildingNode.java index f5f6091..531b18e 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/dto/BuildingNode.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/building/service/vo/BuildingNode.java @@ -1,8 +1,8 @@ -package com.softeer5.uniro_backend.node.dto; +package com.softeer5.uniro_backend.building.service.vo; import com.querydsl.core.annotations.QueryProjection; -import com.softeer5.uniro_backend.node.entity.Building; -import com.softeer5.uniro_backend.node.entity.Node; +import com.softeer5.uniro_backend.building.entity.Building; +import com.softeer5.uniro_backend.map.entity.Node; import lombok.Getter; diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/config/EnversConfig.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/config/EnversConfig.java new file mode 100644 index 0000000..5f870f4 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/config/EnversConfig.java @@ -0,0 +1,30 @@ +package com.softeer5.uniro_backend.common.config; + +import org.hibernate.envers.boot.internal.EnversService; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.EventType; +import org.hibernate.internal.SessionFactoryImpl; +import org.hibernate.service.spi.ServiceRegistryImplementor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.softeer5.uniro_backend.admin.aspect.CustomPostUpdateListener; + +import jakarta.persistence.EntityManagerFactory; + +@Configuration +public class EnversConfig { + + @Bean + public EventListenerRegistry listenerRegistry(EntityManagerFactory entityManagerFactory) { + ServiceRegistryImplementor serviceRegistry = entityManagerFactory.unwrap(SessionFactoryImpl.class).getServiceRegistry(); + + EnversService enversService = serviceRegistry.getService(EnversService.class); + EventListenerRegistry listenerRegistry = serviceRegistry.getService(EventListenerRegistry.class); + + // listenerRegistry.setListeners(EventType.POST_INSERT, new CustomPostInsertListener(enversService)); + listenerRegistry.setListeners(EventType.POST_UPDATE, new CustomPostUpdateListener(enversService)); + + return listenerRegistry; + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/config/WebMvcConfig.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/config/WebMvcConfig.java new file mode 100644 index 0000000..b227431 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/config/WebMvcConfig.java @@ -0,0 +1,36 @@ +package com.softeer5.uniro_backend.common.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import com.softeer5.uniro_backend.admin.interceptor.AdminInterceptor; +import com.softeer5.uniro_backend.admin.interceptor.JwtInterceptor; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + private final AdminInterceptor adminInterceptor; + private final JwtInterceptor jwtInterceptor; // JWT 인터셉터 추가 + + public WebMvcConfig(AdminInterceptor adminInterceptor, JwtInterceptor jwtInterceptor) { + this.adminInterceptor = adminInterceptor; + this.jwtInterceptor = jwtInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + // JWT 인터셉터는 더 먼저 실행되도록 우선순위 낮춤 + registry.addInterceptor(jwtInterceptor) + .addPathPatterns("/admin/{univId}/**") // "/admin/{univId}/" 패턴만 적용 + .excludePathPatterns("/admin/auth/login") + .order(0); // 가장 먼저 실행되도록 설정 + + // AdminInterceptor는 그 다음에 실행 + registry.addInterceptor(adminInterceptor) + .addPathPatterns("/admin/{univId}/**") // "/admin/{univId}/" 패턴만 적용 + .excludePathPatterns("/admin/auth/login") + .order(1); // JWT 이후에 실행되도록 설정 + } +} + + diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/constant/UniroConst.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/constant/UniroConst.java index 8142946..ae802da 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/constant/UniroConst.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/constant/UniroConst.java @@ -4,6 +4,10 @@ public final class UniroConst { public static final String NODE_KEY_DELIMITER = " "; public static final int CORE_NODE_CONDITION = 3; public static final int BEFORE_DEFAULT_ORDER = -1; - public static final double SECONDS_PER_MITER = 1.0; - public static final double METERS_PER_DEGREE = 113000.0; + public static final double PEDESTRIAN_SECONDS_PER_MITER = 1.2; + public static final double MANUAL_WHEELCHAIR_SECONDS_PER_MITER = 2.5; + public static final double ELECTRIC_WHEELCHAIR_SECONDS_PER_MITER = 1.0; + public static final double EARTH_RADIUS = 6378137; + public static final double BUILDING_ROUTE_DISTANCE = 1e8; + public static final int IS_SINGLE_ROUTE = 2; } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/error/ErrorCode.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/error/ErrorCode.java index 6ed116c..d60712b 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/error/ErrorCode.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/error/ErrorCode.java @@ -1,13 +1,15 @@ package com.softeer5.uniro_backend.common.error; -import org.springframework.http.HttpStatus; - import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor public enum ErrorCode { + + // common + INVALID_INPUT_VALUE(400, "적절하지 않은 요청값입니다."), + // 길찾기 FASTEST_ROUTE_NOT_FOUND(422, "경로가 없습니다."), SAME_START_AND_END_POINT(400, "출발지와 도착지가 같습니다."), @@ -19,15 +21,24 @@ public enum ErrorCode { //길 생성 ELEVATION_API_ERROR(500, "구글 해발고도 API에서 오류가 발생했습니다."), + DUPLICATE_NEAREST_NODE(400, "중복된 인접 노드가 존재합니다."), // 건물 노드 BUILDING_NOT_FOUND(404, "유효한 건물을 찾을 수 없습니다."), + NOT_BUILDING_NODE(400, "빌딩 노드가 아닙니다."), // 노드 NODE_NOT_FOUND(404, "유효한 노드를 찾을 수 없습니다."), // 경로 계산 - INTERSECTION_ONLY_ALLOWED_POINT(400, "기존 경로와 겹칠 수 없습니다.") + INTERSECTION_ONLY_ALLOWED_POINT(400, "기존 경로와 겹칠 수 없습니다."), + + // 어드민 + INVALID_VERSION_ID(400, "유효하지 않은 버전 id 입니다."), + INVALID_ADMIN_CODE(403, "유효하지 않은 어드민 코드 입니다."), + INVALID_TOKEN(401, "유효하지 않은 토큰입니다."), + UNAUTHORIZED_UNIV(401, "해당 대학교의 권한이 없습니다."), + INVALID_UNIV_ID(400, "유효하지 않은 대학교 id 입니다."), ; private final int httpStatus; diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/GlobalExceptionHandler.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/GlobalExceptionHandler.java index 3906221..e9bb8e8 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/GlobalExceptionHandler.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/GlobalExceptionHandler.java @@ -1,10 +1,13 @@ package com.softeer5.uniro_backend.common.exception; +import static com.softeer5.uniro_backend.common.error.ErrorCode.*; + import com.softeer5.uniro_backend.common.error.ErrorResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -17,4 +20,12 @@ public ResponseEntity handleCustomException(CustomException ex) { ErrorResponse response = new ErrorResponse(ex.getErrorCode()); return new ResponseEntity<>(response, HttpStatus.valueOf(ex.getErrorCode().getHttpStatus())); } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleCustomException(MethodArgumentNotValidException ex) { + log.error(ex.getMessage()); + ErrorResponse response = new ErrorResponse(INVALID_INPUT_VALUE); + return new ResponseEntity<>(response, HttpStatus.valueOf(INVALID_INPUT_VALUE.getHttpStatus())); + } + } \ No newline at end of file diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/NodeNotFoundException.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/AdminException.java similarity index 66% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/NodeNotFoundException.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/AdminException.java index 5489fa9..a548188 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/NodeNotFoundException.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/AdminException.java @@ -6,8 +6,8 @@ import lombok.Getter; @Getter -public class NodeNotFoundException extends CustomException { - public NodeNotFoundException(String message, ErrorCode errorCode) { +public class AdminException extends CustomException { + public AdminException(String message, ErrorCode errorCode) { super(message, errorCode); } } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/BuildingNotFoundException.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/BuildingException.java similarity index 64% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/BuildingNotFoundException.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/BuildingException.java index 1c6e74a..d78bffb 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/BuildingNotFoundException.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/BuildingException.java @@ -6,8 +6,8 @@ import lombok.Getter; @Getter -public class BuildingNotFoundException extends CustomException { - public BuildingNotFoundException(String message, ErrorCode errorCode) { +public class BuildingException extends CustomException { + public BuildingException(String message, ErrorCode errorCode) { super(message, errorCode); } } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/DangerCautionConflictException.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/DangerCautionConflictException.java deleted file mode 100644 index c2bd504..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/DangerCautionConflictException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.softeer5.uniro_backend.common.exception.custom; - -import com.softeer5.uniro_backend.common.error.ErrorCode; -import com.softeer5.uniro_backend.common.exception.CustomException; - -public class DangerCautionConflictException extends CustomException { - public DangerCautionConflictException(String message, ErrorCode errorCode) { - super(message, errorCode); - } -} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/InvalidMapException.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/InvalidMapException.java deleted file mode 100644 index 24c03f5..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/InvalidMapException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.softeer5.uniro_backend.common.exception.custom; - -import com.softeer5.uniro_backend.common.error.ErrorCode; -import com.softeer5.uniro_backend.common.exception.CustomException; - -public class InvalidMapException extends CustomException { - public InvalidMapException(String message, ErrorCode errorCode) { - super(message, errorCode); - } -} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/SameStartAndEndPointException.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/NodeException.java similarity index 53% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/SameStartAndEndPointException.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/NodeException.java index 8eb52d4..6ab56d8 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/SameStartAndEndPointException.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/NodeException.java @@ -2,11 +2,12 @@ import com.softeer5.uniro_backend.common.error.ErrorCode; import com.softeer5.uniro_backend.common.exception.CustomException; + import lombok.Getter; @Getter -public class SameStartAndEndPointException extends CustomException { - public SameStartAndEndPointException(String message, ErrorCode errorCode) { - super(message, errorCode); - } -} \ No newline at end of file +public class NodeException extends CustomException { + public NodeException(String message, ErrorCode errorCode) { + super(message, errorCode); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/UnreachableDestinationException.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/RouteException.java similarity index 52% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/UnreachableDestinationException.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/RouteException.java index 7d2d81e..cfef4e9 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/UnreachableDestinationException.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/RouteException.java @@ -1,13 +1,13 @@ package com.softeer5.uniro_backend.common.exception.custom; - import com.softeer5.uniro_backend.common.error.ErrorCode; import com.softeer5.uniro_backend.common.exception.CustomException; + import lombok.Getter; @Getter -public class UnreachableDestinationException extends CustomException { - public UnreachableDestinationException(String message, ErrorCode errorCode) { - super(message, errorCode); - } -} \ No newline at end of file +public class RouteException extends CustomException { + public RouteException(String message, ErrorCode errorCode) { + super(message, errorCode); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/RouteNotFoundException.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/RouteNotFoundException.java deleted file mode 100644 index a7efd57..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/RouteNotFoundException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.softeer5.uniro_backend.common.exception.custom; - -import com.softeer5.uniro_backend.common.error.ErrorCode; -import com.softeer5.uniro_backend.common.exception.CustomException; - -public class RouteNotFoundException extends CustomException { - public RouteNotFoundException(String message, ErrorCode errorCode) { - super(message, errorCode); - } -} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/UnivException.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/UnivException.java new file mode 100644 index 0000000..2c10ca5 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/UnivException.java @@ -0,0 +1,13 @@ +package com.softeer5.uniro_backend.common.exception.custom; + +import com.softeer5.uniro_backend.common.error.ErrorCode; +import com.softeer5.uniro_backend.common.exception.CustomException; + +import lombok.Getter; + +@Getter +public class UnivException extends CustomException { + public UnivException(String message, ErrorCode errorCode) { + super(message, errorCode); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/logging/ArgType.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/logging/ArgType.java new file mode 100644 index 0000000..ceed19e --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/logging/ArgType.java @@ -0,0 +1,34 @@ +package com.softeer5.uniro_backend.common.logging; + +import java.util.List; + +public enum ArgType { + NULL, + LIST, + CUSTOM_DTO, + ENTITY, + OTHER; + + public static ArgType getArgType(Object arg) { + if (arg == null) { + return NULL; + } else if (arg instanceof List) { + return LIST; + } else if (isCustomDto(arg)) { + return CUSTOM_DTO; + } else if (isEntity(arg)) { + return ENTITY; + } else { + return OTHER; + } + } + + private static boolean isCustomDto(Object arg) { + return arg.getClass().getName().contains("dto"); + } + + private static boolean isEntity(Object arg) { + return arg.getClass().getName().contains("entity"); + } +} + diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/logging/ExecutionLoggingAop.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/logging/ExecutionLoggingAop.java index 48c0898..5b30196 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/logging/ExecutionLoggingAop.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/logging/ExecutionLoggingAop.java @@ -1,9 +1,8 @@ package com.softeer5.uniro_backend.common.logging; -import java.lang.annotation.Annotation; -import java.lang.reflect.Field; +import java.util.Arrays; import java.util.Enumeration; -import java.util.Objects; +import java.util.List; import java.util.UUID; import org.aspectj.lang.ProceedingJoinPoint; @@ -12,7 +11,6 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import org.springframework.util.StopWatch; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; @@ -30,10 +28,8 @@ public class ExecutionLoggingAop { @Around("execution(* com.softeer5.uniro_backend..*(..)) " + "&& !within(com.softeer5.uniro_backend.common..*) " - + "&& !within(com.softeer5.uniro_backend.resolver..*) " ) public Object logExecutionTrace(ProceedingJoinPoint pjp) throws Throwable { - // 요청에서 userId 가져오기 (ThreadLocal 사용) String userId = userIdThreadLocal.get(); if (userId == null) { userId = UUID.randomUUID().toString().substring(0, 12); @@ -41,76 +37,120 @@ public Object logExecutionTrace(ProceedingJoinPoint pjp) throws Throwable { } Object target = pjp.getTarget(); - Annotation[] declaredAnnotations = target.getClass().getDeclaredAnnotations(); + boolean isController = isRestController(target); String className = pjp.getSignature().getDeclaringType().getSimpleName(); String methodName = pjp.getSignature().getName(); String task = className + "." + methodName; - for(Annotation annotation : declaredAnnotations){ - if(annotation instanceof RestController){ - logHttpRequest(userId); - - HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest(); - RequestMethod httpMethod = RequestMethod.valueOf(request.getMethod()); - - log.info(""); - log.info("🚨 [ userId = {} ] {} Start", userId, className); - log.info("[ userId = {} ] [Call Method] {}: {}", userId, httpMethod, task); - } + if (isController) { + logHttpRequest(userId); } + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); + log.info("✅ [ userId = {} Start] [Call Method] {}: {}", userId, request.getMethod(), task); - Object[] paramArgs = pjp.getArgs(); - for (Object object : paramArgs) { - if (Objects.nonNull(object)) { - log.info("[Parameter] {} {}", object.getClass().getSimpleName(), object); - - String packageName = object.getClass().getPackage().getName(); - if (packageName.contains("java")) { - break; - } - - Field[] fields = object.getClass().getDeclaredFields(); - for (Field field : fields) { - field.setAccessible(true); - try { - Object value = field.get(object); - log.info("[Field] {} = {}", field.getName(), value); - } catch (IllegalAccessException e) { - log.warn("[Field Access Error] Cannot access field: {}", field.getName()); - } - } + try{ + if (isController) { + logParameters(pjp.getArgs()); } } + catch (Exception e){ + // 로깅 중에 발생한 에러는 무시하고 로깅을 계속 진행 + log.error("🚨🚨🚨 [ userId = {} ] {} 메서드 파라미터 로깅 중 에러 발생 : {} 🚨🚨🚨", userId, task, e.getMessage()); + } + log.info(""); StopWatch sw = new StopWatch(); sw.start(); - Object result = null; + Object result; try { result = pjp.proceed(); } catch (Exception e) { log.warn("[ERROR] [ userId = {} ] {} 메서드 예외 발생 : {}", userId, task, e.getMessage()); throw e; } finally { - // Controller 클래스일 때만 ThreadLocal 값 삭제 - for(Annotation annotation : declaredAnnotations){ - if(annotation instanceof RestController){ - userIdThreadLocal.remove(); - } + if (isController) { + userIdThreadLocal.remove(); } } sw.stop(); - long executionTime = sw.getTotalTimeMillis(); - - log.info("[ExecutionTime] {} --> {} (ms)", task, executionTime); - log.info("🚨 [ userId = {} ] {} End", userId, className); - log.info(""); + log.info("[ExecutionTime] {} --> {} (ms)", task, sw.getTotalTimeMillis()); + log.info("🚨 [ userId = {} ] {} End\n", userId, className); return result; } + private boolean isRestController(Object target) { + return Arrays.stream(target.getClass().getDeclaredAnnotations()) + .anyMatch(RestController.class::isInstance); + } + + private void logParameters(Object[] args) { + StringBuilder parametersLogMessage = new StringBuilder(); + + Arrays.stream(args) + .forEach(arg -> logDetail(arg, "[Parameter]", parametersLogMessage, 0)); + + log.info("\n{}", parametersLogMessage.toString()); + } + + private void logDetail(Object arg, String requestType, StringBuilder logMessage, int depth) { + String indent = " ".repeat(depth); // depth 수준에 따른 들여쓰기 + ArgType argType = ArgType.getArgType(arg); + + switch (argType) { + case NULL -> logMessage.append(indent).append(requestType).append(" null\n"); + case LIST -> { + logMessage.append(indent) + .append(requestType) + .append(" ") + .append(arg.getClass().getSimpleName()) + .append("\n"); + List list = (List)arg; + for (int i = 0; i < list.size(); i++) { + logDetail(list.get(i), "[List Element " + i + "] ", logMessage, depth + 1); + } + } + case CUSTOM_DTO -> { + logMessage.append(indent) + .append(requestType) + .append("DTO: ") + .append(arg.getClass().getSimpleName()) + .append("\n"); + logObjectFields(arg, logMessage, depth + 1); + } + case ENTITY -> { + logMessage.append(indent) + .append(requestType) + .append(arg.getClass().getSimpleName()) + .append(" : ") + .append("\n"); + logObjectFields(arg, logMessage, depth + 1); + } + default -> logMessage.append(indent) + .append(requestType) + .append(" ") + .append(arg.getClass().getSimpleName()) + .append(": ") + .append(arg) + .append("\n"); + } + } + + private void logObjectFields(Object object, StringBuilder logMessage, int depth) { + String indent = " ".repeat(depth); // depth 기반 들여쓰기 + Arrays.stream(object.getClass().getDeclaredFields()).forEach(field -> { + try { + field.setAccessible(true); + Object value = field.get(object); + logDetail(value, "[Field] " + field.getName(), logMessage, depth + 1); + } catch (IllegalAccessException e) { + logMessage.append(indent).append("[Field Access Error] Cannot access field: ").append(field.getName()).append("\n"); + } + }); + } private void logHttpRequest(String userId) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/resolver/CautionListConverter.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/resolver/CautionListConverter.java similarity index 82% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/resolver/CautionListConverter.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/common/resolver/CautionListConverter.java index 2feb160..5beeec8 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/resolver/CautionListConverter.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/resolver/CautionListConverter.java @@ -1,4 +1,4 @@ -package com.softeer5.uniro_backend.resolver; +package com.softeer5.uniro_backend.common.resolver; import java.io.IOException; import java.util.Set; @@ -7,18 +7,18 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.softeer5.uniro_backend.route.entity.CautionType; +import com.softeer5.uniro_backend.map.enums.CautionFactor; import jakarta.persistence.AttributeConverter; import jakarta.persistence.Converter; @Converter -public class CautionListConverter implements AttributeConverter, String> { +public class CautionListConverter implements AttributeConverter, String> { private static final ObjectMapper mapper = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false); @Override - public String convertToDatabaseColumn(Set attribute) { + public String convertToDatabaseColumn(Set attribute) { try { if (attribute == null) { return "[]"; // List가 null일 경우, DB에 저장할 값은 [] @@ -30,7 +30,7 @@ public String convertToDatabaseColumn(Set attribute) { } @Override - public Set convertToEntityAttribute(String dbData) { + public Set convertToEntityAttribute(String dbData) { try { if (dbData == null || dbData.trim().isEmpty()) { return null; // dbData가 null 또는 빈 문자열일 경우 빈 리스트 반환 diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/resolver/DangerListConverter.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/resolver/DangerListConverter.java similarity index 82% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/resolver/DangerListConverter.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/common/resolver/DangerListConverter.java index f6246cf..abedf03 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/resolver/DangerListConverter.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/resolver/DangerListConverter.java @@ -1,4 +1,4 @@ -package com.softeer5.uniro_backend.resolver; +package com.softeer5.uniro_backend.common.resolver; import java.io.IOException; import java.util.Set; @@ -7,16 +7,16 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.softeer5.uniro_backend.route.entity.DangerType; +import com.softeer5.uniro_backend.map.enums.DangerFactor; import jakarta.persistence.AttributeConverter; -public class DangerListConverter implements AttributeConverter, String> { +public class DangerListConverter implements AttributeConverter, String> { private static final ObjectMapper mapper = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false); @Override - public String convertToDatabaseColumn(Set attribute) { + public String convertToDatabaseColumn(Set attribute) { try { if (attribute == null) { return "[]"; // List가 null일 경우, DB에 저장할 값은 [] @@ -28,7 +28,7 @@ public String convertToDatabaseColumn(Set attribute) { } @Override - public Set convertToEntityAttribute(String dbData) { + public Set convertToEntityAttribute(String dbData) { try { if (dbData == null || dbData.trim().isEmpty()) { return null; // dbData가 null 또는 빈 문자열일 경우 빈 리스트 반환 diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/utils/GeoUtils.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/utils/GeoUtils.java index f63bca1..49a5165 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/utils/GeoUtils.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/utils/GeoUtils.java @@ -16,8 +16,8 @@ public static GeometryFactory getInstance() { return geometryFactory; } - public static Point convertDoubleToPoint(double lat, double lng) { - return geometryFactory.createPoint(new Coordinate(lat, lng)); + public static Point convertDoubleToPoint(double lng, double lat) { + return geometryFactory.createPoint(new Coordinate(lng, lat)); } public static String convertDoubleToPointWTK(double lat, double lng) { diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/utils/RouteUtils.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/utils/RouteUtils.java new file mode 100644 index 0000000..ed2bbd7 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/utils/RouteUtils.java @@ -0,0 +1,16 @@ +package com.softeer5.uniro_backend.common.utils; + +import com.softeer5.uniro_backend.map.entity.Route; + +import static com.softeer5.uniro_backend.common.constant.UniroConst.BUILDING_ROUTE_DISTANCE; + +public final class RouteUtils { + + private RouteUtils(){ + // 인스턴스화 방지 + } + + public static boolean isBuildingRoute(Route route){ + return route.getDistance() > BUILDING_ROUTE_DISTANCE - 1; + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/external/MapClient.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/external/MapClient.java index 2c16746..7a079bb 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/external/MapClient.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/external/MapClient.java @@ -1,6 +1,6 @@ package com.softeer5.uniro_backend.external; -import com.softeer5.uniro_backend.node.entity.Node; +import com.softeer5.uniro_backend.map.entity.Node; import java.util.List; diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/external/MapClientImpl.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/external/MapClientImpl.java index 6f64e59..3a3fec4 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/external/MapClientImpl.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/external/MapClientImpl.java @@ -2,7 +2,7 @@ import com.softeer5.uniro_backend.common.error.ErrorCode; import com.softeer5.uniro_backend.common.exception.custom.ElevationApiException; -import com.softeer5.uniro_backend.node.entity.Node; +import com.softeer5.uniro_backend.map.entity.Node; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -21,7 +21,7 @@ public class MapClientImpl implements MapClient{ @Value("${map.api.key}") private String apiKey; private final String baseUrl = "https://maps.googleapis.com/maps/api/elevation/json"; - private final Integer MAX_BATCH_SIZE = 512; + private final Integer MAX_BATCH_SIZE = 100; private final String SUCCESS_STATUS = "OK"; private final WebClient webClient; diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/controller/RouteApi.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/controller/MapApi.java similarity index 68% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/controller/RouteApi.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/controller/MapApi.java index 5b9dfc8..57ba7c4 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/controller/RouteApi.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/controller/MapApi.java @@ -1,12 +1,14 @@ -package com.softeer5.uniro_backend.route.controller; +package com.softeer5.uniro_backend.map.controller; -import com.softeer5.uniro_backend.route.dto.request.CreateRoutesReqDTO; -import com.softeer5.uniro_backend.route.dto.response.FastestRouteResDTO; -import com.softeer5.uniro_backend.route.dto.response.GetAllRoutesResDTO; -import com.softeer5.uniro_backend.route.dto.response.GetRiskResDTO; -import com.softeer5.uniro_backend.route.dto.response.GetRiskRoutesResDTO; -import com.softeer5.uniro_backend.route.dto.request.PostRiskReqDTO; +import com.softeer5.uniro_backend.map.dto.request.CreateBuildingRouteReqDTO; +import com.softeer5.uniro_backend.map.dto.request.CreateRoutesReqDTO; +import com.softeer5.uniro_backend.map.dto.response.FastestRouteResDTO; +import com.softeer5.uniro_backend.map.dto.response.GetAllRoutesResDTO; +import com.softeer5.uniro_backend.map.dto.response.GetRiskResDTO; +import com.softeer5.uniro_backend.map.dto.response.GetRiskRoutesResDTO; +import com.softeer5.uniro_backend.map.dto.request.PostRiskReqDTO; +import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; @@ -18,8 +20,10 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; +import java.util.List; + @Tag(name = "간선 및 위험&주의 요소 관련 Api") -public interface RouteApi { +public interface MapApi { @Operation(summary = "모든 지도(노드,루트) 조회") @ApiResponses(value = { @@ -66,7 +70,16 @@ ResponseEntity createRoute (@PathVariable("univId") Long univId, @ApiResponse(responseCode = "200", description = "빠른 길 계산 성공"), @ApiResponse(responseCode = "400", description = "EXCEPTION(임시)", content = @Content), }) - ResponseEntity calculateFastestRoute(@PathVariable("univId") Long univId, - @RequestParam Long startNodeId, - @RequestParam Long endNodeId); + ResponseEntity> findFastestRoute(@PathVariable("univId") Long univId, + @RequestParam Long startNodeId, + @RequestParam Long endNodeId); + + + @Operation(summary = "빌딩 노드와 연결된 길 생성") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "빌딩 노드와 연결된 길 생성 성공"), + @ApiResponse(responseCode = "400", description = "EXCEPTION(임시)", content = @Content), + }) + ResponseEntity createBuildingRoute(@PathVariable("univId") Long univId, + @RequestBody @Valid CreateBuildingRouteReqDTO createBuildingRouteReqDTO); } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/controller/MapController.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/controller/MapController.java new file mode 100644 index 0000000..a554e7c --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/controller/MapController.java @@ -0,0 +1,86 @@ +package com.softeer5.uniro_backend.map.controller; + +import com.softeer5.uniro_backend.map.dto.request.CreateBuildingRouteReqDTO; +import com.softeer5.uniro_backend.map.dto.request.CreateRoutesReqDTO; +import com.softeer5.uniro_backend.map.dto.response.FastestRouteResDTO; +import com.softeer5.uniro_backend.map.dto.response.GetAllRoutesResDTO; +import com.softeer5.uniro_backend.map.dto.response.GetRiskResDTO; +import com.softeer5.uniro_backend.map.dto.response.GetRiskRoutesResDTO; +import com.softeer5.uniro_backend.map.dto.request.PostRiskReqDTO; +import com.softeer5.uniro_backend.map.service.RouteCalculator; + +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import com.softeer5.uniro_backend.map.service.MapService; + +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@RequiredArgsConstructor +@RestController +public class MapController implements MapApi { + + private final MapService mapService; + private final RouteCalculator routeCalculator; + + @Override + @GetMapping("/{univId}/routes") + public ResponseEntity getAllRoutesAndNodes(@PathVariable("univId") Long univId){ + GetAllRoutesResDTO allRoutes = mapService.getAllRoutes(univId); + return ResponseEntity.ok().body(allRoutes); + } + + @Override + @GetMapping("/{univId}/routes/risks") + public ResponseEntity getRiskRoutes(@PathVariable("univId") Long univId) { + GetRiskRoutesResDTO riskRoutes = mapService.getRiskRoutes(univId); + return ResponseEntity.ok().body(riskRoutes); + } + + @Override + @GetMapping("/{univId}/routes/{routeId}/risk") + public ResponseEntity getRisk(@PathVariable("univId") Long univId, + @PathVariable(value = "routeId") Long routeId){ + GetRiskResDTO riskResDTO = mapService.getRisk(univId, routeId); + return ResponseEntity.ok().body(riskResDTO); + } + + @Override + @PostMapping("/{univId}/route/risk/{routeId}") + public ResponseEntity updateRisk (@PathVariable("univId") Long univId, + @PathVariable("routeId") Long routeId, + @RequestBody @Valid PostRiskReqDTO postRiskReqDTO){ + mapService.updateRisk(univId,routeId,postRiskReqDTO); + return ResponseEntity.ok().build(); + } + + @Override + @PostMapping("/{univId}/route") + public ResponseEntity createRoute (@PathVariable("univId") Long univId, + @RequestBody @Valid CreateRoutesReqDTO routes){ + mapService.createRoute(univId, routes); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + + @Override + @GetMapping("/{univId}/routes/fastest") + public ResponseEntity> findFastestRoute(@PathVariable("univId") Long univId, + @RequestParam(value = "start-node-id") Long startNodeId, + @RequestParam(value = "end-node-id") Long endNodeId) { + List fastestRouteResDTO = mapService.findFastestRoute(univId, startNodeId, endNodeId); + return ResponseEntity.ok(fastestRouteResDTO); + } + + @Override + @PostMapping("/{univId}/routes/building") + public ResponseEntity createBuildingRoute(@PathVariable("univId") Long univId, + @RequestBody @Valid CreateBuildingRouteReqDTO createBuildingRouteReqDTO){ + mapService.createBuildingRoute(univId,createBuildingRouteReqDTO); + return ResponseEntity.status(HttpStatus.CREATED).build(); + } + +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/request/CreateBuildingRouteReqDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/request/CreateBuildingRouteReqDTO.java new file mode 100644 index 0000000..61141c0 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/request/CreateBuildingRouteReqDTO.java @@ -0,0 +1,19 @@ +package com.softeer5.uniro_backend.map.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +@Schema(name = "CreateBuildingRouteReqDTO", description = "빌딩과 연결된 길 추가 요청 DTO") +public class CreateBuildingRouteReqDTO { + @Schema(description = "빌딩 노드의 id", example = "16") + @NotNull + private final Long buildingNodeId; + + @Schema(description = "빌딩 노드와 연결될 노드의 id", example = "27") + @NotNull + private final Long nodeId; +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/request/CreateRouteReqDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/request/CreateRouteReqDTO.java similarity index 89% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/request/CreateRouteReqDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/request/CreateRouteReqDTO.java index ec4a021..b8f7233 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/request/CreateRouteReqDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/request/CreateRouteReqDTO.java @@ -1,4 +1,4 @@ -package com.softeer5.uniro_backend.route.dto.request; +package com.softeer5.uniro_backend.map.dto.request; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/request/CreateRoutesReqDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/request/CreateRoutesReqDTO.java similarity index 77% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/request/CreateRoutesReqDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/request/CreateRoutesReqDTO.java index 9ca0ba7..ac95924 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/request/CreateRoutesReqDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/request/CreateRoutesReqDTO.java @@ -1,8 +1,10 @@ -package com.softeer5.uniro_backend.route.dto.request; +package com.softeer5.uniro_backend.map.dto.request; import java.util.List; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -12,11 +14,13 @@ public class CreateRoutesReqDTO { @Schema(description = "시작 노드 id", example = "3") + @NotNull private final Long startNodeId; @Schema(description = "종료 노드 id", example = "4") private final Long endNodeId; @Schema(description = "노드 좌표", example = "") + @NotEmpty private final List coordinates; } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/request/PostRiskReqDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/request/PostRiskReqDTO.java similarity index 57% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/request/PostRiskReqDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/request/PostRiskReqDTO.java index 3c65ab3..3205f63 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/request/PostRiskReqDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/request/PostRiskReqDTO.java @@ -1,12 +1,12 @@ -package com.softeer5.uniro_backend.route.dto.request; +package com.softeer5.uniro_backend.map.dto.request; -import com.softeer5.uniro_backend.route.entity.CautionType; -import com.softeer5.uniro_backend.route.entity.DangerType; +import com.softeer5.uniro_backend.map.enums.CautionFactor; +import com.softeer5.uniro_backend.map.enums.DangerFactor; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.RequiredArgsConstructor; -import lombok.Setter; import java.util.List; @@ -16,8 +16,10 @@ public class PostRiskReqDTO { @Schema(description = "주의 요소 목록", example = "[\"SLOPE\", \"CURB\"]") - private List cautionTypes; + @NotNull + private List cautionFactors; @Schema(description = "위험 요소 목록", example = "[\"CURB\", \"STAIRS\"]") - private List dangerTypes; + @NotNull + private List dangerFactors; } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/BuildingRouteResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/BuildingRouteResDTO.java new file mode 100644 index 0000000..446d04f --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/BuildingRouteResDTO.java @@ -0,0 +1,27 @@ +package com.softeer5.uniro_backend.map.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@Schema(name = "BuildingRouteResDTO", description = "빌딩 루트 정보 DTO") +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class BuildingRouteResDTO { + + @Schema(description = "코어 노드 1", example = "32") + private final Long coreNode1Id; + + @Schema(description = "코어 노드 2", example = "13") + private final Long coreNode2Id; + + @Schema(description = "간선 정보", example = "") + private final List routes; + + public static BuildingRouteResDTO of(Long startNode, Long endNode, List routes){ + return new BuildingRouteResDTO(startNode, endNode, routes); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/CoreRouteResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/CoreRouteResDTO.java similarity index 89% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/CoreRouteResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/CoreRouteResDTO.java index 452f909..e7d06a6 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/CoreRouteResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/CoreRouteResDTO.java @@ -1,4 +1,4 @@ -package com.softeer5.uniro_backend.route.dto.response; +package com.softeer5.uniro_backend.map.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AccessLevel; @@ -16,7 +16,7 @@ public class CoreRouteResDTO { private final Long coreNode1Id; @Schema(description = "코어 노드 2", example = "13") - private final Long cordNode2Id; + private final Long coreNode2Id; @Schema(description = "간선 정보", example = "") private final List routes; diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/FastestRouteResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/FastestRouteResDTO.java new file mode 100644 index 0000000..ee909b3 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/FastestRouteResDTO.java @@ -0,0 +1,49 @@ +package com.softeer5.uniro_backend.map.dto.response; + +import com.softeer5.uniro_backend.map.entity.RoadExclusionPolicy; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.List; + +@Getter +@Schema(name = "FastestRouteResDTO", description = "빠른 경로 조회 DTO") +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class FastestRouteResDTO { + @Schema(description = "길찾기 타입 (PEDES-도보, WHEEL_FAST-휠체어빠른, WHEEL_SAFE-휠체어안전)", example = "PEDES") + private final RoadExclusionPolicy routeType; + @Schema(description = "길 찾기 결과에 위험요소가 포함되어있는지 여부", example = "true") + private final boolean hasCaution; + @Schema(description = "총 이동거리", example = "150.3421234") + private final double totalDistance; + @Schema(description = "총 걸리는 시간(초) - 도보", example = "1050.32198432") + private final Double pedestrianTotalCost; + @Schema(description = "총 걸리는 시간(초) - 수동휠체어", example = "2253.51234432") + private final Double manualTotalCost; + @Schema(description = "총 걸리는 시간(초) - 전동휠체어", example = "935.3125632") + private final Double electricTotalCost; + @Schema(description = "길 찾기 결과에 포함된 모든 길", example = "") + private final List routes; + @Schema(description = "상세안내 관련 정보", example = "") + private final List routeDetails; + + public static FastestRouteResDTO of(RoadExclusionPolicy routeType, + boolean hasCaution, + double totalDistance, + Double pedestrianTotalCost, + Double manualTotalCost, + Double electricTotalCost, + List routes, + List routeDetails) { + return new FastestRouteResDTO(routeType, + hasCaution, + totalDistance, + pedestrianTotalCost, + manualTotalCost, + electricTotalCost, + routes, + routeDetails); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetAllRoutesResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/GetAllRoutesResDTO.java similarity index 62% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetAllRoutesResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/GetAllRoutesResDTO.java index 9b0de0f..a4c3a99 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetAllRoutesResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/GetAllRoutesResDTO.java @@ -1,4 +1,4 @@ -package com.softeer5.uniro_backend.route.dto.response; +package com.softeer5.uniro_backend.map.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AccessLevel; @@ -7,8 +7,6 @@ import java.util.List; -import com.softeer5.uniro_backend.route.dto.response.CoreRouteResDTO; - @Getter @Schema(name = "GetAllRoutesResDTO", description = "모든 노드,루트 조회 DTO") @RequiredArgsConstructor(access = AccessLevel.PRIVATE) @@ -18,8 +16,11 @@ public class GetAllRoutesResDTO { private final List nodeInfos; @Schema(description = "루트 정보 (id, startNodeId, endNodeId)", example = "") private final List coreRoutes; + @Schema(description = "빌딩 루트 정보 (id, startNodeId, endNodeId)", example = "") + private final List buildingRoutes; - public static GetAllRoutesResDTO of(List nodeInfos, List coreRoutes){ - return new GetAllRoutesResDTO(nodeInfos, coreRoutes); + public static GetAllRoutesResDTO of(List nodeInfos, List coreRoutes, + List buildingRoutes){ + return new GetAllRoutesResDTO(nodeInfos, coreRoutes, buildingRoutes); } } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetCautionResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/GetCautionResDTO.java similarity index 71% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetCautionResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/GetCautionResDTO.java index 8d44017..1d54807 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetCautionResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/GetCautionResDTO.java @@ -1,10 +1,9 @@ -package com.softeer5.uniro_backend.route.dto.response; +package com.softeer5.uniro_backend.map.dto.response; import java.util.List; import java.util.Map; -import com.softeer5.uniro_backend.node.entity.Node; -import com.softeer5.uniro_backend.route.entity.CautionType; +import com.softeer5.uniro_backend.map.enums.CautionFactor; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AccessLevel; @@ -27,9 +26,9 @@ public class GetCautionResDTO { private final Long routeId; @Schema(description = "위험 요소 타입 리스트", example = "[\"SLOPE\", \"STAIRS\"]") - private final List cautionTypes; + private final List cautionFactors; - public static GetCautionResDTO of(Map node1, Map node2, Long routeId, List cautionTypes){ - return new GetCautionResDTO(node1, node2, routeId, cautionTypes); + public static GetCautionResDTO of(Map node1, Map node2, Long routeId, List cautionFactors){ + return new GetCautionResDTO(node1, node2, routeId, cautionFactors); } } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetDangerResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/GetDangerResDTO.java similarity index 68% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetDangerResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/GetDangerResDTO.java index caecb7f..3972e81 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetDangerResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/GetDangerResDTO.java @@ -1,10 +1,9 @@ -package com.softeer5.uniro_backend.route.dto.response; +package com.softeer5.uniro_backend.map.dto.response; import java.util.List; import java.util.Map; -import com.softeer5.uniro_backend.node.entity.Node; -import com.softeer5.uniro_backend.route.entity.DangerType; +import com.softeer5.uniro_backend.map.enums.DangerFactor; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AccessLevel; @@ -26,13 +25,13 @@ public class GetDangerResDTO { private final Long routeId; @Schema(description = "위험 요소 타입 리스트", example = "[\"CURB\", \"STAIRS\"]") - private final List dangerTypes; + private final List dangerFactors; - public static GetDangerResDTO of(Map node1, Map node2, Long routeId, List dangerTypes){ - return new GetDangerResDTO(node1, node2, routeId, dangerTypes); + public static GetDangerResDTO of(Map node1, Map node2, Long routeId, List dangerFactors){ + return new GetDangerResDTO(node1, node2, routeId, dangerFactors); } - public List getDangerTypes() { - return dangerTypes; + public List getDangerFactors() { + return dangerFactors; } } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetRiskResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/GetRiskResDTO.java similarity index 71% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetRiskResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/GetRiskResDTO.java index fa980e8..26855c5 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetRiskResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/GetRiskResDTO.java @@ -1,8 +1,8 @@ -package com.softeer5.uniro_backend.route.dto.response; +package com.softeer5.uniro_backend.map.dto.response; -import com.softeer5.uniro_backend.route.entity.CautionType; -import com.softeer5.uniro_backend.route.entity.DangerType; -import com.softeer5.uniro_backend.route.entity.Route; +import com.softeer5.uniro_backend.map.enums.CautionFactor; +import com.softeer5.uniro_backend.map.enums.DangerFactor; +import com.softeer5.uniro_backend.map.entity.Route; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AccessLevel; import lombok.Getter; @@ -17,9 +17,9 @@ public class GetRiskResDTO { @Schema(description = "route ID", example = "3") private final Long routeId; @Schema(description = "위험 요소 타입 리스트", example = "[\"SLOPE\", \"STAIRS\"]") - private final List cautionTypes; + private final List cautionFactors; @Schema(description = "위험 요소 타입 리스트", example = "[\"SLOPE\", \"STAIRS\"]") - private final List dangerTypes; + private final List dangerFactors; public static GetRiskResDTO of(Route route) { return new GetRiskResDTO(route.getId(),route.getCautionFactorsByList(), route.getDangerFactorsByList()); diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetRiskRoutesResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/GetRiskRoutesResDTO.java similarity index 93% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetRiskRoutesResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/GetRiskRoutesResDTO.java index ac096ca..f0b79ca 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetRiskRoutesResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/GetRiskRoutesResDTO.java @@ -1,4 +1,4 @@ -package com.softeer5.uniro_backend.route.dto.response; +package com.softeer5.uniro_backend.map.dto.response; import java.util.List; diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/NodeInfoResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/NodeInfoResDTO.java similarity index 89% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/NodeInfoResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/NodeInfoResDTO.java index 9fa9641..707b479 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/NodeInfoResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/NodeInfoResDTO.java @@ -1,11 +1,9 @@ -package com.softeer5.uniro_backend.route.dto.response; +package com.softeer5.uniro_backend.map.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; -import java.util.Map; - @Getter @Schema(name = "NodeInfoDTO", description = "노드 정보 DTO") @RequiredArgsConstructor(access = AccessLevel.PRIVATE) diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/RouteCoordinatesInfoResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/RouteCoordinatesInfoResDTO.java similarity index 92% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/RouteCoordinatesInfoResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/RouteCoordinatesInfoResDTO.java index 2771147..5875648 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/RouteCoordinatesInfoResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/RouteCoordinatesInfoResDTO.java @@ -1,4 +1,4 @@ -package com.softeer5.uniro_backend.route.dto.response; +package com.softeer5.uniro_backend.map.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/RouteDetailResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/RouteDetailResDTO.java similarity index 62% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/RouteDetailResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/RouteDetailResDTO.java index 540274e..f100af1 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/RouteDetailResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/RouteDetailResDTO.java @@ -1,11 +1,13 @@ -package com.softeer5.uniro_backend.route.dto.response; +package com.softeer5.uniro_backend.map.dto.response; -import com.softeer5.uniro_backend.route.entity.DirectionType; +import com.softeer5.uniro_backend.map.enums.CautionFactor; +import com.softeer5.uniro_backend.map.enums.DirectionType; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; +import java.util.List; import java.util.Map; @Getter @@ -18,8 +20,12 @@ public class RouteDetailResDTO { private final DirectionType directionType; @Schema(description = "상세 경로의 좌표", example = "{\"lng\": 127.123456, \"lat\": 37.123456}") private final Map coordinates; + @Schema(description = "주의 요소 타입 리스트", example = "[\"SLOPE\", \"STAIRS\"]") + private final List cautionFactors; - public static RouteDetailResDTO of(double dist, DirectionType directionType, Map coordinates) { - return new RouteDetailResDTO(dist, directionType, coordinates); + public static RouteDetailResDTO of(double dist, DirectionType directionType, + Map coordinates, + List cautionFactors) { + return new RouteDetailResDTO(dist, directionType, coordinates, cautionFactors); } } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/RouteInfoResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/RouteInfoResDTO.java similarity index 65% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/RouteInfoResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/RouteInfoResDTO.java index 6a38995..3a18597 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/RouteInfoResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/dto/response/RouteInfoResDTO.java @@ -1,15 +1,13 @@ -package com.softeer5.uniro_backend.route.dto.response; +package com.softeer5.uniro_backend.map.dto.response; -import com.softeer5.uniro_backend.node.entity.Node; -import com.softeer5.uniro_backend.route.entity.CautionType; -import com.softeer5.uniro_backend.route.entity.Route; +import com.softeer5.uniro_backend.map.entity.Node; +import com.softeer5.uniro_backend.map.entity.Route; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; import java.util.Map; -import java.util.Set; @Getter @RequiredArgsConstructor(access = AccessLevel.PRIVATE) @@ -21,13 +19,10 @@ public class RouteInfoResDTO { private final Map node1; @Schema(description = "노드 2의 좌표", example = "{\"lng\": 127.123456, \"lat\": 37.123456}") private final Map node2; - @Schema(description = "위험 요소 타입 리스트", example = "[\"SLOPE\", \"STAIRS\"]") - private final Set cautionFactors; public static RouteInfoResDTO of(Route route, Node node1, Node node2) { return new RouteInfoResDTO(route.getId(), node1.getXY(), - node2.getXY(), - route.getCautionFactors()); + node2.getXY()); } } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/entity/Node.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/entity/Node.java similarity index 78% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/node/entity/Node.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/entity/Node.java index f07c6ea..970f4c8 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/entity/Node.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/entity/Node.java @@ -1,13 +1,17 @@ -package com.softeer5.uniro_backend.node.entity; +package com.softeer5.uniro_backend.map.entity; import static com.softeer5.uniro_backend.common.constant.UniroConst.*; +import java.time.LocalDateTime; import java.util.Map; +import jakarta.persistence.EntityListeners; import lombok.*; import org.hibernate.envers.Audited; import org.locationtech.jts.geom.Point; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -18,11 +22,10 @@ @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Builder -@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @ToString @Audited +@EntityListeners(AuditingEntityListener.class) public class Node { @Id @@ -30,7 +33,6 @@ public class Node { private Long id; @NotNull - @Column(columnDefinition = "POINT SRID 4326") private Point coordinates; private double height; @@ -42,6 +44,9 @@ public class Node { @NotNull private Long univId; + @CreatedDate + private LocalDateTime createdAt; + public Map getXY(){ return Map.of("lat", coordinates.getY(), "lng", coordinates.getX()); } @@ -65,6 +70,9 @@ public void setCoordinates(Point coordinates) { public void setCore(boolean isCore){ this.isCore = isCore; } + public void updateFromRevision(boolean isCore){ + this.isCore = isCore; + } public String getNodeKey() { return coordinates.getX() + NODE_KEY_DELIMITER + coordinates.getY(); diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/entity/RoadExclusionPolicy.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/entity/RoadExclusionPolicy.java new file mode 100644 index 0000000..e241799 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/entity/RoadExclusionPolicy.java @@ -0,0 +1,29 @@ +package com.softeer5.uniro_backend.map.entity; + +import static com.softeer5.uniro_backend.common.constant.UniroConst.PEDESTRIAN_SECONDS_PER_MITER; + +public enum RoadExclusionPolicy { + + PEDES, + WHEEL_FAST, + WHEEL_SAFE; + + public static boolean isAvailableRoute(RoadExclusionPolicy policy, Route route) { + boolean isCaution = !route.getCautionFactors().isEmpty(); + boolean isDanger = !route.getDangerFactors().isEmpty(); + return switch(policy){ + case PEDES -> true; + case WHEEL_FAST -> !isDanger; + case WHEEL_SAFE -> !isCaution && !isDanger; + default -> false; + }; + } + + public static Double calculateCost(RoadExclusionPolicy policy, double type, double distance){ + boolean isWheel = (type!=PEDESTRIAN_SECONDS_PER_MITER); + double totalCost = type * distance; + if(isWheel && policy == PEDES) return null; + if(!isWheel && policy != PEDES) return null; + return totalCost; + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/entity/Route.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/entity/Route.java similarity index 52% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/entity/Route.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/entity/Route.java index 56ab4e3..2ee456d 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/entity/Route.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/entity/Route.java @@ -1,21 +1,26 @@ -package com.softeer5.uniro_backend.route.entity; +package com.softeer5.uniro_backend.map.entity; import static jakarta.persistence.FetchType.*; +import java.time.LocalDateTime; import java.util.HashSet; import java.util.List; import java.util.Set; -import com.softeer5.uniro_backend.resolver.CautionListConverter; -import com.softeer5.uniro_backend.resolver.DangerListConverter; -import com.softeer5.uniro_backend.node.entity.Node; +import com.softeer5.uniro_backend.common.resolver.CautionListConverter; +import com.softeer5.uniro_backend.common.resolver.DangerListConverter; +import com.softeer5.uniro_backend.map.enums.CautionFactor; +import com.softeer5.uniro_backend.map.enums.DangerFactor; import org.hibernate.envers.Audited; import org.hibernate.envers.RelationTargetAuditMode; import org.locationtech.jts.geom.LineString; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -31,15 +36,15 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @Audited +@EntityListeners(AuditingEntityListener.class) public class Route { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private double cost; + private double distance; - @Column(columnDefinition = "LINESTRING SRID 4326") // WGS84 좌표계 private LineString path; @ManyToOne(fetch = LAZY) @@ -57,46 +62,61 @@ public class Route { @Column(name = "univ_id") private Long univId; - @Column(name = "core_route_id") - private Long coreRouteId; - @Convert(converter = CautionListConverter.class) @Column(name = "caution_factors") @NotNull - private Set cautionFactors = new HashSet<>(); + private Set cautionFactors = new HashSet<>(); @Convert(converter = DangerListConverter.class) @Column(name = "danger_factors") @NotNull - private Set dangerFactors = new HashSet<>(); + private Set dangerFactors = new HashSet<>(); + + @CreatedDate + @Column(name = "created_at") + private LocalDateTime createdAt; - public List getCautionFactorsByList(){ + public List getCautionFactorsByList(){ return cautionFactors.stream().toList(); } - public List getDangerFactorsByList(){ + public List getDangerFactorsByList(){ return dangerFactors.stream().toList(); } - public void setCautionFactors(List cautionFactors) { + public void setCautionFactorsByList(List cautionFactors) { this.cautionFactors.clear(); this.cautionFactors.addAll(cautionFactors); } - public void setDangerFactors(List dangerFactors) { + public void setDangerFactorsByList(List dangerFactors) { this.dangerFactors.clear(); this.dangerFactors.addAll(dangerFactors); } + public void updateFromRevision(Route revRoute){ + this.cautionFactors = revRoute.getCautionFactors(); + this.dangerFactors = revRoute.getDangerFactors(); + this.distance = revRoute.distance; + } + + public boolean isEqualRoute(Route route) { + if(!route.getId().equals(this.getId())) return false; + if(route.getDistance() != this.getDistance())return false; + if(!route.getCautionFactors().equals(this.getCautionFactors())) return false; + if(!route.getDangerFactors().equals(this.getDangerFactors())) return false; + + return true; + } + @Builder - private Route(double cost, LineString path, Node node1, Node node2, Long univId, Long coreRouteId, - Set cautionFactors, Set dangerFactors) { - this.cost = cost; + private Route(double distance, LineString path, Node node1, Node node2, Long univId, + Set cautionFactors, Set dangerFactors) { + this.distance = distance; this.path = path; this.node1 = node1; this.node2 = node2; this.univId = univId; - this.coreRouteId = coreRouteId; this.cautionFactors = cautionFactors; this.dangerFactors = dangerFactors; } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/enums/CautionFactor.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/enums/CautionFactor.java new file mode 100644 index 0000000..f7963f3 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/enums/CautionFactor.java @@ -0,0 +1,8 @@ +package com.softeer5.uniro_backend.map.enums; + +public enum CautionFactor { + CURB, // 턱 + CRACK, // 균열 + SLOPE, // 경사 + ETC // 기타 +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/entity/DangerType.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/enums/DangerFactor.java similarity index 50% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/entity/DangerType.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/enums/DangerFactor.java index 481bcc7..d23c8d7 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/entity/DangerType.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/enums/DangerFactor.java @@ -1,6 +1,6 @@ -package com.softeer5.uniro_backend.route.entity; +package com.softeer5.uniro_backend.map.enums; -public enum DangerType { +public enum DangerFactor { CURB, // 턱 STAIRS, // 계단 SLOPE, // 경사 diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/entity/DirectionType.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/enums/DirectionType.java similarity index 81% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/entity/DirectionType.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/enums/DirectionType.java index d561b41..c4b90f6 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/entity/DirectionType.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/enums/DirectionType.java @@ -1,4 +1,4 @@ -package com.softeer5.uniro_backend.route.entity; +package com.softeer5.uniro_backend.map.enums; public enum DirectionType { STRAIGHT, // 직진 diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/repository/NodeRepository.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/repository/NodeRepository.java new file mode 100644 index 0000000..2172d6e --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/repository/NodeRepository.java @@ -0,0 +1,18 @@ +package com.softeer5.uniro_backend.map.repository; + +import com.softeer5.uniro_backend.map.entity.Node; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + + +public interface NodeRepository extends JpaRepository { + + @Modifying(clearAutomatically = true) + @Transactional + @Query("DELETE FROM Node n WHERE n.univId = :univId AND n.createdAt > :versionTimeStamp") + void deleteAllByCreatedAt(Long univId, LocalDateTime versionTimeStamp); +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/repository/RouteRepository.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/repository/RouteRepository.java similarity index 72% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/repository/RouteRepository.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/map/repository/RouteRepository.java index 8925506..a979e1b 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/repository/RouteRepository.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/repository/RouteRepository.java @@ -1,17 +1,18 @@ -package com.softeer5.uniro_backend.route.repository; +package com.softeer5.uniro_backend.map.repository; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; -import com.softeer5.uniro_backend.route.entity.Route; +import com.softeer5.uniro_backend.map.entity.Route; -@Repository public interface RouteRepository extends JpaRepository { @EntityGraph(attributePaths = {"node1", "node2"}) @@ -58,4 +59,20 @@ Optional findRouteByLineStringAndUnivId(@Param("univId") Long univId, Optional findByIdAndUnivId (Long id, Long univId); + @Modifying(clearAutomatically = true) + @Transactional + @Query("DELETE FROM Route r WHERE r.univId =:univId AND r.createdAt > :versionTimeStamp") + void deleteAllByCreatedAt(@Param("univId") Long univId, @Param("versionTimeStamp") LocalDateTime versionTimeStamp); + + @Query(value = """ + SELECT COUNT(*) FROM Route r + WHERE r.univId = :univId + AND ( + r.node1.id = :nodeId + OR + r.node2.id = :nodeId + ) + """) + int countByUnivIdAndNodeId(Long univId, Long nodeId); + } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/service/MapService.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/service/MapService.java new file mode 100644 index 0000000..354a589 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/service/MapService.java @@ -0,0 +1,166 @@ +package com.softeer5.uniro_backend.map.service; + +import static com.softeer5.uniro_backend.common.constant.UniroConst.BUILDING_ROUTE_DISTANCE; +import static com.softeer5.uniro_backend.common.constant.UniroConst.CORE_NODE_CONDITION; +import static com.softeer5.uniro_backend.common.error.ErrorCode.*; +import static com.softeer5.uniro_backend.common.utils.GeoUtils.getInstance; + +import java.util.*; + +import com.softeer5.uniro_backend.admin.annotation.RevisionOperation; +import com.softeer5.uniro_backend.admin.enums.RevisionOperationType; +import com.softeer5.uniro_backend.building.entity.Building; +import com.softeer5.uniro_backend.common.error.ErrorCode; +import com.softeer5.uniro_backend.common.exception.custom.BuildingException; +import com.softeer5.uniro_backend.common.exception.custom.NodeException; +import com.softeer5.uniro_backend.common.exception.custom.RouteCalculationException; +import com.softeer5.uniro_backend.common.exception.custom.RouteException; +import com.softeer5.uniro_backend.external.MapClient; +import com.softeer5.uniro_backend.map.dto.request.CreateRoutesReqDTO; +import com.softeer5.uniro_backend.map.entity.Node; + +import com.softeer5.uniro_backend.building.repository.BuildingRepository; +import com.softeer5.uniro_backend.map.repository.NodeRepository; +import com.softeer5.uniro_backend.map.dto.request.CreateBuildingRouteReqDTO; +import com.softeer5.uniro_backend.map.dto.response.*; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.softeer5.uniro_backend.map.dto.request.PostRiskReqDTO; +import com.softeer5.uniro_backend.map.entity.Route; +import com.softeer5.uniro_backend.map.repository.RouteRepository; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class MapService { + private final RouteRepository routeRepository; + private final NodeRepository nodeRepository; + private final BuildingRepository buildingRepository; + + private final RouteCalculator routeCalculator; + + private final MapClient mapClient; + + public GetAllRoutesResDTO getAllRoutes(Long univId) { + List routes = routeRepository.findAllRouteByUnivIdWithNodes(univId); + + // 맵이 존재하지 않을 경우 예외 + if(routes.isEmpty()) { + throw new RouteException("Route Not Found", ROUTE_NOT_FOUND); + } + + return routeCalculator.assembleRoutes(routes); + } + + public List findFastestRoute(Long univId, Long startNodeId, Long endNodeId){ + + if(startNodeId.equals(endNodeId)){ + throw new RouteCalculationException("Start and end nodes cannot be the same", SAME_START_AND_END_POINT); + } + + List buildings = buildingRepository.findAllByNodeIdIn(List.of(startNodeId, endNodeId)); + + if(buildings.size() != 2 + || buildings.get(0).getNodeId().equals(buildings.get(1).getNodeId()) + || buildings.stream().anyMatch(building -> !Objects.equals(building.getUnivId(), univId))){ + + throw new BuildingException("Building not found", BUILDING_NOT_FOUND); + } + + List routesWithNode = routeRepository.findAllRouteByUnivIdWithNodes(univId); + + return routeCalculator.calculateFastestRoute(startNodeId, endNodeId, routesWithNode); + } + + public GetRiskRoutesResDTO getRiskRoutes(Long univId) { + List riskRoutes = routeRepository.findRiskRouteByUnivId(univId); + return routeCalculator.mapRisks(riskRoutes); + } + + + + public GetRiskResDTO getRisk(Long univId, Long routeId) { + Route route = routeRepository.findById(routeId) + .orElseThrow(() -> new RouteException("Route not found", ROUTE_NOT_FOUND)); + return GetRiskResDTO.of(route); + } + + @RevisionOperation(RevisionOperationType.UPDATE_RISK) + @Transactional + public void updateRisk(Long univId, Long routeId, PostRiskReqDTO postRiskReqDTO) { + Route route = routeRepository.findByIdAndUnivId(routeId, univId) + .orElseThrow(() -> new RouteException("Route not Found", ROUTE_NOT_FOUND)); + + if(!postRiskReqDTO.getCautionFactors().isEmpty() && !postRiskReqDTO.getDangerFactors().isEmpty()){ + throw new RouteException("DangerFactors and CautionFactors can't exist simultaneously.", + ErrorCode.CAUTION_DANGER_CANT_EXIST_SIMULTANEOUSLY); + } + + route.setCautionFactorsByList(postRiskReqDTO.getCautionFactors()); + route.setDangerFactorsByList(postRiskReqDTO.getDangerFactors()); + } + + @RevisionOperation(RevisionOperationType.CREATE_BUILDING_ROUTE) + @Transactional + public void createBuildingRoute(Long univId, CreateBuildingRouteReqDTO createBuildingRouteReqDTO) { + GeometryFactory geometryFactory = getInstance(); + Long buildingNodeId = createBuildingRouteReqDTO.getBuildingNodeId(); + Long nodeId = createBuildingRouteReqDTO.getNodeId(); + + if(!buildingRepository.existsByNodeIdAndUnivId(buildingNodeId, univId)) { + throw new BuildingException("Not Building Node", NOT_BUILDING_NODE); + } + + Node buildingNode = nodeRepository.findById(buildingNodeId) + .orElseThrow(()-> new NodeException("Node not found", NODE_NOT_FOUND)); + + Node connectedNode = nodeRepository.findById(nodeId) + .orElseThrow(()-> new NodeException("Node not found", NODE_NOT_FOUND)); + + int connectedRouteCount = routeRepository.countByUnivIdAndNodeId(univId, nodeId); + if(connectedRouteCount>= CORE_NODE_CONDITION - 1){ + connectedNode.setCore(true); + } + + int buildingRouteCount = routeRepository.countByUnivIdAndNodeId(univId, buildingNodeId); + if(buildingRouteCount>=CORE_NODE_CONDITION-1){ + buildingNode.setCore(true); + } + + Route route = Route.builder() + .distance(BUILDING_ROUTE_DISTANCE) + .path(geometryFactory.createLineString( + new Coordinate[] {buildingNode.getCoordinates().getCoordinate(), + connectedNode.getCoordinates().getCoordinate()})) + .node1(buildingNode) + .node2(connectedNode) + .cautionFactors(Collections.EMPTY_SET) + .dangerFactors(Collections.EMPTY_SET) + .univId(univId).build(); + + routeRepository.save(route); + } + + @Transactional + @RevisionOperation(RevisionOperationType.CREATE_ROUTE) + public void createRoute(Long univId, CreateRoutesReqDTO requests){ + + List savedRoutes = routeRepository.findAllRouteByUnivIdWithNodes(univId); + + List nodesForSave = routeCalculator.createValidRouteNodes(univId, requests.getStartNodeId(), + requests.getEndNodeId(), + requests.getCoordinates(), savedRoutes); + + mapClient.fetchHeights(nodesForSave); + + List routes = routeCalculator.createLinkedRouteAndSave(univId, nodesForSave); + + nodeRepository.saveAll(nodesForSave); + routeRepository.saveAll(routes); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/service/RouteCalculator.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/service/RouteCalculator.java new file mode 100644 index 0000000..6e0e5d7 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/map/service/RouteCalculator.java @@ -0,0 +1,748 @@ +package com.softeer5.uniro_backend.map.service; + +import static com.softeer5.uniro_backend.common.constant.UniroConst.*; +import static com.softeer5.uniro_backend.common.error.ErrorCode.*; +import static com.softeer5.uniro_backend.map.entity.RoadExclusionPolicy.calculateCost; +import static com.softeer5.uniro_backend.map.entity.RoadExclusionPolicy.isAvailableRoute; + +import com.softeer5.uniro_backend.common.error.ErrorCode; +import com.softeer5.uniro_backend.common.exception.custom.NodeException; +import com.softeer5.uniro_backend.common.exception.custom.RouteCalculationException; +import com.softeer5.uniro_backend.common.exception.custom.RouteException; +import com.softeer5.uniro_backend.common.utils.GeoUtils; +import com.softeer5.uniro_backend.map.dto.response.*; +import com.softeer5.uniro_backend.map.entity.*; +import com.softeer5.uniro_backend.map.dto.request.CreateRouteReqDTO; +import com.softeer5.uniro_backend.map.enums.CautionFactor; +import com.softeer5.uniro_backend.map.enums.DirectionType; +import com.softeer5.uniro_backend.map.entity.Route; +import lombok.AllArgsConstructor; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.index.strtree.STRtree; +import org.springframework.stereotype.Component; + +import java.util.*; + +@Component +public class RouteCalculator { + private final GeometryFactory geometryFactory = GeoUtils.getInstance(); + private final List policies = List.of( + RoadExclusionPolicy.PEDES, + RoadExclusionPolicy.WHEEL_FAST, + RoadExclusionPolicy.WHEEL_SAFE + ); + + @AllArgsConstructor + private class CostToNextNode implements Comparable { + private double cost; + private Node nextNode; + + @Override + public int compareTo(CostToNextNode o) { + return Double.compare(this.cost, o.cost); + } + } + + public GetAllRoutesResDTO assembleRoutes(List routes) { + Map> adjMap = new HashMap<>(); + Map nodeMap = new HashMap<>(); + List buildingRoutes = new ArrayList<>(); + Node startNode = null; + + for (Route route : routes) { + nodeMap.put(route.getNode1().getId(), route.getNode1()); + nodeMap.put(route.getNode2().getId(), route.getNode2()); + + if (isBuildingRoute(route)) { + List routeCoordinates = new ArrayList<>(); + routeCoordinates.add(RouteCoordinatesInfoResDTO.of(route.getId(), route.getNode1().getId(), route.getNode2().getId())); + buildingRoutes.add(BuildingRouteResDTO.of(route.getNode1().getId(), route.getNode2().getId(), routeCoordinates)); + continue; + } + + adjMap.computeIfAbsent(route.getNode1().getId(), k -> new ArrayList<>()).add(route); + adjMap.computeIfAbsent(route.getNode2().getId(), k -> new ArrayList<>()).add(route); + + if (startNode == null) { + if (route.getNode1().isCore()) startNode = route.getNode1(); + else if (route.getNode2().isCore()) startNode = route.getNode2(); + } + } + + List nodeInfos = nodeMap.entrySet().stream() + .map(entry -> NodeInfoResDTO.of(entry.getKey(), entry.getValue().getX(), entry.getValue().getY())) + .toList(); + + startNode = determineStartNode(startNode, adjMap, nodeMap, routes); + + return GetAllRoutesResDTO.of(nodeInfos, getCoreRoutes(adjMap, List.of(startNode)), buildingRoutes); + } + + private Node determineStartNode(Node startNode, Map> adjMap, Map nodeMap, List routes) { + if (startNode != null) return startNode; + + List endNodes = adjMap.entrySet().stream() + .filter(entry -> entry.getValue().size() == 1) + .map(Map.Entry::getKey) + .toList(); + + if (endNodes.size() == IS_SINGLE_ROUTE) { + return nodeMap.get(endNodes.get(0)); + } else if (endNodes.isEmpty()) { + return routes.get(0).getNode1(); + } else { + throw new RouteException("Invalid Map", ErrorCode.INVALID_MAP); + } + } + + + public List calculateFastestRoute(Long startNodeId, Long endNodeId, List routes){ + List result = new ArrayList<>(); + + for (RoadExclusionPolicy policy : policies) { + //인접 리스트 + Map> adjMap = new HashMap<>(); + Map nodeMap = new HashMap<>(); + + for (Route route : routes) { + if (!isAvailableRoute(policy, route)) continue; + addRouteToGraph(route, adjMap, nodeMap); + } + + Node startNode = nodeMap.get(startNodeId); + Node endNode = nodeMap.get(endNodeId); + + if(startNode==null || endNode==null){ + continue; + } + + //길찾기 알고리즘 수행 + Map prevRoute = findFastestRoute(startNode, endNode, adjMap); + + //길찾기 결과가 null인 경우 continue + if(prevRoute == null) continue; + + //길찾기 경로 결과 정리 + List shortestRoutes = reorderRoute(startNode, endNode, prevRoute); + Route startRoute = shortestRoutes.get(0); + Route endRoute = shortestRoutes.get(shortestRoutes.size() - 1); + + if (isBuildingRoute(startRoute)) { + if (startRoute.getId().equals(endRoute.getId())) { + //출발점과 도착점이 같은 경우 + continue; + } + startNode = startNode.getId().equals(startRoute.getNode1().getId()) ? startRoute.getNode2() : startRoute.getNode1(); + shortestRoutes.remove(0); + } + //만약 종료 route가 건물과 이어진 노드라면 해당 route는 결과에서 제외 + if (isBuildingRoute(endRoute)) { + endNode = endNode.getId().equals(endRoute.getNode1().getId()) ? endRoute.getNode2() : endRoute.getNode1(); + shortestRoutes.remove(shortestRoutes.size() - 1); + } + + boolean hasCaution = false; + double totalDistance = 0.0; + + // 결과를 DTO로 정리 + List routeInfoDTOS = new ArrayList<>(); + Node currentNode = startNode; + // 외부 변수를 수정해야하기 때문에 for-loop문 사용 + for (Route route : shortestRoutes) { + totalDistance += route.getDistance(); + if (!route.getCautionFactors().isEmpty()) { + hasCaution = true; + } + + Node firstNode = route.getNode1(); + Node secondNode = route.getNode2(); + if (currentNode.getId().equals(secondNode.getId())) { + Node temp = firstNode; + firstNode = secondNode; + secondNode = temp; + } + currentNode = secondNode; + + routeInfoDTOS.add(RouteInfoResDTO.of(route, firstNode, secondNode)); + } + + //처음과 마지막을 제외한 구간에서 빌딩노드를 거쳐왔다면, 이는 유효한 길이 없는 것이므로 예외처리 + if (totalDistance > BUILDING_ROUTE_DISTANCE - 1) continue; + + List details = getRouteDetail(startNode, endNode, shortestRoutes); + + result.add(FastestRouteResDTO.of(policy, hasCaution, totalDistance, + calculateCost(policy, PEDESTRIAN_SECONDS_PER_MITER, totalDistance), + calculateCost(policy, MANUAL_WHEELCHAIR_SECONDS_PER_MITER,totalDistance), + calculateCost(policy, ELECTRIC_WHEELCHAIR_SECONDS_PER_MITER,totalDistance), + routeInfoDTOS, details)); + } + + if(result.isEmpty()) { + throw new RouteCalculationException("Unable to find a valid route", ErrorCode.FASTEST_ROUTE_NOT_FOUND); + } + + return result; + } + + private void addRouteToGraph(Route route, Map> adjMap, Map nodeMap) { + adjMap.computeIfAbsent(route.getNode1().getId(), k -> new ArrayList<>()).add(route); + adjMap.computeIfAbsent(route.getNode2().getId(), k -> new ArrayList<>()).add(route); + nodeMap.putIfAbsent(route.getNode1().getId(), route.getNode1()); + nodeMap.putIfAbsent(route.getNode2().getId(), route.getNode2()); + } + + private Map findFastestRoute(Node startNode, Node endNode, Map> adjMap){ + //key : nodeId, value : 최단거리 중 해당 노드를 향한 route + Map prevRoute = new HashMap<>(); + // startNode로부터 각 노드까지 걸리는 cost를 저장하는 자료구조 + Map costMap = new HashMap<>(); + PriorityQueue pq = new PriorityQueue<>(); + pq.add(new CostToNextNode(0.0, startNode)); + costMap.put(startNode.getId(), 0.0); + + // 길찾기 알고리즘 + while(!pq.isEmpty()){ + CostToNextNode costToNextNode = pq.poll(); + double currentDistance = costToNextNode.cost; + Node currentNode = costToNextNode.nextNode; + if (currentNode.getId().equals(endNode.getId())) break; + if(costMap.containsKey(currentNode.getId()) + && currentDistance > costMap.get(currentNode.getId())) continue; + + for(Route route : adjMap.getOrDefault(currentNode.getId(), Collections.emptyList())){ + double newDistance = currentDistance + route.getDistance(); + Node nextNode = route.getNode1().getId().equals(currentNode.getId())?route.getNode2():route.getNode1(); + if(!costMap.containsKey(nextNode.getId()) || costMap.get(nextNode.getId()) > newDistance){ + costMap.put(nextNode.getId(), newDistance); + pq.add(new CostToNextNode(newDistance, nextNode)); + prevRoute.put(nextNode.getId(), route); + } + } + } + //길 없는 경우 + if(!costMap.containsKey(endNode.getId())){ + return null; + } + + return prevRoute; + } + + // 길찾기 결과를 파싱하여 출발지 -> 도착지 형태로 재배열하는 메서드 + private List reorderRoute(Node startNode, Node endNode, Map prevRoute){ + List shortestRoutes = new ArrayList<>(); + Node currentNode = endNode; + + //endNode부터 역순회하여 배열에 저장 + while(!currentNode.getId().equals(startNode.getId())){ + Route previousRoute = prevRoute.get(currentNode.getId()); + shortestRoutes.add(previousRoute); + currentNode = previousRoute.getNode1().getId().equals(currentNode.getId()) ? previousRoute.getNode2() : previousRoute.getNode1(); + } + + //이후 reverse를 통해 시작점 -> 도착점 방향으로 재정렬 + Collections.reverse(shortestRoutes); + + return shortestRoutes; + } + + private boolean isBuildingRoute(Route route){ + return route.getDistance() > BUILDING_ROUTE_DISTANCE - 1; + } + + // 두 route 간의 각도를 통한 계산으로 방향성을 정하는 메서드 + private DirectionType calculateDirection(Node secondNode, Route inBoundRoute, Route outBoundRoute) { + Node firstNode = inBoundRoute.getNode1().equals(secondNode) ? inBoundRoute.getNode2() : inBoundRoute.getNode1(); + Node thirdNode = outBoundRoute.getNode1().equals(secondNode) ? outBoundRoute.getNode2() : outBoundRoute.getNode1(); + + Point p1 = firstNode.getCoordinates(); + Point p2 = secondNode.getCoordinates(); + Point p3 = thirdNode.getCoordinates(); + + double v1x = p2.getX() - p1.getX(); + double v1y = p2.getY() - p1.getY(); + + double v2x = p3.getX() - p2.getX(); + double v2y = p3.getY() - p2.getY(); + + double dotProduct = (v1x * v2x) + (v1y * v2y); + double magnitudeV1 = Math.sqrt(v1x * v1x + v1y * v1y); + double magnitudeV2 = Math.sqrt(v2x * v2x + v2y * v2y); + + double cosTheta = dotProduct / (magnitudeV1 * magnitudeV2); + double angle = Math.toDegrees(Math.acos(cosTheta)); + + double crossProduct = (v1x * v2y) - (v1y * v2x); + + if (angle < 30) { + return DirectionType.STRAIGHT; + } else if (angle >= 30 && angle < 150) { + return (crossProduct > 0) ? DirectionType.LEFT : DirectionType.RIGHT; + } else if (angle >= 150 && angle < 180) { + return (crossProduct > 0) ? DirectionType.SHARP_LEFT : DirectionType.SHARP_RIGHT; + } else { + return DirectionType.STRAIGHT; + } + } + + private double calculateRouteDistance(Route route){ + return calculateDistance(route.getNode1().getCoordinates().getX(), + route.getNode1().getCoordinates().getY(), + route.getNode2().getCoordinates().getX(), + route.getNode2().getCoordinates().getY()); + } + + // 하버사인 공식으로 길의 길이를 리턴하는 메서드 + private double calculateDistance(double lng1, double lat1, double lng2, double lat2) { + double radLat1 = Math.toRadians(lat1); + double radLat2 = Math.toRadians(lat2); + double radLng1 = Math.toRadians(lng1); + double radLng2 = Math.toRadians(lng2); + + double deltaLat = radLat2 - radLat1; + double deltaLng = radLng2 - radLng1; + + double a = Math.pow(Math.sin(deltaLat / 2), 2) + + Math.cos(radLat1) * Math.cos(radLat2) + * Math.pow(Math.sin(deltaLng / 2), 2); + double c = 2 * Math.asin(Math.sqrt(a)); + + return EARTH_RADIUS * c; + } + + // 길 상세정보를 추출하는 메서드 + private List getRouteDetail(Node startNode, Node endNode, List shortestRoutes){ + List details = new ArrayList<>(); + double accumulatedDistance = 0.0; + Node now = startNode; + Map checkPointNodeCoordinates = startNode.getXY(); + DirectionType checkPointType = DirectionType.STRAIGHT; + List checkPointCautionFactors = new ArrayList<>(); + + // 길찾기 결과 상세정보 정리 + for(int i=0;i())); + break; + } + if(nxt.isCore()){ + DirectionType directionType = calculateDirection(nxt, nowRoute, shortestRoutes.get(i+1)); + if(directionType == DirectionType.STRAIGHT){ + now = nxt; + continue; + } + details.add(RouteDetailResDTO.of(accumulatedDistance, checkPointType, + checkPointNodeCoordinates, checkPointCautionFactors)); + checkPointNodeCoordinates = nxt.getXY(); + checkPointType = directionType; + accumulatedDistance = 0.0; + checkPointCautionFactors = Collections.emptyList(); + } + + now = nxt; + } + + return details; + } + + private Map getCenter(Node n1, Node n2){ + return Map.of("lat", (n1.getCoordinates().getY() + n2.getCoordinates().getY())/2 + , "lng", (n1.getCoordinates().getX() + n2.getCoordinates().getX())/2); + } + + // coreRoute를 만들어주는 메서드 + public List getCoreRoutes(Map> adjMap, List startNode) { + List result = new ArrayList<>(); + // core node간의 BFS 할 때 방문여부를 체크하는 set + Set visitedCoreNodes = new HashSet<>(); + // 길 중복을 처리하기 위한 set + Set routeSet = new HashSet<>(); + + // BFS 전처리 + Queue nodeQueue = new LinkedList<>(); + startNode.forEach(n-> { + nodeQueue.add(n); + visitedCoreNodes.add(n.getId()); + }); + + // BFS + while(!nodeQueue.isEmpty()) { + // 현재 노드 (코어노드) + Node now = nodeQueue.poll(); + for(Route r : adjMap.get(now.getId())) { + // 만약 now-nxt를 연결하는 길이 이미 등록되어있다면, 해당 coreRoute는 이미 등록된 것이므로 continue; + if(routeSet.contains(r.getId())) continue; + + // 다음 노드 (서브노드일수도 있고 코어노드일 수도 있음) + Node currentNode = now.getId().equals(r.getNode1().getId()) ? r.getNode2() : r.getNode1(); + + // 코어루트를 이루는 node들을 List로 저장 + List coreRoute = new ArrayList<>(); + coreRoute.add(RouteCoordinatesInfoResDTO.of(r.getId(),now.getId(), currentNode.getId())); + routeSet.add(r.getId()); + + while (true) { + //코어노드를 만나면 queue에 넣을지 판단한 뒤 종료 (제자리로 돌아오는 경우도 포함) + if (currentNode.isCore() || currentNode.getId().equals(now.getId())) { + if (!visitedCoreNodes.contains(currentNode.getId())) { + visitedCoreNodes.add(currentNode.getId()); + nodeQueue.add(currentNode); + } + break; + } + // 끝점인 경우 종료 + if (adjMap.get(currentNode.getId()).size() == 1) break; + + // 서브노드에 연결된 두 route 중 방문하지 않았던 route를 선택한 뒤, currentNode를 업데이트 + for (Route R : adjMap.get(currentNode.getId())) { + if (routeSet.contains(R.getId())) continue; + Node nextNode = R.getNode1().getId().equals(currentNode.getId()) ? R.getNode2() : R.getNode1(); + coreRoute.add(RouteCoordinatesInfoResDTO.of(R.getId(), currentNode.getId(), nextNode.getId())); + routeSet.add(R.getId()); + currentNode = nextNode; + } + } + result.add(CoreRouteResDTO.of(now.getId(), currentNode.getId(), coreRoute)); + } + + } + return result; + } + + /** + * @param univId : 경로를 생성할 대학교 id + * @param nodes : 경로를 생성할 노드들 + * + * @apiNote : 두 노드 간의 경로의 cost를 계산하여 Route 객체 생성 + * + * @implNote : 첫번째 노드는 항상 코어노드여야 함 + * @implNote : 노드들은 순서대로 연결되어 있어야 함 + * @implNote : 중복된 노드들은 메모리 주소가 같기 때문에 하나의 객체로 인식됨 + * */ + public List createLinkedRouteAndSave(Long univId, List nodes) { + + List routesForSave = new ArrayList<>(); + for(int i=1;i createValidRouteNodes(Long univId, Long startNodeId, Long endNodeId, List requests, List routes) { + LinkedList createdNodes = new LinkedList<>(); + + Map nodeMap = new HashMap<>(); + STRtree strTree = new STRtree(); + + int startNodeCount = 0; + int endNodeCount = 0; + + for (Route route : routes) { + Node node1 = route.getNode1(); + Node node2 = route.getNode2(); + + LineString line = geometryFactory.createLineString( + new Coordinate[] {node1.getCoordinates().getCoordinate(), node2.getCoordinates().getCoordinate()}); + Envelope envelope = line.getEnvelopeInternal(); // MBR 생성 + strTree.insert(envelope, line); + + nodeMap.putIfAbsent(node1.getNodeKey(), node1); + nodeMap.putIfAbsent(node2.getNodeKey(), node2); + + if(startNodeId.equals(node1.getId()) || startNodeId.equals(node2.getId())) startNodeCount++; + if(endNodeId != null){ + if(endNodeId.equals(node1.getId()) || endNodeId.equals(node2.getId())){ + endNodeCount++; + } + } + } + + // 중복된 노드가 있는지 확인 + validateDuplicateNodes(requests); + + // 1. 첫번째 노드: + // 서브 -> 코어 : 처리 필요 + // 코어 -> 코어 : 처리 필요 X + // 코어 -> 서브 : 불가한 케이스 + CreateRouteReqDTO startCoordinate = requests.get(0); + Node startNode = nodeMap.get(getNodeKey(new Coordinate(startCoordinate.getLng(), startCoordinate.getLat()))); + + if (startNode == null) { + throw new NodeException("Start Node Not Found", NODE_NOT_FOUND); + } + + if(!startNode.isCore() && startNodeCount == CORE_NODE_CONDITION - 1){ + startNode.setCore(true); + } + createdNodes.add(startNode); + + // 기존에 저장된 노드와 일치하거나 or 새로운 노드와 겹칠 경우 -> 동일한 것으로 판단 + for (int i = 1; i < requests.size(); i++) { + CreateRouteReqDTO cur = requests.get(i); + + // 정확히 그 점과 일치하는 노드가 있는지 확인 + Node curNode = nodeMap.get(getNodeKey(new Coordinate(cur.getLng(), cur.getLat()))); + if(curNode != null){ + createdNodes.add(curNode); + if(i == requests.size() - 1 && endNodeCount < CORE_NODE_CONDITION - 1){ // 마지막 노드일 경우, 해당 노드가 끝점일 경우 + continue; + } + + curNode.setCore(true); + continue; + } + + Coordinate coordinate = new Coordinate(cur.getLng(), cur.getLat()); + curNode = Node.builder() + .coordinates(geometryFactory.createPoint(coordinate)) + .isCore(false) + .univId(univId) + .build(); + + createdNodes.add(curNode); + nodeMap.putIfAbsent(curNode.getNodeKey(), curNode); + } + + // 2. 두번째 노드 ~ N-1번째 노드 + // 현재 노드와 다음 노드가 기존 route와 겹치는지 확인 + List crossCheckedNodes = insertMidNodesForIntersectingRoutes(createdNodes, strTree, nodeMap); + + // 3. 자가 크로스 or 중복점 (첫점과 끝점 동일) 확인 + List selfCrossCheckedNodes = insertMidNodesForSelfIntersectingRoutes(crossCheckedNodes); + + return selfCrossCheckedNodes; + } + + private void validateDuplicateNodes(List requests) { + for (int i = 0; i < requests.size(); i++) { + Coordinate curCoordinate = new Coordinate(requests.get(i).getLng(), requests.get(i).getLat()); + String curNodeKey = getNodeKey(curCoordinate); + + for (int j = i + 1; j < Math.min(i + 3, requests.size()); j++) { + Coordinate nextCoordinate = new Coordinate(requests.get(j).getLng(), requests.get(j).getLat()); + String nextNodeKey = getNodeKey(nextCoordinate); + + if (curNodeKey.equals(nextNodeKey)) { + throw new RouteCalculationException("has duplicate nearest node", DUPLICATE_NEAREST_NODE); + } + } + } + } + + private List insertMidNodesForIntersectingRoutes(List nodes, STRtree strTree, Map nodeMap) { + ListIterator iterator = nodes.listIterator(); + if (!iterator.hasNext()) return Collections.emptyList(); + Node prev = iterator.next(); + + while (iterator.hasNext()) { + Node cur = iterator.next(); + LineString intersectLine = findIntersectLineString(prev.getCoordinates().getCoordinate(), cur.getCoordinates() + .getCoordinate(), strTree, false); + if (intersectLine != null) { + Node midNode = getClosestNode(intersectLine, prev, cur, nodeMap); + midNode.setCore(true); + iterator.previous(); // 이전으로 이동 + iterator.add(midNode); // 이전 위치에 삽입 + cur = midNode; + } + prev = cur; + } + + return nodes; + } + + + private List insertMidNodesForSelfIntersectingRoutes(List nodes) { + if(nodes.get(0).getCoordinates().equals(nodes.get(nodes.size()-1).getCoordinates())){ + throw new RouteCalculationException("Start and end nodes cannot be the same", SAME_START_AND_END_POINT); + } + + ListIterator iterator = nodes.listIterator(); + if (!iterator.hasNext()) return Collections.emptyList(); + + Node prev = iterator.next(); + Map nodeMap = new HashMap<>(); + + while(iterator.hasNext()){ + STRtree strTree = new STRtree(); + + int last = iterator.nextIndex(); + for (int i = 0; i < last; i++) { + Node prevCurNode = nodes.get(i); + Node prevNextNode = nodes.get(i + 1); + LineString line = geometryFactory.createLineString( + new Coordinate[] {prevCurNode.getCoordinates().getCoordinate(), prevNextNode.getCoordinates().getCoordinate()}); + Envelope envelope = line.getEnvelopeInternal(); // MBR 생성 + strTree.insert(envelope, line); + + nodeMap.putIfAbsent(prevCurNode.getNodeKey(), prevCurNode); + nodeMap.putIfAbsent(prevNextNode.getNodeKey(), prevNextNode); + } + + Node cur = iterator.next(); + LineString intersectLine = findIntersectLineString(prev.getCoordinates().getCoordinate(), + cur.getCoordinates().getCoordinate(), strTree, true); + if(intersectLine != null){ + Node midNode = getClosestNode(intersectLine, prev, cur, nodeMap); + midNode.setCore(true); + iterator.previous(); + iterator.add(midNode); + cur = midNode; + } + prev = cur; + } + + return nodes; + } + + /** + * @param start 시작 좌표 + * @param end 끝 좌표 + * @implSpec 선분과 겹치는 선분이 있는지 확인 + * @return 겹치는 선분이 있으면 해당 선분 반환, 없으면 null 반환 + * + * @implNote + * */ + private LineString findIntersectLineString(Coordinate start, Coordinate end, STRtree strTree, boolean isSelfCheck) { + LineString newLine = geometryFactory.createLineString(new Coordinate[] {start, end}); + Envelope searchEnvelope = newLine.getEnvelopeInternal(); + + // 1️⃣ 후보 선분들 검색 (MBR이 겹치는 선분만 가져옴) + List candidates = strTree.query(searchEnvelope); + + LineString closestLine = null; + double minDistance = Double.MAX_VALUE; + + // 2️⃣ 실제로 선분이 겹치는지 확인 + for (Object obj : candidates) { + LineString existingLine = (LineString)obj; + + if (existingLine.intersects(newLine)) { + Geometry intersection = existingLine.intersection(newLine); + + Coordinate intersectionCoord = null; + + if (intersection instanceof Point) { + intersectionCoord = ((Point) intersection).getCoordinate(); + + // 교차점이 start 또는 end 좌표와 동일하면 continue + if (intersectionCoord.equals2D(start) || intersectionCoord.equals2D(end)) { + continue; + } + + double distance = start.distance(intersectionCoord); + if (distance < minDistance) { + minDistance = distance; + closestLine = existingLine; + } + } + else if (intersection instanceof LineString) { + if (isSelfCheck && existingLine.equalsTopo(newLine)) { + continue; + } + + throw new RouteCalculationException("intersection is only allowed by point", INTERSECTION_ONLY_ALLOWED_POINT); + } + + } + } + + return closestLine; // 겹치는 선분이 없으면 null 반환 + } + + private Node getClosestNode(LineString intersectLine, Node start, Node end, Map nodeMap) { + Coordinate[] coordinates = intersectLine.getCoordinates(); + + double distance1 = start.getCoordinates().getCoordinate().distance(coordinates[0]) + + end.getCoordinates().getCoordinate().distance(coordinates[0]); + double distance2 = start.getCoordinates().getCoordinate().distance(coordinates[1]) + + end.getCoordinates().getCoordinate().distance(coordinates[1]); + + return nodeMap.getOrDefault( + getNodeKey(distance1 <= distance2 ? coordinates[0] : coordinates[1]), + Node.builder().coordinates(geometryFactory.createPoint(distance1 <= distance2 ? coordinates[0] : coordinates[1])) + .isCore(true) + .build() + ); + } + + private String getNodeKey(Coordinate coordinate) { + return coordinate.getX() + NODE_KEY_DELIMITER + coordinate.getY(); + } + + public GetRiskRoutesResDTO mapRisks(List riskRoutes) { + List dangerRoutes = mapRoutesToDangerDTO(riskRoutes); + List cautionRoutes = mapRoutesToCautionDTO(riskRoutes); + + return GetRiskRoutesResDTO.of(dangerRoutes, cautionRoutes); + } + + private List mapRoutesToDangerDTO(List routes) { + return routes.stream() + .filter(route -> !route.getDangerFactors().isEmpty() && route.getCautionFactors().isEmpty()) // 위험 요소가 있는 경로만 필터링 + .map(route -> GetDangerResDTO.of( + getPoint(route.getPath().getCoordinates()[0]), + getPoint(route.getPath().getCoordinates()[1]), + route.getId(), + route.getDangerFactorsByList() + )).toList(); + } + + private List mapRoutesToCautionDTO(List routes) { + return routes.stream() + .filter(route -> route.getDangerFactors().isEmpty() && !route.getCautionFactors().isEmpty()) + .map(route -> GetCautionResDTO.of( + getPoint(route.getPath().getCoordinates()[0]), + getPoint(route.getPath().getCoordinates()[1]), + route.getId(), + route.getCautionFactorsByList() + )).toList(); + } + + private Map getPoint(Coordinate c) { + Map point = new HashMap<>(); + point.put("lat", c.getY()); + point.put("lng", c.getX()); + return point; + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/repository/BuildingRepository.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/repository/BuildingRepository.java deleted file mode 100644 index a231d1b..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/repository/BuildingRepository.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.softeer5.uniro_backend.node.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - -import com.softeer5.uniro_backend.node.dto.BuildingNode; -import com.softeer5.uniro_backend.node.entity.Building; - -public interface BuildingRepository extends JpaRepository, BuildingCustomRepository { - - // 추후에 인덱싱 작업 필요. - @Query(""" - SELECT new com.softeer5.uniro_backend.node.dto.BuildingNode(b, n) - FROM Building b - JOIN FETCH Node n ON b.nodeId = n.id - WHERE b.univId = :univId - AND b.level >= :level - AND ST_Within(n.coordinates, ST_PolygonFromText((:polygon),4326)) - """) - List findByUnivIdAndLevelWithNode(Long univId, int level, String polygon); - - @Query(""" - SELECT new com.softeer5.uniro_backend.node.dto.BuildingNode(b, n) - FROM Building b - JOIN FETCH Node n ON b.nodeId = n.id - WHERE b.nodeId = :nodeId - """) - Optional findByNodeIdWithNode(Long nodeId); -} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/repository/NodeRepository.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/repository/NodeRepository.java deleted file mode 100644 index 3933599..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/repository/NodeRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.softeer5.uniro_backend.node.repository; - -import com.softeer5.uniro_backend.node.entity.Node; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.Optional; - - -public interface NodeRepository extends JpaRepository { - - Optional findByIdAndUnivId(Long id, Long univId); -} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/service/NodeService.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/service/NodeService.java deleted file mode 100644 index 2a56491..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/node/service/NodeService.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.softeer5.uniro_backend.node.service; - -import java.util.List; -import java.util.Optional; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import com.softeer5.uniro_backend.common.CursorPage; -import com.softeer5.uniro_backend.common.error.ErrorCode; -import com.softeer5.uniro_backend.common.exception.custom.BuildingNotFoundException; -import com.softeer5.uniro_backend.common.utils.GeoUtils; -import com.softeer5.uniro_backend.node.dto.BuildingNode; -import com.softeer5.uniro_backend.node.dto.GetBuildingResDTO; -import com.softeer5.uniro_backend.node.dto.SearchBuildingResDTO; -import com.softeer5.uniro_backend.node.repository.BuildingRepository; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class NodeService { - private final BuildingRepository buildingRepository; - - public List getBuildings( - Long univId, int level, - double leftUpLat, double leftUpLng, double rightDownLat, double rightDownLng) { - - String polygon = GeoUtils.makeSquarePolygonString(leftUpLat, leftUpLng, rightDownLat, rightDownLng); - List buildingNodes = buildingRepository.findByUnivIdAndLevelWithNode(univId, level, polygon); - - return buildingNodes.stream() - .map(buildingNode -> GetBuildingResDTO.of(buildingNode.getBuilding(), buildingNode.getNode())) - .toList(); - } - - public SearchBuildingResDTO searchBuildings(Long univId, String name, Long cursorId, Integer pageSize){ - - CursorPage> buildingNodes = buildingRepository.searchBuildings(univId, name, cursorId, pageSize); - - List data = buildingNodes.getData().stream() - .map(buildingNode -> GetBuildingResDTO.of(buildingNode.getBuilding(), buildingNode.getNode())) - .toList(); - - return SearchBuildingResDTO.of(data, buildingNodes.getNextCursor(), buildingNodes.isHasNext()); - } - - public GetBuildingResDTO getBuilding(Long nodeId){ - Optional buildingNode = buildingRepository.findByNodeIdWithNode(nodeId); - if(buildingNode.isEmpty()){ - throw new BuildingNotFoundException("Building Not Found", ErrorCode.BUILDING_NOT_FOUND); - } - - return GetBuildingResDTO.of(buildingNode.get().getBuilding(), buildingNode.get().getNode()); - } - -} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/controller/RouteController.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/controller/RouteController.java deleted file mode 100644 index e1a6260..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/controller/RouteController.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.softeer5.uniro_backend.route.controller; - -import com.softeer5.uniro_backend.route.dto.request.CreateRoutesReqDTO; -import com.softeer5.uniro_backend.route.dto.response.FastestRouteResDTO; -import com.softeer5.uniro_backend.route.dto.response.GetAllRoutesResDTO; -import com.softeer5.uniro_backend.route.dto.response.GetRiskResDTO; -import com.softeer5.uniro_backend.route.dto.response.GetRiskRoutesResDTO; -import com.softeer5.uniro_backend.route.dto.request.PostRiskReqDTO; -import com.softeer5.uniro_backend.route.service.RouteCalculationService; - -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import com.softeer5.uniro_backend.route.service.RouteService; - -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -@RestController -public class RouteController implements RouteApi { - - private final RouteService routeService; - private final RouteCalculationService routeCalculationService; - - @Override - @GetMapping("/{univId}/routes") - public ResponseEntity getAllRoutesAndNodes(@PathVariable("univId") Long univId){ - GetAllRoutesResDTO allRoutes = routeService.getAllRoutes(univId); - return ResponseEntity.ok().body(allRoutes); - } - - @Override - @GetMapping("/{univId}/routes/risks") - public ResponseEntity getRiskRoutes(@PathVariable("univId") Long univId) { - GetRiskRoutesResDTO riskRoutes = routeService.getRiskRoutes(univId); - return ResponseEntity.ok().body(riskRoutes); - } - - @Override - @GetMapping("/{univId}/routes/{routeId}/risk") - public ResponseEntity getRisk(@PathVariable("univId") Long univId, - @PathVariable(value = "routeId") Long routeId){ - GetRiskResDTO riskResDTO = routeService.getRisk(univId, routeId); - return ResponseEntity.ok().body(riskResDTO); - } - - @Override - @PostMapping("/{univId}/route/risk/{routeId}") - public ResponseEntity updateRisk (@PathVariable("univId") Long univId, - @PathVariable("routeId") Long routeId, - @RequestBody PostRiskReqDTO postRiskReqDTO){ - routeService.updateRisk(univId,routeId,postRiskReqDTO); - return ResponseEntity.ok().build(); - } - - @Override - @PostMapping("/{univId}/route") - public ResponseEntity createRoute (@PathVariable("univId") Long univId, - @RequestBody CreateRoutesReqDTO routes){ - routeCalculationService.createRoute(univId, routes); - return ResponseEntity.status(HttpStatus.CREATED).build(); - } - - @Override - @GetMapping("/{univId}/routes/fastest") - public ResponseEntity calculateFastestRoute(@PathVariable("univId") Long univId, - @RequestParam(value = "start-node-id") Long startNodeId, - @RequestParam(value = "end-node-id") Long endNodeId) { - FastestRouteResDTO fastestRouteResDTO = routeCalculationService.calculateFastestRoute(univId, startNodeId, endNodeId); - return ResponseEntity.ok(fastestRouteResDTO); - } -} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/FastestRouteResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/FastestRouteResDTO.java deleted file mode 100644 index 0aa2aab..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/FastestRouteResDTO.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.softeer5.uniro_backend.route.dto.response; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -import java.util.List; - -@Getter -@Schema(name = "FastestRouteResDTO", description = "빠른 경로 조회 DTO") -@RequiredArgsConstructor(access = AccessLevel.PRIVATE) -public class FastestRouteResDTO { - @Schema(description = "길 찾기 결과에 위험요소가 포함되어있는지 여부", example = "true") - private final boolean hasCaution; - @Schema(description = "총 이동거리", example = "150.3421234") - private final double totalDistance; - @Schema(description = "총 걸리는 시간(초)", example = "1050.32198432") - private final double totalCost; - @Schema(description = "길 찾기 결과에 포함된 모든 길", example = "") - private final List routes; - @Schema(description = "상세안내 관련 정보", example = "") - private final List routeDetails; - - public static FastestRouteResDTO of(boolean hasCaution, - double totalDistance, - double totalCost, - List routes, - List routeDetails) { - return new FastestRouteResDTO(hasCaution,totalDistance,totalCost,routes,routeDetails); - } -} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/entity/CautionType.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/entity/CautionType.java deleted file mode 100644 index d17113c..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/entity/CautionType.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.softeer5.uniro_backend.route.entity; - -public enum CautionType { - CURB, // 턱 - CRACK, // 균열 - SLOPE, // 경사 - ETC // 기타 -} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/entity/CoreRoute.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/entity/CoreRoute.java deleted file mode 100644 index 3b0c52c..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/entity/CoreRoute.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.softeer5.uniro_backend.route.entity; - -import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.LineString; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -@Entity -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Getter -public class CoreRoute { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(columnDefinition = "geometry(LineString, 4326)") // WGS84 좌표계 - private LineString path; - - private Long node1Id; - - private Long node2Id; - - private Long univId; - - public List> getPathAsList() { - List> coordinatesList = new ArrayList<>(); - for (Coordinate coordinate : path.getCoordinates()) { - coordinatesList.add(Map.of("lat", coordinate.getY(), "lng", coordinate.getX())); - } - return coordinatesList; - } -} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/repository/CoreRouteRepository.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/repository/CoreRouteRepository.java deleted file mode 100644 index df73569..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/repository/CoreRouteRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.softeer5.uniro_backend.route.repository; - -import com.softeer5.uniro_backend.route.entity.CoreRoute; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface CoreRouteRepository extends JpaRepository { - List findByUnivId(Long univId); -} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/service/RouteCalculationService.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/service/RouteCalculationService.java deleted file mode 100644 index bcc1ce9..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/service/RouteCalculationService.java +++ /dev/null @@ -1,551 +0,0 @@ -package com.softeer5.uniro_backend.route.service; - -import static com.softeer5.uniro_backend.common.constant.UniroConst.*; -import static com.softeer5.uniro_backend.common.error.ErrorCode.*; - -import com.softeer5.uniro_backend.admin.annotation.RevisionOperation; -import com.softeer5.uniro_backend.admin.entity.RevisionOperationType; -import com.softeer5.uniro_backend.common.error.ErrorCode; -import com.softeer5.uniro_backend.common.exception.custom.NodeNotFoundException; -import com.softeer5.uniro_backend.common.exception.custom.RouteCalculationException; -import com.softeer5.uniro_backend.common.exception.custom.SameStartAndEndPointException; -import com.softeer5.uniro_backend.common.exception.custom.UnreachableDestinationException; -import com.softeer5.uniro_backend.common.utils.GeoUtils; -import com.softeer5.uniro_backend.external.MapClient; -import com.softeer5.uniro_backend.node.entity.Node; -import com.softeer5.uniro_backend.node.repository.NodeRepository; -import com.softeer5.uniro_backend.route.dto.request.CreateRouteReqDTO; -import com.softeer5.uniro_backend.route.dto.request.CreateRoutesReqDTO; -import com.softeer5.uniro_backend.route.dto.response.FastestRouteResDTO; -import com.softeer5.uniro_backend.route.dto.response.RouteDetailResDTO; -import com.softeer5.uniro_backend.route.dto.response.RouteInfoResDTO; -import com.softeer5.uniro_backend.route.entity.DirectionType; -import com.softeer5.uniro_backend.route.entity.Route; -import com.softeer5.uniro_backend.route.repository.RouteRepository; -import lombok.AllArgsConstructor; -import lombok.RequiredArgsConstructor; - -import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.Envelope; -import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.geom.LineString; -import org.locationtech.jts.geom.Point; -import org.locationtech.jts.index.strtree.STRtree; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.*; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class RouteCalculationService { - private final RouteRepository routeRepository; - private final MapClient mapClient; - private final NodeRepository nodeRepository; - - @AllArgsConstructor - private class CostToNextNode implements Comparable { - private double cost; - private Node nextNode; - - @Override - public int compareTo(CostToNextNode o) { - return Double.compare(this.cost, o.cost); - } - } - - public FastestRouteResDTO calculateFastestRoute(Long univId, Long startNodeId, Long endNodeId){ - - if(startNodeId.equals(endNodeId)){ - throw new SameStartAndEndPointException("Start and end nodes cannot be the same", SAME_START_AND_END_POINT); - } - - //인접 리스트 - Map> adjMap = new HashMap<>(); - Map nodeMap = new HashMap<>(); - - routeRepository.findAllRouteByUnivIdWithNodes(univId).forEach(route -> { - adjMap.computeIfAbsent(route.getNode1().getId(), k -> new ArrayList<>()).add(route); - adjMap.computeIfAbsent(route.getNode2().getId(), k -> new ArrayList<>()).add(route); - - nodeMap.putIfAbsent(route.getNode1().getId(), route.getNode1()); - nodeMap.putIfAbsent(route.getNode2().getId(), route.getNode2()); - }); - - - Node startNode = nodeMap.get(startNodeId); - Node endNode = nodeMap.get(endNodeId); - - //길찾기 알고리즘 수행 - Map prevRoute = findFastestRoute(startNode, endNode, adjMap); - - //길찾기 경로 결과 정리 - List shortestRoutes = reorderRoute(startNode, endNode, prevRoute); - - boolean hasCaution = false; - double totalCost = 0.0; - double totalDistance = 0.0; - - List routeInfoDTOS = new ArrayList<>(); - Node currentNode = startNode; - // 외부 변수를 수정해야하기 때문에 for-loop문 사용 - for (Route route : shortestRoutes) { - totalCost += route.getCost(); - totalDistance += calculateDistance(route); - - if (!route.getCautionFactors().isEmpty()) { - hasCaution = true; - } - - Node firstNode = route.getNode1(); - Node secondNode = route.getNode2(); - if(currentNode.getId().equals(secondNode.getId())){ - Node temp = firstNode; - firstNode = secondNode; - secondNode = temp; - } - - routeInfoDTOS.add(RouteInfoResDTO.of(route, firstNode, secondNode)); - } - - List details = getRouteDetail(startNode, endNode, shortestRoutes); - - return FastestRouteResDTO.of(hasCaution, totalDistance, totalCost, routeInfoDTOS, details); - } - - private Map findFastestRoute(Node startNode, Node endNode, Map> adjMap){ - //key : nodeId, value : 최단거리 중 해당 노드를 향한 route - Map prevRoute = new HashMap<>(); - // startNode로부터 각 노드까지 걸리는 cost를 저장하는 자료구조 - Map costMap = new HashMap<>(); - PriorityQueue pq = new PriorityQueue<>(); - pq.add(new CostToNextNode(0.0, startNode)); - costMap.put(startNode.getId(), 0.0); - - // 길찾기 알고리즘 - while(!pq.isEmpty()){ - CostToNextNode costToNextNode = pq.poll(); - double currentDistance = costToNextNode.cost; - Node currentNode = costToNextNode.nextNode; - if (currentNode.getId().equals(endNode.getId())) break; - if(costMap.containsKey(currentNode.getId()) - && currentDistance > costMap.get(currentNode.getId())) continue; - - for(Route route : adjMap.getOrDefault(currentNode.getId(), Collections.emptyList())){ - double newDistance = currentDistance + route.getCost(); - Node nextNode = route.getNode1().getId().equals(currentNode.getId())?route.getNode2():route.getNode1(); - if(!costMap.containsKey(nextNode.getId()) || costMap.get(nextNode.getId()) > newDistance){ - costMap.put(nextNode.getId(), newDistance); - pq.add(new CostToNextNode(newDistance, nextNode)); - prevRoute.put(nextNode.getId(), route); - } - } - } - //길 없는 경우 - if(!costMap.containsKey(endNode.getId())){ - throw new UnreachableDestinationException("Unable to find a valid route", ErrorCode.FASTEST_ROUTE_NOT_FOUND); - } - - return prevRoute; - } - - // 길찾기 결과를 파싱하여 출발지 -> 도착지 형태로 재배열하는 메서드 - private List reorderRoute(Node startNode, Node endNode, Map prevRoute){ - List shortestRoutes = new ArrayList<>(); - Node currentNode = endNode; - - //endNode부터 역순회하여 배열에 저장 - while(!currentNode.getId().equals(startNode.getId())){ - Route previousRoute = prevRoute.get(currentNode.getId()); - shortestRoutes.add(previousRoute); - currentNode = previousRoute.getNode1().getId().equals(currentNode.getId()) ? previousRoute.getNode2() : previousRoute.getNode1(); - } - - //이후 reverse를 통해 시작점 -> 도착점 방향으로 재정렬 - Collections.reverse(shortestRoutes); - - return shortestRoutes; - } - - // 두 route 간의 각도를 통한 계산으로 방향성을 정하는 메서드 - private DirectionType calculateDirection(Node secondNode, Route inBoundRoute, Route outBoundRoute) { - Node firstNode = inBoundRoute.getNode1().equals(secondNode) ? inBoundRoute.getNode2() : inBoundRoute.getNode1(); - Node thirdNode = outBoundRoute.getNode1().equals(secondNode) ? outBoundRoute.getNode2() : outBoundRoute.getNode1(); - - Point p1 = firstNode.getCoordinates(); - Point p2 = secondNode.getCoordinates(); - Point p3 = thirdNode.getCoordinates(); - - double v1x = p2.getX() - p1.getX(); - double v1y = p2.getY() - p1.getY(); - - double v2x = p3.getX() - p2.getX(); - double v2y = p3.getY() - p2.getY(); - - double dotProduct = (v1x * v2x) + (v1y * v2y); - double magnitudeV1 = Math.sqrt(v1x * v1x + v1y * v1y); - double magnitudeV2 = Math.sqrt(v2x * v2x + v2y * v2y); - - double cosTheta = dotProduct / (magnitudeV1 * magnitudeV2); - double angle = Math.toDegrees(Math.acos(cosTheta)); - - double crossProduct = (v1x * v2y) - (v1y * v2x); - - if (angle < 30) { - return DirectionType.STRAIGHT; - } else if (angle >= 30 && angle < 150) { - return (crossProduct > 0) ? DirectionType.RIGHT : DirectionType.LEFT; - } else if (angle >= 150 && angle < 180) { - return (crossProduct > 0) ? DirectionType.SHARP_RIGHT : DirectionType.SHARP_LEFT; - } else { - return DirectionType.STRAIGHT; - } - } - - // 길의 길이를 리턴하는 메서드 - private double calculateDistance(Route route) { - Point p1 = route.getNode1().getCoordinates(); - Point p2 = route.getNode2().getCoordinates(); - - double deltaX = p2.getX() - p1.getX(); - double deltaY = p2.getY() - p1.getY(); - - return Math.sqrt(deltaX * deltaX + deltaY * deltaY); - } - - // 길 상세정보를 추출하는 메서드 - private List getRouteDetail(Node startNode, Node endNode, List shortestRoutes){ - List details = new ArrayList<>(); - double accumulatedDistance = 0.0; - Node now = startNode; - Map checkPointNodeCoordinates = startNode.getXY(); - DirectionType checkPointType = DirectionType.STRAIGHT; - - // 길찾기 결과 상세정보 정리 - for(int i=0;i getCenter(Node n1, Node n2){ - return Map.of("lat", (n1.getCoordinates().getY() + n2.getCoordinates().getY())/2 - , "lng", (n1.getCoordinates().getX() + n2.getCoordinates().getX())/2); - } - - @Transactional - @RevisionOperation(RevisionOperationType.CREATE_ROUTE) - public void createRoute(Long univId, CreateRoutesReqDTO requests){ - List nodes = checkRouteCross(univId, requests.getStartNodeId(), requests.getEndNodeId(), requests.getCoordinates()); - mapClient.fetchHeights(nodes); - createLinkedRouteAndSave(univId,nodes); - } - - private void createLinkedRouteAndSave(Long univId, List nodes) { - GeometryFactory geometryFactory = GeoUtils.getInstance(); - Set nodeSet = new HashSet<>(); - List nodeForSave = new ArrayList<>(); - List routeForSave = new ArrayList<>(); - for(int i=1;i checkRouteCross(Long univId, Long startNodeId, Long endNodeId, List requests) { - LinkedList createdNodes = new LinkedList<>(); - - Map nodeMap = new HashMap<>(); - STRtree strTree = new STRtree(); - GeometryFactory geometryFactory = GeoUtils.getInstance(); - - int startNodeCount = 0; - int endNodeCount = 0; - - List routes = routeRepository.findAllRouteByUnivIdWithNodes(univId); - for (Route route : routes) { - Node node1 = route.getNode1(); - Node node2 = route.getNode2(); - - LineString line = geometryFactory.createLineString( - new Coordinate[] {node1.getCoordinates().getCoordinate(), node2.getCoordinates().getCoordinate()}); - Envelope envelope = line.getEnvelopeInternal(); // MBR 생성 - strTree.insert(envelope, line); - - nodeMap.putIfAbsent(node1.getNodeKey(), node1); - nodeMap.putIfAbsent(node2.getNodeKey(), node2); - - if(startNodeId.equals(node1.getId()) || startNodeId.equals(node2.getId())) startNodeCount++; - if(endNodeId != null){ - if(endNodeId.equals(node1.getId()) || endNodeId.equals(node2.getId())){ - endNodeCount++; - } - } - } - - // 1. 첫번째 노드: - // 서브 -> 코어 : 처리 필요 - // 코어 -> 코어 : 처리 필요 X - // 코어 -> 서브 : 불가한 케이스 - CreateRouteReqDTO startCoordinate = requests.get(0); - Node startNode = nodeMap.get(getNodeKey(new Coordinate(startCoordinate.getLng(), startCoordinate.getLat()))); - - if (startNode == null) { - throw new NodeNotFoundException("Start Node Not Found", NODE_NOT_FOUND); - } - - if(!startNode.isCore() && startNodeCount == CORE_NODE_CONDITION - 1){ - startNode.setCore(true); - } - createdNodes.add(startNode); - - for (int i = 1; i < requests.size(); i++) { - CreateRouteReqDTO cur = requests.get(i); - - // 정확히 그 점과 일치하는 노드가 있는지 확인 - Node curNode = nodeMap.get(getNodeKey(new Coordinate(cur.getLng(), cur.getLat()))); - if(curNode != null){ - createdNodes.add(curNode); - if(i == requests.size() - 1 && endNodeCount < CORE_NODE_CONDITION - 1){ // 마지막 노드일 경우, 해당 노드가 끝점일 경우 - continue; - } - - curNode.setCore(true); - continue; - } - - Coordinate coordinate = new Coordinate(cur.getLng(), cur.getLat()); - curNode = Node.builder() - .coordinates(geometryFactory.createPoint(coordinate)) - .isCore(false) - .univId(univId) - .build(); - - createdNodes.add(curNode); - } - - // 2. 두번째 노드 ~ N-1번째 노드 - // 현재 노드와 다음 노드가 기존 route와 겹치는지 확인 - checkForRouteIntersections(createdNodes, strTree, nodeMap); - - // 3. 자가 크로스 or 중복점 (첫점과 끝점 동일) 확인 - List checkedSelfRouteCrossNodes = checkSelfRouteCross(createdNodes); - - return checkedSelfRouteCrossNodes; - } - - private void checkForRouteIntersections(List nodes, STRtree strTree, Map nodeMap) { - ListIterator iterator = nodes.listIterator(); - if (!iterator.hasNext()) return; - Node prev = iterator.next(); - - while (iterator.hasNext()) { - Node cur = iterator.next(); - LineString intersectLine = findIntersectLineString(prev.getCoordinates().getCoordinate(), cur.getCoordinates() - .getCoordinate(), strTree); - if (intersectLine != null) { - Node midNode = getClosestNode(intersectLine, prev, cur, nodeMap); - midNode.setCore(true); - iterator.previous(); // 이전으로 이동 - iterator.add(midNode); // 이전 위치에 삽입 - cur = midNode; - } - prev = cur; - } - } - - - private List checkSelfRouteCross(List nodes) { - - GeometryFactory geometryFactory = GeoUtils.getInstance(); - - if(nodes.get(0).getCoordinates().equals(nodes.get(nodes.size()-1).getCoordinates())){ - throw new SameStartAndEndPointException("Start and end nodes cannot be the same", SAME_START_AND_END_POINT); - } - - STRtree strTree = new STRtree(); - Map nodeMap = new HashMap<>(); - - for (int i = 0; i < nodes.size() - 1; i++) { - Node curNode = nodes.get(i); - Node nextNode = nodes.get(i + 1); - LineString line = geometryFactory.createLineString( - new Coordinate[] {curNode.getCoordinates().getCoordinate(), nextNode.getCoordinates().getCoordinate()}); - Envelope envelope = line.getEnvelopeInternal(); // MBR 생성 - strTree.insert(envelope, line); - - nodeMap.putIfAbsent(curNode.getNodeKey(), curNode); - nodeMap.putIfAbsent(nextNode.getNodeKey(), nextNode); - } - - for (int i = 0; i < nodes.size() - 1; i++) { - Node curNode = nodes.get(i); - Node nextNode = nodes.get(i + 1); - - LineString intersectLine = findIntersectLineString(curNode.getCoordinates().getCoordinate(), - nextNode.getCoordinates().getCoordinate(), strTree); - - if (intersectLine != null) { - Coordinate[] coordinates = intersectLine.getCoordinates(); - - double distance1 = - curNode.getCoordinates().getCoordinate().distance(coordinates[0]) + nextNode.getCoordinates() - .getCoordinate() - .distance(coordinates[0]); - double distance2 = - curNode.getCoordinates().getCoordinate().distance(coordinates[1]) + nextNode.getCoordinates() - .getCoordinate() - .distance(coordinates[1]); - - Node midNode; - - if (distance1 <= distance2) { - midNode = nodeMap.get(getNodeKey(coordinates[0])); - } else { - midNode = nodeMap.get(getNodeKey(coordinates[1])); - } - - midNode.setCore(true); - - nodes.add(midNode); - } - } - - return nodes; - } - - private LineString findIntersectLineString(Coordinate start, Coordinate end, STRtree strTree) { - GeometryFactory geometryFactory = GeoUtils.getInstance(); - LineString newLine = geometryFactory.createLineString(new Coordinate[] {start, end}); - Envelope searchEnvelope = newLine.getEnvelopeInternal(); - - // 1️⃣ 후보 선분들 검색 (MBR이 겹치는 선분만 가져옴) - List candidates = strTree.query(searchEnvelope); - - LineString closestLine = null; - double minDistance = Double.MAX_VALUE; - - // 2️⃣ 실제로 선분이 겹치는지 확인 - for (Object obj : candidates) { - LineString existingLine = (LineString)obj; - - // 동일한 선이면 continue - if(existingLine.equalsTopo(newLine)){ - continue; - } - - if (existingLine.intersects(newLine)) { - Geometry intersection = existingLine.intersection(newLine); - - Coordinate intersectionCoord = null; - - if (intersection instanceof Point) { - intersectionCoord = ((Point) intersection).getCoordinate(); - - // 교차점이 start 또는 end 좌표와 동일하면 continue - if (intersectionCoord.equals2D(start) || intersectionCoord.equals2D(end)) { - continue; - } - - double distance = start.distance(intersectionCoord); - if (distance < minDistance) { - minDistance = distance; - closestLine = existingLine; - } - } - else if (intersection instanceof LineString) { - throw new RouteCalculationException("intersection is only allowed by point", INTERSECTION_ONLY_ALLOWED_POINT); - } - - } - } - - return closestLine; // 겹치는 선분이 없으면 null 반환 - } - - private Node getClosestNode(LineString intersectLine, Node start, Node end, Map nodeMap) { - GeometryFactory geometryFactory = GeoUtils.getInstance(); - Coordinate[] coordinates = intersectLine.getCoordinates(); - - double distance1 = start.getCoordinates().getCoordinate().distance(coordinates[0]) + - end.getCoordinates().getCoordinate().distance(coordinates[0]); - double distance2 = start.getCoordinates().getCoordinate().distance(coordinates[1]) + - end.getCoordinates().getCoordinate().distance(coordinates[1]); - - return nodeMap.getOrDefault( - getNodeKey(distance1 <= distance2 ? coordinates[0] : coordinates[1]), - Node.builder().coordinates(geometryFactory.createPoint(distance1 <= distance2 ? coordinates[0] : coordinates[1])) - .isCore(true) - .build() - ); - } - - private String getNodeKey(Coordinate coordinate) { - return coordinate.getX() + NODE_KEY_DELIMITER + coordinate.getY(); - } -} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/service/RouteService.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/service/RouteService.java deleted file mode 100644 index 1071e0b..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/service/RouteService.java +++ /dev/null @@ -1,233 +0,0 @@ -package com.softeer5.uniro_backend.route.service; - -import static com.softeer5.uniro_backend.common.error.ErrorCode.*; - -import java.util.*; -import java.util.stream.Collectors; - -import com.softeer5.uniro_backend.admin.annotation.RevisionOperation; -import com.softeer5.uniro_backend.admin.entity.RevisionOperationType; -import com.softeer5.uniro_backend.common.error.ErrorCode; -import com.softeer5.uniro_backend.common.exception.custom.DangerCautionConflictException; -import com.softeer5.uniro_backend.common.exception.custom.InvalidMapException; -import com.softeer5.uniro_backend.common.exception.custom.RouteNotFoundException; -import com.softeer5.uniro_backend.node.entity.Node; - -import org.locationtech.jts.geom.Coordinate; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import com.softeer5.uniro_backend.route.dto.response.CoreRouteResDTO; -import com.softeer5.uniro_backend.route.dto.response.GetAllRoutesResDTO; -import com.softeer5.uniro_backend.route.dto.response.GetCautionResDTO; -import com.softeer5.uniro_backend.route.dto.response.GetDangerResDTO; -import com.softeer5.uniro_backend.route.dto.response.GetRiskResDTO; -import com.softeer5.uniro_backend.route.dto.response.GetRiskRoutesResDTO; -import com.softeer5.uniro_backend.route.dto.response.NodeInfoResDTO; -import com.softeer5.uniro_backend.route.dto.request.PostRiskReqDTO; -import com.softeer5.uniro_backend.route.dto.response.RouteCoordinatesInfoResDTO; -import com.softeer5.uniro_backend.route.entity.Route; -import com.softeer5.uniro_backend.route.repository.RouteRepository; - -import lombok.RequiredArgsConstructor; - -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class RouteService { - private final RouteRepository routeRepository; - - - public GetAllRoutesResDTO getAllRoutes(Long univId) { - List routes = routeRepository.findAllRouteByUnivIdWithNodes(univId); - - // 맵이 존재하지 않을 경우 예외 - if(routes.isEmpty()) { - throw new RouteNotFoundException("Route Not Found", ROUTE_NOT_FOUND); - } - - //인접 리스트 - Map> adjMap = new HashMap<>(); - Map nodeMap = new HashMap<>(); - //BFS를 시작할 노드 - Node startNode = null; - for(Route route : routes) { - adjMap.computeIfAbsent(route.getNode1().getId(), k -> new ArrayList<>()).add(route); - adjMap.computeIfAbsent(route.getNode2().getId(), k -> new ArrayList<>()).add(route); - nodeMap.put(route.getNode1().getId(), route.getNode1()); - nodeMap.put(route.getNode2().getId(), route.getNode2()); - - if(startNode != null) continue; - if(route.getNode1().isCore()) startNode = route.getNode1(); - else if(route.getNode2().isCore()) startNode = route.getNode2(); - } - - List nodeInfos = nodeMap.entrySet().stream() - .map(entry -> { - Node node = entry.getValue(); - return NodeInfoResDTO.of(entry.getKey(), node.getX(), node.getY()); - }) - .collect(Collectors.toList()); - - // 맵에 코어노드가 없는 경우 서브노드끼리 순서 매겨서 리턴 - if(startNode==null){ - List endNodes = adjMap.entrySet() - .stream() - .filter(entry -> entry.getValue().size() == 1) // 리스트 크기가 1인 항목 필터링 - .map(Map.Entry::getKey) - .collect(Collectors.toList()); - - //끝 노드가 2개인 경우 둘 중 하나에서 출발 - if(endNodes.size()==2){ - startNode = nodeMap.get(endNodes.get(0)); - return GetAllRoutesResDTO.of(nodeInfos, List.of(getSingleRoutes(adjMap, startNode))); - } - - // 그 외의 경우의 수는 모두 사이클만 존재하거나, 규칙에 어긋난 맵 - throw new InvalidMapException("Invalid Map", ErrorCode.INVALID_MAP); - - } - - - return GetAllRoutesResDTO.of(nodeInfos, getCoreRoutes(adjMap, startNode)); - } - - // coreRoute를 만들어주는 메서드 - private List getCoreRoutes(Map> adjMap, Node startNode) { - List result = new ArrayList<>(); - // core node간의 BFS 할 때 방문여부를 체크하는 set - Set visitedCoreNodes = new HashSet<>(); - // 길 중복을 처리하기 위한 set - Set routeSet = new HashSet<>(); - - // BFS 전처리 - Queue nodeQueue = new LinkedList<>(); - nodeQueue.add(startNode); - visitedCoreNodes.add(startNode.getId()); - - // BFS - while(!nodeQueue.isEmpty()) { - // 현재 노드 (코어노드) - Node now = nodeQueue.poll(); - for(Route r : adjMap.get(now.getId())) { - // 만약 now-nxt를 연결하는 길이 이미 등록되어있다면, 해당 coreRoute는 이미 등록된 것이므로 continue; - if(routeSet.contains(r.getId())) continue; - - // 다음 노드 (서브노드일수도 있고 코어노드일 수도 있음) - Node currentNode = now.getId().equals(r.getNode1().getId()) ? r.getNode2() : r.getNode1(); - - // 코어루트를 이루는 node들을 List로 저장 - List coreRoute = new ArrayList<>(); - coreRoute.add(RouteCoordinatesInfoResDTO.of(r.getId(),now.getId(), currentNode.getId())); - routeSet.add(r.getId()); - - while (true) { - //코어노드를 만나면 queue에 넣을지 판단한 뒤 종료 - if (currentNode.isCore()) { - if (!visitedCoreNodes.contains(currentNode.getId())) { - visitedCoreNodes.add(currentNode.getId()); - nodeQueue.add(currentNode); - } - break; - } - // 끝점인 경우 종료 - if (adjMap.get(currentNode.getId()).size() == 1) break; - - // 서브노드에 연결된 두 route 중 방문하지 않았던 route를 선택한 뒤, currentNode를 업데이트 - for (Route R : adjMap.get(currentNode.getId())) { - if (routeSet.contains(R.getId())) continue; - Node nextNode = R.getNode1().getId().equals(currentNode.getId()) ? R.getNode2() : R.getNode1(); - coreRoute.add(RouteCoordinatesInfoResDTO.of(R.getId(), currentNode.getId(), nextNode.getId())); - routeSet.add(R.getId()); - currentNode = nextNode; - } - } - result.add(CoreRouteResDTO.of(now.getId(), currentNode.getId(), coreRoute)); - } - - } - return result; - } - - private CoreRouteResDTO getSingleRoutes(Map> adjMap, Node startNode) { - List coreRoute = new ArrayList<>(); - Set visitedNodes = new HashSet<>(); - visitedNodes.add(startNode.getId()); - - - Node currentNode = startNode; - boolean flag = true; - while(flag){ - flag = false; - for (Route r : adjMap.get(currentNode.getId())) { - Node nextNode = r.getNode1().getId().equals(currentNode.getId()) ? r.getNode2() : r.getNode1(); - if(visitedNodes.contains(nextNode.getId())) continue; - coreRoute.add(RouteCoordinatesInfoResDTO.of(r.getId(), currentNode.getId(), nextNode.getId())); - flag = true; - visitedNodes.add(nextNode.getId()); - currentNode = nextNode; - } - } - return CoreRouteResDTO.of(startNode.getId(), currentNode.getId(), coreRoute); - } - - public GetRiskRoutesResDTO getRiskRoutes(Long univId) { - List riskRoutes = routeRepository.findRiskRouteByUnivId(univId); - - List dangerRoutes = mapRoutesToDangerDTO(riskRoutes); - List cautionRoutes = mapRoutesToCautionDTO(riskRoutes); - - return GetRiskRoutesResDTO.of(dangerRoutes, cautionRoutes); - } - - private List mapRoutesToDangerDTO(List routes) { - return routes.stream() - .filter(route -> !route.getDangerFactors().isEmpty() && route.getCautionFactors().isEmpty()) // 위험 요소가 있는 경로만 필터링 - .map(route -> GetDangerResDTO.of( - getPoint(route.getPath().getCoordinates()[0]), - getPoint(route.getPath().getCoordinates()[1]), - route.getId(), - route.getDangerFactorsByList() - )).toList(); - } - - private List mapRoutesToCautionDTO(List routes) { - return routes.stream() - .filter(route -> route.getDangerFactors().isEmpty() && !route.getCautionFactors().isEmpty()) - .map(route -> GetCautionResDTO.of( - getPoint(route.getPath().getCoordinates()[0]), - getPoint(route.getPath().getCoordinates()[1]), - route.getId(), - route.getCautionFactorsByList() - )).toList(); - } - - private Map getPoint(Coordinate c) { - Map point = new HashMap<>(); - point.put("lat", c.getY()); - point.put("lng", c.getX()); - return point; - } - - - public GetRiskResDTO getRisk(Long univId, Long routeId) { - Route route = routeRepository.findById(routeId) - .orElseThrow(() -> new RouteNotFoundException("Route not found", ROUTE_NOT_FOUND)); - return GetRiskResDTO.of(route); - } - - @RevisionOperation(RevisionOperationType.UPDATE_RISK) - @Transactional - public void updateRisk(Long univId, Long routeId, PostRiskReqDTO postRiskReqDTO) { - Route route = routeRepository.findByIdAndUnivId(routeId, univId) - .orElseThrow(() -> new RouteNotFoundException("Route not Found", ROUTE_NOT_FOUND)); - - if(!postRiskReqDTO.getCautionTypes().isEmpty() && !postRiskReqDTO.getDangerTypes().isEmpty()){ - throw new DangerCautionConflictException("DangerFactors and CautionFactors can't exist simultaneously.", - ErrorCode.CAUTION_DANGER_CANT_EXIST_SIMULTANEOUSLY); - } - - route.setCautionFactors(postRiskReqDTO.getCautionTypes()); - route.setDangerFactors(postRiskReqDTO.getDangerTypes()); - } -} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/controller/UnivAPI.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/controller/UnivApi.java similarity index 97% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/controller/UnivAPI.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/controller/UnivApi.java index 39e76c0..8c0d9de 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/controller/UnivAPI.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/controller/UnivApi.java @@ -10,7 +10,7 @@ import org.springframework.web.bind.annotation.RequestParam; @Tag(name = "대학교 관련 Api") -public interface UnivAPI { +public interface UnivApi { @Operation(summary = "대학 이름 검색") @ApiResponses(value = { diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/controller/UnivController.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/controller/UnivController.java index df57eda..b740dce 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/controller/UnivController.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/controller/UnivController.java @@ -10,7 +10,7 @@ @RestController @RequiredArgsConstructor -public class UnivController implements UnivAPI{ +public class UnivController implements UnivApi { private final UnivService univService; @Override diff --git a/uniro_backend/src/main/resources/application-dev.yml b/uniro_backend/src/main/resources/application-dev.yml index 70c6f01..625b098 100644 --- a/uniro_backend/src/main/resources/application-dev.yml +++ b/uniro_backend/src/main/resources/application-dev.yml @@ -16,9 +16,6 @@ spring: show_sql: true open-in-view: false defer-datasource-initialization: true - sql: - init: - schema-locations: classpath:h2gis-setting.sql h2: console: enabled: true @@ -41,4 +38,7 @@ management: enabled: true cors: - allowed-origins: ${allowed-origins} \ No newline at end of file + allowed-origins: ${allowed-origins} + +jwt: + secret: ${JWT_SECRET} \ No newline at end of file diff --git a/uniro_backend/src/main/resources/application-local.yml b/uniro_backend/src/main/resources/application-local.yml index cec95d4..2cb0e51 100644 --- a/uniro_backend/src/main/resources/application-local.yml +++ b/uniro_backend/src/main/resources/application-local.yml @@ -6,17 +6,16 @@ spring: password: jpa: hibernate: - ddl-auto: create + ddl-auto: update properties: hibernate: format_sql: true dialect: org.hibernate.spatial.dialect.mysql.MySQLSpatialDialect show_sql: true open-in-view: false - defer-datasource-initialization: true + #defer-datasource-initialization: true sql: init: - #schema-locations: classpath:h2gis-setting.sql mode: always h2: console: @@ -40,4 +39,7 @@ management: enabled: true cors: - allowed-origins: ${allowed-origins} \ No newline at end of file + allowed-origins: ${allowed-origins} + +jwt: + secret: ${JWT_SECRET} \ No newline at end of file diff --git a/uniro_backend/src/main/resources/application-test.yml b/uniro_backend/src/main/resources/application-test.yml index b2d34ab..451ab16 100644 --- a/uniro_backend/src/main/resources/application-test.yml +++ b/uniro_backend/src/main/resources/application-test.yml @@ -4,30 +4,26 @@ spring: on-profile: test datasource: - url: jdbc:h2:mem:uniro-local-db;DATABASE_TO_UPPER=FALSE;mode=mysql - driverClassName: org.h2.Driver - username: sa + url: jdbc:tc:mysql://localhost:3306/uniro-test?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC + driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver + username: root password: jpa: - database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create properties: hibernate: format_sql: true + dialect: org.hibernate.spatial.dialect.mysql.MySQLSpatialDialect show_sql: true open-in-view: false defer-datasource-initialization: true - sql: - init: - schema-locations: classpath:h2gis-setting.sql - h2: - console: - enabled: true - path: /h2-console map: api: key: ${google.api.key} cors: - allowed-origins: ${allowed-origins} \ No newline at end of file + allowed-origins: ${allowed-origins} + +jwt: + secret: ${JWT_SECRET} \ No newline at end of file diff --git a/uniro_backend/src/main/resources/data.sql b/uniro_backend/src/main/resources/data.sql index fbfa9e7..4ebd7c8 100644 --- a/uniro_backend/src/main/resources/data.sql +++ b/uniro_backend/src/main/resources/data.sql @@ -1,23 +1,23 @@ INSERT INTO node (id, coordinates, height, is_core, univ_id) VALUES - (1, ST_PointFromText('POINT(127.001 37.001)', 4326), 50.0, TRUE, 1001), - (2, ST_PointFromText('POINT(127.002 37.002)', 4326), 52.3, FALSE, 1001), - (3, ST_PointFromText('POINT(127.003 37.003)', 4326), 55.1, FALSE, 1001), - (4, ST_PointFromText('POINT(127.004 37.004)', 4326), 49.8, TRUE, 1002), - (5, ST_PointFromText('POINT(127.005 37.005)', 4326), 51.0, FALSE, 1002), - (6, ST_PointFromText('POINT(127.006 37.006)', 4326), 48.5, FALSE, 1003); + (1, ST_PointFromText('POINT(30.001 37.001)', 4326), 50.0, TRUE, 1001), + (2, ST_PointFromText('POINT(30.002 37.002)', 4326), 52.3, FALSE, 1001), + (3, ST_PointFromText('POINT(30.003 37.003)', 4326), 55.1, FALSE, 1001), + (4, ST_PointFromText('POINT(30.004 37.004)', 4326), 49.8, TRUE, 1002), + (5, ST_PointFromText('POINT(30.005 37.005)', 4326), 51.0, FALSE, 1002), + (6, ST_PointFromText('POINT(30.006 37.006)', 4326), 48.5, FALSE, 1003); -- 목 데이터 삽입 INSERT INTO - route (id, cost, path, node1_id, node2_id, univ_id, core_route_id, caution_factors, danger_factors) + route (id, distance, path, node1_id, node2_id, univ_id, core_route_id, caution_factors, danger_factors) VALUES - (1, 10.5, ST_GeomFromText('LINESTRING(127.001 37.001, 127.002 37.002)', 4326), 1, 2, 1001, NULL, '["SLOPE", "CURB"]', '["STAIRS"]'), - (2, 20.0, ST_GeomFromText('LINESTRING(127.002 37.002, 127.003 37.003)', 4326), 2, 3, 1001, NULL, '["CRACK"]', '["SLOPE", "STAIRS"]'), - (3, 15.2, ST_GeomFromText('LINESTRING(127.003 37.003, 127.004 37.004)', 4326), 3, 4, 1002, 1, NULL, NULL), - (4, 25.7, ST_GeomFromText('LINESTRING(127.004 37.004, 127.005 37.005)', 4326), 4, 5, 1002, 1, '["CURB", "CRACK"]', NULL), - (5, 30.0, ST_GeomFromText('LINESTRING(127.005 37.005, 127.006 37.006)', 4326), 5, 6, 1003, NULL, NULL, '["CURB"]'); + (1, 10.5, ST_GeomFromText('LINESTRING(30.001 37.001, 30.002 37.002)', 4326), 1, 2, 1001, NULL, '["SLOPE", "CURB"]', '["STAIRS"]'), + (2, 20.0, ST_GeomFromText('LINESTRING(30.002 37.002, 30.003 37.003)', 4326), 2, 3, 1001, NULL, '["CRACK"]', '["SLOPE", "STAIRS"]'), + (3, 15.2, ST_GeomFromText('LINESTRING(30.003 37.003, 30.004 37.004)', 4326), 3, 4, 1002, 1, '["SLOPE", "CURB"]', '["SLOPE", "STAIRS"]'), + (4, 25.7, ST_GeomFromText('LINESTRING(30.004 37.004, 30.005 37.005)', 4326), 4, 5, 1002, 1, '["CURB", "CRACK"]', '[]'), + (5, 30.0, ST_GeomFromText('LINESTRING(30.005 37.005, 30.006 37.006)', 4326), 5, 6, 1003, NULL, '[]', '["CURB"]'); INSERT INTO building (id, phone_number, address, name, image_url, level, node_id, univ_id) diff --git a/uniro_backend/src/main/resources/h2gis-setting.sql b/uniro_backend/src/main/resources/h2gis-setting.sql deleted file mode 100644 index 9865ced..0000000 --- a/uniro_backend/src/main/resources/h2gis-setting.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE ALIAS IF NOT EXISTS H2GIS_SPATIAL FOR "org.h2gis.functions.factory.H2GISFunctions.load"; -CALL H2GIS_SPATIAL(); \ No newline at end of file diff --git a/uniro_backend/src/test/java/com/softeer5/uniro_backend/UniroBackendApplicationTests.java b/uniro_backend/src/test/java/com/softeer5/uniro_backend/UniroBackendApplicationTests.java index eb2017e..5a2500e 100644 --- a/uniro_backend/src/test/java/com/softeer5/uniro_backend/UniroBackendApplicationTests.java +++ b/uniro_backend/src/test/java/com/softeer5/uniro_backend/UniroBackendApplicationTests.java @@ -1,11 +1,15 @@ package com.softeer5.uniro_backend; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; +import org.testcontainers.junit.jupiter.Testcontainers; @SpringBootTest +@Testcontainers @ActiveProfiles("test") +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) class UniroBackendApplicationTests { @Test diff --git a/uniro_backend/src/test/java/com/softeer5/uniro_backend/admin/AdminServiceTest.java b/uniro_backend/src/test/java/com/softeer5/uniro_backend/admin/AdminServiceTest.java new file mode 100644 index 0000000..d1f9cf2 --- /dev/null +++ b/uniro_backend/src/test/java/com/softeer5/uniro_backend/admin/AdminServiceTest.java @@ -0,0 +1,746 @@ + + +package com.softeer5.uniro_backend.admin; + +import static org.assertj.core.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import com.softeer5.uniro_backend.admin.dto.response.ChangedRouteDTO; +import com.softeer5.uniro_backend.admin.dto.response.GetAllRoutesByRevisionResDTO; +import com.softeer5.uniro_backend.admin.dto.response.LostRoutesDTO; +import com.softeer5.uniro_backend.map.dto.response.*; +import com.softeer5.uniro_backend.map.enums.CautionFactor; +import org.hibernate.envers.AuditReader; +import org.hibernate.envers.AuditReaderFactory; +import org.hibernate.envers.query.AuditEntity; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; +import org.testcontainers.junit.jupiter.Testcontainers; + +import com.softeer5.uniro_backend.admin.entity.RevInfo; +import com.softeer5.uniro_backend.admin.test_repository.RevInfoTestRepository; +import com.softeer5.uniro_backend.admin.service.AdminService; +import com.softeer5.uniro_backend.admin.setting.RevisionContext; +import com.softeer5.uniro_backend.fixture.NodeFixture; +import com.softeer5.uniro_backend.fixture.RouteFixture; +import com.softeer5.uniro_backend.map.enums.DangerFactor; +import com.softeer5.uniro_backend.map.entity.Node; +import com.softeer5.uniro_backend.map.entity.Route; +import com.softeer5.uniro_backend.map.repository.NodeRepository; +import com.softeer5.uniro_backend.map.repository.RouteRepository; +import com.softeer5.uniro_backend.map.test_repository.NodeTestRepository; +import com.softeer5.uniro_backend.map.test_repository.RouteTestRepository; + +import jakarta.persistence.EntityManager; + +@SpringBootTest +@Testcontainers +@ActiveProfiles("test") +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@SqlGroup({ + @Sql(value = "/sql/delete-all-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) +}) +class AdminServiceTest { + + @Autowired + private AdminService adminService; + + @Autowired + private NodeRepository nodeRepository; + + @Autowired + private RouteRepository routeRepository; + + @Autowired + private EntityManager entityManager; + + @Autowired + private NodeTestRepository nodeTestRepository; + + @Autowired + private RouteTestRepository routeTestRepository; + + @Autowired + private RevInfoTestRepository revInfoTestRepository; + + @BeforeEach + void revisionContextInit(){ + RevisionContext.setUnivId(1001L); + } + + @AfterEach + void revisionContextClear(){ + RevisionContext.clear(); + } + + @Test + void save_시점마다_version은_다르다() { + // given + Node node1 = NodeFixture.createNode(1, 1); + Node node2 = NodeFixture.createNode(1, 2); + Node node3 = NodeFixture.createNode(1, 3); + + Node savedNode1 = nodeRepository.save(node1); + Node savedNode2 = nodeRepository.save(node2); + nodeRepository.save(node3); + + savedNode1.setCore(true); + savedNode2.setCore(true); + nodeRepository.save(savedNode1); + nodeRepository.save(savedNode2); + + // when + EntityManager freshEntityManager = entityManager.getEntityManagerFactory().createEntityManager(); // ✅ 새로운 EntityManager 생성 + AuditReader auditReader = AuditReaderFactory.get(freshEntityManager); + + // then + List revNodes = auditReader.createQuery() + .forEntitiesAtRevision(Node.class, 4) + .add(AuditEntity.property("univId").eq(1001)) + .getResultList(); + + assertThat(revNodes).hasSize(3); + for(Node revNode : revNodes) { + if(revNode.getId() == 1) { + assertThat(revNode.isCore()).isTrue(); + } else { + assertThat(revNode.isCore()).isFalse(); + } + } + } + + @Test + void 롤백시_수정에_관해서_audit_관련_쿼리가_발생하지_않는다() { + // given + Node node1 = NodeFixture.createNode(1, 1); + Node node2 = NodeFixture.createNode(1, 2); + Node node3 = NodeFixture.createNode(1, 3); + Node node4 = NodeFixture.createNode(2,2); + + Route route1 = RouteFixture.createRoute(node1, node2); + Route route2 = RouteFixture.createRoute(node2, node3); + Route route3 = RouteFixture.createRoute(node1, node4); + + Node savedNode1 = nodeRepository.save(node1); // 1 + nodeRepository.save(node2); // 2 + nodeRepository.save(node3); // 3 + + routeRepository.save(route1); // 4 + routeRepository.save(route2); // 5 + + nodeRepository.save(node4); // 6 + savedNode1.setCore(true); + nodeRepository.save(savedNode1); // 7 + routeRepository.save(route3); // 8 + + long versionId = 4L; + + // when + adminService.rollbackRev(1001L, versionId); + + // then + EntityManager freshEntityManager = entityManager.getEntityManagerFactory().createEntityManager(); // ✅ 새로운 EntityManager 생성 + AuditReader auditReader = AuditReaderFactory.get(freshEntityManager); + + List auditChanges = auditReader.createQuery() + .forRevisionsOfEntity(Node.class, false, true) + .add(AuditEntity.id().eq(savedNode1.getId())) + .add(AuditEntity.revisionNumber().gt(versionId)) // 4버전 이후 데이터 확인 + .getResultList(); + + assertThat(auditChanges).isEmpty(); + } + + @Test + void 하나의_트랜잭션에는_하나의_버전이_생긴다() { + // Given + Node node1 = NodeFixture.createNode(1, 1); + Node node2 = NodeFixture.createNode(1, 2); + Node node3 = NodeFixture.createNode(1, 3); + Node node4 = NodeFixture.createNode(2,2); + + Route route1 = RouteFixture.createRoute(node1, node2); + Route route2 = RouteFixture.createRoute(node2, node3); + Route route3 = RouteFixture.createRoute(node3, node4); + + // When + List nodes = nodeRepository.saveAll(List.of(node1, node2)); // ver 1 + List nodes2 = nodeRepository.saveAll(List.of(node3, node4)); // ver 2 + + // Then + EntityManager freshEntityManager = entityManager.getEntityManagerFactory().createEntityManager(); // ✅ 새로운 EntityManager 생성 + AuditReader auditReader = AuditReaderFactory.get(freshEntityManager); + + List resultList = auditReader.createQuery() + .forRevisionsOfEntity(Node.class, false, true) + .addOrder(AuditEntity.revisionNumber().desc()) // 가장 최신 버전부터 내림차순 정렬 + .getResultList(); + + RevInfo revInfo1 = (RevInfo)resultList.get(0)[1]; + RevInfo revInfo2 = (RevInfo)resultList.get(1)[1]; + + assertThat(revInfo1.getRev()).isEqualTo(2); + assertThat(revInfo2.getRev()).isEqualTo(2); + } + + @Test + void 정상적으로_리비전_롤백() { + Node node1 = NodeFixture.createNode(1, 1); + Node node2 = NodeFixture.createNode(1, 2); + Node node3 = NodeFixture.createNode(1, 3); + Node node4 = NodeFixture.createNode(2,2); + Node rollbackNode = NodeFixture.createNode(1, 4); + + Route route1 = RouteFixture.createRoute(node1, node2); + Route route2 = RouteFixture.createRoute(node2, node3); + Route route3 = RouteFixture.createRoute(node3, node4); + Route rollbackRoute = RouteFixture.createRoute(node1, rollbackNode); + + List nodes = nodeRepository.saveAll(List.of(node1, node2, node3, node4)); // ver 1 + List routes = routeRepository.saveAll(List.of(route1, route2, route3)); // ver2 + + nodes.forEach(node -> node.setCore(true)); + nodeRepository.saveAll(List.of(node1, node2, node3, node4)); // ver3 + + List dangerFactorsList = new ArrayList<>(); + dangerFactorsList.add(DangerFactor.SLOPE); + routes.get(0).setDangerFactorsByList(dangerFactorsList); + Route savedRoute = routeRepository.save(routes.get(0)); // ver4 + + dangerFactorsList.add(DangerFactor.CURB); + routes.get(1).setDangerFactorsByList(dangerFactorsList); + Route deletedRoute = routeRepository.save(routes.get(1));// ver5 + + nodeRepository.save(rollbackNode); // ver6 + routeRepository.save(rollbackRoute); // ver7 + + // When + adminService.rollbackRev(1001L, 4L); + + // Then + EntityManager freshEntityManager = entityManager.getEntityManagerFactory().createEntityManager(); // ✅ 새로운 EntityManager 생성 + AuditReader auditReader = AuditReaderFactory.get(freshEntityManager); + + // a. 롤백 버전 이후 버전 데이터 확인 + // a-1. routeRepository + List rollbackRoutes = routeTestRepository.findAll(); + assertThat(rollbackRoutes).hasSize(3); + + // a-2. nodeRepository + List rollbackNodes = nodeTestRepository.findAll(); + assertThat(rollbackNodes).hasSize(4); + rollbackNodes.forEach(node -> { + assertThat(node.isCore()).isTrue(); + }); + + // a-3. routeAuditRepository + List resultList1 = auditReader.createQuery() + .forEntitiesAtRevision(Route.class, 4) + .add(AuditEntity.property("univId").eq(1001)) + .add(AuditEntity.property("id").eq(savedRoute.getId())) + .getResultList(); + + assertThat(resultList1).hasSize(1); + Route revRoute = resultList1.get(0); + assertThat(revRoute.getDangerFactors()).isEqualTo(Set.of(DangerFactor.SLOPE)); + + // a-4. nodeAuditRepository + List resultList2 = auditReader.createQuery() + .forEntitiesAtRevision(Node.class, 4) + .add(AuditEntity.property("univId").eq(1001)) + .getResultList(); + + resultList2.forEach(node -> { + assertThat(node.isCore()).isTrue(); + }); + + // a-5. revInfoRepository + List revInfos = revInfoTestRepository.findAll(); + assertThat(revInfos).hasSize(4); + } + + @Test + @DisplayName("wiki 페이지 TC.1") + void 특정_버전_조회_테스트_with_길추가(){ + Node node1 = NodeFixture.createNode(1, 1); + Node node2 = NodeFixture.createNode(1, 2); + Node node3 = NodeFixture.createNode(1, 3); + Node node4 = NodeFixture.createNode(2,2); + + Route route1 = RouteFixture.createRoute(node1, node2); + Route route2 = RouteFixture.createRoute(node2, node3); + Route route3 = RouteFixture.createRoute(node3, node4); + + nodeRepository.saveAll(List.of(node1, node2)); // ver 1 + routeRepository.saveAll(List.of(route1)); // ver 2 + nodeRepository.saveAll(List.of(node3,node4)); // ver 3 + routeRepository.saveAll(List.of(route2, route3)); // ver 4 + + //when + GetAllRoutesByRevisionResDTO allRoutesByRevision = adminService.getAllRoutesByRevision(1001L, 2L); + + //then + GetAllRoutesResDTO routesInfo = allRoutesByRevision.getRoutesInfo(); // 버전 2에 존재하는 map 정보 + LostRoutesDTO lostRoutes = allRoutesByRevision.getLostRoutes(); // 버전 3,4에 생성하여 버전2엔 존재하지 않는 routes + + //a. version2에 존재하는 맵 정보 확인 + + //a-1. ver 2의 노드의 개수가 2개인지? (node1, node2) + List nodeInfos = routesInfo.getNodeInfos(); // node 정보 + assertThat(nodeInfos).hasSize(2); + + //a-2. ver 2의 코어route 의 개수가 1개인지? (route1) + List coreRoutes = routesInfo.getCoreRoutes(); + assertThat(coreRoutes).hasSize(1); + + //a-3. ver 2의 route의 개수가 1개인지? (route1) + List routes = routesInfo.getCoreRoutes().get(0).getRoutes(); // core_route 정보 + assertThat(routes).hasSize(1); + + //a-4. ver2에 존재하는 route의 id가 route1과 같은지? + Long routeId = routes.get(0).getRouteId(); // ver2의 route의 id + assertThat(routeId).isEqualTo(route1.getId()); + + + + //b. version2에 존재하지 않는 맵 정보 확인 + + //b-1. ver 2에 존재하지 않는 노드가 3개인지? (node2, node3, node4) + List lostNodes = lostRoutes.getNodeInfos(); + assertThat(lostNodes).hasSize(3); + + //b-2. ver 2에 존재하는 코어route가 1개인지? + List lostCoreRoutes = lostRoutes.getCoreRoutes(); + assertThat(lostCoreRoutes).hasSize(1); + + //b-2. ver 2에 존재하는 간선이 2개인지? (route2, route3) + List lostRouteInfos = lostRoutes.getCoreRoutes().get(0).getRoutes(); + assertThat(lostRouteInfos).hasSize(2); + + //b-2. ver 2에 존재하지 않는 route가 route2, route3 인지? + List lostRouteIds = lostRouteInfos.stream() + .map(RouteCoordinatesInfoResDTO::getRouteId) + .collect(Collectors.toList()); + + assertThat(lostRouteIds) + .containsExactlyInAnyOrder(route2.getId(), route3.getId()); + + + } + + @Test + @DisplayName("wiki 페이지 TC.2") + void 특정_버전_조회_테스트_with_서로다른_길_추가(){ + Node node1 = NodeFixture.createNode(1, 1); + Node node2 = NodeFixture.createNode(1, 2); + Node node3 = NodeFixture.createNode(1, 3); + Node node4 = NodeFixture.createNode(2,2); + Node node5 = NodeFixture.createNode(0, 0); + Node node6 = NodeFixture.createNode(-1, -1); + + Route route1 = RouteFixture.createRoute(node1, node2); + Route route2 = RouteFixture.createRoute(node2, node3); + Route route3 = RouteFixture.createRoute(node3, node4); + Route route4 = RouteFixture.createRoute(node1, node5); + Route route5 = RouteFixture.createRoute(node5, node6); + + nodeRepository.saveAll(List.of(node1, node2)); // ver 1 + routeRepository.saveAll(List.of(route1)); // ver 2 + + nodeRepository.saveAll(List.of(node3,node4)); // ver 3 + routeRepository.saveAll(List.of(route2, route3)); // ver 4 + + nodeRepository.saveAll(List.of(node5,node6)); // ver 5 + routeRepository.saveAll(List.of(route4, route5)); // ver 6 + + //when + GetAllRoutesByRevisionResDTO allRoutesByRevision = adminService.getAllRoutesByRevision(1001L, 2L); + + //then + GetAllRoutesResDTO routesInfo = allRoutesByRevision.getRoutesInfo(); // 버전 2에 존재하는 map 정보 + LostRoutesDTO lostRoutes = allRoutesByRevision.getLostRoutes(); // 버전 3,4,5,6에 생성하여 버전2엔 존재하지 않는 routes + + //a. version2에 존재하는 맵 정보 확인 + + //a-1. ver 2의 노드의 개수가 2개인지? (node1, node2) + List nodeInfos = routesInfo.getNodeInfos(); // node 정보 + assertThat(nodeInfos).hasSize(2); + + //a-2. ver 2의 코어route 의 개수가 1개인지? (route1) + List coreRoutes = routesInfo.getCoreRoutes(); + assertThat(coreRoutes).hasSize(1); + + //a-3. ver 2의 route의 개수가 1개인지? (route1) + List routes = routesInfo.getCoreRoutes().get(0).getRoutes(); // core_route 정보 + assertThat(routes).hasSize(1); + + //a-4. ver2에 존재하는 route의 id가 route1과 같은지? + Long routeId = routes.get(0).getRouteId(); // ver2의 route의 id + assertThat(routeId).isEqualTo(route1.getId()); + + + //b. version2에 존재하지 않는 맵 정보 확인 + + //b-1. ver 2에 존재하지 않는 노드가 6개인지? (node1, node2, node3, node4, node5, node6) + List lostNodes = lostRoutes.getNodeInfos(); + assertThat(lostNodes).hasSize(6); + + //b-2. ver 2에 존재하지 않는 코어route가 2개인지? + List lostCoreRoutes = lostRoutes.getCoreRoutes(); + assertThat(lostCoreRoutes).hasSize(2); + + //b-2. ver 2에 존재하지 않는 루트가 4개인지? (route2, route3, route4, route5) + int cnt = 0; + for(CoreRouteResDTO coreRoute : lostCoreRoutes) { + cnt += coreRoute.getRoutes().size(); + } + assertThat(cnt).isEqualTo(4); + + //b-2. ver 2에 존재하지 않는 route가 route2, route3, route4, route5 인지? + List lostRouteIds = new ArrayList<>(); + for(CoreRouteResDTO coreRoute : lostCoreRoutes) { + for(RouteCoordinatesInfoResDTO route : coreRoute.getRoutes()) { + lostRouteIds.add(route.getRouteId()); + } + } + + assertThat(lostRouteIds) + .containsExactlyInAnyOrder(route2.getId(), route3.getId(), route4.getId(), route5.getId()); + + + } + + @Test + @DisplayName("wiki 페이지 TC.3") + void 특정_버전_조회_테스트_with_서로다른_길_여러개_추가(){ + Node node1 = NodeFixture.createNode(1, 1); + Node node2 = NodeFixture.createNode(1, 2); + Node node3 = NodeFixture.createNode(1, 3); + Node node4 = NodeFixture.createNode(2,2); + Node node5 = NodeFixture.createNode(0, 0); + Node node6 = NodeFixture.createNode(-1, -1); + Node node7 = NodeFixture.createNode(3, 3); + Node node8 = NodeFixture.createNode(2, 3); + + Route route1 = RouteFixture.createRoute(node1, node2); + Route route2 = RouteFixture.createRoute(node2, node3); + Route route3 = RouteFixture.createRoute(node3, node4); + Route route4 = RouteFixture.createRoute(node1, node5); + Route route5 = RouteFixture.createRoute(node5, node6); + Route route6 = RouteFixture.createRoute(node4, node7); + Route route7 = RouteFixture.createRoute(node4, node8); + + node4.setCore(true); + + nodeRepository.saveAll(List.of(node1, node2)); // ver 1 + routeRepository.saveAll(List.of(route1)); // ver 2 + + nodeRepository.saveAll(List.of(node3,node4, node7, node8)); // ver 3 + routeRepository.saveAll(List.of(route2, route3, route6, route7)); // ver 4 + + nodeRepository.saveAll(List.of(node5,node6)); // ver 5 + routeRepository.saveAll(List.of(route4, route5)); // ver 6 + + //when + GetAllRoutesByRevisionResDTO allRoutesByRevision = adminService.getAllRoutesByRevision(1001L, 2L); + + //then + GetAllRoutesResDTO routesInfo = allRoutesByRevision.getRoutesInfo(); // 버전 2에 존재하는 map 정보 + LostRoutesDTO lostRoutes = allRoutesByRevision.getLostRoutes(); // 버전 3,4,5,6에 생성하여 버전2엔 존재하지 않는 routes + + //a. version2에 존재하는 맵 정보 확인 + + //a-1. ver 2의 노드의 개수가 2개인지? (node1, node2) + List nodeInfos = routesInfo.getNodeInfos(); // node 정보 + assertThat(nodeInfos).hasSize(2); + + //a-2. ver 2의 코어route 의 개수가 1개인지? (route1) + List coreRoutes = routesInfo.getCoreRoutes(); + assertThat(coreRoutes).hasSize(1); + + //a-3. ver 2의 route의 개수가 1개인지? (route1) + List routes = routesInfo.getCoreRoutes().get(0).getRoutes(); // core_route 정보 + assertThat(routes).hasSize(1); + + //a-4. ver2에 존재하는 route의 id가 route1과 같은지? + Long routeId = routes.get(0).getRouteId(); // ver2의 route의 id + assertThat(routeId).isEqualTo(route1.getId()); + + + //b. version2에 존재하지 않는 맵 정보 확인 + + //b-1. ver 2에 존재하지 않는 노드가 8개인지? (node1, node2, node3, node4, node5, node6, node7, node8) + List lostNodes = lostRoutes.getNodeInfos(); + assertThat(lostNodes).hasSize(8); + + //b-2. ver 2에 존재하지 않는 코어route가 4개인지? + List lostCoreRoutes = lostRoutes.getCoreRoutes(); + assertThat(lostCoreRoutes).hasSize(4); + + //b-2. ver 2에 존재하지 않는 루트가 6개인지? (route2, route3, route4, route5, route6, route7) + int cnt = 0; + for(CoreRouteResDTO coreRoute : lostCoreRoutes) { + cnt += coreRoute.getRoutes().size(); + } + assertThat(cnt).isEqualTo(6); + + //b-2. ver 2에 존재하지 않는 route가 route2, route3, route4, route5, route6, route7 인지? + List lostRouteIds = new ArrayList<>(); + for(CoreRouteResDTO coreRoute : lostCoreRoutes) { + for(RouteCoordinatesInfoResDTO route : coreRoute.getRoutes()) { + lostRouteIds.add(route.getRouteId()); + } + } + + assertThat(lostRouteIds) + .containsExactlyInAnyOrder(route2.getId(), route3.getId(), route4.getId(), route5.getId() + , route6.getId(), route7.getId()); + + + } + + @Test + @DisplayName("wiki 페이지 TC.4") + void 특정_버전_조회_테스트_with_사이클이_있는_서로다른_길_여러개_추가(){ + Node node1 = NodeFixture.createNode(1, 1); + Node node2 = NodeFixture.createNode(1, 2); + Node node3 = NodeFixture.createNode(1, 3); + Node node4 = NodeFixture.createNode(2,2); + Node node5 = NodeFixture.createNode(0, 0); + Node node6 = NodeFixture.createNode(-1, -1); + Node node7 = NodeFixture.createNode(3, 3); + Node node8 = NodeFixture.createNode(2, 3); + + Route route1 = RouteFixture.createRoute(node1, node2); + Route route2 = RouteFixture.createRoute(node2, node3); + Route route3 = RouteFixture.createRoute(node3, node4); + Route route4 = RouteFixture.createRoute(node1, node5); + Route route5 = RouteFixture.createRoute(node5, node6); + Route route6 = RouteFixture.createRoute(node4, node7); + Route route7 = RouteFixture.createRoute(node4, node8); + Route route8 = RouteFixture.createRoute(node7, node8); + + node4.setCore(true); + + nodeRepository.saveAll(List.of(node1, node2, node3)); // ver 1 + routeRepository.saveAll(List.of(route1, route2)); // ver 2 + + nodeRepository.saveAll(List.of(node4, node7, node8)); // ver 3 + routeRepository.saveAll(List.of(route3, route6, route7, route8)); // ver 4 + + nodeRepository.saveAll(List.of(node5,node6)); // ver 5 + routeRepository.saveAll(List.of(route4, route5)); // ver 6 + + //when + GetAllRoutesByRevisionResDTO allRoutesByRevision = adminService.getAllRoutesByRevision(1001L, 2L); + + //then + GetAllRoutesResDTO routesInfo = allRoutesByRevision.getRoutesInfo(); // 버전 2에 존재하는 map 정보 + LostRoutesDTO lostRoutes = allRoutesByRevision.getLostRoutes(); // 버전 3,4,5,6에 생성하여 버전2엔 존재하지 않는 routes + + //a. version2에 존재하는 맵 정보 확인 + + //a-1. ver 2의 노드의 개수가 3개인지? (node1, node2, node3) + List nodeInfos = routesInfo.getNodeInfos(); // node 정보 + assertThat(nodeInfos).hasSize(3); + + //a-2. ver 2의 코어route 의 개수가 1개인지? (route1, route2) + List coreRoutes = routesInfo.getCoreRoutes(); + assertThat(coreRoutes).hasSize(1); + + //a-3. ver 2의 route의 개수가 2개인지? (route1, route2) + List routes = routesInfo.getCoreRoutes().get(0).getRoutes(); // core_route 정보 + assertThat(routes).hasSize(2); + + + //b. version2에 존재하지 않는 맵 정보 확인 + + //b-1. ver 2에 존재하지 않는 노드가 7개인지? (node1, node3, node4, node5, node6, node7, node8) + List lostNodes = lostRoutes.getNodeInfos(); + assertThat(lostNodes).hasSize(7); + + //b-2. ver 2에 존재하지 않는 코어route가 3개인지? + List lostCoreRoutes = lostRoutes.getCoreRoutes(); + assertThat(lostCoreRoutes).hasSize(3); + + //b-2. ver 2에 존재하지 않는 루트가 6개인지? (route3, route4, route5, route6, route7, route8) + int cnt = 0; + for(CoreRouteResDTO coreRoute : lostCoreRoutes) { + cnt += coreRoute.getRoutes().size(); + } + assertThat(cnt).isEqualTo(6); + + //b-2. ver 2에 존재하지 않는 route가 route3, route4, route5, route6, route7, route8 인지? + List lostRouteIds = new ArrayList<>(); + for(CoreRouteResDTO coreRoute : lostCoreRoutes) { + for(RouteCoordinatesInfoResDTO route : coreRoute.getRoutes()) { + lostRouteIds.add(route.getRouteId()); + } + } + + assertThat(lostRouteIds) + .containsExactlyInAnyOrder(route3.getId(), route4.getId(), route5.getId() + , route6.getId(), route7.getId(), route8.getId()); + + + } + + + @Test + @DisplayName("wiki 페이지 TC.5") + void 특정_버전_조회_테스트_with_사이클이_있으며_완전그래프인_서로다른_길_여러개_추가(){ + Node node1 = NodeFixture.createNode(1, 1); + Node node2 = NodeFixture.createNode(1, 2); + Node node3 = NodeFixture.createNode(1, 3); + Node node4 = NodeFixture.createNode(2,2); + Node node5 = NodeFixture.createNode(0, 0); + Node node6 = NodeFixture.createNode(-1, -1); + Node node7 = NodeFixture.createNode(3, 3); + Node node8 = NodeFixture.createNode(2, 3); + + Route route1 = RouteFixture.createRoute(node1, node2); + Route route2 = RouteFixture.createRoute(node2, node3); + Route route3 = RouteFixture.createRoute(node3, node4); + Route route4 = RouteFixture.createRoute(node1, node5); + Route route5 = RouteFixture.createRoute(node5, node6); + Route route6 = RouteFixture.createRoute(node4, node7); + Route route7 = RouteFixture.createRoute(node4, node8); + Route route8 = RouteFixture.createRoute(node7, node8); + + nodeRepository.saveAll(List.of(node1, node2, node3, node4)); // ver 1 + routeRepository.saveAll(List.of(route1, route2, route3)); // ver 2 + + nodeRepository.saveAll(List.of(node5,node6)); // ver 3 + routeRepository.saveAll(List.of(route4, route5)); // ver 4 + + node4.setCore(true); + nodeRepository.saveAll(List.of(node4, node7, node8)); // ver 5 + routeRepository.saveAll(List.of(route6, route7, route8)); // ver 6 + + //when + GetAllRoutesByRevisionResDTO allRoutesByRevision = adminService.getAllRoutesByRevision(1001L, 2L); + + //then + GetAllRoutesResDTO routesInfo = allRoutesByRevision.getRoutesInfo(); // 버전 2에 존재하는 map 정보 + LostRoutesDTO lostRoutes = allRoutesByRevision.getLostRoutes(); // 버전 3,4,5,6에 생성하여 버전2엔 존재하지 않는 routes + + //a. version2에 존재하는 맵 정보 확인 + + //a-1. ver 2의 노드의 개수가 4개인지? (node1, node2, node3, node4) + List nodeInfos = routesInfo.getNodeInfos(); // node 정보 + assertThat(nodeInfos).hasSize(4); + + //a-2. ver 2의 코어route 의 개수가 1개인지? (route1, route2, route3) + List coreRoutes = routesInfo.getCoreRoutes(); + assertThat(coreRoutes).hasSize(1); + + //a-3. ver 2의 route의 개수가 3개인지? (route1, route2, route3) + List routes = routesInfo.getCoreRoutes().get(0).getRoutes(); // core_route 정보 + assertThat(routes).hasSize(3); + + + //b. version2에 존재하지 않는 맵 정보 확인 + + //b-1. ver 2에 존재하지 않는 노드가 6개인지? (node1, node4, node5, node6, node7, node8) + List lostNodes = lostRoutes.getNodeInfos(); + assertThat(lostNodes).hasSize(6); + + //b-2. ver 2에 존재하지 않는 코어route가 2개인지? + List lostCoreRoutes = lostRoutes.getCoreRoutes(); + assertThat(lostCoreRoutes).hasSize(2); + + //b-2. ver 2에 존재하지 않는 루트가 5개인지? (route4, route5, route6, route7, route8) + int cnt = 0; + for(CoreRouteResDTO coreRoute : lostCoreRoutes) { + cnt += coreRoute.getRoutes().size(); + } + assertThat(cnt).isEqualTo(5); + + //b-2. ver 2에 존재하지 않는 route가 route4, route5, route6, route7, route8 인지? + List lostRouteIds = new ArrayList<>(); + for(CoreRouteResDTO coreRoute : lostCoreRoutes) { + for(RouteCoordinatesInfoResDTO route : coreRoute.getRoutes()) { + lostRouteIds.add(route.getRouteId()); + } + } + + assertThat(lostRouteIds) + .containsExactlyInAnyOrder(route4.getId(), route5.getId() + , route6.getId(), route7.getId(), route8.getId()); + + + } + + @Test + @DisplayName("wiki 페이지 TC.6") + void 특정_버전_조회_테스트_with_위험_주의요소_변경(){ + Node node1 = NodeFixture.createNode(1, 1); + Node node2 = NodeFixture.createNode(1, 2); + Node node3 = NodeFixture.createNode(1, 3); + Node node4 = NodeFixture.createNode(2,2); + Node node5 = NodeFixture.createNode(3, 3); + + Route route1 = RouteFixture.createRouteWithPath(node1, node2); + Route route2 = RouteFixture.createRouteWithPath(node2, node3); + Route route3 = RouteFixture.createRouteWithPath(node3, node4); + Route route4 = RouteFixture.createRouteWithPath(node4, node5); + + List cautionFactorsList1 = new ArrayList<>(); + cautionFactorsList1.add(CautionFactor.ETC); + route1.setCautionFactorsByList(cautionFactorsList1); + + nodeRepository.saveAll(List.of(node1, node2, node3)); // ver 1 + routeRepository.saveAll(List.of(route1, route2)); // ver 2 + nodeRepository.saveAll(List.of(node4,node5)); // ver 3 + routeRepository.saveAll(List.of(route3, route4)); // ver 4 + + List cautionFactorsList2 = new ArrayList<>(); + cautionFactorsList2.add(CautionFactor.CRACK); + route2.setCautionFactorsByList(cautionFactorsList2); + + List dangerFactorsList = new ArrayList<>(); + dangerFactorsList.add(DangerFactor.SLOPE); + route3.setDangerFactorsByList(dangerFactorsList); + + routeRepository.save(route2); // ver 5 + routeRepository.save(route3); // ver 6 + + //when + GetAllRoutesByRevisionResDTO allRoutesByRevision = adminService.getAllRoutesByRevision(1001L, 2L); + + //then + GetRiskRoutesResDTO getRiskRoutesResDTO = allRoutesByRevision.getGetRiskRoutesResDTO(); + List changedList = allRoutesByRevision.getChangedList(); + + //a. version 2에 존재하는 맵 정보 확인 + + //a-1. 기존의 caution이 1개인지? (route1) + assertThat(getRiskRoutesResDTO.getCautionRoutes().size()).isEqualTo(1); + + //a-2. 기존의 caution이 route1인지? + assertThat(getRiskRoutesResDTO.getCautionRoutes().get(0).getRouteId()).isEqualTo(route1.getId()); + + //a-3. 변경된 caution이 1개인지? (route2) + assertThat(changedList).hasSize(1); + + //a-4. 변경된 caution이 route2인지? + assertThat(changedList.get(0).getRouteId()).isEqualTo(route2.getId()); + + + } + + +} + diff --git a/uniro_backend/src/test/java/com/softeer5/uniro_backend/admin/test_repository/RevInfoTestRepository.java b/uniro_backend/src/test/java/com/softeer5/uniro_backend/admin/test_repository/RevInfoTestRepository.java new file mode 100644 index 0000000..d8873c6 --- /dev/null +++ b/uniro_backend/src/test/java/com/softeer5/uniro_backend/admin/test_repository/RevInfoTestRepository.java @@ -0,0 +1,8 @@ +package com.softeer5.uniro_backend.admin.test_repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.softeer5.uniro_backend.admin.entity.RevInfo; + +public interface RevInfoTestRepository extends JpaRepository { +} diff --git a/uniro_backend/src/test/java/com/softeer5/uniro_backend/building/service/BuildingServiceTest.java b/uniro_backend/src/test/java/com/softeer5/uniro_backend/building/service/BuildingServiceTest.java new file mode 100644 index 0000000..74bf350 --- /dev/null +++ b/uniro_backend/src/test/java/com/softeer5/uniro_backend/building/service/BuildingServiceTest.java @@ -0,0 +1,4 @@ +package com.softeer5.uniro_backend.building.service; + +public class BuildingServiceTest { +} diff --git a/uniro_backend/src/test/java/com/softeer5/uniro_backend/node/client/MapClientImplTest.java b/uniro_backend/src/test/java/com/softeer5/uniro_backend/external/MapClientImplTest.java similarity index 97% rename from uniro_backend/src/test/java/com/softeer5/uniro_backend/node/client/MapClientImplTest.java rename to uniro_backend/src/test/java/com/softeer5/uniro_backend/external/MapClientImplTest.java index 5485fc7..e9b1301 100644 --- a/uniro_backend/src/test/java/com/softeer5/uniro_backend/node/client/MapClientImplTest.java +++ b/uniro_backend/src/test/java/com/softeer5/uniro_backend/external/MapClientImplTest.java @@ -1,8 +1,8 @@ -package com.softeer5.uniro_backend.node.client; +package com.softeer5.uniro_backend.external; import com.softeer5.uniro_backend.external.MapClientImpl; import com.softeer5.uniro_backend.common.exception.custom.ElevationApiException; -import com.softeer5.uniro_backend.node.entity.Node; +import com.softeer5.uniro_backend.map.entity.Node; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/uniro_backend/src/test/java/com/softeer5/uniro_backend/fixture/NodeFixture.java b/uniro_backend/src/test/java/com/softeer5/uniro_backend/fixture/NodeFixture.java index 3f20cff..c005d5b 100644 --- a/uniro_backend/src/test/java/com/softeer5/uniro_backend/fixture/NodeFixture.java +++ b/uniro_backend/src/test/java/com/softeer5/uniro_backend/fixture/NodeFixture.java @@ -4,7 +4,7 @@ import org.locationtech.jts.geom.GeometryFactory; import com.softeer5.uniro_backend.common.utils.GeoUtils; -import com.softeer5.uniro_backend.node.entity.Node; +import com.softeer5.uniro_backend.map.entity.Node; public class NodeFixture { static GeometryFactory geometryFactory = GeoUtils.getInstance(); diff --git a/uniro_backend/src/test/java/com/softeer5/uniro_backend/fixture/RouteFixture.java b/uniro_backend/src/test/java/com/softeer5/uniro_backend/fixture/RouteFixture.java index 91d6250..7d8f8d2 100644 --- a/uniro_backend/src/test/java/com/softeer5/uniro_backend/fixture/RouteFixture.java +++ b/uniro_backend/src/test/java/com/softeer5/uniro_backend/fixture/RouteFixture.java @@ -1,10 +1,15 @@ package com.softeer5.uniro_backend.fixture; +import java.util.HashSet; +import java.util.List; + import org.locationtech.jts.geom.GeometryFactory; import com.softeer5.uniro_backend.common.utils.GeoUtils; -import com.softeer5.uniro_backend.node.entity.Node; -import com.softeer5.uniro_backend.route.entity.Route; +import com.softeer5.uniro_backend.map.entity.Node; +import com.softeer5.uniro_backend.map.entity.Route; + +import static com.softeer5.uniro_backend.common.utils.GeoUtils.convertDoubleToLineString; public class RouteFixture { static GeometryFactory geometryFactory = GeoUtils.getInstance(); @@ -14,6 +19,31 @@ public static Route createRoute(Node node1, Node node2){ .node1(node1) .node2(node2) .univId(1001L) + .cautionFactors(new HashSet<>()) + .dangerFactors(new HashSet<>()) .build(); } + + public static Route createRouteWithPath(Node node1, Node node2){ + return Route.builder() + .node1(node1) + .node2(node2) + .univId(1001L) + .cautionFactors(new HashSet<>()) + .dangerFactors(new HashSet<>()) + .path(convertDoubleToLineString(List.of(new double[]{1.0, 2.0}, new double[]{3.0, 4.0}))) + .build(); + } + + public static Route createRouteWithDistance(Node node1, Node node2, double distance){ + return Route.builder() + .distance(distance) + .node1(node1) + .node2(node2) + .univId(1001L) + .cautionFactors(new HashSet<>()) + .dangerFactors(new HashSet<>()) + .build(); + } + } diff --git a/uniro_backend/src/test/java/com/softeer5/uniro_backend/map/service/MapServiceTest.java b/uniro_backend/src/test/java/com/softeer5/uniro_backend/map/service/MapServiceTest.java new file mode 100644 index 0000000..6f85cb7 --- /dev/null +++ b/uniro_backend/src/test/java/com/softeer5/uniro_backend/map/service/MapServiceTest.java @@ -0,0 +1,482 @@ +package com.softeer5.uniro_backend.map.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.assertj.core.api.ThrowableAssert; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.junit.jupiter.Testcontainers; + +import com.softeer5.uniro_backend.common.exception.custom.RouteCalculationException; +import com.softeer5.uniro_backend.external.MapClient; +import com.softeer5.uniro_backend.fixture.NodeFixture; +import com.softeer5.uniro_backend.fixture.RouteFixture; +import com.softeer5.uniro_backend.map.dto.request.CreateRoutesReqDTO; +import com.softeer5.uniro_backend.map.entity.Node; +import com.softeer5.uniro_backend.map.repository.NodeRepository; +import com.softeer5.uniro_backend.map.dto.request.CreateRouteReqDTO; +import com.softeer5.uniro_backend.map.entity.Route; +import com.softeer5.uniro_backend.map.repository.RouteRepository; + +@SpringBootTest +@Testcontainers +@ActiveProfiles("test") +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Transactional +class MapServiceTest { + + @Autowired + private RouteCalculator routeCalculator; + @Autowired + private MapService mapService; + @Autowired + private RouteRepository routeRepository; + @Autowired + private NodeRepository nodeRepository; + + @MockBean + private MapClient mapClient; + + @Nested + class 경로추가{ + @Test + void 기본_경로_생성() { + // Given + Long univId = 1001L; + List requests = List.of( + new CreateRouteReqDTO(0, 0), + new CreateRouteReqDTO(1, 1), + new CreateRouteReqDTO(2, 2) + ); + + Node node0 = NodeFixture.createNode(-1, 0); + Node node1 = NodeFixture.createNode(0, 0); + Node node2 = NodeFixture.createNode(1, 0); + + Route route0 = RouteFixture.createRoute(node0, node1); + Route route = RouteFixture.createRoute(node1, node2); + + nodeRepository.saveAll(List.of(node0, node1, node2)); + routeRepository.saveAll(List.of(route0, route)); + + // When + mapService.createRoute(univId, new CreateRoutesReqDTO(node1.getId(), null, requests)); + + // Then + List results = nodeRepository.findAll(); + assertThat(results).hasSize(5); + assertThat(results.get(1).isCore()).isTrue(); + } + + @Test + @DisplayName("wiki 페이지 TC.2") + void 기존_경로와_겹치는_경우_예외발생() { + // Given + Long univId = 1001L; + List requests = List.of( + new CreateRouteReqDTO(0, 0), + new CreateRouteReqDTO(1, 1), + new CreateRouteReqDTO(2, 2) + ); + + Node node1 = NodeFixture.createNode(0, 0); + Node node2 = NodeFixture.createNode(1.5, 1.5); + Route route = RouteFixture.createRoute(node1, node2); + + nodeRepository.saveAll(List.of(node1, node2)); + routeRepository.save(route); + + // when, then + assertThatThrownBy(() -> mapService.createRoute(univId, new CreateRoutesReqDTO(node1.getId(), null, requests))) + .isInstanceOf(RouteCalculationException.class) + .hasMessageContaining("intersection is only allowed by point"); + } + + @Test + void 연결된_간선이_3개가_된_경우_시작노드가_서브에서_코어노드로_변경된다() { + // Given + Long univId = 1001L; + List requests = List.of( + new CreateRouteReqDTO(0, 0), + new CreateRouteReqDTO(3, 3), + new CreateRouteReqDTO(5, 3) + ); + + Node node0 = NodeFixture.createNode(0, -1); + Node node1 = NodeFixture.createNode(0, 0); + Node node2 = NodeFixture.createNode(0, 1); + Node node3 = NodeFixture.createNode(0, 2); + Node node4 = NodeFixture.createNode(0, 3); + + Route route0 = RouteFixture.createRoute(node0, node1); + Route route1 = RouteFixture.createRoute(node1, node2); + Route route2 = RouteFixture.createRoute(node2, node3); + Route route3 = RouteFixture.createRoute(node3, node4); + + nodeRepository.saveAll(List.of(node0, node1, node2, node3, node4)); + routeRepository.saveAll(List.of(route0, route1, route2, route3)); + + // When + mapService.createRoute(univId, new CreateRoutesReqDTO(node1.getId(), null, requests)); + + // Then + List results = nodeRepository.findAll(); + assertThat(results).hasSize(7); + assertThat(results.get(1).isCore()).isTrue(); + } + + @Test + void 출발점과_도착점이_같은_경우_예외처리() { + // Given + Long univId = 1001L; + List requests = List.of( + new CreateRouteReqDTO(0, 0), + new CreateRouteReqDTO(1, 1), + new CreateRouteReqDTO(0, 0) + ); + + Node node1 = NodeFixture.createNode(0, 0); + Node node2 = NodeFixture.createNode(0, 1.5); + Route route = RouteFixture.createRoute(node1, node2); + + nodeRepository.saveAll(List.of(node1, node2)); + routeRepository.save(route); + + // when, Then + assertThatThrownBy(() -> mapService.createRoute(univId, new CreateRoutesReqDTO(node1.getId(), null, requests))) + .isInstanceOf(RouteCalculationException.class); + + } + + @Test + @DisplayName("wiki 페이지 TC.3") + void 기존에_존재하는_노드와_연결할_경우_코어노드가_될_수_있다() { + // Given + Long univId = 1001L; + List requests = List.of( + new CreateRouteReqDTO(0, 0), + new CreateRouteReqDTO(2, 1), + new CreateRouteReqDTO(2, 2), + new CreateRouteReqDTO(0, 2), + new CreateRouteReqDTO(-1, 2) + ); + + Node node1 = NodeFixture.createNode(0, 0); + Node node2 = NodeFixture.createNode(0, 1); + Node node3 = NodeFixture.createNode(0, 2); + Node node3_1 = NodeFixture.createNode(0, 3); + + Route route1 = RouteFixture.createRoute(node1, node2); + Route route2 = RouteFixture.createRoute(node2, node3); + Route route2_1 = RouteFixture.createRoute(node3, node3_1); + + + nodeRepository.saveAll(List.of(node1, node2, node3, node3_1)); + routeRepository.saveAll(List.of(route1, route2, route2_1)); + + // When + mapService.createRoute(univId, new CreateRoutesReqDTO(node1.getId(), null, requests) ); + + // Then + List results = nodeRepository.findAll(); + assertThat(results).hasSize(7); + assertThat(results.get(0).isCore()).isFalse(); + assertThat(results.get(2).isCore()).isTrue(); + } + + @Test + void 기존_경로와_동일한_경우_예외발생() { + // Given + Long univId = 1001L; + List requests = List.of( + new CreateRouteReqDTO(0, 0), + new CreateRouteReqDTO(1, 1), + new CreateRouteReqDTO(2, 2) + ); + + Node node1 = NodeFixture.createNode(0, 0); + Node node2 = NodeFixture.createNode(1, 1); + Node node3 = NodeFixture.createNode(2, 2); + Route route = RouteFixture.createRoute(node1, node2); + Route route2 = RouteFixture.createRoute(node2, node3); + + nodeRepository.saveAll(List.of(node1, node2, node3)); + routeRepository.saveAll(List.of(route, route2)); + + // when, then + assertThatThrownBy(() -> mapService.createRoute(univId, new CreateRoutesReqDTO(node1.getId(), null, requests))) + .isInstanceOf(RouteCalculationException.class) + .hasMessageContaining("intersection is only allowed by point"); + } + + @Test + @DisplayName("wiki 페이지 TC.4") + void 지나치면서_셀프_크로스_되는_경우_새로_추가될_노드와_연결된다() { + // Given + Long univId = 1001L; + List requests = List.of( + new CreateRouteReqDTO(0, 0), + new CreateRouteReqDTO(0, 0.5), + new CreateRouteReqDTO(0, 1.5), + new CreateRouteReqDTO(0, 2.5), + new CreateRouteReqDTO(1, 2), + new CreateRouteReqDTO(-3, 1) + ); + + Node node1 = NodeFixture.createNode(0, 0); + Node node2 = NodeFixture.createNode(0, -1); + + Route route1 = RouteFixture.createRoute(node1, node2); + + nodeRepository.saveAll(List.of(node1, node2)); + routeRepository.saveAll(List.of(route1)); + + // When + mapService.createRoute(univId, new CreateRoutesReqDTO(node1.getId(), null, requests)); + + // Then + List results = nodeRepository.findAll(); + assertThat(results).hasSize(7); + assertThat(results.get(0).isCore()).isFalse(); + assertThat(results.get(3).isCore()).isTrue(); + System.out.println(results); + } + + @Test + @DisplayName("wiki 페이지 TC.5") + void 새로_추가될_노드와_연결되면서_셀프_크로스_되는_경우_새로_추가될_노드와_연결된다() { + // Given + Long univId = 1001L; + List requests = List.of( + new CreateRouteReqDTO(0, 0), + new CreateRouteReqDTO(0, 0.5), + new CreateRouteReqDTO(0, 1.5), + new CreateRouteReqDTO(0, 2.5), + new CreateRouteReqDTO(1, 2), + new CreateRouteReqDTO(0, 1.5), + new CreateRouteReqDTO(-3, 1) + ); + + Node node1 = NodeFixture.createNode(0, 0); + Node node2 = NodeFixture.createNode(0, -1); + + Route route1 = RouteFixture.createRoute(node1, node2); + + List savedNodes = nodeRepository.saveAll(List.of(node1, node2)); + routeRepository.saveAll(List.of(route1)); + + // When + mapService.createRoute(univId, new CreateRoutesReqDTO(savedNodes.get(0).getId(), null, requests)); + + // Then + List results = nodeRepository.findAll(); + + assertThat(results).hasSize(7); + assertThat(results.get(0).isCore()).isFalse(); + System.out.println(results); + assertThat(results.get(3).isCore()).isTrue(); + } + + @Test + @DisplayName("wiki 페이지 TC.6") + void 첫번째_노드에_연결되면서_셀프_크로스_되는_경우_새로_추가될_노드와_연결된다_() { + // Given + Long univId = 1001L; + List requests = List.of( + new CreateRouteReqDTO(0, 0), + new CreateRouteReqDTO(0, 1), + new CreateRouteReqDTO(0, 2), + new CreateRouteReqDTO(1, 2), + new CreateRouteReqDTO(0, 0), + new CreateRouteReqDTO(-2, 0) + ); + + Node node0 = NodeFixture.createNode(1, -1); + Node node1 = NodeFixture.createNode(0, 0); + + Route route1 = RouteFixture.createRoute(node0, node1); + + nodeRepository.saveAll(List.of(node0, node1)); + routeRepository.saveAll(List.of(route1)); + + // When + mapService.createRoute(univId, new CreateRoutesReqDTO(node1.getId(), null, requests) ); + + // Then + List results = nodeRepository.findAll(); + System.out.println(results); + assertThat(results).hasSize(6); + assertThat(results.get(1).isCore()).isTrue(); + } + + @Test + @DisplayName("인접한 노드는 중복될 수 없다. a->b->b 경우 ") + void nearest_test1(){ + // given + List requests = List.of( + new CreateRouteReqDTO(37.554533318727884, 127.040754103711), + new CreateRouteReqDTO(37.554506962309226, 127.0407538544841), + new CreateRouteReqDTO(37.554480605890554, 127.04075360525738), + new CreateRouteReqDTO(37.5544542494719, 127.04075335603085), + new CreateRouteReqDTO(37.554427893053244, 127.04075310680449), + new CreateRouteReqDTO(37.55437473334863, 127.04076651784956), + + new CreateRouteReqDTO(37.55437473334863, 127.04076651784956), + + new CreateRouteReqDTO(37.554367645389384, 127.0407906577353), + new CreateRouteReqDTO(37.55436055742524, 127.04081479761643), + new CreateRouteReqDTO(37.554353469456174, 127.04083893749296), + new CreateRouteReqDTO(37.55436126622311, 127.04086665364699), + new CreateRouteReqDTO(37.55436906298358, 127.04089436980678), + new CreateRouteReqDTO(37.55437685973755, 127.04092208597243), + new CreateRouteReqDTO(37.55440095878911, 127.04091538046072), + new CreateRouteReqDTO(37.554425057840305, 127.04090867494469), + new CreateRouteReqDTO(37.554449156891096, 127.04090196942433), + new CreateRouteReqDTO(37.55447325594152, 127.04089526389961), + new CreateRouteReqDTO(37.55449735499156, 127.04088855837058), + new CreateRouteReqDTO(37.55452145404122, 127.0408818528372), + new CreateRouteReqDTO(37.55453208597017, 127.04085637186247), + new CreateRouteReqDTO(37.554542717893675, 127.04083089088046), + new CreateRouteReqDTO(37.554553349811684, 127.0408054098912), + new CreateRouteReqDTO(37.554563981724215, 127.04077992889464) + ); + + Node node1 = NodeFixture.createNode(37.554533318727884, 127.040754103711); + Node node2 = NodeFixture.createNode(0, 0); + List savedNode = nodeRepository.saveAll(List.of(node1, node2)); + + routeRepository.save(RouteFixture.createRoute(savedNode.get(0), savedNode.get(1))); + + doNothing().when(mapClient).fetchHeights(anyList()); + + // when, then + Assertions.assertThatThrownBy(() -> mapService.createRoute(1001L, new CreateRoutesReqDTO(savedNode.get(0).getId(), null, requests))) + .isInstanceOf(RouteCalculationException.class) + .hasMessageContaining("has duplicate nearest node"); + } + + @Test + @DisplayName("인접한 노드는 중복될 수 없다. a->b->a 경우 ") + void nearest_test2(){ + // given + List requests = List.of( + new CreateRouteReqDTO(37.554533318727884, 127.040754103711), + new CreateRouteReqDTO(37.554506962309226, 127.0407538544841), + new CreateRouteReqDTO(37.554480605890554, 127.04075360525738), + new CreateRouteReqDTO(37.5544542494719, 127.04075335603085), + new CreateRouteReqDTO(37.554427893053244, 127.04075310680449), + + new CreateRouteReqDTO(37.55437473334863, 127.04076651784956), + + new CreateRouteReqDTO(37.554367645389384, 127.0407906577353), + + new CreateRouteReqDTO(37.55437473334863, 127.04076651784956), + + new CreateRouteReqDTO(37.55436055742524, 127.04081479761643), + new CreateRouteReqDTO(37.554353469456174, 127.04083893749296), + new CreateRouteReqDTO(37.55436126622311, 127.04086665364699), + new CreateRouteReqDTO(37.55436906298358, 127.04089436980678), + new CreateRouteReqDTO(37.55437685973755, 127.04092208597243), + new CreateRouteReqDTO(37.55440095878911, 127.04091538046072), + new CreateRouteReqDTO(37.554425057840305, 127.04090867494469), + new CreateRouteReqDTO(37.554449156891096, 127.04090196942433), + new CreateRouteReqDTO(37.55447325594152, 127.04089526389961), + new CreateRouteReqDTO(37.55449735499156, 127.04088855837058), + new CreateRouteReqDTO(37.55452145404122, 127.0408818528372), + new CreateRouteReqDTO(37.55453208597017, 127.04085637186247), + new CreateRouteReqDTO(37.554542717893675, 127.04083089088046), + new CreateRouteReqDTO(37.554553349811684, 127.0408054098912), + new CreateRouteReqDTO(37.554563981724215, 127.04077992889464) + ); + + Node node1 = NodeFixture.createNode(37.554533318727884, 127.040754103711); + Node node2 = NodeFixture.createNode(0, 0); + List savedNode = nodeRepository.saveAll(List.of(node1, node2)); + + routeRepository.save(RouteFixture.createRoute(savedNode.get(0), savedNode.get(1))); + + doNothing().when(mapClient).fetchHeights(anyList()); + + // when, then + Assertions.assertThatThrownBy(() -> mapService.createRoute(1001L, new CreateRoutesReqDTO(savedNode.get(0).getId(), null, requests))) + .isInstanceOf(RouteCalculationException.class) + .hasMessageContaining("has duplicate nearest node"); + } + + @Test + @DisplayName("동일한 노드가 2노드 이후에 있을 경우 정상 입력이다. a -> b -> c -> a 경우") + void nearest_test3(){ + // given + List requests = List.of( + new CreateRouteReqDTO(37.554533318727884, 127.040754103711), + new CreateRouteReqDTO(37.554506962309226, 127.0407538544841), + new CreateRouteReqDTO(37.554480605890554, 127.04075360525738), + new CreateRouteReqDTO(37.5544542494719, 127.04075335603085), + new CreateRouteReqDTO(37.554427893053244, 127.04075310680449), + + new CreateRouteReqDTO(37.55437473334863, 127.04076651784956), + + new CreateRouteReqDTO(37.554367645389384, 127.0407906577353), + new CreateRouteReqDTO(37.55436055742524, 127.04081479761643), + + new CreateRouteReqDTO(37.55437473334863, 127.04076651784956), + + new CreateRouteReqDTO(37.554353469456174, 127.04083893749296), + new CreateRouteReqDTO(37.55436126622311, 127.04086665364699), + new CreateRouteReqDTO(37.55436906298358, 127.04089436980678), + new CreateRouteReqDTO(37.55437685973755, 127.04092208597243), + new CreateRouteReqDTO(37.55440095878911, 127.04091538046072), + new CreateRouteReqDTO(37.554425057840305, 127.04090867494469), + new CreateRouteReqDTO(37.554449156891096, 127.04090196942433), + new CreateRouteReqDTO(37.55447325594152, 127.04089526389961), + new CreateRouteReqDTO(37.55449735499156, 127.04088855837058), + new CreateRouteReqDTO(37.55452145404122, 127.0408818528372), + new CreateRouteReqDTO(37.55453208597017, 127.04085637186247), + new CreateRouteReqDTO(37.554542717893675, 127.04083089088046), + new CreateRouteReqDTO(37.554553349811684, 127.0408054098912), + new CreateRouteReqDTO(37.554563981724215, 127.04077992889464) + ); + + Node node1 = NodeFixture.createNode(37.554533318727884, 127.040754103711); + Node node2 = NodeFixture.createNode(0, 0); + List savedNode = nodeRepository.saveAll(List.of(node1, node2)); + + routeRepository.save(RouteFixture.createRoute(savedNode.get(0), savedNode.get(1))); + + doNothing().when(mapClient).fetchHeights(anyList()); + + // when + mapService.createRoute(1001L, new CreateRoutesReqDTO(savedNode.get(0).getId(), null, requests)); + + // then + List savedNodes = nodeRepository.findAll(); + + assertThat(savedNodes).hasSize(23); + + int coreCount = 0; + + for(Node n : savedNodes){ + if(n.isCore()){ + coreCount++; + } + } + + assertThat(coreCount).isEqualTo(1); + } + + + } + + +} diff --git a/uniro_backend/src/test/java/com/softeer5/uniro_backend/map/service/RouteCalculatorTest.java b/uniro_backend/src/test/java/com/softeer5/uniro_backend/map/service/RouteCalculatorTest.java new file mode 100644 index 0000000..4ba1a26 --- /dev/null +++ b/uniro_backend/src/test/java/com/softeer5/uniro_backend/map/service/RouteCalculatorTest.java @@ -0,0 +1,285 @@ +package com.softeer5.uniro_backend.map.service; + +import com.softeer5.uniro_backend.building.entity.Building; +import com.softeer5.uniro_backend.building.repository.BuildingRepository; +import com.softeer5.uniro_backend.fixture.NodeFixture; +import com.softeer5.uniro_backend.fixture.RouteFixture; +import com.softeer5.uniro_backend.map.dto.response.FastestRouteResDTO; +import com.softeer5.uniro_backend.map.entity.Node; +import com.softeer5.uniro_backend.map.entity.Route; +import com.softeer5.uniro_backend.map.enums.CautionFactor; +import com.softeer5.uniro_backend.map.enums.DangerFactor; +import com.softeer5.uniro_backend.map.repository.NodeRepository; +import com.softeer5.uniro_backend.map.repository.RouteRepository; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.SqlGroup; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.ArrayList; +import java.util.List; + +import static com.softeer5.uniro_backend.common.constant.UniroConst.BUILDING_ROUTE_DISTANCE; +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Testcontainers +@ActiveProfiles("test") +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Transactional +class RouteCalculatorTest { + @Autowired + MapService mapService; + @Autowired + NodeRepository nodeRepository; + @Autowired + BuildingRepository buildingRepository; + @Autowired + RouteRepository routeRepository; + + private final double epsilon = 0.0000001; + + @Nested + @SqlGroup({ + @Sql(value = "/sql/delete-all-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD), + @Sql(value = "/sql/delete-all-data.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) + }) + class 길찾기 { + @Test + void 기본_길찾기() { + Node n1 = NodeFixture.createNode(0, 0); + Node n2 = NodeFixture.createNode(0, 0); + Node n3 = NodeFixture.createNode(0, 0); + Node n4 = NodeFixture.createNode(0, 0); + Node n5 = NodeFixture.createNode(0, 0); + Node n6 = NodeFixture.createNode(0, 0); + Node n7 = NodeFixture.createNode(0, 0); + Node n8 = NodeFixture.createNode(0, 0); + + Route r1 = RouteFixture.createRouteWithDistance(n1, n2, BUILDING_ROUTE_DISTANCE); + Route r2 = RouteFixture.createRouteWithDistance(n2, n3, 15); + Route r3 = RouteFixture.createRouteWithDistance(n3, n7, 1); + Route r4 = RouteFixture.createRouteWithDistance(n3, n4, 6); + Route r5 = RouteFixture.createRouteWithDistance(n4, n5, BUILDING_ROUTE_DISTANCE); + Route r6 = RouteFixture.createRouteWithDistance(n5, n6, BUILDING_ROUTE_DISTANCE); + Route r7 = RouteFixture.createRouteWithDistance(n6, n7, 18); + Route r8 = RouteFixture.createRouteWithDistance(n7, n8, 3); + Route r9 = RouteFixture.createRouteWithDistance(n8, n1, BUILDING_ROUTE_DISTANCE); + + Building b1 = Building.builder().nodeId(1L).univId(1001L).build(); + Building b5 = Building.builder().nodeId(5L).univId(1001L).build(); + + nodeRepository.saveAll(List.of(n1, n2, n3, n4, n5, n6, n7, n8)); + routeRepository.saveAll(List.of(r1, r2, r3, r4, r5, r6, r7, r8, r9)); + buildingRepository.saveAll(List.of(b1, b5)); + + List fastestRoute = mapService.findFastestRoute(1001L, 1L, 5L); + + + //a. distance 확안 + + //a-1. 보도, 휠체어안전, 휠체어위험 모두 가능한지? + assertThat(fastestRoute).hasSize(3); + + //a-2. distance가 정상적으로 제공되는지? + double distance0 = fastestRoute.get(0).getTotalDistance(); + assertThat(Math.abs(distance0 - 10.0)).isLessThan(epsilon); + double distance1 = fastestRoute.get(1).getTotalDistance(); + assertThat(Math.abs(distance1 - 10.0)).isLessThan(epsilon); + double distance2 = fastestRoute.get(2).getTotalDistance(); + assertThat(Math.abs(distance2 - 10.0)).isLessThan(epsilon); + + //b. cost 확인 + + //b.1- PEDES일때 휠체어, 전동휠체어의 cost가 정상적으로 null이 나왔는지? + assertThat(fastestRoute.get(0).getElectricTotalCost()).isNull(); + assertThat(fastestRoute.get(0).getManualTotalCost()).isNull(); + + //b-2- WHEEL_FAST, WHEEL_SAFE일 때 도보의 cost가 정상적으로 null이 나왔는지? + assertThat(fastestRoute.get(1).getPedestrianTotalCost()).isNull(); + assertThat(fastestRoute.get(1).getPedestrianTotalCost()).isNull(); + assertThat(fastestRoute.get(2).getPedestrianTotalCost()).isNull(); + assertThat(fastestRoute.get(2).getPedestrianTotalCost()).isNull(); + + } + + @Test + void 주의요소가_포함된_길찾기() { + Node n1 = NodeFixture.createNode(0, 0); + Node n2 = NodeFixture.createNode(0, 0); + Node n3 = NodeFixture.createNode(0, 0); + Node n4 = NodeFixture.createNode(0, 0); + Node n5 = NodeFixture.createNode(0, 0); + Node n6 = NodeFixture.createNode(0, 0); + Node n7 = NodeFixture.createNode(0, 0); + Node n8 = NodeFixture.createNode(0, 0); + + Route r1 = RouteFixture.createRouteWithDistance(n1, n2, BUILDING_ROUTE_DISTANCE); + Route r2 = RouteFixture.createRouteWithDistance(n2, n3, 14); + Route r3 = RouteFixture.createRouteWithDistance(n3, n7, 1); + Route r4 = RouteFixture.createRouteWithDistance(n3, n4, 6); + Route r5 = RouteFixture.createRouteWithDistance(n4, n5, BUILDING_ROUTE_DISTANCE); + Route r6 = RouteFixture.createRouteWithDistance(n5, n6, BUILDING_ROUTE_DISTANCE); + Route r7 = RouteFixture.createRouteWithDistance(n6, n7, 18); + Route r8 = RouteFixture.createRouteWithDistance(n7, n8, 3); + Route r9 = RouteFixture.createRouteWithDistance(n8, n1, BUILDING_ROUTE_DISTANCE); + + Building b1 = Building.builder().nodeId(1L).univId(1001L).build(); + Building b5 = Building.builder().nodeId(5L).univId(1001L).build(); + + List cautionFactorsList = new ArrayList<>(); + cautionFactorsList.add(CautionFactor.CRACK); + r3.setCautionFactorsByList(cautionFactorsList); + + nodeRepository.saveAll(List.of(n1, n2, n3, n4, n5, n6, n7, n8)); + routeRepository.saveAll(List.of(r1, r2, r3, r4, r5, r6, r7, r8, r9)); + buildingRepository.saveAll(List.of(b1, b5)); + + List fastestRoute = mapService.findFastestRoute(1001L, 1L, 5L); + + //a. distance 확안 + + //a-1. 보도, 휠체어위험 모두 가능한지? + assertThat(fastestRoute).hasSize(3); + + //a-2. distance가 정상적으로 제공되는지? + double distance0 = fastestRoute.get(0).getTotalDistance(); + assertThat(Math.abs(distance0 - 10.0)).isLessThan(epsilon); + double distance1 = fastestRoute.get(1).getTotalDistance(); + assertThat(Math.abs(distance1 - 10.0)).isLessThan(epsilon); + double distance2 = fastestRoute.get(2).getTotalDistance(); + assertThat(Math.abs(distance2 - 20.0)).isLessThan(epsilon); + + //b. cost 확인 + + //b.1- PEDES일때 휠체어, 전동휠체어의 cost가 정상적으로 null이 나왔는지? + assertThat(fastestRoute.get(0).getElectricTotalCost()).isNull(); + assertThat(fastestRoute.get(0).getManualTotalCost()).isNull(); + + //b-2- WHEEL_FAST, WHEEL_SAFE일 때 도보의 cost가 정상적으로 null이 나왔는지? + assertThat(fastestRoute.get(1).getPedestrianTotalCost()).isNull(); + assertThat(fastestRoute.get(1).getPedestrianTotalCost()).isNull(); + assertThat(fastestRoute.get(2).getPedestrianTotalCost()).isNull(); + assertThat(fastestRoute.get(2).getPedestrianTotalCost()).isNull(); + + } + + @Test + void 위험요소가_포함된_길찾기() { + Node n1 = NodeFixture.createNode(0, 0); + Node n2 = NodeFixture.createNode(0, 0); + Node n3 = NodeFixture.createNode(0, 0); + Node n4 = NodeFixture.createNode(0, 0); + Node n5 = NodeFixture.createNode(0, 0); + Node n6 = NodeFixture.createNode(0, 0); + Node n7 = NodeFixture.createNode(0, 0); + Node n8 = NodeFixture.createNode(0, 0); + + Route r1 = RouteFixture.createRouteWithDistance(n1, n2, BUILDING_ROUTE_DISTANCE); + Route r2 = RouteFixture.createRouteWithDistance(n2, n3, 15); + Route r3 = RouteFixture.createRouteWithDistance(n3, n7, 1); + Route r4 = RouteFixture.createRouteWithDistance(n3, n4, 6); + Route r5 = RouteFixture.createRouteWithDistance(n4, n5, BUILDING_ROUTE_DISTANCE); + Route r6 = RouteFixture.createRouteWithDistance(n5, n6, BUILDING_ROUTE_DISTANCE); + Route r7 = RouteFixture.createRouteWithDistance(n6, n7, 18); + Route r8 = RouteFixture.createRouteWithDistance(n7, n8, 3); + Route r9 = RouteFixture.createRouteWithDistance(n8, n1, BUILDING_ROUTE_DISTANCE); + + Building b1 = Building.builder().nodeId(1L).univId(1001L).build(); + Building b5 = Building.builder().nodeId(5L).univId(1001L).build(); + + List dangerFactorsList = new ArrayList<>(); + dangerFactorsList.add(DangerFactor.ETC); + r2.setDangerFactorsByList(dangerFactorsList); + r4.setDangerFactorsByList(dangerFactorsList); + + nodeRepository.saveAll(List.of(n1, n2, n3, n4, n5, n6, n7, n8)); + routeRepository.saveAll(List.of(r1, r2, r3, r4, r5, r6, r7, r8, r9)); + buildingRepository.saveAll(List.of(b1, b5)); + + List fastestRoute = mapService.findFastestRoute(1001L, 1L, 5L); + + + //a. distance 확안 + + //a-1. 보도, 휠체어 위험으로 가능한지? + assertThat(fastestRoute).hasSize(3); + + //a-2. distance가 정상적으로 제공되는지? + double distance0 = fastestRoute.get(0).getTotalDistance(); + assertThat(Math.abs(distance0 - 10.0)).isLessThan(epsilon); + double distance1 = fastestRoute.get(1).getTotalDistance(); + assertThat(Math.abs(distance1 - 21.0)).isLessThan(epsilon); + double distance2 = fastestRoute.get(2).getTotalDistance(); + assertThat(Math.abs(distance2 - 21.0)).isLessThan(epsilon); + + //b. cost 확인 + + //b.1- PEDES일때 휠체어, 전동휠체어의 cost가 정상적으로 null이 나왔는지? + assertThat(fastestRoute.get(0).getElectricTotalCost()).isNull(); + assertThat(fastestRoute.get(0).getManualTotalCost()).isNull(); + + //b-2- WHEEL_FAST일 때 도보의 cost가 정상적으로 null이 나왔는지? + assertThat(fastestRoute.get(1).getPedestrianTotalCost()).isNull(); + assertThat(fastestRoute.get(1).getPedestrianTotalCost()).isNull(); + assertThat(fastestRoute.get(2).getPedestrianTotalCost()).isNull(); + assertThat(fastestRoute.get(2).getPedestrianTotalCost()).isNull(); + + } + + @Test + void 위험요소를_지나지_않으면_도착할_수_없는_길찾기() { + Node n1 = NodeFixture.createNode(0, 0); + Node n2 = NodeFixture.createNode(0, 0); + Node n3 = NodeFixture.createNode(0, 0); + Node n4 = NodeFixture.createNode(0, 0); + Node n5 = NodeFixture.createNode(0, 0); + Node n6 = NodeFixture.createNode(0, 0); + Node n7 = NodeFixture.createNode(0, 0); + Node n8 = NodeFixture.createNode(0, 0); + + Route r1 = RouteFixture.createRouteWithDistance(n1, n2, BUILDING_ROUTE_DISTANCE); + Route r2 = RouteFixture.createRouteWithDistance(n2, n3, 15); + Route r3 = RouteFixture.createRouteWithDistance(n3, n7, 1); + Route r4 = RouteFixture.createRouteWithDistance(n3, n4, 6); + Route r5 = RouteFixture.createRouteWithDistance(n4, n5, BUILDING_ROUTE_DISTANCE); + Route r6 = RouteFixture.createRouteWithDistance(n5, n6, BUILDING_ROUTE_DISTANCE); + Route r7 = RouteFixture.createRouteWithDistance(n6, n7, 18); + Route r8 = RouteFixture.createRouteWithDistance(n7, n8, 3); + Route r9 = RouteFixture.createRouteWithDistance(n8, n1, BUILDING_ROUTE_DISTANCE); + + Building b1 = Building.builder().nodeId(1L).univId(1001L).build(); + Building b5 = Building.builder().nodeId(5L).univId(1001L).build(); + + List dangerFactorsList = new ArrayList<>(); + dangerFactorsList.add(DangerFactor.ETC); + r2.setDangerFactorsByList(dangerFactorsList); + r8.setDangerFactorsByList(dangerFactorsList); + + nodeRepository.saveAll(List.of(n1, n2, n3, n4, n5, n6, n7, n8)); + routeRepository.saveAll(List.of(r1, r2, r3, r4, r5, r6, r7, r8, r9)); + buildingRepository.saveAll(List.of(b1, b5)); + + List fastestRoute = mapService.findFastestRoute(1001L, 1L, 5L); + + //a. distance 확안 + + //a-1. 보도로 가능한지? + + assertThat(fastestRoute).hasSize(1); + + double distance0 = fastestRoute.get(0).getTotalDistance(); + assertThat(Math.abs(distance0 - 10.0)).isLessThan(epsilon); + + } + + } + + +} \ No newline at end of file diff --git a/uniro_backend/src/test/java/com/softeer5/uniro_backend/map/test_repository/NodeTestRepository.java b/uniro_backend/src/test/java/com/softeer5/uniro_backend/map/test_repository/NodeTestRepository.java new file mode 100644 index 0000000..efd738e --- /dev/null +++ b/uniro_backend/src/test/java/com/softeer5/uniro_backend/map/test_repository/NodeTestRepository.java @@ -0,0 +1,12 @@ +package com.softeer5.uniro_backend.map.test_repository; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.softeer5.uniro_backend.map.entity.Node; + +public interface NodeTestRepository extends JpaRepository { + List findAllByCreatedAtAfter(LocalDateTime localDateTime); +} diff --git a/uniro_backend/src/test/java/com/softeer5/uniro_backend/map/test_repository/RouteTestRepository.java b/uniro_backend/src/test/java/com/softeer5/uniro_backend/map/test_repository/RouteTestRepository.java new file mode 100644 index 0000000..b842127 --- /dev/null +++ b/uniro_backend/src/test/java/com/softeer5/uniro_backend/map/test_repository/RouteTestRepository.java @@ -0,0 +1,8 @@ +package com.softeer5.uniro_backend.map.test_repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.softeer5.uniro_backend.map.entity.Route; + +public interface RouteTestRepository extends JpaRepository { +} diff --git a/uniro_backend/src/test/java/com/softeer5/uniro_backend/route/RouteCalculationServiceTest.java b/uniro_backend/src/test/java/com/softeer5/uniro_backend/route/RouteCalculationServiceTest.java deleted file mode 100644 index 73caf8f..0000000 --- a/uniro_backend/src/test/java/com/softeer5/uniro_backend/route/RouteCalculationServiceTest.java +++ /dev/null @@ -1,185 +0,0 @@ -package com.softeer5.uniro_backend.route; - -import static org.assertj.core.api.Assertions.*; - -import java.util.List; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.transaction.annotation.Transactional; - -import com.softeer5.uniro_backend.common.exception.custom.RouteCalculationException; -import com.softeer5.uniro_backend.common.exception.custom.SameStartAndEndPointException; -import com.softeer5.uniro_backend.fixture.NodeFixture; -import com.softeer5.uniro_backend.fixture.RouteFixture; -import com.softeer5.uniro_backend.node.entity.Node; -import com.softeer5.uniro_backend.node.repository.NodeRepository; -import com.softeer5.uniro_backend.route.dto.request.CreateRouteReqDTO; -import com.softeer5.uniro_backend.route.entity.Route; -import com.softeer5.uniro_backend.route.repository.RouteRepository; -import com.softeer5.uniro_backend.route.service.RouteCalculationService; - -@SpringBootTest -@ActiveProfiles("test") -@Transactional -class RouteCalculationServiceTest { - - @Autowired - private RouteCalculationService routeCalculationService; - @Autowired - private RouteRepository routeRepository; - @Autowired - private NodeRepository nodeRepository; - - @Nested - class 경로추가{ - @Test - void 기본_경로_생성() { - // Given - Long univId = 1001L; - List requests = List.of( - new CreateRouteReqDTO(0, 0), - new CreateRouteReqDTO(1, 1), - new CreateRouteReqDTO(2, 2) - ); - - Node node0 = NodeFixture.createNode(-1, 0); - Node node1 = NodeFixture.createNode(0, 0); - Node node2 = NodeFixture.createNode(1, 0); - - Route route0 = RouteFixture.createRoute(node0, node1); - Route route = RouteFixture.createRoute(node1, node2); - - nodeRepository.saveAll(List.of(node0, node1, node2)); - routeRepository.saveAll(List.of(route0, route)); - - // When - List result = routeCalculationService.checkRouteCross(univId, node1.getId(), null, requests); - - // Then - assertThat(result).hasSize(3); - assertThat(result.get(0).isCore()).isTrue(); - } - - @Test - @DisplayName("wiki 페이지 TC.2") - void 기존_경로와_겹치는_경우_예외발생() { - // Given - Long univId = 1001L; - List requests = List.of( - new CreateRouteReqDTO(0, 0), - new CreateRouteReqDTO(1, 1), - new CreateRouteReqDTO(2, 2) - ); - - Node node1 = NodeFixture.createNode(0, 0); - Node node2 = NodeFixture.createNode(1.5, 1.5); - Route route = RouteFixture.createRoute(node1, node2); - - nodeRepository.saveAll(List.of(node1, node2)); - routeRepository.save(route); - - // when, then - assertThatThrownBy(() -> routeCalculationService.checkRouteCross(univId, node1.getId(), null, requests)) - .isInstanceOf(RouteCalculationException.class) - .hasMessageContaining("intersection is only allowed by point"); - } - - @Test - void 연결된_간선이_3개가_된_경우_시작노드가_서브에서_코어노드로_변경된다() { - // Given - Long univId = 1001L; - List requests = List.of( - new CreateRouteReqDTO(0, 0), - new CreateRouteReqDTO(3, 3), - new CreateRouteReqDTO(5, 3) - ); - - Node node0 = NodeFixture.createNode(0, -1); - Node node1 = NodeFixture.createNode(0, 0); - Node node2 = NodeFixture.createNode(0, 1); - Node node3 = NodeFixture.createNode(0, 2); - Node node4 = NodeFixture.createNode(0, 3); - - Route route0 = RouteFixture.createRoute(node0, node1); - Route route1 = RouteFixture.createRoute(node1, node2); - Route route2 = RouteFixture.createRoute(node2, node3); - Route route3 = RouteFixture.createRoute(node3, node4); - - nodeRepository.saveAll(List.of(node0, node1, node2, node3, node4)); - routeRepository.saveAll(List.of(route0, route1, route2, route3)); - - // When - List result = routeCalculationService.checkRouteCross(univId, node1.getId(), null, requests); - - // Then - assertThat(result).hasSize(3); - assertThat(result.get(0).isCore()).isTrue(); - } - - @Test - void 출발점과_도착점이_같은_경우_예외처리() { - // Given - Long univId = 1001L; - List requests = List.of( - new CreateRouteReqDTO(0, 0), - new CreateRouteReqDTO(1, 1), - new CreateRouteReqDTO(0, 0) - ); - - Node node1 = NodeFixture.createNode(0, 0); - Node node2 = NodeFixture.createNode(0, 1.5); - Route route = RouteFixture.createRoute(node1, node2); - - nodeRepository.saveAll(List.of(node1, node2)); - routeRepository.save(route); - - // when, Then - assertThatThrownBy(() -> routeCalculationService.checkRouteCross(univId, node1.getId(), null, requests)) - .isInstanceOf(SameStartAndEndPointException.class); - - } - - @Test - @DisplayName("wiki 페이지 TC.3") - void 기존에_존재하는_노드와_연결할_경우_코어노드가_될_수_있다() { - // Given - Long univId = 1001L; - List requests = List.of( - new CreateRouteReqDTO(0, 0), - new CreateRouteReqDTO(2, 1), - new CreateRouteReqDTO(2, 2), - new CreateRouteReqDTO(0, 2), - new CreateRouteReqDTO(-1, 2) - ); - - Node node1 = NodeFixture.createNode(0, 0); - Node node2 = NodeFixture.createNode(0, 1); - Node node3 = NodeFixture.createNode(0, 2); - Node node3_1 = NodeFixture.createNode(0, 3); - - Route route1 = RouteFixture.createRoute(node1, node2); - Route route2 = RouteFixture.createRoute(node2, node3); - Route route2_1 = RouteFixture.createRoute(node3, node3_1); - - - nodeRepository.saveAll(List.of(node1, node2, node3, node3_1)); - routeRepository.saveAll(List.of(route1, route2, route2_1)); - - // When - List result = routeCalculationService.checkRouteCross(univId, node1.getId(), null, requests); - - // Then - assertThat(result).hasSize(5); - assertThat(result.get(0).isCore()).isFalse(); - assertThat(result.get(3).isCore()).isTrue(); - } - - } - - -} diff --git a/uniro_backend/src/test/resources/sql/delete-all-data.sql b/uniro_backend/src/test/resources/sql/delete-all-data.sql new file mode 100644 index 0000000..fc4bb84 --- /dev/null +++ b/uniro_backend/src/test/resources/sql/delete-all-data.sql @@ -0,0 +1,21 @@ +-- 외래 키 제약 조건 비활성화 +SET FOREIGN_KEY_CHECKS = 0; + +-- 모든 데이터 삭제 +DELETE FROM `uniro-test`.building; +DELETE FROM `uniro-test`.node; +DELETE FROM `uniro-test`.rev_info; +DELETE FROM `uniro-test`.node_aud; +DELETE FROM `uniro-test`.route; +DELETE FROM `uniro-test`.route_aud; +DELETE FROM `uniro-test`.univ; + +-- AUTO_INCREMENT 시퀀스 초기화 +ALTER TABLE `uniro-test`.building AUTO_INCREMENT = 1; +ALTER TABLE `uniro-test`.node AUTO_INCREMENT = 1; +ALTER TABLE `uniro-test`.rev_info AUTO_INCREMENT = 1; +ALTER TABLE `uniro-test`.route AUTO_INCREMENT = 1; +ALTER TABLE `uniro-test`.univ AUTO_INCREMENT = 1; + +-- 외래 키 제약 조건 활성화 +SET FOREIGN_KEY_CHECKS = 1; diff --git a/uniro_frontend/package-lock.json b/uniro_frontend/package-lock.json index 81f3d3e..8aea922 100644 --- a/uniro_frontend/package-lock.json +++ b/uniro_frontend/package-lock.json @@ -17,7 +17,6 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7.1.3", - "react-spring-bottom-sheet": "^3.4.1", "tailwindcss": "^4.0.0", "zustand": "^5.0.3" }, @@ -280,18 +279,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", - "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", @@ -1016,12 +1003,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@juggle/resize-observer": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz", - "integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==", - "license": "Apache-2.0" - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2046,12 +2027,6 @@ "@types/react": "^18.0.0" } }, - "node_modules/@types/warning": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", - "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.21.0.tgz", @@ -2523,12 +2498,6 @@ "dev": true, "license": "MIT" }, - "node_modules/body-scroll-lock": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz", - "integrity": "sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg==", - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3625,15 +3594,6 @@ "dev": true, "license": "ISC" }, - "node_modules/focus-trap": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-6.9.4.tgz", - "integrity": "sha512-v2NTsZe2FF59Y+sDykKY+XjqZ0cPfhq/hikWVL88BqLivnNiEffAsac6rP6H45ff9wG9LL5ToiDqrLEP9GX9mw==", - "license": "MIT", - "dependencies": { - "tabbable": "^5.3.3" - } - }, "node_modules/for-each": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.4.tgz", @@ -4959,6 +4919,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5298,6 +5259,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -5365,6 +5327,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, "license": "MIT" }, "node_modules/react-refresh": { @@ -5401,127 +5364,6 @@ } } }, - "node_modules/react-spring": { - "version": "8.0.27", - "resolved": "https://registry.npmjs.org/react-spring/-/react-spring-8.0.27.tgz", - "integrity": "sha512-nDpWBe3ZVezukNRandTeLSPcwwTMjNVu1IDq9qA/AMiUqHuRN4BeSWvKr3eIxxg1vtiYiOLy4FqdfCP5IoP77g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.3.1", - "prop-types": "^15.5.8" - }, - "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0" - } - }, - "node_modules/react-spring-bottom-sheet": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/react-spring-bottom-sheet/-/react-spring-bottom-sheet-3.4.1.tgz", - "integrity": "sha512-yDFqiPMm/fjefjnOe6Q9zxccbCl6HMUKsK5bWgfGHJIj4zmXVKio5d4icQvmOLuwpuCA2pwv4J6nGWS6fUZidQ==", - "license": "MIT", - "dependencies": { - "@juggle/resize-observer": "^3.2.0", - "@reach/portal": "^0.13.0", - "@xstate/react": "^1.2.0", - "body-scroll-lock": "^3.1.5", - "focus-trap": "^6.2.2", - "react-spring": "^8.0.27", - "react-use-gesture": "^8.0.1", - "xstate": "^4.15.1" - }, - "peerDependencies": { - "react": "^16.14.0 || 17 || 18" - } - }, - "node_modules/react-spring-bottom-sheet/node_modules/@reach/portal": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@reach/portal/-/portal-0.13.2.tgz", - "integrity": "sha512-g74BnCdtuTGthzzHn2cWW+bcyIYb0iIE/yRsm89i8oNzNgpopbkh9UY8TPbhNlys52h7U60s4kpRTmcq+JqsTA==", - "license": "MIT", - "dependencies": { - "@reach/utils": "0.13.2", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "react": "^16.8.0 || 17.x", - "react-dom": "^16.8.0 || 17.x" - } - }, - "node_modules/react-spring-bottom-sheet/node_modules/@reach/portal/node_modules/@reach/utils": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@reach/utils/-/utils-0.13.2.tgz", - "integrity": "sha512-3ir6cN60zvUrwjOJu7C6jec/samqAeyAB12ZADK+qjnmQPdzSYldrFWwDVV5H0WkhbYXR3uh+eImu13hCetNPQ==", - "license": "MIT", - "dependencies": { - "@types/warning": "^3.0.0", - "tslib": "^2.1.0", - "warning": "^4.0.3" - }, - "peerDependencies": { - "react": "^16.8.0 || 17.x", - "react-dom": "^16.8.0 || 17.x" - } - }, - "node_modules/react-spring-bottom-sheet/node_modules/@xstate/react": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@xstate/react/-/react-1.6.3.tgz", - "integrity": "sha512-NCUReRHPGvvCvj2yLZUTfR0qVp6+apc8G83oXSjN4rl89ZjyujiKrTff55bze/HrsvCsP/sUJASf2n0nzMF1KQ==", - "license": "MIT", - "dependencies": { - "use-isomorphic-layout-effect": "^1.0.0", - "use-subscription": "^1.3.0" - }, - "peerDependencies": { - "@xstate/fsm": "^1.0.0", - "react": "^16.8.0 || ^17.0.0", - "xstate": "^4.11.0" - }, - "peerDependenciesMeta": { - "@xstate/fsm": { - "optional": true - }, - "xstate": { - "optional": true - } - } - }, - "node_modules/react-spring-bottom-sheet/node_modules/react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" - }, - "peerDependencies": { - "react": "17.0.2" - } - }, - "node_modules/react-spring-bottom-sheet/node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "node_modules/react-use-gesture": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/react-use-gesture/-/react-use-gesture-8.0.1.tgz", - "integrity": "sha512-CXzUNkulUdgouaAlvAsC5ZVo0fi9KGSBSk81WrE4kOIcJccpANe9zZkAYr5YZZhqpicIFxitsrGVS4wmoMun9A==", - "deprecated": "This package is no longer maintained. Please use @use-gesture/react instead", - "license": "MIT", - "peerDependencies": { - "react": ">= 16.8.0" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5545,12 +5387,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -6082,12 +5918,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/tabbable": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz", - "integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==", - "license": "MIT" - }, "node_modules/tailwindcss": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.0.tgz", @@ -6329,37 +6159,13 @@ "punycode": "^2.1.0" } }, - "node_modules/use-isomorphic-layout-effect": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz", - "integrity": "sha512-q6ayo8DWoPZT0VdG4u3D3uxcgONP3Mevx2i2b0434cwWBoL+aelL1DzkXI6w3PhTZzUeR2kaVlZn70iCiseP6w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-subscription": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/use-subscription/-/use-subscription-1.10.0.tgz", - "integrity": "sha512-ZRLhsMSjz01kBA8106zdzEjttJ20Rauscf0umwVRjaz8idRfoddOnAVH6VEBdu55eCq02L1g+j3NcleAar1aWw==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/use-sync-external-store": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", "license": "MIT", + "optional": true, + "peer": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -6450,15 +6256,6 @@ "vite": ">=2.6.0" } }, - "node_modules/warning": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", - "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6573,16 +6370,6 @@ "node": ">=0.10.0" } }, - "node_modules/xstate": { - "version": "4.38.3", - "resolved": "https://registry.npmjs.org/xstate/-/xstate-4.38.3.tgz", - "integrity": "sha512-SH7nAaaPQx57dx6qvfcIgqKRXIh4L0A1iYEqim4s1u7c9VoCgzZc+63FY90AKU4ZzOC2cfJzTnpO4zK7fCUzzw==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/xstate" - } - }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/uniro_frontend/package.json b/uniro_frontend/package.json index ef0e237..3e665f8 100644 --- a/uniro_frontend/package.json +++ b/uniro_frontend/package.json @@ -19,7 +19,6 @@ "react": "^18.3.1", "react-dom": "^18.3.1", "react-router": "^7.1.3", - "react-spring-bottom-sheet": "^3.4.1", "tailwindcss": "^4.0.0", "zustand": "^5.0.3" }, diff --git a/uniro_frontend/src/App.tsx b/uniro_frontend/src/App.tsx index df5068e..7278175 100644 --- a/uniro_frontend/src/App.tsx +++ b/uniro_frontend/src/App.tsx @@ -16,6 +16,8 @@ import useNetworkStatus from "./hooks/useNetworkStatus"; import ErrorPage from "./pages/error"; import { Suspense } from "react"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import ErrorBoundary from "./components/error/ErrorBoundary"; +import Errortest from "./pages/errorTest"; const queryClient = new QueryClient(); @@ -24,22 +26,25 @@ function App() { useNetworkStatus(); return ( - - - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - /** 에러 페이지 */ - } /> - } /> - - + }> + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + /** 에러 페이지 */ + } /> + } /> + } /> + + + ); diff --git a/uniro_frontend/src/api/nodes.ts b/uniro_frontend/src/api/nodes.ts index e553f3b..60c2f21 100644 --- a/uniro_frontend/src/api/nodes.ts +++ b/uniro_frontend/src/api/nodes.ts @@ -1,5 +1,7 @@ import { Building } from "../data/types/node"; import { getFetch } from "../utils/fetch/fetch"; +import { transformGetBuildings } from "./transformer/nodes"; +import { GetBuildingListResponse } from "./type/response/nodes"; export const getAllBuildings = ( univId: number, @@ -17,3 +19,7 @@ export const getAllBuildings = ( "right-down-lat": params.rightDownLat, }); }; + +export const getSearchBuildings = (univId: number, params: { name: string }): Promise => { + return getFetch(`/${univId}/nodes/buildings/search`, params).then(transformGetBuildings); +}; diff --git a/uniro_frontend/src/api/route.ts b/uniro_frontend/src/api/route.ts index c167382..e8dc8af 100644 --- a/uniro_frontend/src/api/route.ts +++ b/uniro_frontend/src/api/route.ts @@ -28,20 +28,20 @@ export const getSingleRouteRisk = ( routeId: RouteId, ): Promise<{ routeId: NodeId; - dangerTypes: IssueTypeKey[]; - cautionTypes: IssueTypeKey[]; + dangerFactors: IssueTypeKey[]; + cautionFactors: IssueTypeKey[]; }> => { return getFetch<{ routeId: NodeId; - dangerTypes: IssueTypeKey[]; - cautionTypes: IssueTypeKey[]; + dangerFactors: IssueTypeKey[]; + cautionFactors: IssueTypeKey[]; }>(`/${univId}/routes/${routeId}/risk`); }; export const postReport = ( univId: number, routeId: RouteId, - body: { dangerTypes: DangerIssueType[]; cautionTypes: CautionIssueType[] }, + body: { dangerFactors: DangerIssueType[]; cautionFactors: CautionIssueType[] }, ): Promise => { return postFetch(`/${univId}/route/risk/${routeId}`, body); }; diff --git a/uniro_frontend/src/api/search.ts b/uniro_frontend/src/api/search.ts index d7c47e9..86355ce 100644 --- a/uniro_frontend/src/api/search.ts +++ b/uniro_frontend/src/api/search.ts @@ -3,6 +3,6 @@ import { getFetch } from "../utils/fetch/fetch"; import { transformGetUniversityList } from "./transformer/search"; import { GetUniversityListResponse } from "./type/response/search"; -export const getUniversityList = (): Promise => { - return getFetch("/univ/search").then(transformGetUniversityList); +export const getUniversityList = (query: string): Promise => { + return getFetch("/univ/search", { name: query }).then(transformGetUniversityList); }; diff --git a/uniro_frontend/src/api/transformer/nodes.ts b/uniro_frontend/src/api/transformer/nodes.ts new file mode 100644 index 0000000..998db28 --- /dev/null +++ b/uniro_frontend/src/api/transformer/nodes.ts @@ -0,0 +1,6 @@ +import { Building } from "../../data/types/node"; +import { GetBuildingListResponse } from "../type/response/nodes"; + +export const transformGetBuildings = (res: GetBuildingListResponse): Building[] => { + return res.data; +}; diff --git a/uniro_frontend/src/api/transformer/route.ts b/uniro_frontend/src/api/transformer/route.ts index fbf6508..ffba53d 100644 --- a/uniro_frontend/src/api/transformer/route.ts +++ b/uniro_frontend/src/api/transformer/route.ts @@ -1,6 +1,5 @@ -import { DangerIssueType } from "../../constant/enum/reportEnum"; import { CoreRoutesList } from "../../data/types/route"; -import { GetAllRouteRepsonse, GetSingleRouteRiskResponse } from "../type/response/route"; +import { GetAllRouteRepsonse } from "../type/response/route"; export const transformAllRoutes = (data: GetAllRouteRepsonse): CoreRoutesList => { const { nodeInfos, coreRoutes } = data; diff --git a/uniro_frontend/src/api/type/response/nodes.d.ts b/uniro_frontend/src/api/type/response/nodes.d.ts new file mode 100644 index 0000000..da826d4 --- /dev/null +++ b/uniro_frontend/src/api/type/response/nodes.d.ts @@ -0,0 +1,7 @@ +import { Building } from "../../../data/types/node"; + +export type GetBuildingListResponse = { + data: Building[]; + nextCursor: number | null; + hasNext: boolean; +}; diff --git a/uniro_frontend/src/api/type/response/route.d.ts b/uniro_frontend/src/api/type/response/route.d.ts index 5120f82..e0a241c 100644 --- a/uniro_frontend/src/api/type/response/route.d.ts +++ b/uniro_frontend/src/api/type/response/route.d.ts @@ -18,6 +18,6 @@ export type GetAllRouteRepsonse = { export type GetSingleRouteRiskResponse = { routeId: NodeId; - dangerTypes?: IssueTypeKey[]; - cautionTypes?: IssueTypeKey[]; + dangerFactors?: IssueTypeKey[]; + cautionFactors?: IssueTypeKey[]; }; diff --git a/uniro_frontend/src/assets/icon/search.svg b/uniro_frontend/src/assets/icon/search.svg new file mode 100644 index 0000000..bae015e --- /dev/null +++ b/uniro_frontend/src/assets/icon/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/uniro_frontend/src/assets/undo.svg b/uniro_frontend/src/assets/undo.svg new file mode 100644 index 0000000..638b2a9 --- /dev/null +++ b/uniro_frontend/src/assets/undo.svg @@ -0,0 +1,3 @@ + + + diff --git a/uniro_frontend/src/component/NavgationMap.tsx b/uniro_frontend/src/component/NavgationMap.tsx index d585c08..f048791 100644 --- a/uniro_frontend/src/component/NavgationMap.tsx +++ b/uniro_frontend/src/component/NavgationMap.tsx @@ -4,7 +4,6 @@ import { CautionRoute, DangerRoute, NavigationRouteList } from "../data/types/ro import createAdvancedMarker from "../utils/markers/createAdvanedMarker"; import createMarkerElement from "../components/map/mapMarkers"; import { Markers } from "../constant/enum/markerEnum"; -import { Coord } from "../data/types/coord"; import useRoutePoint from "../hooks/useRoutePoint"; import { AdvancedMarker } from "../data/types/marker"; @@ -39,15 +38,56 @@ const NavigationMap = ({ style, routeResult, risks, isDetailView, topPadding = 0 if (routeResult.routes.length === 0) return; - const cautionFactor: Coord[] = []; - const { routes, routeDetails } = routeResult; + if (!origin || !destination) { + return; + } + + const startingBuildingPath = [{ lat: origin?.lat, lng: origin?.lng }, routes[0].node1]; + + const endingBuildingPath = [routes[routes.length - 1].node2, { lat: destination?.lat, lng: destination?.lng }]; + // 하나의 길 완성 const paths = [routes[0].node1, ...routes.map((el) => el.node2)]; const bounds = new google.maps.LatLngBounds(); + const dashSymbol = { + path: "M 0,-1 0,1", + strokeOpacity: 1, + scale: 3, + }; + + // 시작 building과 첫번째 점을 이어주는 polyline + new Polyline({ + path: startingBuildingPath, + map, + strokeOpacity: 0, + icons: [ + { + icon: dashSymbol, + offset: "0", + repeat: "20px", + }, + ], + geodesic: true, + }); + + // 마지막 building과 마지막 점을 이어주는 polyline + new Polyline({ + path: endingBuildingPath, + map, + strokeOpacity: 0, + icons: [ + { + icon: dashSymbol, + offset: "0", + repeat: "20px", + }, + ], + }); + new Polyline({ path: paths, map, @@ -55,18 +95,27 @@ const NavigationMap = ({ style, routeResult, risks, isDetailView, topPadding = 0 strokeWeight: 2.0, }); - // waypoint 마커 찍기 - routeDetails.forEach((routeDetail, index) => { + // [간선] 마커 찍기 + routeDetails.forEach((routeDetail) => { const { coordinates } = routeDetail; bounds.extend(coordinates); const markerElement = createMarkerElement({ type: Markers.WAYPOINT, className: "translate-waypoint", }); + // routeDetail에 cautionTypes가 있다면 [주의] 마커를 넣기 + if (routeDetail.cautionFactors && routeDetail.cautionFactors.length > 0) { + const markerElement = createMarkerElement({ + type: Markers.CAUTION, + className: "traslate-marker", + hasAnimation: true, + }); + createAdvancedMarker(AdvancedMarker, map, coordinates, markerElement); + } createAdvancedMarker(AdvancedMarker, map, coordinates, markerElement); }); - // 시작 마커는 출발지 빌딩 표시 + // [시작] 마커는 출발지 (건물 기준) const startMarkerElement = createMarkerElement({ type: Markers.ORIGIN, title: origin?.buildingName, @@ -78,7 +127,7 @@ const NavigationMap = ({ style, routeResult, risks, isDetailView, topPadding = 0 createAdvancedMarker(AdvancedMarker, map, originCoord, startMarkerElement); bounds.extend(originCoord); - // 끝 마커는 도착지 빌딩 표시 + // [끝] 마커는 도착지 빌딩 (건물 기준) const endMarkerElement = createMarkerElement({ type: Markers.DESTINATION, title: destination?.buildingName, @@ -86,33 +135,24 @@ const NavigationMap = ({ style, routeResult, risks, isDetailView, topPadding = 0 hasAnimation: true, }); - // 위험요소 마커 찍기 - // risks.dangerRoutes.forEach((route) => { - // const { node1, node2, dangerTypes } = route; - // const type = Markers.DANGER; - - // createAdvancedMarker( - // AdvancedMarker, - // map, - // new google.maps.LatLng({ - // lat: (node1.lat + node2.lat) / 2, - // lng: (node1.lng + node2.lng) / 2, - // }), - // createMarkerElement({ type }), - // ); - // }); - const { lat: destinationLat, lng: destinationLng }: google.maps.LatLngLiteral = destination!; const destinationCoord = { lat: destinationLat, lng: destinationLng }; createAdvancedMarker(AdvancedMarker, map, destinationCoord, endMarkerElement); bounds.extend(destinationCoord); - cautionFactor.forEach((coord) => { - const markerElement = createMarkerElement({ - type: Markers.CAUTION, - hasAnimation: true, - }); - createAdvancedMarker(AdvancedMarker, map, coord, markerElement); + // React Query Cache 혹은 API에서 불러온 [위험] 마커 찍기 + risks.dangerRoutes.forEach((route) => { + const { node1, node2 } = route; + const type = Markers.DANGER; + createAdvancedMarker( + AdvancedMarker, + map, + new google.maps.LatLng({ + lat: (node1.lat + node2.lat) / 2, + lng: (node1.lng + node2.lng) / 2, + }), + createMarkerElement({ type }), + ); }); boundsRef.current = bounds; @@ -141,7 +181,7 @@ const NavigationMap = ({ style, routeResult, risks, isDetailView, topPadding = 0 if (isDetailView) { const { routeDetails } = routeResult; markersRef.current = []; - + // [그림] 마커 찍기 routeDetails.forEach((routeDetail, index) => { const { coordinates } = routeDetail; const markerElement = createMarkerElement({ @@ -170,7 +210,7 @@ const NavigationMap = ({ style, routeResult, risks, isDetailView, topPadding = 0 }); markersRef.current = []; }; - }, [isDetailView, AdvancedMarker, map]); + }, [isDetailView, AdvancedMarker, map, routeResult]); return
; }; diff --git a/uniro_frontend/src/components/customInput.tsx b/uniro_frontend/src/components/customInput.tsx index e3248d3..15a508a 100644 --- a/uniro_frontend/src/components/customInput.tsx +++ b/uniro_frontend/src/components/customInput.tsx @@ -1,35 +1,44 @@ -import { ChangeEvent, InputHTMLAttributes, useCallback, useState } from "react"; +import { ChangeEvent, InputHTMLAttributes, useCallback, useEffect, useRef, useState } from "react"; import Search from "../../public/icons/search.svg?react"; import ChevronLeft from "../../public/icons/chevron-left.svg?react"; import Mic from "../../public/icons/mic.svg?react"; import Close from "../../public/icons/close-circle.svg?react"; interface CustomInputProps extends InputHTMLAttributes { - onLengthChange: (e: string) => void; + onChangeDebounce: (e: string) => void; placeholder: string; handleVoiceInput: () => void; } -export default function Input({ onLengthChange, placeholder, handleVoiceInput, ...rest }: CustomInputProps) { +export default function Input({ onChangeDebounce, placeholder, handleVoiceInput, ...rest }: CustomInputProps) { const [isFocus, setIsFocus] = useState(false); - const [tempValue, setTempValue] = useState(""); - const [currentValue, setCurrentValue] = useState(""); + const [value, setValue] = useState(""); + const timeOutRef = useRef(); const onFoucs = () => setIsFocus(true); const onBlur = () => setIsFocus(false); const resetInput = () => { - setCurrentValue(""); - setTempValue(""); + setValue(''); + handleDebounce(''); + }; + + const handleDebounce = (input: string) => { + if (timeOutRef.current) { + clearTimeout(timeOutRef.current); + } + + timeOutRef.current = setTimeout(() => { + onChangeDebounce(input); + timeOutRef.current = undefined; + }, 300); }; const handleChange = (e: ChangeEvent) => { const currentInput: string = e.target.value; - setTempValue(currentInput); - if (currentValue.length !== currentInput.length) { - setCurrentValue(currentInput); - onLengthChange(currentInput); - } + + setValue(currentInput); + handleDebounce(currentInput); }; return ( @@ -39,14 +48,14 @@ export default function Input({ onLengthChange, placeholder, handleVoiceInput, . {isFocus ? : } - {currentValue.length === 0 ? ( + {value.length === 0 ? ( diff --git a/uniro_frontend/src/components/error/ErrorBoundary.tsx b/uniro_frontend/src/components/error/ErrorBoundary.tsx new file mode 100644 index 0000000..9578751 --- /dev/null +++ b/uniro_frontend/src/components/error/ErrorBoundary.tsx @@ -0,0 +1,31 @@ +import React, { Component, ReactNode } from 'react'; + +interface ErrorBoundaryProps { + fallback: ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; +} + +export default class ErrorBoundary extends Component< + React.PropsWithChildren, + ErrorBoundaryState +> { + constructor(props: React.PropsWithChildren) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { hasError: true }; + } + + render() { + if (this.state.hasError) { + return this.props.fallback; + } + + return this.props.children; + } +} diff --git a/uniro_frontend/src/components/map/TopSheet.tsx b/uniro_frontend/src/components/map/TopSheet.tsx index a7c7b61..44f4c17 100644 --- a/uniro_frontend/src/components/map/TopSheet.tsx +++ b/uniro_frontend/src/components/map/TopSheet.tsx @@ -1,34 +1,84 @@ -import RouteInput from "../map/routeSearchInput"; +import SearchIcon from "../../assets/icon/search.svg?react"; import OriginIcon from "../../assets/map/origin.svg?react"; -import Destination from "../../assets/map/destination.svg?react"; -import LocationIcon from "../../../public/icons/location-thick.svg?react"; import SwitchIcon from "../../assets/switch.svg?react"; -import { Link } from "react-router"; -import { motion } from "framer-motion"; +import DestinationIcon from "../../assets/map/destination.svg?react"; +import LocationIcon from "../../assets/location-thin.svg?react"; +import ChevronLeft from "../../../public/icons/chevron-left.svg?react"; import useRoutePoint from "../../hooks/useRoutePoint"; import useSearchBuilding from "../../hooks/useSearchBuilding"; -import { RoutePoint } from "../../constant/enum/routeEnum"; +import AnimatedContainer from "../../container/animatedContainer"; +import { BuildingInput, RouteInput } from "./mapSearchInput"; -export default function TopSheet({ open }: { open: boolean }) { +interface MapTopSheetProps { + isVisible: boolean; +} + +export function MapTopBuildingSheet({ isVisible }: MapTopSheetProps) { + const { searchMode, setSearchMode } = useSearchBuilding(); + + return ( + + +
+ { }} + placeholder={`한양대학교 건물을 검색해보세요`} + > + + + +
+
+ ); +} + + +export function MapTopRouteSheet({ isVisible }: MapTopSheetProps) { const { origin, setOrigin, destination, setDestination, switchBuilding } = useRoutePoint(); - const { setMode } = useSearchBuilding(); + const { searchMode, setSearchMode } = useSearchBuilding(); + + const resetRoutePoint = () => { + setOrigin(undefined); + setDestination(undefined); + setSearchMode("BUILDING"); + } return ( - -

- - 한양대학교 -

+
+ +
setMode(RoutePoint.ORIGIN)} + onClick={() => { setSearchMode("ORIGIN") }} placeholder="출발지를 입력하세요" value={origin ? origin.buildingName : ""} onCancel={() => setOrigin(undefined)} @@ -36,12 +86,12 @@ export default function TopSheet({ open }: { open: boolean }) { setMode(RoutePoint.DESTINATION)} + onClick={() => { setSearchMode("DESTINATION") }} placeholder="도착지를 입력하세요" value={destination ? destination.buildingName : ""} onCancel={() => setDestination(undefined)} > - +
@@ -50,6 +100,6 @@ export default function TopSheet({ open }: { open: boolean }) {
-
+ ); } diff --git a/uniro_frontend/src/components/map/floatingButtons.tsx b/uniro_frontend/src/components/map/floatingButtons.tsx index 51d9f10..e3636cb 100644 --- a/uniro_frontend/src/components/map/floatingButtons.tsx +++ b/uniro_frontend/src/components/map/floatingButtons.tsx @@ -1,6 +1,6 @@ -import React from "react"; import DangerIcon from "../../assets/danger.svg?react"; import CautionIcon from "../../assets/caution.svg?react"; +import UndoIcon from "../../assets/undo.svg?react"; interface ToggleButtonProps { isActive: boolean; @@ -11,7 +11,7 @@ export function DangerToggleButton({ isActive, onClick }: ToggleButtonProps) { return ( @@ -22,9 +22,26 @@ export function CautionToggleButton({ isActive, onClick }: ToggleButtonProps) { return ( ); } + +interface UndoButtonProps { + onClick: () => void; + disabled: boolean; +} + +export function UndoButton({ onClick, disabled }: UndoButtonProps) { + return ( + + ); +} diff --git a/uniro_frontend/src/components/map/mapBottomSheet.tsx b/uniro_frontend/src/components/map/mapBottomSheet.tsx index aa376b2..c37ff48 100644 --- a/uniro_frontend/src/components/map/mapBottomSheet.tsx +++ b/uniro_frontend/src/components/map/mapBottomSheet.tsx @@ -2,58 +2,74 @@ import { SelectedMarkerTypes } from "../../pages/map"; import Button from "../customButton"; import Call from "/public/icons/call-thick.svg?react"; import Location from "/public/icons/location-thick.svg?react"; +import AnimatedContainer from "../../container/animatedContainer"; +import BottomSheetHandle from "../navigation/bottomSheet/bottomSheetHandle"; +import { useNavigationBottomSheet } from "../../hooks/useNavigationBottomSheet"; +import { RoutePointType } from "../../data/types/route"; +import { RoutePoint } from "../../constant/enum/routeEnum"; +import useSearchBuilding from "../../hooks/useSearchBuilding"; -interface MapBottomSheetFromListProps { - building: SelectedMarkerTypes; - buttonText: string; - onClick: () => void; +interface MapBottomSheetProps { + isVisible: boolean; + selectedMarker: SelectedMarkerTypes | undefined; + selectRoutePoint: (type?: RoutePointType) => void; } -export function MapBottomSheetFromList({ building, buttonText, onClick }: MapBottomSheetFromListProps) { - if (building.property === undefined) return; - - const { nodeId, lng, lat, buildingName, buildingImageUrl, phoneNumber, address } = building.property; +export default function MapBottomSheet({ isVisible, selectedMarker, selectRoutePoint }: MapBottomSheetProps) { + const { dragControls, scrollRef, preventScroll } = useNavigationBottomSheet(); return ( -
-
- -
-

{buildingName}

-

- - {address} -

-

- - {phoneNumber} -

-
+ + +
+ selectRoutePoint(RoutePoint.ORIGIN)} + onClickRight={() => selectRoutePoint(RoutePoint.DESTINATION)} + />
- -
+ ); } -interface MapBottomSheetProps { - building: SelectedMarkerTypes; +interface MapBottomSheetFromMarkerProps { + building: SelectedMarkerTypes | undefined; onClickLeft: () => void; onClickRight: () => void; } -export function MapBottomSheetFromMarker({ building, onClickLeft, onClickRight }: MapBottomSheetProps) { - if (building.property === undefined) return; +function MapBottomSheetFromMarker({ building, onClickLeft, onClickRight }: MapBottomSheetFromMarkerProps) { + if (!building || building.property === undefined) return; + + const { buildingName, buildingImageUrl, phoneNumber, address } = building.property; + + const { setSearchMode } = useSearchBuilding(); - const { nodeId, lng, lat, buildingName, buildingImageUrl, phoneNumber, address } = building.property; + const handlLeftClick = () => { + setSearchMode('ORIGIN'); + onClickLeft(); + } + + const handleRightClick = () => { + setSearchMode('DESTINATION'); + onClickRight(); + } return (
-

{buildingName}

+

{buildingName}

{address} @@ -65,13 +81,13 @@ export function MapBottomSheetFromMarker({ building, onClickLeft, onClickRight }

- -
); -} +} \ No newline at end of file diff --git a/uniro_frontend/src/components/map/mapMarkers.tsx b/uniro_frontend/src/components/map/mapMarkers.tsx index b44877f..41061b3 100644 --- a/uniro_frontend/src/components/map/mapMarkers.tsx +++ b/uniro_frontend/src/components/map/mapMarkers.tsx @@ -1,5 +1,6 @@ import { Markers } from "../../constant/enum/markerEnum"; import { MarkerTypes } from "../../data/types/enum"; +import { animate } from "framer-motion"; const markerImages = import.meta.glob("/src/assets/markers/*.svg", { eager: true }); @@ -37,6 +38,70 @@ function createTextElement(type: MarkerTypes, title: string): HTMLElement { } } +function createAnimatedTextElement(type: MarkerTypes, titles: string[]): HTMLElement { + const titleContainer = document.createElement("div"); + + const elements = []; + + const _title = [...titles, titles[0]]; + + for (const title of _title) { + const factorTitle = document.createElement("p"); + factorTitle.innerText = title; + factorTitle.className = "block w-[128px] h-[22px] mb-4"; + + titleContainer.appendChild(factorTitle); + elements.push(factorTitle); + } + + switch (type) { + case Markers.CAUTION: + titleContainer.className = + "overflow-hidden w-[160px] h-[38px] py-2 px-4 mb-2 text-kor-body3 font-semibold text-gray-100 bg-system-orange text-center rounded-200"; + break; + case Markers.DANGER: + titleContainer.className = + "overflow-hidden w-[160px] h-[38px] py-2 px-4 mb-2 text-kor-body3 font-semibold text-gray-100 bg-system-red text-center rounded-200"; + break; + default: + break; + } + + const len = _title.length; + + if (len >= 3) { + for (let i = 1; i < len; i++) { + elements.forEach((el, index) => { + setTimeout(() => { + animate( + el, + { + y: [-38 * (i - 1), -38 * i], + }, + { + duration: 0.5, + }, + ); + + if (i === len - 1) { + setTimeout(() => { + animate( + el, + { + y: [-38 * (len - 1), 0], + }, + { duration: 0 }, + ); + }, 1000); + } + }, 1000 * i); + }); + } + } + + return titleContainer; +} + function getImage(type: MarkerTypes): string { return (markerImages[`/src/assets/markers/${type}.svg`] as { default: string })?.default; } @@ -99,7 +164,7 @@ export default function createMarkerElement({ }: { type: MarkerTypes; className?: string; - title?: string; + title?: string | string[]; hasTopContent?: boolean; hasAnimation?: boolean; number?: number; @@ -113,13 +178,14 @@ export default function createMarkerElement({ const markerImage = createImageElement(type); if (title) { - const markerTitle = createTextElement(type, title); if (hasTopContent) { + const markerTitle = createAnimatedTextElement(type, title as string[]); container.appendChild(markerTitle); container.appendChild(markerImage); return attachAnimation(container, hasAnimation); } + const markerTitle = createTextElement(type, title as string); container.appendChild(markerImage); container.appendChild(markerTitle); return attachAnimation(container, hasAnimation); diff --git a/uniro_frontend/src/components/map/routeSearchInput.tsx b/uniro_frontend/src/components/map/mapSearchInput.tsx similarity index 60% rename from uniro_frontend/src/components/map/routeSearchInput.tsx rename to uniro_frontend/src/components/map/mapSearchInput.tsx index 152787f..5e6cd50 100644 --- a/uniro_frontend/src/components/map/routeSearchInput.tsx +++ b/uniro_frontend/src/components/map/mapSearchInput.tsx @@ -10,7 +10,24 @@ interface RouteInputProps extends InputHTMLAttributes { onClick: () => void; } -export default function RouteInput({ placeholder, children, value, onCancel, onClick }: RouteInputProps) { +export function BuildingInput({ placeholder, children, onClick }: RouteInputProps) { + return ( +
+ {children} + + {placeholder} + +
+ ); +} + +export function RouteInput({ placeholder, children, value, onCancel, onClick }: RouteInputProps) { return (
); -} +} \ No newline at end of file diff --git a/uniro_frontend/src/components/map/reportButton.tsx b/uniro_frontend/src/components/map/reportButton.tsx index f23a3a6..29ca35e 100644 --- a/uniro_frontend/src/components/map/reportButton.tsx +++ b/uniro_frontend/src/components/map/reportButton.tsx @@ -5,7 +5,7 @@ export default function ReportButton({ ...rest }: ButtonHTMLAttributes 제보하기 diff --git a/uniro_frontend/src/components/navigation/navigationDescription.tsx b/uniro_frontend/src/components/navigation/navigationDescription.tsx index 85ad9ee..e1c5065 100644 --- a/uniro_frontend/src/components/navigation/navigationDescription.tsx +++ b/uniro_frontend/src/components/navigation/navigationDescription.tsx @@ -9,6 +9,8 @@ import { NavigationRouteList } from "../../data/types/route"; import useRoutePoint from "../../hooks/useRoutePoint"; import { formatDistance } from "../../utils/navigation/formatDistance"; import { Link } from "react-router"; +import useUniversityInfo from "../../hooks/useUniversityInfo"; +import { useQueryClient } from "@tanstack/react-query"; const TITLE = "전동휠체어 예상소요시간"; @@ -21,12 +23,20 @@ const NavigationDescription = ({ isDetailView, navigationRoute }: TopBarProps) = const { origin, destination } = useRoutePoint(); const { totalCost, totalDistance, hasCaution } = navigationRoute; + const { university } = useUniversityInfo(); + const queryClient = useQueryClient(); + + /** 지도 페이지로 돌아가게 될 경우, 캐시를 삭제하기 */ + const removeQuery = () => { + queryClient.removeQueries({ queryKey: ['fastRoute', university?.id, origin?.nodeId, destination?.nodeId] }) + } + return (
{TITLE} {!isDetailView && ( - + )} diff --git a/uniro_frontend/src/components/navigation/route/routeCard.tsx b/uniro_frontend/src/components/navigation/route/routeCard.tsx index 2ea7487..1573093 100644 --- a/uniro_frontend/src/components/navigation/route/routeCard.tsx +++ b/uniro_frontend/src/components/navigation/route/routeCard.tsx @@ -4,10 +4,10 @@ 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 { Building } from "../../../data/types/node"; import { RouteDetail } from "../../../data/types/route"; import useRoutePoint from "../../../hooks/useRoutePoint"; import { formatDistance } from "../../../utils/navigation/formatDistance"; +import { CautionIssue } from "../../../constant/enum/reportEnum"; const NumberIcon = ({ index }: { index: number }) => { return ( @@ -17,18 +17,8 @@ const NumberIcon = ({ index }: { index: number }) => { ); }; -export const RouteCard = ({ - index, - route, - originBuilding, - destinationBuilding, -}: { - index: number; - route: RouteDetail; - originBuilding: Building; - destinationBuilding: Building; -}) => { - const { dist: distance, directionType } = route; +export const RouteCard = ({ index, route }: { index: number; route: RouteDetail }) => { + const { dist: distance, directionType, cautionFactors } = route; const formattedDistance = formatDistance(distance); const { origin, destination } = useRoutePoint(); switch (directionType.toLocaleLowerCase()) { @@ -67,7 +57,7 @@ export const RouteCard = ({
-
우회전
+
급격한 우회전
); @@ -93,7 +83,7 @@ export const RouteCard = ({
-
좌회전
+
급격한 좌회전
); @@ -144,8 +134,15 @@ export const RouteCard = ({
{formattedDistance}
+ {/* TODO: Auto Resize Text 적용 */} -
+
+ {cautionFactors && cautionFactors.length > 0 + ? cautionFactors + .map((factor) => CautionIssue[factor as keyof typeof CautionIssue]) + .join(", ") + : "주의 요소가 없습니다."} +
); diff --git a/uniro_frontend/src/components/navigation/route/routeList.tsx b/uniro_frontend/src/components/navigation/route/routeList.tsx index 39e8963..ae9a841 100644 --- a/uniro_frontend/src/components/navigation/route/routeList.tsx +++ b/uniro_frontend/src/components/navigation/route/routeList.tsx @@ -1,43 +1,44 @@ -import { Fragment, useEffect } from "react"; +import { Fragment } from "react"; import { RouteCard } from "./routeCard"; import useRoutePoint from "../../../hooks/useRoutePoint"; import { RouteDetail } from "../../../data/types/route"; import { Direction } from "../../../data/types/route"; +import { CautionIssue } from "../../../constant/enum/reportEnum"; type RouteListProps = { routes: RouteDetail[]; }; -// export type RouteDetail = { -// dist: number; -// directionType: Direction; -// coordinates: Coord; -// }; - const Divider = () =>
; const RouteList = ({ routes }: RouteListProps) => { const { origin, destination } = useRoutePoint(); + const addOriginAndDestination = (routes: RouteDetail[]) => { + return [ + { + dist: 0, + directionType: "origin" as Direction, + coordinates: { lat: origin!.lat, lng: origin!.lng }, + cautionFactors: [], + }, + ...routes.slice(0, -1), + { + dist: 0, + directionType: "finish" as Direction, + coordinates: { lat: destination!.lat, lng: destination!.lng }, + cautionFactors: [], + }, + ]; + }; + return (
- {[ - { - dist: 0, - directionType: "origin" as Direction, - coordinates: { lat: origin!.lat, lng: origin!.lng }, - }, - ...routes, - ].map((route, index) => ( - + {addOriginAndDestination(routes).map((route, index) => ( +
- +
))} diff --git a/uniro_frontend/src/constant/error.ts b/uniro_frontend/src/constant/error.ts new file mode 100644 index 0000000..ac84f33 --- /dev/null +++ b/uniro_frontend/src/constant/error.ts @@ -0,0 +1,27 @@ +export class NotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = "Not Found"; + } +} + +export class BadRequestError extends Error { + constructor(message: string) { + super(message); + this.name = "Bad Request"; + } +} + +export class UnProcessableError extends Error { + constructor(message: string) { + super(message); + this.name = "UnProcessable"; + } +} + +export enum ERROR_STATUS { + NOT_FOUND = 404, + BAD_REQUEST = 400, + INTERNAL_ERROR = 500, + UNPROCESSABLE = 422, +} diff --git a/uniro_frontend/src/data/types/route.d.ts b/uniro_frontend/src/data/types/route.d.ts index 1d8ac84..595bb49 100644 --- a/uniro_frontend/src/data/types/route.d.ts +++ b/uniro_frontend/src/data/types/route.d.ts @@ -16,15 +16,11 @@ export type Route = { export type Direction = "origin" | "right" | "straight" | "left" | "uturn" | "destination" | "caution"; export interface CautionRoute extends Route { - cautionTypes: CautionIssueType[]; + cautionFactors: CautionIssueType[]; } export interface DangerRoute extends Route { - dangerTypes: DangerIssueType[]; -} - -export interface NavigationRoute extends Route { - cautionTypes: CautionIssueType[]; + dangerFactors: DangerIssueType[]; } export interface CoreRoute { @@ -47,12 +43,13 @@ export type RouteDetail = { dist: number; directionType: Direction; coordinates: Coord; + cautionFactors: CautionIssueType[]; }; export type NavigationRouteList = { hasCaution: boolean; totalDistance: number; totalCost: number; - routes: NavigationRoute[]; + routes: Route[]; routeDetails: RouteDetail[]; }; diff --git a/uniro_frontend/src/hooks/useDebounceMutation.tsx b/uniro_frontend/src/hooks/useDebounceMutation.tsx new file mode 100644 index 0000000..0bcbc2c --- /dev/null +++ b/uniro_frontend/src/hooks/useDebounceMutation.tsx @@ -0,0 +1,40 @@ +import { QueryClient, useMutation, UseMutationOptions } from "@tanstack/react-query"; +import { useCallback, useRef } from "react"; + +export function useDebounceMutation( + options: UseMutationOptions, + delay: number, + immediate: boolean, + queryClient?: QueryClient, +) { + const mutation = useMutation(options, queryClient); + const timeoutRef = useRef | null>(null); + + const debouncedMutate = useCallback( + (...args: Parameters) => { + const later = () => { + timeoutRef.current = null; + if (!immediate) { + mutation.mutate(...args); + } + }; + + const callNow = immediate && !timeoutRef.current; + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(later, delay); + + if (callNow) { + mutation.mutate(...args); + } + }, + [mutation.mutate, delay, immediate], + ); + + return { + ...mutation, + mutate: debouncedMutate, + }; +} diff --git a/uniro_frontend/src/hooks/useMutationError.tsx b/uniro_frontend/src/hooks/useMutationError.tsx new file mode 100644 index 0000000..e3605f6 --- /dev/null +++ b/uniro_frontend/src/hooks/useMutationError.tsx @@ -0,0 +1,86 @@ +import { QueryClient, useMutation, UseMutationOptions, UseMutationResult } from "@tanstack/react-query"; +import { NotFoundError, BadRequestError, ERROR_STATUS } from "../constant/error"; +import { useEffect, useRef, useState } from "react"; +import { useDebounceMutation } from "./useDebounceMutation"; + +type Fallback = { + [K in Exclude]?: { + mainTitle: string; + subTitle: string[]; + }; +}; + +type HandleError = { + fallback: Fallback; + onClose?: () => void; +}; + +type UseMutationErrorReturn = [ + React.FC, + UseMutationResult, +]; + +export default function useMutationError( + options: UseMutationOptions, + queryClient?: QueryClient, + handleError?: HandleError, +): UseMutationErrorReturn { + const [isOpen, setOpen] = useState(false); + const result = useDebounceMutation(options, 1000, true, queryClient); + + const { isError, error } = result; + + useEffect(() => { + setOpen(isError); + }, [isError]); + + const close = () => { + if (handleError?.onClose) handleError?.onClose(); + setOpen(false); + }; + + const Modal: React.FC = () => { + if (!isOpen || !handleError || !error) return null; + + const { fallback } = handleError; + + let title: { mainTitle: string; subTitle: string[] } = { + mainTitle: "", + subTitle: [], + }; + + if (error instanceof NotFoundError) { + title = fallback[404] ?? title; + } else if (error instanceof BadRequestError) { + title = fallback[400] ?? title; + } else throw error; + + return ( +
+
+
+

{title.mainTitle}

+
+ {title.subTitle.map((_subtitle, index) => ( +

+ {_subtitle} +

+ ))} +
+
+ +
+
+ ); + }; + + return [Modal, result]; +} diff --git a/uniro_frontend/src/hooks/useNavigationBottomSheet.tsx b/uniro_frontend/src/hooks/useNavigationBottomSheet.tsx new file mode 100644 index 0000000..c7261ad --- /dev/null +++ b/uniro_frontend/src/hooks/useNavigationBottomSheet.tsx @@ -0,0 +1,49 @@ +import { PanInfo, useDragControls } from "framer-motion"; +import { useCallback, useEffect, useRef, useState } from "react"; + +const MAX_SHEET_HEIGHT = window.innerHeight * 0.7; +const MIN_SHEET_HEIGHT = window.innerHeight * 0.35; +const CLOSED_SHEET_HEIGHT = 0; +const ERROR_MARGIN_OF_DRAG = 0.95; + +export const useNavigationBottomSheet = () => { + const [sheetHeight, setSheetHeight] = useState(CLOSED_SHEET_HEIGHT); + const dragControls = useDragControls(); + + const scrollRef = useRef(null); + + 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); + }); + }, []); + + const preventScroll = useCallback( + (event: React.UIEvent) => { + if (sheetHeight < MAX_SHEET_HEIGHT * ERROR_MARGIN_OF_DRAG) { + event.preventDefault(); + if (scrollRef.current) { + scrollRef.current.scrollTo({ top: 0, behavior: "smooth" }); + } + } + }, + [sheetHeight], + ); + useEffect(() => { + if (scrollRef.current) { + if (sheetHeight > MAX_SHEET_HEIGHT * ERROR_MARGIN_OF_DRAG) { + scrollRef.current.style.overflowY = "auto"; + } else { + scrollRef.current.scrollTo({ top: 0, behavior: "smooth" }); + setTimeout(() => { + if (scrollRef.current) { + scrollRef.current.style.overflowY = "hidden"; + } + }, 300); + } + } + }, [sheetHeight]); + + return { sheetHeight, setSheetHeight, dragControls, handleDrag, preventScroll, scrollRef }; +}; diff --git a/uniro_frontend/src/hooks/useQueryError.tsx b/uniro_frontend/src/hooks/useQueryError.tsx new file mode 100644 index 0000000..e8b5b61 --- /dev/null +++ b/uniro_frontend/src/hooks/useQueryError.tsx @@ -0,0 +1,92 @@ +import { QueryClient, useQuery, UseQueryOptions, UseQueryResult } from "@tanstack/react-query"; +import { NotFoundError, BadRequestError, ERROR_STATUS, UnProcessableError } from "../constant/error"; +import { useEffect, useState } from "react"; + +type Fallback = { + [K in Exclude]?: { + mainTitle: string; + subTitle: string[]; + }; +}; + +type HandleError = { + fallback: Fallback; + onClose?: () => void; +} + +type UseQueryErrorReturn = [ + React.FC, + UseQueryResult +]; + +export default function useQueryError( + options: UseQueryOptions, + queryClient?: QueryClient, + handleSuccess?: () => void, + handleError?: HandleError, +): UseQueryErrorReturn { + const [isOpen, setOpen] = useState(false); + const result = useQuery(options, queryClient); + + const { isError, error, status } = result; + + useEffect(() => { + setOpen(isError) + }, [isError]) + + /** 페이지 이동하여도 status는 success로 남아있으므로 필요시, removeQueries하여 캐시 삭제 */ + /** 추가적인 로직 고민하기 */ + useEffect(() => { + if (status === "success" && handleSuccess) handleSuccess(); + }, [status]) + + const close = () => { + if (handleError?.onClose) handleError?.onClose(); + setOpen(false); + } + + const Modal: React.FC = () => { + if (!isOpen || !handleError || !error) return null; + + const { fallback } = handleError; + + let title: { mainTitle: string, subTitle: string[] } = { + mainTitle: "", + subTitle: [], + } + + if (error instanceof NotFoundError) { + title = fallback[404] ?? title; + } + else if (error instanceof BadRequestError) { + title = fallback[400] ?? title; + } + else if (error instanceof UnProcessableError) { + title = fallback[422] ?? title; + } + + else throw error; + + return ( +
+ < div className="w-full max-w-[365px] flex flex-col bg-gray-100 rounded-400 overflow-hidden" > +
+

{title.mainTitle}

+
+ {title.subTitle.map((_subtitle, index) => +

{_subtitle}

)} +
+
+ +
+
+ ) + } + + return [Modal, result]; +} diff --git a/uniro_frontend/src/hooks/useRoutePoint.ts b/uniro_frontend/src/hooks/useRoutePoint.ts index 2dd5389..6a51973 100644 --- a/uniro_frontend/src/hooks/useRoutePoint.ts +++ b/uniro_frontend/src/hooks/useRoutePoint.ts @@ -1,31 +1,10 @@ import { create } from "zustand"; import { Building } from "../data/types/node"; -const originMockInfo = { - buildingName: "5호관", - buildingImageUrl: "", - phoneNumber: "", - address: "인하로 100", - nodeId: 1, - lng: 127.042012, - lat: 37.557643, -}; - -const destMockInfo = { - buildingName: "하이테크", - buildingImageUrl: "", - phoneNumber: "", - address: "인하로 100", - nodeId: 2, - lng: 127.042012, - lat: 37.557643, -}; - interface RouteStore { - origin: Building; + origin: Building | undefined; setOrigin: (origin: Building | undefined) => void; - destination: Building; - setDemoBuildingInfo: (building: Building) => void; + destination: Building | undefined; setDestination: (destination: Building | undefined) => void; setOriginCoord: (lng: number, lat: number) => void; setDestinationCoord: (lng: number, lat: number) => void; @@ -34,9 +13,9 @@ interface RouteStore { /** 출발지, 도착지 관리 전역 상태 */ const useRoutePoint = create((set) => ({ - origin: originMockInfo, + origin: undefined, setOrigin: (newOrigin: Building | undefined) => set(() => ({ origin: newOrigin })), - destination: destMockInfo, + destination: undefined, setDemoBuildingInfo: (building: Building) => set(() => ({ origin: building, destination: building })), setOriginCoord: (lng: number, lat: number) => set(({ origin }) => ({ origin: { ...origin, lng, lat } })), setDestinationCoord: (lng: number, lat: number) => diff --git a/uniro_frontend/src/hooks/useSearchBuilding.ts b/uniro_frontend/src/hooks/useSearchBuilding.ts index 8ab44f2..265df24 100644 --- a/uniro_frontend/src/hooks/useSearchBuilding.ts +++ b/uniro_frontend/src/hooks/useSearchBuilding.ts @@ -3,19 +3,21 @@ import { Building } from "../data/types/node"; import { RoutePointType } from "../data/types/route"; import { RoutePoint } from "../constant/enum/routeEnum"; +export type SearchMode = "BUILDING" | "ORIGIN" | "DESTINATION"; + interface SearchModeStore { - mode: RoutePointType; - setMode: (mode: RoutePointType) => void; + searchMode: SearchMode; + setSearchMode: (mode: SearchMode) => void; building: Building | undefined; - setBuilding: (newBuilding: Building) => void; + setBuilding: (newBuilding: Building | undefined) => void; } /** 건물 리스트에서 건물을 출발지, 도착지로 결정하는 경우 */ const useSearchBuilding = create((set) => ({ - mode: RoutePoint.ORIGIN, - setMode: (newMode: RoutePointType) => set(() => ({ mode: newMode })), + searchMode: "BUILDING", + setSearchMode: (newMode: SearchMode) => set(() => ({ searchMode: newMode })), building: undefined, - setBuilding: (newBuilding: Building) => set(() => ({ building: newBuilding })), + setBuilding: (newBuilding: Building | undefined) => set(() => ({ building: newBuilding })), })); export default useSearchBuilding; diff --git a/uniro_frontend/src/map/initializer/googleMapInitializer.ts b/uniro_frontend/src/map/initializer/googleMapInitializer.ts index 7707924..09f590c 100644 --- a/uniro_frontend/src/map/initializer/googleMapInitializer.ts +++ b/uniro_frontend/src/map/initializer/googleMapInitializer.ts @@ -27,7 +27,7 @@ export const initializeMap = async ( draggable: true, scrollwheel: true, disableDoubleClickZoom: false, - gestureHandling: "auto", + gestureHandling: "greedy", clickableIcons: false, disableDefaultUI: true, // restriction: { diff --git a/uniro_frontend/src/pages/buildingSearch.tsx b/uniro_frontend/src/pages/buildingSearch.tsx index 009031c..9a0a109 100644 --- a/uniro_frontend/src/pages/buildingSearch.tsx +++ b/uniro_frontend/src/pages/buildingSearch.tsx @@ -1,24 +1,47 @@ import Input from "../components/customInput"; -import { hanyangBuildings } from "../data/mock/hanyangBuildings"; import BuildingCard from "../components/building/buildingCard"; import useSearchBuilding from "../hooks/useSearchBuilding"; import useUniversityInfo from "../hooks/useUniversityInfo"; import useRedirectUndefined from "../hooks/useRedirectUndefined"; +import { useQuery } from "@tanstack/react-query"; +import { getAllBuildings, getSearchBuildings } from "../api/nodes"; +import { University } from "../data/types/university"; +import { useState } from "react"; +import CloseIcon from "../assets/icon/close.svg?react"; +import { useNavigate } from "react-router"; export default function BuildingSearchPage() { + const { university } = useUniversityInfo(); const { setBuilding } = useSearchBuilding(); + const navigate = useNavigate(); - const { university } = useUniversityInfo(); - useRedirectUndefined([university]); + if (!university) return; + + const [input, setInput] = useState(''); + + const { data: buildings } = useQuery({ + queryKey: [university.id, "buildings", input], + queryFn: () => + getSearchBuildings(university.id, { name: input }) + },) + + const handleBack = () => { + navigate(-1); + } + + useRedirectUndefined([university]); return (
-
- {}} handleVoiceInput={() => {}} placeholder="" /> +
+ setInput(e)} handleVoiceInput={() => { }} placeholder="" /> +
    - {hanyangBuildings.map((building) => ( + {(buildings ?? []).map((building) => ( setBuilding(building)} key={`building-${building.buildingName}`} @@ -27,6 +50,7 @@ export default function BuildingSearchPage() { ))}
+
); -} +} \ No newline at end of file diff --git a/uniro_frontend/src/pages/demo.tsx b/uniro_frontend/src/pages/demo.tsx index 7a47634..20f0f44 100644 --- a/uniro_frontend/src/pages/demo.tsx +++ b/uniro_frontend/src/pages/demo.tsx @@ -3,7 +3,7 @@ import Button from "../components/customButton"; import LandingButton from "../components/landingButton"; import Input from "../components/customInput"; import Map from "../component/Map"; -import RouteInput from "../components/map/routeSearchInput"; +import { RouteInput } from "../components/map/mapSearchInput"; import OriginIcon from "../assets/map/origin.svg?react"; import DestinationIcon from "../assets/map/destination.svg?react"; import { useEffect, useState } from "react"; @@ -70,30 +70,30 @@ export default function Demo() {
- {}} isActive={false} /> - {}} isActive={true} /> + { }} isActive={false} /> + { }} isActive={true} />
- {}} isActive={false} /> - {}} isActive={true} /> + { }} isActive={false} /> + { }} isActive={true} />
{}} + handleVoiceInput={() => { }} onLengthChange={(e: string) => { console.log(e); }} /> - {}} placeholder="출발지를 입력하세요"> + { }} placeholder="출발지를 입력하세요"> {}} + onClick={() => { }} placeholder="도착지를 입력하세요" value={destination} onCancel={() => setDestination("")} diff --git a/uniro_frontend/src/pages/errorTest.tsx b/uniro_frontend/src/pages/errorTest.tsx new file mode 100644 index 0000000..4be96c0 --- /dev/null +++ b/uniro_frontend/src/pages/errorTest.tsx @@ -0,0 +1,78 @@ +import useMutationError from "../hooks/useMutationError"; +import { RouteId } from "../data/types/route"; +import { CautionIssueType, DangerIssueType } from "../data/types/enum"; +import { postFetch } from "../utils/fetch/fetch"; +import { postReportRoute } from "../api/route"; + +const postReport = ( + univId: number, + routeId: RouteId, + body: { dangerFactors: DangerIssueType[]; cautionFactors: CautionIssueType[] }, +): Promise => { + return postFetch(`/${univId}/route/risk/${routeId}`, body); +}; + +export default function Errortest() { + const [Modal400, { mutate: mutate400 }] = useMutationError( + { + //@ts-expect-error 강제 에러 발생 + mutationFn: () => postReport(1001, 1, { cautionFactors: ["TEST"], dangerFactors: [] }), + }, + undefined, + { + fallback: { + 400: { mainTitle: "400 제목", subTitle: ["400 부제목"] }, + 404: { mainTitle: "404 제목", subTitle: ["404 부제목"] }, + }, + onClose: () => { alert('close callback') } + } + ); + + const [Modal404, { mutate: mutate404 }] = useMutationError( + { + mutationFn: () => postReport(1, 1, { cautionFactors: [], dangerFactors: [] }), + }, + undefined, + { + fallback: { + 400: { mainTitle: "400 제목", subTitle: ["400 부제목"] }, + 404: { mainTitle: "404 제목", subTitle: ["404 부제목"] }, + }, + onClose: () => { } + } + ); + + const [Modal500, { mutate: mutate500 }] = useMutationError( + { + //@ts-expect-error 강제 에러 발생 + mutationFn: () => postReportRoute(1001, { startNodeId: 1, endNodeId: 1 }), + }, + undefined, + { + fallback: { + 400: { mainTitle: "400 제목", subTitle: ["400 부제목"] }, + 404: { mainTitle: "404 제목", subTitle: ["404 부제목"] }, + }, + onClose: () => { } + } + ); + + return ( +
+
+ + + +
+ + + +
+ ); +} diff --git a/uniro_frontend/src/pages/map.tsx b/uniro_frontend/src/pages/map.tsx index e19882e..3657989 100644 --- a/uniro_frontend/src/pages/map.tsx +++ b/uniro_frontend/src/pages/map.tsx @@ -1,11 +1,9 @@ import { useEffect, useRef, useState } from "react"; import useMap from "../hooks/useMap"; import createMarkerElement from "../components/map/mapMarkers"; -import { BottomSheet, BottomSheetRef } from "react-spring-bottom-sheet"; import { Building, NodeId } from "../data/types/node"; -import "react-spring-bottom-sheet/dist/style.css"; -import { MapBottomSheetFromList, MapBottomSheetFromMarker } from "../components/map/mapBottomSheet"; -import TopSheet from "../components/map/TopSheet"; +import MapBottomSheet from "../components/map/mapBottomSheet"; +import { MapTopBuildingSheet, MapTopRouteSheet } from "../components/map/TopSheet"; import { CautionToggleButton, DangerToggleButton } from "../components/map/floatingButtons"; import ReportButton from "../components/map/reportButton"; import useRoutePoint from "../hooks/useRoutePoint"; @@ -17,7 +15,7 @@ import { RoutePoint } from "../constant/enum/routeEnum"; import { Markers } from "../constant/enum/markerEnum"; import createAdvancedMarker, { createUniversityMarker } from "../utils/markers/createAdvanedMarker"; import toggleMarkers from "../utils/markers/toggleMarkers"; -import { Link } from "react-router"; +import { useNavigate } from "react-router"; import useModal from "../hooks/useModal"; import ReportModal from "../components/map/reportModal"; import useUniversityInfo from "../hooks/useUniversityInfo"; @@ -28,9 +26,14 @@ import { CautionIssueType, DangerIssueType, MarkerTypes } from "../data/types/en import { CautionIssue, DangerIssue } from "../constant/enum/reportEnum"; /** API 호출 */ -import { useSuspenseQueries } from "@tanstack/react-query"; +import { useQuery, useSuspenseQueries } from "@tanstack/react-query"; import { getAllRisks } from "../api/routes"; import { getAllBuildings } from "../api/nodes"; +import { getNavigationResult } from "../api/route"; +import useQueryError from "../hooks/useQueryError"; +import { Coord } from "../data/types/coord"; +import AnimatedContainer from "../container/animatedContainer"; + export type SelectedMarkerTypes = { type: MarkerTypes; @@ -41,55 +44,110 @@ export type SelectedMarkerTypes = { from: "Marker" | "List"; }; +const BOTTOM_SHEET_HEIGHT = 377; + export default function MapPage() { - const { mapRef, map, AdvancedMarker } = useMap(); + const { mapRef, map, AdvancedMarker } = useMap({ zoom: 16 }); const [zoom, setZoom] = useState(16); const prevZoom = useRef(16); const [selectedMarker, setSelectedMarker] = useState(); - const bottomSheetRef = useRef(null); - const [sheetOpen, setSheetOpen] = useState(false); - + const buildingBoundary = useRef(null); const [buildingMarkers, setBuildingMarkers] = useState<{ element: AdvancedMarker; nodeId: NodeId }[]>([]); - const [dangerMarkers, setDangerMarkers] = useState<{ element: AdvancedMarker, routeId: RouteId }[]>([]); + const [dangerMarkers, setDangerMarkers] = useState<{ element: AdvancedMarker; routeId: RouteId }[]>([]); const [isDangerAcitve, setIsDangerActive] = useState(false); - const [cautionMarkers, setCautionMarkers] = useState<{ element: AdvancedMarker, routeId: RouteId }[]>([]); + const [cautionMarkers, setCautionMarkers] = useState<{ element: AdvancedMarker; routeId: RouteId }[]>([]); const [isCautionAcitve, setIsCautionActive] = useState(false); const [universityMarker, setUniversityMarker] = useState(); const { origin, setOrigin, destination, setDestination } = useRoutePoint(); - const { mode, building: selectedBuilding } = useSearchBuilding(); + const { building: selectedBuilding, setBuilding, searchMode } = useSearchBuilding(); const [_, isOpen, open, close] = useModal(); const { university } = useUniversityInfo(); useRedirectUndefined([university]); + const navigate = useNavigate(); if (!university) return; + const [FailModal, { status, data, refetch: findFastRoute }] = useQueryError( + { + queryKey: ["fastRoute", university.id, origin?.nodeId, destination?.nodeId], + queryFn: () => + getNavigationResult( + university.id, + origin ? origin?.nodeId : -1, + destination ? destination?.nodeId : -1, + ), + enabled: false, + retry: 0, + }, + undefined, + () => { + navigate("/result"); + }, + { + fallback: { + 400: { + mainTitle: "잘못된 요청입니다.", + subTitle: ["새로고침 후 다시 시도 부탁드립니다."], + }, + 404: { + mainTitle: "해당 경로를 찾을 수 없습니다.", + subTitle: ["해당 건물이 길이랑 연결되지 않았습니다."], + }, + 422: { + mainTitle: "해당 경로를 찾을 수 없습니다.", + subTitle: ["위험 요소 버튼을 클릭하여,", "통행할 수 없는 원인을 파악하실 수 있습니다."], + }, + }, + }, + ); + const results = useSuspenseQueries({ queries: [ - { queryKey: [university.id, 'risks'], queryFn: () => getAllRisks(university.id) }, + { queryKey: [university.id, "risks"], queryFn: () => getAllRisks(university.id) }, { - queryKey: [university.id, 'buildings'], queryFn: () => getAllBuildings(university.id, { - leftUpLat: 38, - leftUpLng: 127, - rightDownLat: 37, - rightDownLng: 128 - }) - } - ] + queryKey: [university.id, "buildings"], + queryFn: () => + getAllBuildings(university.id, { + leftUpLat: 38, + leftUpLng: 127, + rightDownLat: 37, + rightDownLng: 128, + }), + }, + ], }); const [risks, buildings] = results; + const moveToBound = (coord: Coord) => { + buildingBoundary.current = new google.maps.LatLngBounds(); + buildingBoundary.current.extend( + coord + ); + // 라이브러리를 다양한 화면을 관찰해보았을 때, h-가 377인것을 확인했습니다. + map?.fitBounds(buildingBoundary.current, { + top: 0, + right: 0, + bottom: BOTTOM_SHEET_HEIGHT, + left: 0, + }); + }; + + const exitBound = () => { + buildingBoundary.current = null; + }; + const initMap = () => { if (map === null || !AdvancedMarker) return; map.addListener("click", (e: unknown) => { - setSheetOpen(false); + exitBound(); setSelectedMarker(undefined); }); map.addListener("zoom_changed", () => { @@ -97,16 +155,16 @@ export default function MapPage() { const curZoom = map.getZoom() as number; prevZoom.current = prev; - return curZoom + return curZoom; }); - }) + }); const centerMarker = createUniversityMarker( AdvancedMarker, map, HanyangUniversity, university ? university.name : "", - ) + ); setUniversityMarker(centerMarker); }; @@ -121,7 +179,7 @@ export default function MapPage() { const buildingMarker = createAdvancedMarker( AdvancedMarker, - null, + (nodeId === origin?.nodeId || nodeId === destination?.nodeId) ? map : null, new google.maps.LatLng(lat, lng), createMarkerElement({ type: Markers.BUILDING, title: buildingName, className: "translate-marker" }), () => { @@ -149,7 +207,7 @@ export default function MapPage() { const dangerMarkersWithId: { routeId: RouteId; element: AdvancedMarker }[] = []; for (const route of dangerRoutes) { - const { routeId, node1, node2, dangerTypes } = route; + const { routeId, node1, node2, dangerFactors } = route; const type = Markers.DANGER; const dangerMarker = createAdvancedMarker( @@ -169,10 +227,10 @@ export default function MapPage() { id: routeId, type: type, element: dangerMarker, - factors: dangerTypes, + factors: dangerFactors, from: "Marker", - } - }) + }; + }); }, ); @@ -184,7 +242,7 @@ export default function MapPage() { const cautionMarkersWithId: { routeId: RouteId; element: AdvancedMarker }[] = []; for (const route of cautionRoutes) { - const { routeId, node1, node2, cautionTypes } = route; + const { routeId, node1, node2, cautionFactors } = route; const type = Markers.CAUTION; const cautionMarker = createAdvancedMarker( @@ -204,10 +262,10 @@ export default function MapPage() { id: routeId, type: type, element: cautionMarker, - factors: cautionTypes, + factors: cautionFactors, from: "Marker", - } - }) + }; + }); }, ); cautionMarkersWithId.push({ routeId, element: cautionMarker }); @@ -225,7 +283,11 @@ export default function MapPage() { }); } setIsCautionActive((isActive) => { - toggleMarkers(!isActive, cautionMarkers.map(marker => marker.element), map); + toggleMarkers( + !isActive, + cautionMarkers.map((marker) => marker.element), + map, + ); return !isActive; }); }; @@ -238,7 +300,11 @@ export default function MapPage() { }); } setIsDangerActive((isActive) => { - toggleMarkers(!isActive, dangerMarkers.map(marker => marker.element), map); + toggleMarkers( + !isActive, + dangerMarkers.map((marker) => marker.element), + map, + ); return !isActive; }); }; @@ -263,7 +329,7 @@ export default function MapPage() { else setDestination(selectedMarker.property); } - setSheetOpen(false); + exitBound(); setSelectedMarker(undefined); }; @@ -271,49 +337,34 @@ export default function MapPage() { const changeMarkerStyle = (marker: SelectedMarkerTypes | undefined, isSelect: boolean) => { if (!map || !marker) return; - if (marker.property && (marker.id === origin?.nodeId || marker.id === destination?.nodeId)) { - if (isSelect) { - map.setOptions({ - center: { lat: marker.property.lat, lng: marker.property.lng }, - zoom: 19, - }) - setSheetOpen(true); - } - - return; - } - if (marker.type === Markers.BUILDING && marker.property) { - if (isSelect) { - marker.element.content = createMarkerElement({ - type: Markers.SELECTED_BUILDING, - title: marker.property.buildingName, - className: "translate-marker", - }); - map.setOptions({ - center: { lat: marker.property.lat, lng: marker.property.lng }, - zoom: 19, - }); - setSheetOpen(true); - - return; - } - if (marker.id === origin?.nodeId) { marker.element.content = createMarkerElement({ type: Markers.ORIGIN, title: marker.property.buildingName, className: "translate-routemarker", }); + return; } - else if (marker.id === destination?.nodeId) { + if (marker.id === destination?.nodeId) { marker.element.content = createMarkerElement({ type: Markers.DESTINATION, title: destination.buildingName, className: "translate-routemarker", }); + return; + } + + + if (isSelect) { + marker.element.content = createMarkerElement({ + type: Markers.SELECTED_BUILDING, + title: marker.property.buildingName, + className: "translate-marker", + }); + return; } marker.element.content = createMarkerElement({ @@ -324,18 +375,15 @@ export default function MapPage() { } else { if (isSelect) { if (marker.type === Markers.DANGER) { - const key = marker.factors && marker.factors[0] as DangerIssueType; marker.element.content = createMarkerElement({ type: marker.type, - title: key && DangerIssue[key], + title: (marker.factors as DangerIssueType[]).map((key) => DangerIssue[key]), hasTopContent: true, }); - } - else if (marker.type === Markers.CAUTION) { - const key = marker.factors && marker.factors[0] as CautionIssueType; + } else if (marker.type === Markers.CAUTION) { marker.element.content = createMarkerElement({ type: marker.type, - title: key && CautionIssue[key], + title: (marker.factors as CautionIssueType[]).map((key) => CautionIssue[key]), hasTopContent: true, }); } @@ -371,18 +419,34 @@ export default function MapPage() { /** 빌딩 리스트에서 넘어온 경우, 일치하는 BuildingMarkerElement를 탐색 */ useEffect(() => { + if (buildingMarkers.length === 0 || !selectedBuilding || !selectedBuilding.nodeId) return; - const matchedMarker = findBuildingMarker(selectedBuilding.nodeId); + if (!selectedMarker) { + const matchedMarker = findBuildingMarker(selectedBuilding.nodeId); - if (matchedMarker) - setSelectedMarker({ - id: selectedBuilding.nodeId, - type: Markers.BUILDING, - element: matchedMarker, - from: "List", - property: selectedBuilding, - }); + if (!matchedMarker) return; + if (searchMode === "BUILDING") { + + setSelectedMarker({ + id: selectedBuilding.nodeId, + type: Markers.BUILDING, + element: matchedMarker, + from: "List", + property: selectedBuilding, + }); + return + } + if (searchMode === "ORIGIN") { + setOrigin(selectedBuilding); + moveToBound(selectedBuilding); + return; + } + if (searchMode === "DESTINATION") { + setDestination(selectedBuilding); + moveToBound(selectedBuilding); + } + } }, [selectedBuilding, buildingMarkers]); /** 출발지 결정 시, Marker Content 변경 */ @@ -392,6 +456,8 @@ export default function MapPage() { const originMarker = findBuildingMarker(origin.nodeId); if (!originMarker) return; + originMarker.map = map; + originMarker.content = createMarkerElement({ type: Markers.ORIGIN, title: origin.buildingName, @@ -399,13 +465,15 @@ export default function MapPage() { }); return () => { + const curZoom = map?.getZoom() as number; originMarker.content = createMarkerElement({ type: Markers.BUILDING, title: origin.buildingName, className: "translate-marker", }); + if (curZoom <= 16) originMarker.map = null; }; - }, [origin]); + }, [origin, buildingMarkers]); /** 도착지 결정 시, Marker Content 변경 */ useEffect(() => { @@ -414,6 +482,8 @@ export default function MapPage() { const destinationMarker = findBuildingMarker(destination.nodeId); if (!destinationMarker) return; + destinationMarker.map = map; + destinationMarker.content = createMarkerElement({ type: Markers.DESTINATION, title: destination.buildingName, @@ -421,83 +491,129 @@ export default function MapPage() { }); return () => { + const curZoom = map?.getZoom() as number; + destinationMarker.content = createMarkerElement({ type: Markers.BUILDING, title: destination.buildingName, className: "translate-marker", }); + if (curZoom <= 16) destinationMarker.map = null; }; - }, [destination]); + }, [destination, buildingMarkers]); + /** 출발 도착 설정시, 출발 도착지가 한 눈에 보이도록 지도 조정 */ useEffect(() => { - if (!map) return; + if (origin && destination) { + const newBound = new google.maps.LatLngBounds(); + newBound.extend(origin); + newBound.extend(destination) + map?.fitBounds(newBound) + } - const _buildingMarkers = buildingMarkers.map(buildingMarker => buildingMarker.element); + }, [origin, destination]); + + useEffect(() => { + if (selectedMarker && selectedMarker.type === Markers.BUILDING && selectedMarker.property) { + moveToBound({ lat: selectedMarker.property.lat, lng: selectedMarker.property.lng }); + setBuilding(selectedMarker.property as Building); + } + + return () => { + setBuilding(undefined) + } + }, [selectedMarker]); + + useEffect(() => { + if (!map) return; if (prevZoom.current >= 17 && zoom <= 16) { if (isCautionAcitve) { - setIsCautionActive(false); - toggleMarkers(false, cautionMarkers.map(marker => marker.element), map); + toggleMarkers( + false, + cautionMarkers.map((marker) => marker.element), + map, + ); } if (isDangerAcitve) { - setIsDangerActive(false); - toggleMarkers(false, dangerMarkers.map(marker => marker.element), map); + toggleMarkers( + false, + dangerMarkers.map((marker) => marker.element), + map, + ); } toggleMarkers(true, universityMarker ? [universityMarker] : [], map); - toggleMarkers(false, _buildingMarkers, map); - } - else if ((prevZoom.current <= 16 && zoom >= 17)) { + toggleMarkers(false, buildingMarkers.filter(el => el.nodeId !== origin?.nodeId && el.nodeId !== destination?.nodeId).map(el => el.element), map); + } else if (prevZoom.current <= 16 && zoom >= 17) { + if (isCautionAcitve) { + toggleMarkers( + true, + cautionMarkers.map((marker) => marker.element), + map, + ); + } + if (isDangerAcitve) { + toggleMarkers( + true, + dangerMarkers.map((marker) => marker.element), + map, + ); + } + toggleMarkers(false, universityMarker ? [universityMarker] : [], map); - toggleMarkers(true, _buildingMarkers, map); + toggleMarkers(true, buildingMarkers.map(el => el.element), map); } - }, [map, zoom]) + }, [map, zoom]); return (
- + +
- minHeight} + + {/* 출발지랑 도착지가 존재하는 경우 길찾기 버튼 보이기 */} + - {selectedMarker && - (selectedMarker.from === "Marker" ? ( - /** 선택된 마커가 Marker 클릭에서 온 경우 */ - selectRoutePoint(RoutePoint.ORIGIN)} - onClickRight={() => selectRoutePoint(RoutePoint.DESTINATION)} - /> - ) : ( - /** 선택된 마커가 리스트에서 온 경우 */ - - ))} - - {origin && destination && origin.nodeId !== destination.nodeId ? ( - /** 출발지랑 도착지가 존재하는 경우 길찾기 버튼 보이기 */ - +
findFastRoute()} className="absolute bottom-6 space-y-2 w-full px-4"> - - ) : ( - /** 출발지랑 도착지가 존재하지 않거나, 같은 경우 기존 Button UI 보이기 */ - <> -
- -
-
- - -
- - )} +
+ + + {/* 출발지랑 도착지가 존재하지 않거나, 같은 경우 기존 Button UI 보이기 */} + +
+ +
+
+ + +
+
+ {isOpen && } +
); -} +} \ No newline at end of file diff --git a/uniro_frontend/src/pages/navigationResult.tsx b/uniro_frontend/src/pages/navigationResult.tsx index 4c99c1b..14c2a32 100644 --- a/uniro_frontend/src/pages/navigationResult.tsx +++ b/uniro_frontend/src/pages/navigationResult.tsx @@ -1,5 +1,4 @@ -import React, { useCallback, useEffect, useState } from "react"; -import { PanInfo, useDragControls } from "framer-motion"; +import React, { useState } from "react"; import { useSuspenseQueries } from "@tanstack/react-query"; import Button from "../components/customButton"; @@ -9,19 +8,17 @@ import BottomSheetHandle from "../components/navigation/bottomSheet/bottomSheetH import NavigationMap from "../component/NavgationMap"; import BackButton from "../components/map/backButton"; import AnimatedContainer from "../container/animatedContainer"; -import { mockNavigationRoute } from "../data/mock/hanyangRoute"; -import { NavigationRoute } from "../data/types/route"; import useScrollControl from "../hooks/useScrollControl"; import useUniversityInfo from "../hooks/useUniversityInfo"; import useRedirectUndefined from "../hooks/useRedirectUndefined"; -import AnimatedSheetContainer from "../container/animatedSheetContainer"; import { University } from "../data/types/university"; import { getNavigationResult } from "../api/route"; import { getAllRisks } from "../api/routes"; import useRoutePoint from "../hooks/useRoutePoint"; import { Building } from "../data/types/node"; import { useLocation } from "react-router"; +import { useNavigationBottomSheet } from "../hooks/useNavigationBottomSheet"; const MAX_SHEET_HEIGHT = window.innerHeight * 0.7; const MIN_SHEET_HEIGHT = window.innerHeight * 0.35; @@ -33,15 +30,15 @@ const PADDING_FOR_MAP_BOUNDARY = 50; const NavigationResultPage = () => { 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); - const location = useLocation(); + const { sheetHeight, setSheetHeight, dragControls, handleDrag, preventScroll, scrollRef } = + useNavigationBottomSheet(); const { university } = useUniversityInfo(); const { origin, destination, setOriginCoord, setDestinationCoord } = useRoutePoint(); // TEST용 Link + const location = useLocation(); const { search } = location; const params = new URLSearchParams(search); @@ -67,30 +64,38 @@ const NavigationResultPage = () => { requestOriginId, requestDestinationId, ); - setOriginCoord(response.routes[0].node1.lng, response.routes[0].node1.lat); - setDestinationCoord( - response.routes[response.routes.length - 1].node1.lng, - response.routes[response.routes.length - 1].node1.lat, - ); + // param으로 받아올 수 있는 ID들이 있으면 추가하고, 아니면 기존 Building의 좌표를 넣는다 + if (originId) { + setOriginCoord(response.routes[0].node1.lng, response.routes[0].node1.lat); + return response; + } + if (destinationId) { + setDestinationCoord( + response.routes[response.routes.length - 1].node2.lng, + response.routes[response.routes.length - 1].node2.lat, + ); + return response; + } return response; } catch (e) { + alert(`경로를 찾을 수 없습니다. (${e})`); return null; } }, retry: 1, staleTime: 0, + gcTime: 0, }, { queryKey: [university?.id, "risks"], queryFn: () => getAllRisks(university?.id ?? 1001), retry: 1, staleTime: 0, + gcTime: 0, }, ], }); - const dragControls = useDragControls(); - const showDetailView = () => { setSheetHeight(MAX_SHEET_HEIGHT); setTopBarHeight(PADDING_FOR_MAP_BOUNDARY); @@ -103,13 +108,6 @@ const NavigationResultPage = () => { 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); - }); - }, []); - return (
{/* 지도 영역 */} @@ -149,35 +147,36 @@ const NavigationResultPage = () => { - -
-
+
); }; diff --git a/uniro_frontend/src/pages/reportForm.tsx b/uniro_frontend/src/pages/reportForm.tsx index 552f03d..6d75df7 100644 --- a/uniro_frontend/src/pages/reportForm.tsx +++ b/uniro_frontend/src/pages/reportForm.tsx @@ -13,12 +13,13 @@ import useScrollControl from "../hooks/useScrollControl"; import useModal from "../hooks/useModal"; import useUniversityInfo from "../hooks/useUniversityInfo"; import useRedirectUndefined from "../hooks/useRedirectUndefined"; -import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; +import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { getSingleRouteRisk, postReport } from "../api/route"; import { University } from "../data/types/university"; import { useNavigate } from "react-router"; import useReportRisk from "../hooks/useReportRisk"; import { RouteId } from "../data/types/route"; +import useMutationError from "../hooks/useMutationError"; const ReportForm = () => { useScrollControl(); @@ -29,7 +30,6 @@ const ReportForm = () => { const [disabled, setDisabled] = useState(true); - const [FailModal, isFailOpen, openFail, closeFail] = useModal(redirectToMap); const [SuccessModal, isSuccessOpen, openSuccess, closeSuccess] = useModal(redirectToMap); const [errorTitle, setErrorTitle] = useState(""); @@ -38,9 +38,6 @@ const ReportForm = () => { const { reportRouteId: routeId } = useReportRisk(); useRedirectUndefined([university, routeId]); - - console.log(routeId) - if (!routeId) return; const { data } = useSuspenseQuery({ @@ -52,36 +49,27 @@ const ReportForm = () => { } catch (e) { return { routeId: -1, - dangerTypes: [], - cautionTypes: [], + dangerFactors: [], + cautionFactors: [], }; } }, retry: 1, }); - // 임시 Error 처리 - useEffect(() => { - if (data.routeId === -1) { - queryClient.invalidateQueries({ queryKey: ["report", university?.id ?? 1001, routeId] }); - setErrorTitle("존재하지 않은 경로예요"); - openFail(); - } - }, [data]); - const [reportMode, setReportMode] = useState( - data.cautionTypes.length > 0 || data.dangerTypes.length > 0 ? "update" : "create", + data.cautionFactors.length > 0 || data.dangerFactors.length > 0 ? "update" : "create", ); const [formData, setFormData] = useState({ passableStatus: reportMode === "create" ? PassableStatus.INITIAL - : data.cautionTypes.length > 0 + : data.cautionFactors.length > 0 ? PassableStatus.CAUTION : PassableStatus.DANGER, - dangerIssues: data.dangerTypes, - cautionIssues: data.cautionTypes, + dangerIssues: data.dangerFactors, + cautionIssues: data.cautionFactors, }); useEffect(() => { @@ -122,27 +110,42 @@ const ReportForm = () => { } }; - const { mutate } = useMutation({ - mutationFn: () => - postReport(university?.id ?? 1001, routeId, { - dangerTypes: formData.dangerIssues, - cautionTypes: formData.cautionIssues, - }), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["report", university?.id ?? 1001, routeId] }); - openSuccess(); + const [ErrorModal, { mutate, status }] = useMutationError( + { + mutationFn: () => + postReport(university?.id ?? 1001, routeId, { + dangerFactors: formData.dangerIssues, + cautionFactors: formData.cautionIssues, + }), + onSuccess: () => { + queryClient.removeQueries({ queryKey: [university?.id ?? 1001, "risks"] }); + openSuccess(); + }, + onError: () => { + setErrorTitle("제보에 실패하였습니다"); + }, }, - onError: () => { - setErrorTitle("제보에 실패하였습니다"); - openFail(); + undefined, + { + fallback: { + 400: { + mainTitle: "불편한 길 제보 실패", + subTitle: ["잘못된 요청입니다.", "잠시 후 다시 시도 부탁드립니다."], + }, + 404: { + mainTitle: "불편한 길 제보 실패", + subTitle: ["해당 경로는 다른 사용자에 의해 삭제되어,", "지도 화면에서 바로 확인할 수 있어요."], + }, + }, + onClose: redirectToMap, }, - }); + ); return (
-
+
{ />
-
@@ -168,15 +174,7 @@ const ReportForm = () => {

- -

{errorTitle}

-
-

- 해당 경로는 다른 사용자에 의해 삭제되어, -

-

지도 화면에서 바로 확인할 수 있어요.

-
-
+
); }; diff --git a/uniro_frontend/src/pages/reportRisk.tsx b/uniro_frontend/src/pages/reportRisk.tsx index ab99561..53fdd06 100644 --- a/uniro_frontend/src/pages/reportRisk.tsx +++ b/uniro_frontend/src/pages/reportRisk.tsx @@ -51,8 +51,8 @@ export default function ReportRiskPage() { queryKey: ["routes", university.id], queryFn: () => getAllRoutes(university.id), }, - { queryKey: [university.id, 'risks'], queryFn: () => getAllRisks(university.id) }, - ] + { queryKey: [university.id, "risks"], queryFn: () => getAllRisks(university.id) }, + ], }); const [routes, risks] = result; @@ -63,8 +63,8 @@ export default function ReportRiskPage() { return; } - setReportRouteId(reportMarker.route) - } + setReportRouteId(reportMarker.route); + }; const resetMarker = (prevMarker: MarkerTypesWithElement) => { if (prevMarker.type === Markers.REPORT) { @@ -80,7 +80,7 @@ export default function ReportRiskPage() { const { dangerRoutes, cautionRoutes } = risks.data; for (const route of dangerRoutes) { - const { routeId, node1, node2, dangerTypes } = route; + const { routeId, node1, node2, dangerFactors } = route; const type = Markers.DANGER; const dangerMarker = createAdvancedMarker( @@ -99,15 +99,15 @@ export default function ReportRiskPage() { type: Markers.DANGER, element: dangerMarker, route: routeId, - factors: dangerTypes, - } - }) + factors: dangerFactors, + }; + }); }, ); } for (const route of cautionRoutes) { - const { routeId, node1, node2, cautionTypes } = route; + const { routeId, node1, node2, cautionFactors } = route; const type = Markers.CAUTION; const cautionMarker = createAdvancedMarker( @@ -126,9 +126,9 @@ export default function ReportRiskPage() { type: Markers.CAUTION, element: cautionMarker, route: routeId, - factors: cautionTypes, - } - }) + factors: cautionFactors, + }; + }); }, ); } @@ -160,8 +160,6 @@ export default function ReportRiskPage() { strokeColor: "#808080", }); - - routePolyLine.addListener("click", (e: ClickEvent) => { const edges: CoreRoute[] = subRoutes.map(({ routeId, node1, node2 }) => { return { routeId, node1, node2 }; @@ -187,10 +185,9 @@ export default function ReportRiskPage() { return { type: Markers.REPORT, element: newReportMarker, - route: nearestEdge.routeId - } - }) - + route: nearestEdge.routeId, + }; + }); }); const startNode = subRoutes[0].node1; @@ -204,7 +201,6 @@ export default function ReportRiskPage() { } }; - useEffect(() => { drawRoute(routes.data); addRiskMarker(); @@ -220,7 +216,6 @@ export default function ReportRiskPage() { return undefined; }); }); - } }, [map, AdvancedMarker, Polyline]); @@ -236,21 +231,17 @@ export default function ReportRiskPage() { const changeMarkerStyle = (marker: reportMarkerTypes | undefined, isSelect: boolean) => { if (!map || !marker) return; - if (isSelect) { if (marker.type === Markers.DANGER) { - const key = marker.factors && marker.factors[0] as DangerIssueType; marker.element.content = createMarkerElement({ type: marker.type, - title: key && DangerIssue[key], + title: (marker.factors as DangerIssueType[]).map((key) => DangerIssue[key]), hasTopContent: true, }); - } - else if (marker.type === Markers.CAUTION) { - const key = marker.factors && marker.factors[0] as CautionIssueType; + } else if (marker.type === Markers.CAUTION) { marker.element.content = createMarkerElement({ type: marker.type, - title: key && CautionIssue[key], + title: (marker.factors as CautionIssueType[]).map((key) => CautionIssue[key]), hasTopContent: true, }); } @@ -266,8 +257,9 @@ export default function ReportRiskPage() { useEffect(() => { if (!reportMarker) return; - if (reportMarker.type === Markers.REPORT) setMessage(ReportRiskMessage.CREATE) - else if (reportMarker.type === Markers.DANGER || reportMarker.type === Markers.CAUTION) setMessage(ReportRiskMessage.UPDATE) + if (reportMarker.type === Markers.REPORT) setMessage(ReportRiskMessage.CREATE); + else if (reportMarker.type === Markers.DANGER || reportMarker.type === Markers.CAUTION) + setMessage(ReportRiskMessage.UPDATE); else setMessage(ReportRiskMessage.DEFAULT); changeMarkerStyle(reportMarker, true); diff --git a/uniro_frontend/src/pages/reportRoute.tsx b/uniro_frontend/src/pages/reportRoute.tsx index bd1fed0..39f6895 100644 --- a/uniro_frontend/src/pages/reportRoute.tsx +++ b/uniro_frontend/src/pages/reportRoute.tsx @@ -9,13 +9,13 @@ import { LatLngToLiteral } from "../utils/coordinates/coordinateTransform"; import findNearestSubEdge from "../utils/polylines/findNearestEdge"; import { AdvancedMarker } from "../data/types/marker"; import Button from "../components/customButton"; -import { CautionToggleButton, DangerToggleButton } from "../components/map/floatingButtons"; +import { CautionToggleButton, DangerToggleButton, UndoButton } from "../components/map/floatingButtons"; import toggleMarkers from "../utils/markers/toggleMarkers"; import BackButton from "../components/map/backButton"; import useUniversityInfo from "../hooks/useUniversityInfo"; import useRedirectUndefined from "../hooks/useRedirectUndefined"; import { University } from "../data/types/university"; -import { useMutation, useQueryClient, useSuspenseQueries } from "@tanstack/react-query"; +import { useQueryClient, useSuspenseQueries } from "@tanstack/react-query"; import { getAllRoutes, postReportRoute } from "../api/route"; import { CoreRoute, CoreRoutesList, Route, RouteId } from "../data/types/route"; import { Node, NodeId } from "../data/types/node"; @@ -25,6 +25,8 @@ import { useNavigate } from "react-router"; import { getAllRisks } from "../api/routes"; import { CautionIssueType, DangerIssueType } from "../data/types/enum"; import { CautionIssue, DangerIssue } from "../constant/enum/reportEnum"; +import removeMarkers from "../utils/markers/removeMarkers"; +import useMutationError from "../hooks/useMutationError"; type SelectedMarkerTypes = { type: Markers.CAUTION | Markers.DANGER; @@ -45,18 +47,20 @@ export default function ReportRoutePage() { const newPolyLine = useRef(); const [isActive, setIsActive] = useState(false); - const [dangerMarkers, setDangerMarkers] = useState<{ element: AdvancedMarker, routeId: RouteId }[]>([]); + const [dangerMarkers, setDangerMarkers] = useState<{ element: AdvancedMarker; routeId: RouteId }[]>([]); const [isDangerAcitve, setIsDangerActive] = useState(false); - const [cautionMarkers, setCautionMarkers] = useState<{ element: AdvancedMarker, routeId: RouteId }[]>([]); + const [cautionMarkers, setCautionMarkers] = useState<{ element: AdvancedMarker; routeId: RouteId }[]>([]); const [isCautionAcitve, setIsCautionActive] = useState(false); const { university } = useUniversityInfo(); useRedirectUndefined([university]); const [selectedMarker, setSelectedMarker] = useState(); - const [SuccessModal, isSuccessOpen, openSuccess, closeSuccess] = useModal(() => { navigate('/map') }); - const [FailModal, isFailOpen, openFail, closeFail] = useModal(); + const [SuccessModal, isSuccessOpen, openSuccess, closeSuccess] = useModal(() => { + navigate("/map"); + }); + const [tempWaypoints, setTempWayPoints] = useState([]); if (!university) return; @@ -66,51 +70,75 @@ export default function ReportRoutePage() { queryKey: ["routes", university.id], queryFn: () => getAllRoutes(university.id), }, - { queryKey: [university.id, 'risks'], queryFn: () => getAllRisks(university.id) }, - ] + { queryKey: [university.id, "risks"], queryFn: () => getAllRisks(university.id) }, + ], }); const [routes, risks] = result; - - const { mutate } = useMutation({ - mutationFn: ({ - startNodeId, - coordinates, - endNodeId, - }: { - startNodeId: NodeId; - endNodeId: NodeId | null; - coordinates: Coord[]; - }) => postReportRoute(university.id, { startNodeId, endNodeId, coordinates }), - onSuccess: () => { - openSuccess(); - if (newPoints.element) newPoints.element.map = null; - if (originPoint.current) { - originPoint.current.element.map = null; - originPoint.current = undefined; - } - setNewPoints({ - element: null, - coords: [], - }); - if (newPolyLine.current) newPolyLine.current.setPath([]); - queryClient.invalidateQueries({ queryKey: ["routes", university.id], }); + const [ErrorModal, { mutate, status }] = useMutationError( + { + mutationFn: ({ + startNodeId, + coordinates, + endNodeId, + }: { + startNodeId: NodeId; + endNodeId: NodeId | null; + coordinates: Coord[]; + }) => postReportRoute(university.id, { startNodeId, endNodeId, coordinates }), + onSuccess: () => { + openSuccess(); + if (newPoints.element) newPoints.element.map = null; + if (originPoint.current) { + originPoint.current.element.map = null; + originPoint.current = undefined; + } + setNewPoints({ + element: null, + coords: [], + }); + setTempWayPoints((prevMarkers) => { + removeMarkers(prevMarkers); + return []; + }); + if (newPolyLine.current) newPolyLine.current.setPath([]); + queryClient.invalidateQueries({ queryKey: ["routes", university.id] }); + }, + onError: () => { + if (newPoints.element) newPoints.element.map = null; + if (originPoint.current) { + originPoint.current.element.map = null; + originPoint.current = undefined; + } + setNewPoints({ + element: null, + coords: [], + }); + setTempWayPoints((prevMarkers) => { + removeMarkers(prevMarkers); + return []; + }); + if (newPolyLine.current) newPolyLine.current.setPath([]); + }, }, - onError: () => { - openFail(); - if (newPoints.element) newPoints.element.map = null; - if (originPoint.current) { - originPoint.current.element.map = null; - originPoint.current = undefined; - } - setNewPoints({ - element: null, - coords: [], - }); - if (newPolyLine.current) newPolyLine.current.setPath([]); + undefined, + { + fallback: { + 400: { + mainTitle: "경로를 생성하는데 실패하였습니다!", + subTitle: [ + "선택하신 경로는 생성이 불가능합니다.", + "선 위에서 시작하여, 빈 곳을 이어주시기 바랍니다.", + ], + }, + 404: { + mainTitle: "경로를 생성하는데 실패하였습니다!", + subTitle: ["선택하신 점이 관리자에 의해 제거되었습니다.", "다른 점을 선택하여 제보 부탁드립니다."], + }, + }, }, - }); + ); const addRiskMarker = () => { if (AdvancedMarker === null || map === null) return; @@ -120,7 +148,7 @@ export default function ReportRoutePage() { const dangerMarkersWithId: { routeId: RouteId; element: AdvancedMarker }[] = []; for (const route of dangerRoutes) { - const { routeId, node1, node2, dangerTypes } = route; + const { routeId, node1, node2, dangerFactors } = route; const type = Markers.DANGER; const dangerMarker = createAdvancedMarker( @@ -137,10 +165,10 @@ export default function ReportRoutePage() { return { type: Markers.DANGER, element: dangerMarker, - factors: dangerTypes, - id: routeId - } - }) + factors: dangerFactors, + id: routeId, + }; + }); }, ); @@ -152,7 +180,7 @@ export default function ReportRoutePage() { const cautionMarkersWithId: { routeId: RouteId; element: AdvancedMarker }[] = []; for (const route of cautionRoutes) { - const { routeId, node1, node2, cautionTypes } = route; + const { routeId, node1, node2, cautionFactors } = route; const type = Markers.CAUTION; const cautionMarker = createAdvancedMarker( @@ -169,10 +197,10 @@ export default function ReportRoutePage() { return { type: Markers.CAUTION, element: cautionMarker, - factors: cautionTypes, - id: routeId - } - }) + factors: cautionFactors, + id: routeId, + }; + }); }, ); cautionMarkersWithId.push({ routeId, element: cautionMarker }); @@ -184,14 +212,22 @@ export default function ReportRoutePage() { const toggleCautionButton = () => { if (!map) return; setIsCautionActive((isActive) => { - toggleMarkers(!isActive, cautionMarkers.map(marker => marker.element), map); + toggleMarkers( + !isActive, + cautionMarkers.map((marker) => marker.element), + map, + ); return !isActive; }); }; const toggleDangerButton = () => { if (!map) return; setIsDangerActive((isActive) => { - toggleMarkers(!isActive, dangerMarkers.map(marker => marker.element), map); + toggleMarkers( + !isActive, + dangerMarkers.map((marker) => marker.element), + map, + ); return !isActive; }); }; @@ -213,13 +249,11 @@ export default function ReportRoutePage() { if (!originPoint.current) return; - if ("nodeId" in lastPoint) { - mutate({ - startNodeId: originPoint.current.point.nodeId, - endNodeId: lastPoint.nodeId, - coordinates: subNodes, - }); - } else mutate({ startNodeId: originPoint.current.point.nodeId, coordinates: subNodes, endNodeId: null }); + mutate({ + startNodeId: originPoint.current.point.nodeId, + endNodeId: "nodeId" in lastPoint ? lastPoint.nodeId : null, + coordinates: subNodes, + }); }; const drawRoute = (coreRouteList: CoreRoutesList) => { @@ -266,6 +300,8 @@ export default function ReportRoutePage() { }), ); + setTempWayPoints((prevMarkers) => [...prevMarkers, tempWaypointMarker]); + if (originPoint.current) { setNewPoints((prevPoints) => { if (prevPoints.element) { @@ -316,6 +352,57 @@ export default function ReportRoutePage() { } }; + /** Undo 함수 + * 되돌리기 버튼을 누를 경우, 마지막에 생성된 점을 제거 + * 기존 점의 개수가 1개, 2개, (0개 혹은 3개 이상) 총 3개의 Case를 나눈다. + * 1개 : originMarker를 제거하고 + * 2개 : , 도착마커를 제거한다. + * 0개 혹은 3개 이상, 도착마커를 이동시킨다. + */ + const undoPoints = () => { + const deleteNode = [...newPoints.coords].pop(); + + if (deleteNode && "nodeId" in deleteNode) { + setTempWayPoints((prevPoints) => { + const lastMarker = prevPoints.slice(-1)[0]; + + lastMarker.map = null; + + return [...prevPoints.slice(0, -1)]; + }); + } + if (newPoints.coords.length === 2) { + setNewPoints((prevPoints) => { + if (prevPoints.element) prevPoints.element.map = null; + return { + element: null, + coords: [prevPoints.coords[0]], + }; + }); + return; + } else if (newPoints.coords.length === 1) { + if (originPoint.current) { + originPoint.current.element.map = null; + } + originPoint.current = undefined; + setNewPoints({ + coords: [], + element: null, + }); + return; + } + + setNewPoints((prevPoints) => { + const tempPoints = prevPoints.coords.slice(0, -1); + const lastPoint = tempPoints.slice(-1)[0]; + if (prevPoints.element) prevPoints.element.position = lastPoint; + return { + element: prevPoints.element, + coords: tempPoints, + }; + }); + }; + useEffect(() => { if (newPolyLine.current) { newPolyLine.current.setPath(newPoints.coords); @@ -331,7 +418,7 @@ export default function ReportRoutePage() { if (map && AdvancedMarker) { map.addListener("click", (e: ClickEvent) => { - setSelectedMarker(undefined) + setSelectedMarker(undefined); if (originPoint.current) { const point = LatLngToLiteral(e.latLng); setNewPoints((prevPoints) => { @@ -364,7 +451,6 @@ export default function ReportRoutePage() { const changeMarkerStyle = (marker: SelectedMarkerTypes | undefined, isSelect: boolean) => { if (!map || !marker) return; - if (isSelect) { if (marker.type === Markers.DANGER) { const key = marker.factors[0] as DangerIssueType; @@ -373,8 +459,7 @@ export default function ReportRoutePage() { title: key && DangerIssue[key], hasTopContent: true, }); - } - else if (marker.type === Markers.CAUTION) { + } else if (marker.type === Markers.CAUTION) { const key = marker.factors[0] as CautionIssueType; marker.element.content = createMarkerElement({ type: marker.type, @@ -409,10 +494,16 @@ export default function ReportRoutePage() {
{isActive && (
- +
)}
+
@@ -427,19 +518,7 @@ export default function ReportRoutePage() {
)} - {isFailOpen && ( - -

경로를 생성하는데 실패하였습니다!

-
-

- 선택하신 경로는 생성이 불가능합니다. -

-

- 선 위에서 시작하여, 빈 곳을 이어주시기 바랍니다. -

-
-
- )} +
); } diff --git a/uniro_frontend/src/pages/universitySearch.tsx b/uniro_frontend/src/pages/universitySearch.tsx index a3d15fd..70e530a 100644 --- a/uniro_frontend/src/pages/universitySearch.tsx +++ b/uniro_frontend/src/pages/universitySearch.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import Input from "../components/customInput"; import UniversityButton from "../components/universityButton"; import Button from "../components/customButton"; @@ -9,18 +9,21 @@ import { getUniversityList } from "../api/search"; import { University } from "../data/types/university"; export default function UniversitySearchPage() { - const { data: universityList, status } = useQuery({ - queryKey: ["university"], - queryFn: getUniversityList, - }); const [selectedUniv, setSelectedUniv] = useState(); const { setUniversity } = useUniversityInfo(); + const [input, setInput] = useState(''); + + const { data: universityList, status } = useQuery({ + queryKey: ["university", input], + queryFn: () => getUniversityList(input), + }); + return (
- {}} placeholder="우리 학교를 검색해보세요" handleVoiceInput={() => {}} /> + setInput(e)} placeholder="우리 학교를 검색해보세요" handleVoiceInput={() => { }} />
    (angle * Math.PI) / 180; +import { Coord } from "../../data/types/coord"; - const dLat = toRad(lat2 - lat1); - const dLon = toRad(lng2 - lng1); +/** 하버사인 공식 */ +export default function distance(point1: Coord, point2: Coord): number { + const R = 6378137; + const { lat: lat_a, lng: lng_a } = point1; + const { lat: lat_b, lng: lng_b } = point2; - const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + const rad_lat_a = toRad(lat_a); + const rad_lng_a = toRad(lng_a); + const rad_lat_b = toRad(lat_b); + const rad_lng_b = toRad(lng_b); - return R * c; + return ( + R * + 2 * + Math.asin( + Math.sqrt( + Math.pow(Math.sin((rad_lat_a - rad_lat_b) / 2), 2) + + Math.cos(rad_lat_a) * Math.cos(rad_lat_b) * Math.pow(Math.sin((rad_lng_a - rad_lng_b) / 2), 2), + ), + ) + ); } + +const toRad = (angle: number) => (angle * Math.PI) / 180; diff --git a/uniro_frontend/src/utils/fetch/fetch.ts b/uniro_frontend/src/utils/fetch/fetch.ts index b31bbf4..d364263 100644 --- a/uniro_frontend/src/utils/fetch/fetch.ts +++ b/uniro_frontend/src/utils/fetch/fetch.ts @@ -1,7 +1,11 @@ +import { BadRequestError, NotFoundError, UnProcessableError } from "../../constant/error"; + export default function Fetch() { const baseURL = import.meta.env.VITE_REACT_SERVER_BASE_URL; const get = async (url: string, params?: Record): Promise => { + console.log("GET : ", url); + const paramsURL = new URLSearchParams( Object.entries(params || {}).map(([key, value]) => [key, String(value)]), ).toString(); @@ -11,13 +15,23 @@ export default function Fetch() { }); if (!response.ok) { - throw new Error(`${response.status}-${response.statusText}`); + if (response.status === 400) { + throw new BadRequestError("Bad Request"); + } else if (response.status === 404) { + throw new NotFoundError("Not Found"); + } else if (response.status === 422) { + throw new UnProcessableError("UnProcessable"); + } else { + throw new Error("UnExpected Error"); + } } return response.json(); }; const post = async (url: string, body?: Record): Promise => { + console.log("POST : ", url); + const response = await fetch(`${baseURL}${url}`, { method: "POST", body: JSON.stringify(body), @@ -27,7 +41,13 @@ export default function Fetch() { }); if (!response.ok) { - throw new Error(`${response.status}-${response.statusText}`); + if (response.status === 400) { + throw new BadRequestError("Bad Request"); + } else if (response.status === 404) { + throw new NotFoundError("Not Found"); + } else { + throw new Error("UnExpected Error"); + } } return response.ok; diff --git a/uniro_frontend/src/utils/interpolate.ts b/uniro_frontend/src/utils/interpolate.ts new file mode 100644 index 0000000..629162c --- /dev/null +++ b/uniro_frontend/src/utils/interpolate.ts @@ -0,0 +1,35 @@ +import { Coord } from "../data/types/coord"; + +function toRad(deg: number) { + return (deg * Math.PI) / 180; +} +function toDeg(rad: number) { + return (rad * 180) / Math.PI; +} + +export function interpolate(point1: Coord, point2: Coord, fraction: number): Coord { + const lat1 = toRad(point1.lat); + const lng1 = toRad(point1.lng); + + const lat2 = toRad(point2.lat); + const lng2 = toRad(point2.lng); + + const angle = Math.acos(Math.sin(lat1) * Math.sin(lat2) + Math.cos(lat1) * Math.cos(lat2) * Math.cos(lng2 - lng1)); + + const sinAngle = Math.sin(angle); + + const A = Math.sin((1 - fraction) * angle) / sinAngle; + const B = Math.sin(fraction * angle) / sinAngle; + + const x = A * Math.cos(lat1) * Math.cos(lng1) + B * Math.cos(lat2) * Math.cos(lng2); + const y = A * Math.cos(lat1) * Math.sin(lng1) + B * Math.cos(lat2) * Math.sin(lng2); + const z = A * Math.sin(lat1) + B * Math.sin(lat2); + + const interpolatedLat = Math.atan2(z, Math.sqrt(x * x + y * y)); + const interpolatedLng = Math.atan2(y, x); + + return { + lat: toDeg(interpolatedLat), + lng: toDeg(interpolatedLng), + }; +} diff --git a/uniro_frontend/src/utils/markers/removeMarkers.ts b/uniro_frontend/src/utils/markers/removeMarkers.ts new file mode 100644 index 0000000..b87752e --- /dev/null +++ b/uniro_frontend/src/utils/markers/removeMarkers.ts @@ -0,0 +1,5 @@ +import { AdvancedMarker } from "../../data/types/marker"; + +export default function removeMarkers(markers: AdvancedMarker[]) { + markers.forEach((marker) => (marker.map = null)); +} diff --git a/uniro_frontend/src/utils/markers/toggleMarkers.ts b/uniro_frontend/src/utils/markers/toggleMarkers.ts index bcfc52a..89e468c 100644 --- a/uniro_frontend/src/utils/markers/toggleMarkers.ts +++ b/uniro_frontend/src/utils/markers/toggleMarkers.ts @@ -1,14 +1,11 @@ import { AdvancedMarker } from "../../data/types/marker"; +import removeMarkers from "./removeMarkers"; /** Marker 보이기 안보이기 토글 */ export default function toggleMarkers(isActive: boolean, markers: AdvancedMarker[], map: google.maps.Map) { if (isActive) { - for (const marker of markers) { - marker.map = map; - } + markers.forEach((marker) => (marker.map = map)); } else { - for (const marker of markers) { - marker.map = null; - } + removeMarkers(markers); } } diff --git a/uniro_frontend/src/utils/navigation/formatDistance.ts b/uniro_frontend/src/utils/navigation/formatDistance.ts index f2e81c1..80da505 100644 --- a/uniro_frontend/src/utils/navigation/formatDistance.ts +++ b/uniro_frontend/src/utils/navigation/formatDistance.ts @@ -3,6 +3,10 @@ export const formatDistance = (distance: number) => { return `${Math.ceil(distance * 1000) / 1000}m`; } + if (distance < 1000) { + return `${Math.ceil(distance)}m`; + } + return distance >= 1000 ? `${(distance / 1000).toFixed(1)} km` // 1000m 이상이면 km 단위로 변환 : `${distance} m`; // 1000m 미만이면 m 단위 유지 diff --git a/uniro_frontend/src/utils/polylines/createSubnodes.ts b/uniro_frontend/src/utils/polylines/createSubnodes.ts index 4560fd3..c17af23 100644 --- a/uniro_frontend/src/utils/polylines/createSubnodes.ts +++ b/uniro_frontend/src/utils/polylines/createSubnodes.ts @@ -2,6 +2,7 @@ import { EDGE_LENGTH } from "../../constant/edge"; import { Coord } from "../../data/types/coord"; import { LatLngToLiteral } from "../coordinates/coordinateTransform"; import distance from "../coordinates/distance"; +import { interpolate } from "../interpolate"; /** 구면 보간 없이 계산한 결과 */ export default function createSubNodes(polyLine: google.maps.Polyline): Coord[] { @@ -12,19 +13,11 @@ export default function createSubNodes(polyLine: google.maps.Polyline): Coord[] const subEdgesCount = Math.ceil(length / EDGE_LENGTH); - const interval = { - lat: (endNode.lat - startNode.lat) / subEdgesCount, - lng: (endNode.lng - startNode.lng) / subEdgesCount, - }; + const subNodes: Coord[] = [startNode]; - const subNodes = []; - - for (let i = 0; i < subEdgesCount; i++) { - const subNode: Coord = { - lat: startNode.lat + interval.lat * i, - lng: startNode.lng + interval.lng * i, - }; - subNodes.push(subNode); + for (let i = 1; i < subEdgesCount; i++) { + const fraction = i / subEdgesCount; + subNodes.push(interpolate(startNode, endNode, fraction)); } subNodes.push(endNode);