diff --git a/.github/workflows/fe-deploy.yml b/.github/workflows/fe-deploy.yml index 86395ac..17836b4 100644 --- a/.github/workflows/fe-deploy.yml +++ b/.github/workflows/fe-deploy.yml @@ -1,66 +1,66 @@ name: FE CI / CD on: - push: - branches: - - fe + push: + branches: + - fe jobs: - CI: - runs-on: ubuntu-latest + CI: + runs-on: ubuntu-latest - env: - GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} - IMAGE_NAME: uniro-fe - IMAGE_TAG: ${{ github.sha }} + env: + GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} + IMAGE_NAME: uniro-fe + IMAGE_TAG: ${{ github.sha }} - steps: - - name: 코드 체크아웃 - uses: actions/checkout@v4 + steps: + - name: 코드 체크아웃 + uses: actions/checkout@v4 - - name: Google Cloud SDK 설정 - uses: "google-github-actions/auth@v2" - with: - credentials_json: ${{ secrets.GCP_SA_KEY }} + - name: Google Cloud SDK 설정 + uses: "google-github-actions/auth@v2" + with: + credentials_json: ${{ secrets.GCP_SA_KEY }} - - name: Docker를 위한 gcloud 인증 설정 - run: gcloud auth configure-docker --quiet + - name: Docker를 위한 gcloud 인증 설정 + run: gcloud auth configure-docker --quiet - - name: Create .env from secret - run: | - echo "${{ secrets.FE_ENV }}" > uniro_frontend/.env + - name: Create .env from secret + run: | + echo "${{ secrets.FE_ENV }}" > uniro_frontend/.env - - name: Docker 이미지 빌드 및 푸시 - run: | - docker build -t gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} -f uniro_frontend/Dockerfile . - docker push gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + - name: Docker 이미지 빌드 및 푸시 + run: | + docker build -t gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} -f uniro_frontend/Dockerfile . + docker push gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} - CD: - runs-on: ubuntu-latest - needs: CI + CD: + runs-on: ubuntu-latest + needs: CI - env: - GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} - IMAGE_NAME: uniro-fe - IMAGE_TAG: ${{ github.sha }} - DEPLOY_PATH: ${{ secrets.DEPLOY_SERVER_PATH }} + env: + GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} + IMAGE_NAME: uniro-fe + IMAGE_TAG: ${{ github.sha }} + DEPLOY_PATH: ${{ secrets.DEPLOY_SERVER_PATH }} - steps: - - name: 배포 서버에 SSH로 연결하여 배포 - uses: appleboy/ssh-action@v0.1.5 - with: - host: ${{ secrets.DEPLOY_SERVER_HOST }} - username: ${{ secrets.DEPLOY_SERVER_USER }} - key: ${{ secrets.DEPLOY_SSH_KEY }} - envs: GCP_PROJECT_ID, IMAGE_NAME, IMAGE_TAG, DEPLOY_PATH, TEST - script: | - cd ${DEPLOY_PATH} - sudo docker ps -a --format '{{.ID}} {{.Names}}' \ - | grep -v 'nginx-container' \ - | awk '{print $1}' \ - | xargs -r sudo docker stop || true - sudo docker rm $(sudo docker ps -a -q) || true - sudo docker login -u _json_key --password-stdin https://gcr.io <<< '${{ secrets.GCP_SA_KEY }}' - sudo docker pull gcr.io/${GCP_PROJECT_ID}/${IMAGE_NAME}:${IMAGE_TAG} - sudo docker run -d --name ${IMAGE_NAME} -p 3000:3000 gcr.io/${GCP_PROJECT_ID}/${IMAGE_NAME}:${IMAGE_TAG} - sudo docker network connect nginx_app-network ${IMAGE_NAME} + steps: + - name: 배포 서버에 SSH로 연결하여 배포 + uses: appleboy/ssh-action@v0.1.5 + with: + host: ${{ secrets.DEPLOY_SERVER_HOST }} + username: ${{ secrets.DEPLOY_SERVER_USER }} + key: ${{ secrets.DEPLOY_SSH_KEY }} + envs: GCP_PROJECT_ID, IMAGE_NAME, IMAGE_TAG, DEPLOY_PATH, TEST + script: | + cd ${DEPLOY_PATH} + sudo docker ps -a --format '{{.ID}} {{.Names}}' \ + | egrep -v 'nginx-container|uniro-backoffice' \ + | awk '{print $1}' \ + | xargs -r sudo docker stop || true + sudo docker rm $(sudo docker ps -a -q) || true + sudo docker login -u _json_key --password-stdin https://gcr.io <<< '${{ secrets.GCP_SA_KEY }}' + sudo docker pull gcr.io/${GCP_PROJECT_ID}/${IMAGE_NAME}:${IMAGE_TAG} + sudo docker run -d --name ${IMAGE_NAME} -p 3000:3000 gcr.io/${GCP_PROJECT_ID}/${IMAGE_NAME}:${IMAGE_TAG} + sudo docker network connect nginx_app-network ${IMAGE_NAME} diff --git a/uniro_frontend/src/App.tsx b/uniro_frontend/src/App.tsx index deda414..c0face0 100644 --- a/uniro_frontend/src/App.tsx +++ b/uniro_frontend/src/App.tsx @@ -8,6 +8,7 @@ import BuildingSearchPage from "./pages/buildingSearch"; import NavigationResultPage from "./pages/navigationResult"; import ReportRoutePage from "./pages/reportRoute"; import ReportForm from "./pages/reportForm"; +import ReportHazardPage from "./pages/reportHazard"; function App() { return ( @@ -19,7 +20,8 @@ function App() { } /> } /> } /> - } /> + } /> + } /> ); } diff --git a/uniro_frontend/src/assets/markers/report.svg b/uniro_frontend/src/assets/markers/report.svg new file mode 100644 index 0000000..6fd82e6 --- /dev/null +++ b/uniro_frontend/src/assets/markers/report.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/uniro_frontend/src/constant/enum/markerEnum.ts b/uniro_frontend/src/constant/enum/markerEnum.ts index 8fc891c..9487ca9 100644 --- a/uniro_frontend/src/constant/enum/markerEnum.ts +++ b/uniro_frontend/src/constant/enum/markerEnum.ts @@ -7,4 +7,5 @@ export const enum Markers { SELECTED_BUILDING = "selectedBuilding", WAYPOINT = "waypoint", NUMBERED_WAYPOINT = "numberedWayPoint", + REPORT = "report", } diff --git a/uniro_frontend/src/constant/enum/messageEnum.ts b/uniro_frontend/src/constant/enum/messageEnum.ts new file mode 100644 index 0000000..e591b49 --- /dev/null +++ b/uniro_frontend/src/constant/enum/messageEnum.ts @@ -0,0 +1,6 @@ +export enum ReportHazardMessage { + DEFAULT = "선 위를 눌러 제보할 지점을 선택하세요", + CREATE = "이 지점으로 새로운 제보를 진행할까요?", + UPDATE = "이 지점에 제보된 기존 정보를 바꿀까요?", + ERROR = "선 위에서만 선택 가능해요", +} diff --git a/uniro_frontend/src/data/types/marker.d.ts b/uniro_frontend/src/data/types/marker.d.ts index 06c1bc4..4782ede 100644 --- a/uniro_frontend/src/data/types/marker.d.ts +++ b/uniro_frontend/src/data/types/marker.d.ts @@ -10,4 +10,10 @@ export type MarkerTypes = | Markers.ORIGIN | Markers.NUMBERED_WAYPOINT | Markers.WAYPOINT - | Markers.SELECTED_BUILDING; + | Markers.SELECTED_BUILDING + | Markers.REPORT; + +export type MarkerTypesWithElement = { + type: MarkerTypes; + element: AdvancedMarker; +}; diff --git a/uniro_frontend/src/hooks/useReportHazard.ts b/uniro_frontend/src/hooks/useReportHazard.ts new file mode 100644 index 0000000..79aea93 --- /dev/null +++ b/uniro_frontend/src/hooks/useReportHazard.ts @@ -0,0 +1,22 @@ +import { create } from "zustand"; + +interface ReportedHazardEdge { + reportType: "CREATE" | "UPDATE" | undefined; + setReportType: (type: "CREATE" | "UPDATE") => void; + startNode: google.maps.LatLng | google.maps.LatLngLiteral | undefined; + endNode: google.maps.LatLng | google.maps.LatLngLiteral | undefined; + setNode: ( + point1: google.maps.LatLng | google.maps.LatLngLiteral, + point2: google.maps.LatLng | google.maps.LatLngLiteral, + ) => void; +} + +const useReportHazard = create((set) => ({ + reportType: undefined, + setReportType: (newType) => set(() => ({ reportType: newType })), + startNode: undefined, + endNode: undefined, + setNode: (point1, point2) => set(() => ({ startNode: point1, endNode: point2 })), +})); + +export default useReportHazard; diff --git a/uniro_frontend/src/pages/reportForm.tsx b/uniro_frontend/src/pages/reportForm.tsx index 436c444..2e6e856 100644 --- a/uniro_frontend/src/pages/reportForm.tsx +++ b/uniro_frontend/src/pages/reportForm.tsx @@ -11,6 +11,7 @@ import Button from "../components/customButton"; import useScrollControl from "../hooks/useScrollControl"; import useModal from "../hooks/useModal"; +import useReportHazard from "../hooks/useReportHazard"; const ReportForm = () => { useScrollControl(); @@ -28,7 +29,11 @@ const ReportForm = () => { const [FailModal, isFailOpen, openFail, closeFail] = useModal(); const [SuccessModal, isSuccessOpen, openSuccess, closeSuccess] = useModal(); + const { reportType, startNode, endNode } = useReportHazard(); + useEffect(() => { + console.log(reportType, startNode, endNode); + setTimeout(() => { setReportMode("update"); }, 2000); diff --git a/uniro_frontend/src/pages/reportHazard.tsx b/uniro_frontend/src/pages/reportHazard.tsx new file mode 100644 index 0000000..6e7adc0 --- /dev/null +++ b/uniro_frontend/src/pages/reportHazard.tsx @@ -0,0 +1,207 @@ +import { useEffect, useState } from "react"; +import useMap from "../hooks/useMap"; +import { mockNavigationRoute } from "../data/mock/hanyangRoute"; +import createAdvancedMarker from "../utils/markers/createAdvanedMarker"; +import createMarkerElement from "../components/map/mapMarkers"; +import { RouteEdge } from "../data/types/edge"; +import { Markers } from "../constant/enum/markerEnum"; +import { mockHazardEdges } from "../data/mock/hanyangHazardEdge"; +import { ClickEvent } from "../data/types/event"; +import createSubNodes from "../utils/polylines/createSubnodes"; +import { LatLngToLiteral } from "../utils/coordinates/coordinateTransform"; +import findNearestSubEdge from "../utils/polylines/findNearestEdge"; +import centerCoordinate from "../utils/coordinates/centerCoordinate"; +import { MarkerTypesWithElement } from "../data/types/marker"; +import Button from "../components/customButton"; +import { Link } from "react-router"; +import { ReportHazardMessage } from "../constant/enum/messageEnum"; +import { motion } from "framer-motion"; +import useReportHazard from "../hooks/useReportHazard"; +import AnimatedContainer from "../container/animatedContainer"; + +interface reportMarkerTypes extends MarkerTypesWithElement { + edge: [google.maps.LatLng | google.maps.LatLngLiteral, google.maps.LatLng | google.maps.LatLngLiteral]; +} + +export default function ReportHazardPage() { + const { map, mapRef, AdvancedMarker, Polyline } = useMap({ zoom: 18, minZoom: 17 }); + const [reportMarker, setReportMarker] = useState(); + const { setReportType, setNode } = useReportHazard(); + + const [message, setMessage] = useState(ReportHazardMessage.DEFAULT); + + const resetMarker = (prevMarker: MarkerTypesWithElement) => { + if (prevMarker.type === Markers.REPORT) { + prevMarker.element.map = null; + return; + } else { + prevMarker.element.content = createMarkerElement({ type: prevMarker.type }); + } + }; + + const addHazardMarker = () => { + if (AdvancedMarker === null || map === null) return; + for (const edge of mockHazardEdges) { + const { id, startNode, endNode, dangerFactors, cautionFactors } = edge; + + const type = dangerFactors ? Markers.DANGER : Markers.CAUTION; + + const hazardMarker = createAdvancedMarker( + AdvancedMarker, + map, + new google.maps.LatLng({ + lat: (startNode.lat + endNode.lat) / 2, + lng: (startNode.lng + endNode.lng) / 2, + }), + createMarkerElement({ type }), + () => { + hazardMarker.content = createMarkerElement({ + type, + title: dangerFactors ? dangerFactors[0] : cautionFactors && cautionFactors[0], + hasTopContent: true, + }); + setMessage(ReportHazardMessage.UPDATE); + setReportMarker((prevMarker) => { + if (prevMarker) { + resetMarker(prevMarker); + } + + return { + type, + element: hazardMarker, + edge: [startNode, endNode], + }; + }); + }, + ); + } + }; + + const drawRoute = (routes: RouteEdge[]) => { + if (!Polyline || !AdvancedMarker || !map) return; + + for (const route of routes) { + const coreNode = route.endNode; + + createAdvancedMarker( + AdvancedMarker, + map, + coreNode, + createMarkerElement({ type: Markers.WAYPOINT, className: "translate-waypoint" }), + ); + + const routePolyLine = new Polyline({ + map: map, + path: [route.startNode, route.endNode], + strokeColor: "#808080", + }); + + routePolyLine.addListener("click", (e: ClickEvent) => { + const subNodes = createSubNodes(routePolyLine); + + const edges = subNodes + .map( + (node, idx) => + [node, subNodes[idx + 1]] as [google.maps.LatLngLiteral, google.maps.LatLngLiteral], + ) + .slice(0, -1); + + const point = LatLngToLiteral(e.latLng); + + const { edge: nearestEdge, point: nearestPoint } = findNearestSubEdge(edges, point); + + const newReportMarker = createAdvancedMarker( + AdvancedMarker, + map, + centerCoordinate(nearestEdge[0], nearestEdge[1]), + createMarkerElement({ + type: Markers.REPORT, + className: "translate-routemarker", + hasAnimation: true, + }), + ); + + setMessage(ReportHazardMessage.CREATE); + + setReportMarker((prevMarker) => { + if (prevMarker) { + resetMarker(prevMarker); + } + + return { + type: Markers.REPORT, + element: newReportMarker, + edge: nearestEdge, + }; + }); + }); + } + + createAdvancedMarker( + AdvancedMarker, + map, + routes[0].startNode, + createMarkerElement({ type: Markers.WAYPOINT, className: "translate-waypoint" }), + ); + }; + + const reportHazard = () => { + if (!reportMarker) return; + + setReportType(reportMarker.type === Markers.REPORT ? "CREATE" : "UPDATE"); + + setNode(...reportMarker.edge); + }; + + useEffect(() => { + drawRoute(mockNavigationRoute.route); + addHazardMarker(); + + if (map) { + map.addListener("click", () => { + setReportMarker((prevMarker) => { + if (prevMarker) { + setMessage(ReportHazardMessage.DEFAULT); + resetMarker(prevMarker); + } else setMessage(ReportHazardMessage.ERROR); + + return undefined; + }); + }); + } + }, [map, AdvancedMarker, Polyline]); + + useEffect(() => { + if (message === ReportHazardMessage.ERROR) { + setTimeout(() => { + setMessage(ReportHazardMessage.DEFAULT); + }, 1000); + } + }, [message]); + + return ( +
+
+ + {message} + +
+
+ + + + + +
+ ); +}