diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..40b6334 Binary files /dev/null and b/.DS_Store differ diff --git a/.github/.DS_Store b/.github/.DS_Store new file mode 100644 index 0000000..0a18455 Binary files /dev/null and b/.github/.DS_Store differ diff --git a/.github/workflows/be-cd.yml b/.github/workflows/be-cd.yml new file mode 100644 index 0000000..a490f33 --- /dev/null +++ b/.github/workflows/be-cd.yml @@ -0,0 +1,70 @@ +name: Uniro-server CD + +on: + push: + branches: + - be + +jobs: + uniro-ci: + name: Build & Push Docker Image + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + + - name: Create application.properties from secret + run: | + echo "${{ secrets.BE_SPRING_APPLICATION_SECRET }}" > ./uniro_backend/src/main/resources/application.properties + shell: bash + + - name: Build Spring Boot Application + run: | + cd uniro_backend + ./gradlew clean build -x test + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Docker Hub Login + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_LOGIN_USERNAME }} + password: ${{ secrets.DOCKERHUB_LOGIN_ACCESSTOKEN }} + + - name: Build & Push Multi-Arch Docker Image + run: | + cd uniro_backend + docker buildx create --use + docker buildx build --platform linux/amd64,linux/arm64 -t uniro5th/uniro-docker-repo:develop --build-arg SPRING_PROFILE=dev --push . + + deploy-run: + name: Deploy to Server + needs: uniro-ci + runs-on: ubuntu-latest + steps: + - name: Run Docker Container on Server + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.BE_SERVER_IP }} + username: ${{ secrets.BE_SERVER_USER }} + key: ${{ secrets.BE_SERVER_KEY }} + script: | + cd ~/myapp + chmod +x ./deploy.sh + ./deploy.sh diff --git a/.github/workflows/be-ci.yml b/.github/workflows/be-ci.yml new file mode 100644 index 0000000..c724bfc --- /dev/null +++ b/.github/workflows/be-ci.yml @@ -0,0 +1,42 @@ +name: Uniro-server CI + +on: + pull_request: + branches: + - be + +jobs: + build-springboot: + name: Build and analyze (SpringBoot) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + + - name: Create application.properties from secret + run: | + echo "${{ secrets.BE_SPRING_APPLICATION_SECRET }}" > ./uniro_backend/src/main/resources/application.properties + shell: bash + + - name: Debug application.properties + run: cat ./uniro_backend/src/main/resources/application.properties + + - name: Build and analyze (SpringBoot) + run: | + cd uniro_backend + ./gradlew clean build -x test \ No newline at end of file diff --git a/.github/workflows/fe-admin-deploy.yml b/.github/workflows/fe-admin-deploy.yml new file mode 100644 index 0000000..f50b2fc --- /dev/null +++ b/.github/workflows/fe-admin-deploy.yml @@ -0,0 +1,66 @@ +name: FE ADMIN CI / CD + +on: + push: + branches: + - fe + +jobs: + CI: + runs-on: ubuntu-latest + + env: + GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} + IMAGE_NAME: uniro-backoffice + IMAGE_TAG: ${{ github.sha }} + + steps: + - name: 코드 체크아웃 + uses: actions/checkout@v4 + + - 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: Create .env from secret + run: | + echo "${{ secrets.FE_ENV }}" > uniro_admin_frontend/.env + + - name: Docker 이미지 빌드 및 푸시 + run: | + docker build -t gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} -f uniro_admin_frontend/Dockerfile . + docker push gcr.io/${{ env.GCP_PROJECT_ID }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} + + CD: + runs-on: ubuntu-latest + needs: CI + + env: + GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} + IMAGE_NAME: uniro-backoffice + 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}}' \ + | egrep -v 'nginx-container|uniro-fe' \ + | 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 3001:3000 gcr.io/${GCP_PROJECT_ID}/${IMAGE_NAME}:${IMAGE_TAG} + sudo docker network connect nginx_app-network ${IMAGE_NAME} 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_admin_frontend/.gitignore b/uniro_admin_frontend/.gitignore new file mode 100644 index 0000000..50c8dda --- /dev/null +++ b/uniro_admin_frontend/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.env diff --git a/uniro_admin_frontend/Dockerfile b/uniro_admin_frontend/Dockerfile new file mode 100644 index 0000000..7550b0e --- /dev/null +++ b/uniro_admin_frontend/Dockerfile @@ -0,0 +1,15 @@ +FROM node:22-slim AS build + +WORKDIR /app +COPY ./uniro_admin_frontend . + +RUN npm install --legacy-peer-deps --no-audit && npm run build + +FROM nginx + +COPY uniro_admin_frontend/nginx/nginx.conf /etc/nginx/nginx.conf +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 3000 + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/uniro_admin_frontend/README.md b/uniro_admin_frontend/README.md new file mode 100644 index 0000000..74872fd --- /dev/null +++ b/uniro_admin_frontend/README.md @@ -0,0 +1,50 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default tseslint.config({ + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) +``` + +- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` +- Optionally add `...tseslint.configs.stylisticTypeChecked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: + +```js +// eslint.config.js +import react from 'eslint-plugin-react' + +export default tseslint.config({ + // Set the react version + settings: { react: { version: '18.3' } }, + plugins: { + // Add the react plugin + react, + }, + rules: { + // other rules... + // Enable its recommended rules + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + }, +}) +``` diff --git a/uniro_admin_frontend/eslint.config.js b/uniro_admin_frontend/eslint.config.js new file mode 100644 index 0000000..092408a --- /dev/null +++ b/uniro_admin_frontend/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/uniro_admin_frontend/index.html b/uniro_admin_frontend/index.html new file mode 100644 index 0000000..7347347 --- /dev/null +++ b/uniro_admin_frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + UNIRO ADMIN + + +
+ + + diff --git a/uniro_admin_frontend/nginx/nginx.conf b/uniro_admin_frontend/nginx/nginx.conf new file mode 100644 index 0000000..eb2f7da --- /dev/null +++ b/uniro_admin_frontend/nginx/nginx.conf @@ -0,0 +1,19 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server { + listen 3000; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri /index.html; + } + } +} \ No newline at end of file diff --git a/uniro_admin_frontend/package-lock.json b/uniro_admin_frontend/package-lock.json new file mode 100644 index 0000000..ceaafc1 --- /dev/null +++ b/uniro_admin_frontend/package-lock.json @@ -0,0 +1,6271 @@ +{ + "name": "uniro_admin_frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "uniro_admin_frontend", + "version": "0.0.0", + "dependencies": { + "@googlemaps/js-api-loader": "^1.16.8", + "@tailwindcss/vite": "^4.0.0", + "@tanstack/react-query": "^5.66.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router": "^7.1.3", + "tailwindcss": "^4.0.0", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@types/google.maps": "^3.58.1", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.19.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.18", + "globals": "^15.14.0", + "prettier": "^3.4.2", + "typescript": "~5.6.2", + "typescript-eslint": "^8.18.2", + "vite": "^6.0.5", + "vite-plugin-svgr": "^4.3.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.7.tgz", + "integrity": "sha512-SRijHmF0PSPgLIBYlWnG0hyeJLwXE2CgpsXaMOrtt2yp9/86ALw6oUlj9KYuZ0JN07T4eBMVIW4li/9S1j2BGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.7", + "@babel/parser": "^7.26.7", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.26.7", + "@babel/types": "^7.26.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", + "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", + "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.26.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", + "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", + "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", + "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", + "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", + "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.10.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@googlemaps/js-api-loader": { + "version": "1.16.8", + "resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.8.tgz", + "integrity": "sha512-CROqqwfKotdO6EBjZO/gQGVTbeDps5V7Mt9+8+5Q+jTg5CRMi3Ii/L9PmV3USROrt2uWxtGzJHORmByxyo9pSQ==", + "license": "Apache-2.0" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.0.tgz", + "integrity": "sha512-Eeao7ewDq79jVEsrtWIj5RNqB8p2knlm9fhR6uJ2gqP7UfbLrTrxevudVrEPDM7Wkpn/HpRC2QfazH7MXLz3vQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.0.tgz", + "integrity": "sha512-yVh0Kf1f0Fq4tWNf6mWcbQBCLDpDrDEl88lzPgKhrgTcDrTtlmun92ywEF9dCjmYO3EFiSuJeeo9cYRxl2FswA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.0.tgz", + "integrity": "sha512-gCs0ErAZ9s0Osejpc3qahTsqIPUDjSKIyxK/0BGKvL+Tn0n3Kwvj8BrCv7Y5sR1Ypz1K2qz9Ny0VvkVyoXBVUQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.0.tgz", + "integrity": "sha512-aIB5Anc8hngk15t3GUkiO4pv42ykXHfmpXGS+CzM9CTyiWyT8HIS5ygRAy7KcFb/wiw4Br+vh1byqcHRTfq2tQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.0.tgz", + "integrity": "sha512-kpdsUdMlVJMRMaOf/tIvxk8TQdzHhY47imwmASOuMajg/GXpw8GKNd8LNwIHE5Yd1onehNpcUB9jHY6wgw9nHQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.0.tgz", + "integrity": "sha512-D0RDyHygOBCQiqookcPevrvgEarN0CttBecG4chOeIYCNtlKHmf5oi5kAVpXV7qs0Xh/WO2RnxeicZPtT50V0g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.0.tgz", + "integrity": "sha512-mCIw8j5LPDXmCOW8mfMZwT6F/Kza03EnSr4wGYEswrEfjTfVsFOxvgYfuRMxTuUF/XmRb9WSMD5GhCWDe2iNrg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.0.tgz", + "integrity": "sha512-AwwldAu4aCJPob7zmjuDUMvvuatgs8B/QiVB0KwkUarAcPB3W+ToOT+18TQwY4z09Al7G0BvCcmLRop5zBLTag==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.0.tgz", + "integrity": "sha512-e7kDUGVP+xw05pV65ZKb0zulRploU3gTu6qH1qL58PrULDGxULIS0OSDQJLH7WiFnpd3ZKUU4VM3u/Z7Zw+e7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.0.tgz", + "integrity": "sha512-SXYJw3zpwHgaBqTXeAZ31qfW/v50wq4HhNVvKFhRr5MnptRX2Af4KebLWR1wpxGJtLgfS2hEPuALRIY3LPAAcA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.0.tgz", + "integrity": "sha512-e5XiCinINCI4RdyU3sFyBH4zzz7LiQRvHqDtRe9Dt8o/8hTBaYpdPimayF00eY2qy5j4PaaWK0azRgUench6WQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.0.tgz", + "integrity": "sha512-3SWN3e0bAsm9ToprLFBSro8nJe6YN+5xmB11N4FfNf92wvLye/+Rh5JGQtKOpwLKt6e61R1RBc9g+luLJsc23A==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.0.tgz", + "integrity": "sha512-B1Oqt3GLh7qmhvfnc2WQla4NuHlcxAD5LyueUi5WtMc76ZWY+6qDtQYqnxARx9r+7mDGfamD+8kTJO0pKUJeJA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.0.tgz", + "integrity": "sha512-UfUCo0h/uj48Jq2lnhX0AOhZPSTAq3Eostas+XZ+GGk22pI+Op1Y6cxQ1JkUuKYu2iU+mXj1QjPrZm9nNWV9rg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.0.tgz", + "integrity": "sha512-chZLTUIPbgcpm+Z7ALmomXW8Zh+wE2icrG+K6nt/HenPLmtwCajhQC5flNSk1Xy5EDMt/QAOz2MhzfOfJOLSiA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.0.tgz", + "integrity": "sha512-jo0UolK70O28BifvEsFD/8r25shFezl0aUk2t0VJzREWHkq19e+pcLu4kX5HiVXNz5qqkD+aAq04Ct8rkxgbyQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.0.tgz", + "integrity": "sha512-Vmg0NhAap2S54JojJchiu5An54qa6t/oKT7LmDaWggpIcaiL8WcWHEN6OQrfTdL6mQ2GFyH7j2T5/3YPEDOOGA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.0.tgz", + "integrity": "sha512-CV2aqhDDOsABKHKhNcs1SZFryffQf8vK2XrxP6lxC99ELZAdvsDgPklIBfd65R8R+qvOm1SmLaZ/Fdq961+m7A==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.0.tgz", + "integrity": "sha512-g2ASy1QwHP88y5KWvblUolJz9rN+i4ZOsYzkEwcNfaNooxNUXG+ON6F5xFo0NIItpHqxcdAyls05VXpBnludGw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.3.tgz", + "integrity": "sha512-QsVJokOl0pJ4AbJV33D2npvLcHGPWi5MOSZtrtE0GT3tSx+3D0JE2lokLA8yHS1x3oCY/3IyRyy7XX6tmzid7A==", + "license": "MIT", + "dependencies": { + "enhanced-resolve": "^5.18.0", + "jiti": "^2.4.2", + "tailwindcss": "4.0.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.3.tgz", + "integrity": "sha512-FFcp3VNvRjjmFA39ORM27g2mbflMQljhvM7gxBAujHxUy4LXlKa6yMF9wbHdTbPqTONiCyyOYxccvJyVyI/XBg==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.0.3", + "@tailwindcss/oxide-darwin-arm64": "4.0.3", + "@tailwindcss/oxide-darwin-x64": "4.0.3", + "@tailwindcss/oxide-freebsd-x64": "4.0.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.0.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.0.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.0.3", + "@tailwindcss/oxide-linux-x64-musl": "4.0.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.0.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.0.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.3.tgz", + "integrity": "sha512-S8XOTQuMnpijZRlPm5HBzPJjZ28quB+40LSRHjRnQF6rRYKsvpr1qkY7dfwsetNdd+kMLOMDsvmuT8WnqqETvg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.3.tgz", + "integrity": "sha512-smrY2DpzhXvgDhZtQlYAl8+vxJ04lv2/64C1eiRxvsRT2nkw/q+zA1/eAYKvUHat6cIuwqDku3QucmrUT6pCeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.3.tgz", + "integrity": "sha512-NTz8x/LcGUjpZAWUxz0ZuzHao90Wj9spoQgomwB+/hgceh5gcJDfvaBYqxLFpKzVglpnbDSq1Fg0p0zI4oa5Pg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.3.tgz", + "integrity": "sha512-yQc9Q0JCOp3kkAV8gKgDctXO60IkQhHpqGB+KgOccDtD5UmN6Q5+gd+lcsDyQ7N8dRuK1fAud51xQpZJgKfm7g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.3.tgz", + "integrity": "sha512-e1ivVMLSnxTOU1O3npnxN16FEyWM/g3SuH2pP6udxXwa0/SnSAijRwcAYRpqIlhVKujr158S8UeHxQjC4fGl4w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.3.tgz", + "integrity": "sha512-PLrToqQqX6sdJ9DmMi8IxZWWrfjc9pdi9AEEPTrtMts3Jm9HBi1WqEeF1VwZZ2aW9TXloE5OwA35zuuq1Bhb/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.3.tgz", + "integrity": "sha512-YlzRxx7N1ampfgSKzEDw0iwDkJXUInR4cgNEqmR4TzHkU2Vhg59CGPJrTI7dxOBofD8+O35R13Nk9Ytyv0JUFg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.3.tgz", + "integrity": "sha512-Xfc3z/li6XkuD7Hs+Uk6pjyCXnfnd9zuQTKOyDTZJ544xc2yoMKUkuDw6Et9wb31MzU2/c0CIUpTDa71lL9KHw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.3.tgz", + "integrity": "sha512-ugKVqKzwa/cjmqSQG17aS9DYrEcQ/a5NITcgmOr3JLW4Iz64C37eoDlkC8tIepD3S/Td/ywKAolTQ8fKbjEL4g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.3.tgz", + "integrity": "sha512-qHPDMl+UUwsk1RMJMgAXvhraWqUUT+LR/tkXix5RA39UGxtTrHwsLIN1AhNxI5i2RFXAXfmFXDqZCdyQ4dWmAQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.3.tgz", + "integrity": "sha512-+ujwN4phBGyOsPyLgGgeCyUm4Mul+gqWVCIGuSXWgrx9xVUnf6LVXrw0BDBc9Aq1S2qMyOTX4OkCGbZeoIo8Qw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.3.tgz", + "integrity": "sha512-Qj6rSO+EvXnNDymloKZ11D54JJTnDrkRWJBzNHENDxjt0HtrCZJbSLIrcJ/WdaoU4othrel/oFqHpO/doxIS/Q==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "^4.0.3", + "@tailwindcss/oxide": "^4.0.3", + "lightningcss": "^1.29.1", + "tailwindcss": "4.0.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.66.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.66.0.tgz", + "integrity": "sha512-J+JeBtthiKxrpzUu7rfIPDzhscXF2p5zE/hVdrqkACBP8Yu0M96mwJ5m/8cPPYQE9aRNvXztXHlNwIh4FEeMZw==", + "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", + "integrity": "sha512-z3sYixFQJe8hndFnXgWu7C79ctL+pI0KAelYyW+khaNJ1m22lWrhJU2QrsTcRKMuVPtoZvfBYrTStIdKo+x0Xw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.66.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "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", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "license": "MIT" + }, + "node_modules/@types/google.maps": { + "version": "3.58.1", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz", + "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.14", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", + "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", + "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", + "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.22.0.tgz", + "integrity": "sha512-4Uta6REnz/xEJMvwf72wdUnC3rr4jAQf5jnTkeRQ9b6soxLxhDEbS/pfMPoJLDfFPNVRdryqWUIV/2GZzDJFZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.22.0", + "@typescript-eslint/type-utils": "8.22.0", + "@typescript-eslint/utils": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.22.0.tgz", + "integrity": "sha512-MqtmbdNEdoNxTPzpWiWnqNac54h8JDAmkWtJExBVVnSrSmi9z+sZUt0LfKqk9rjqmKOIeRhO4fHHJ1nQIjduIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.22.0", + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/typescript-estree": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.22.0.tgz", + "integrity": "sha512-/lwVV0UYgkj7wPSw0o8URy6YI64QmcOdwHuGuxWIYznO6d45ER0wXUbksr9pYdViAofpUCNJx/tAzNukgvaaiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.22.0.tgz", + "integrity": "sha512-NzE3aB62fDEaGjaAYZE4LH7I1MUwHooQ98Byq0G0y3kkibPJQIXVUspzlFOmOfHhiDLwKzMlWxaNv+/qcZurJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.22.0", + "@typescript-eslint/utils": "8.22.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.22.0.tgz", + "integrity": "sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.22.0.tgz", + "integrity": "sha512-SJX99NAS2ugGOzpyhMza/tX+zDwjvwAtQFLsBo3GQxiGcvaKlqGBkmZ+Y1IdiSi9h4Q0Lr5ey+Cp9CGWNY/F/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/visitor-keys": "8.22.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.22.0.tgz", + "integrity": "sha512-T8oc1MbF8L+Bk2msAvCUzjxVB2Z2f+vXYfcucE2wOmYs7ZUwco5Ep0fYZw8quNwOiw9K8GYVL+Kgc2pETNTLOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.22.0", + "@typescript-eslint/types": "8.22.0", + "@typescript-eslint/typescript-estree": "8.22.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.22.0.tgz", + "integrity": "sha512-AWpYAXnUgvLNabGTy3uBylkgZoosva/miNd1I8Bz3SjotmQPbVqhO4Cczo8AsZ44XVErEBPr/CRSgaj8sG7g0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.22.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", + "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001696", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001696.tgz", + "integrity": "sha512-pDCPkvzfa39ehJtJ+OwGT/2yvT2SbjfHhiIW2LWOAcMQ7BzwxT/XuyUp4OTOd0XFWA6BKw0JalnBHgSi5DGJBQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.90", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.90.tgz", + "integrity": "sha512-C3PN4aydfW91Natdyd449Kw+BzhLmof6tzy5W1pFC5SpQxVXT+oyiyOG9AgYYSN9OdA/ik3YkCrpwqI8ug5Tug==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz", + "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.10.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.19.0", + "@eslint/plugin-kit": "^0.2.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", + "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "build/bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.3.tgz", + "integrity": "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", + "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz", + "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.18.tgz", + "integrity": "sha512-IRGEoFn3OKalm3hjfolEWGqoF/jPqeEYFp+C8B0WMzwGwBMvlRDQd06kghDhF0C61uJ6WfSDhEZE/sAQjduKgw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", + "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.4.tgz", + "integrity": "sha512-kKaIINnFpzW6ffJNDjjyjrk21BkDx38c0xa/klsT8VzLCaMEefv4ZTacrcVR4DmgTeBra++jMDAfS/tS799YDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", + "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.1.tgz", + "integrity": "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.0.tgz", + "integrity": "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.1.tgz", + "integrity": "sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.29.1", + "lightningcss-darwin-x64": "1.29.1", + "lightningcss-freebsd-x64": "1.29.1", + "lightningcss-linux-arm-gnueabihf": "1.29.1", + "lightningcss-linux-arm64-gnu": "1.29.1", + "lightningcss-linux-arm64-musl": "1.29.1", + "lightningcss-linux-x64-gnu": "1.29.1", + "lightningcss-linux-x64-musl": "1.29.1", + "lightningcss-win32-arm64-msvc": "1.29.1", + "lightningcss-win32-x64-msvc": "1.29.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.1.tgz", + "integrity": "sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.29.1.tgz", + "integrity": "sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.29.1.tgz", + "integrity": "sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.29.1.tgz", + "integrity": "sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.29.1.tgz", + "integrity": "sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.29.1.tgz", + "integrity": "sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.29.1.tgz", + "integrity": "sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.29.1.tgz", + "integrity": "sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.29.1.tgz", + "integrity": "sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.29.1.tgz", + "integrity": "sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "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" + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prop-types": { + "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", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "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": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.1.5.tgz", + "integrity": "sha512-8BUF+hZEU4/z/JD201yK6S+UYhsf58bzYIDq2NS1iGpwxSXDu7F+DeGSkIXMFBuHZB21FSiCzEcUb18cQNdRkA==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.34.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.0.tgz", + "integrity": "sha512-+4C/cgJ9w6sudisA0nZz0+O7lTP9a3CzNLsoDwaRumM8QHwghUsu6tqHXiTmNUp/rqNiM14++7dkzHDyCRs0Jg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.34.0", + "@rollup/rollup-android-arm64": "4.34.0", + "@rollup/rollup-darwin-arm64": "4.34.0", + "@rollup/rollup-darwin-x64": "4.34.0", + "@rollup/rollup-freebsd-arm64": "4.34.0", + "@rollup/rollup-freebsd-x64": "4.34.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.0", + "@rollup/rollup-linux-arm-musleabihf": "4.34.0", + "@rollup/rollup-linux-arm64-gnu": "4.34.0", + "@rollup/rollup-linux-arm64-musl": "4.34.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.0", + "@rollup/rollup-linux-riscv64-gnu": "4.34.0", + "@rollup/rollup-linux-s390x-gnu": "4.34.0", + "@rollup/rollup-linux-x64-gnu": "4.34.0", + "@rollup/rollup-linux-x64-musl": "4.34.0", + "@rollup/rollup-win32-arm64-msvc": "4.34.0", + "@rollup/rollup-win32-ia32-msvc": "4.34.0", + "@rollup/rollup-win32-x64-msvc": "4.34.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/tailwindcss": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.3.tgz", + "integrity": "sha512-ImmZF0Lon5RrQpsEAKGxRvHwCvMgSC4XVlFRqmbzTEDb/3wvin9zfEZrMwgsa3yqBbPqahYcVI6lulM2S7IZAA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", + "license": "ISC" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.22.0.tgz", + "integrity": "sha512-Y2rj210FW1Wb6TWXzQc5+P+EWI9/zdS57hLEc0gnyuvdzWo8+Y8brKlbj0muejonhMI/xAZCnZZwjbIfv1CkOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.22.0", + "@typescript-eslint/parser": "8.22.0", + "@typescript-eslint/utils": "8.22.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz", + "integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.24.2", + "postcss": "^8.4.49", + "rollup": "^4.23.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-svgr": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.3.0.tgz", + "integrity": "sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.3", + "@svgr/core": "^8.1.0", + "@svgr/plugin-jsx": "^8.1.0" + }, + "peerDependencies": { + "vite": ">=2.6.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", + "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/uniro_admin_frontend/package.json b/uniro_admin_frontend/package.json new file mode 100644 index 0000000..70f430a --- /dev/null +++ b/uniro_admin_frontend/package.json @@ -0,0 +1,41 @@ +{ + "name": "uniro_admin_frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@googlemaps/js-api-loader": "^1.16.8", + "@tailwindcss/vite": "^4.0.0", + "@tanstack/react-query": "^5.66.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router": "^7.1.3", + "tailwindcss": "^4.0.0", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@types/google.maps": "^3.58.1", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.19.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.3", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.18", + "globals": "^15.14.0", + "prettier": "^3.4.2", + "typescript": "~5.6.2", + "typescript-eslint": "^8.18.2", + "vite": "^6.0.5", + "vite-plugin-svgr": "^4.3.0" + } +} diff --git a/uniro_admin_frontend/public/fonts/AppleSDGothicNeo/AppleSDGothicNeoB.woff b/uniro_admin_frontend/public/fonts/AppleSDGothicNeo/AppleSDGothicNeoB.woff new file mode 100644 index 0000000..f9589fa Binary files /dev/null and b/uniro_admin_frontend/public/fonts/AppleSDGothicNeo/AppleSDGothicNeoB.woff differ diff --git a/uniro_admin_frontend/public/fonts/AppleSDGothicNeo/AppleSDGothicNeoM.woff b/uniro_admin_frontend/public/fonts/AppleSDGothicNeo/AppleSDGothicNeoM.woff new file mode 100644 index 0000000..1d8feb9 Binary files /dev/null and b/uniro_admin_frontend/public/fonts/AppleSDGothicNeo/AppleSDGothicNeoM.woff differ diff --git a/uniro_admin_frontend/public/fonts/AppleSDGothicNeo/AppleSDGothicNeoR.woff b/uniro_admin_frontend/public/fonts/AppleSDGothicNeo/AppleSDGothicNeoR.woff new file mode 100644 index 0000000..e1e9b19 Binary files /dev/null and b/uniro_admin_frontend/public/fonts/AppleSDGothicNeo/AppleSDGothicNeoR.woff differ diff --git a/uniro_admin_frontend/public/fonts/AppleSDGothicNeo/AppleSDGothicNeoSB.woff b/uniro_admin_frontend/public/fonts/AppleSDGothicNeo/AppleSDGothicNeoSB.woff new file mode 100644 index 0000000..2517813 Binary files /dev/null and b/uniro_admin_frontend/public/fonts/AppleSDGothicNeo/AppleSDGothicNeoSB.woff differ diff --git a/uniro_admin_frontend/public/fonts/SF_Pro_Display/SF-Pro-Display-Regular.woff b/uniro_admin_frontend/public/fonts/SF_Pro_Display/SF-Pro-Display-Regular.woff new file mode 100644 index 0000000..9a2b91f Binary files /dev/null and b/uniro_admin_frontend/public/fonts/SF_Pro_Display/SF-Pro-Display-Regular.woff differ diff --git a/uniro_admin_frontend/public/logo.svg b/uniro_admin_frontend/public/logo.svg new file mode 100644 index 0000000..8be492d --- /dev/null +++ b/uniro_admin_frontend/public/logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/uniro_admin_frontend/src/App.css b/uniro_admin_frontend/src/App.css new file mode 100644 index 0000000..c3441a7 --- /dev/null +++ b/uniro_admin_frontend/src/App.css @@ -0,0 +1,7 @@ +#root { + margin: 0; + padding: 0; + text-align: center; + height: 100dvh; + width: 100%; +} diff --git a/uniro_admin_frontend/src/App.tsx b/uniro_admin_frontend/src/App.tsx new file mode 100644 index 0000000..416b32f --- /dev/null +++ b/uniro_admin_frontend/src/App.tsx @@ -0,0 +1,19 @@ +import "./App.css"; +import NavBar from "./components/navBar"; +import LogListContainer from "./container/logListContainer"; +import MainContainer from "./container/mainContainer"; +import MapContainer from "./container/mapContainer"; + +function App() { + return ( +
+ + + + + +
+ ); +} + +export default App; diff --git a/uniro_admin_frontend/src/assets/navbar/UNIRO_ADMIN.svg b/uniro_admin_frontend/src/assets/navbar/UNIRO_ADMIN.svg new file mode 100644 index 0000000..7fee63d --- /dev/null +++ b/uniro_admin_frontend/src/assets/navbar/UNIRO_ADMIN.svg @@ -0,0 +1,3 @@ + + + diff --git a/uniro_admin_frontend/src/assets/navbar/dropDownArrow.svg b/uniro_admin_frontend/src/assets/navbar/dropDownArrow.svg new file mode 100644 index 0000000..038f23f --- /dev/null +++ b/uniro_admin_frontend/src/assets/navbar/dropDownArrow.svg @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/uniro_admin_frontend/src/assets/react.svg b/uniro_admin_frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/uniro_admin_frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/uniro_admin_frontend/src/component/Map.tsx b/uniro_admin_frontend/src/component/Map.tsx new file mode 100644 index 0000000..35e798d --- /dev/null +++ b/uniro_admin_frontend/src/component/Map.tsx @@ -0,0 +1,29 @@ +import { useEffect } from "react"; +import useMap from "../hooks/useMap"; +import useSearchBuilding from "../hooks/useUniversityRecord"; + +type MapProps = { + style?: React.CSSProperties; +}; +const Map = ({ 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(); + console.log("Setting center to:", universityLatLng); + map.setCenter(universityLatLng); + }, [currentUniversity, mapLoaded]); + + return ( +
+ ); +}; + +export default Map; diff --git a/uniro_admin_frontend/src/components/log/logCard.tsx b/uniro_admin_frontend/src/components/log/logCard.tsx new file mode 100644 index 0000000..34ad7ea --- /dev/null +++ b/uniro_admin_frontend/src/components/log/logCard.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +type LogCardProps = { + date: string; + change: string; +}; + +export const LogCard = ({ date, change }: LogCardProps) => { + return ( +
+

{date}

+

{change}

+
+ ); +}; + +export default LogCard; diff --git a/uniro_admin_frontend/src/components/log/logList.tsx b/uniro_admin_frontend/src/components/log/logList.tsx new file mode 100644 index 0000000..4f93b31 --- /dev/null +++ b/uniro_admin_frontend/src/components/log/logList.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import LogCard from "./logCard"; +import { logCardData } from "../../data/mock/logMockData"; + +const LogList = () => { + return ( +
+ {logCardData.map((log) => { + return ; + })} +
+ ); +}; + +export default LogList; diff --git a/uniro_admin_frontend/src/components/log/logTitle.tsx b/uniro_admin_frontend/src/components/log/logTitle.tsx new file mode 100644 index 0000000..025c77f --- /dev/null +++ b/uniro_admin_frontend/src/components/log/logTitle.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +const LogTitle = () => { + return ( +
+ 로그 목록 +
+ ); +}; + +export default LogTitle; diff --git a/uniro_admin_frontend/src/components/navBar.tsx b/uniro_admin_frontend/src/components/navBar.tsx new file mode 100644 index 0000000..df7c9bd --- /dev/null +++ b/uniro_admin_frontend/src/components/navBar.tsx @@ -0,0 +1,63 @@ +import React, { useState, useRef, useEffect } from "react"; +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 { currentUniversity, getUniversityNameList, setCurrentUniversity } = + useSearchBuilding(); + const [selectedUniversity, setSelectedUniversity] = + useState(currentUniversity); + const [dropdownOpen, setDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setDropdownOpen(false); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + return ( + + ); +}; + +export default NavBar; diff --git a/uniro_admin_frontend/src/constant/university.ts b/uniro_admin_frontend/src/constant/university.ts new file mode 100644 index 0000000..1beed2a --- /dev/null +++ b/uniro_admin_frontend/src/constant/university.ts @@ -0,0 +1,19 @@ +import { UniversityLatLng, UniversityRecord } from "../data/types/university"; + +const universities: UniversityLatLng[] = [ + { name: "한양대학교", latlng: { lat: 37.5585, lng: 127.0458 } }, + { name: "인하대학교", latlng: { lat: 37.4509, lng: 126.6539 } }, + { name: "고려대학교", latlng: { lat: 37.5906, lng: 127.0324 } }, + { name: "서울시립대학교", latlng: { lat: 37.5834, lng: 127.058 } }, + { name: "이화여자대학교", latlng: { lat: 37.5618, lng: 126.946 } }, +]; + +const universityRecord: UniversityRecord = universities.reduce( + (acc, { name, latlng }) => { + acc[name] = latlng; + return acc; + }, + {} as UniversityRecord +); + +export { universities, universityRecord }; diff --git a/uniro_admin_frontend/src/container/logListContainer.tsx b/uniro_admin_frontend/src/container/logListContainer.tsx new file mode 100644 index 0000000..aae4570 --- /dev/null +++ b/uniro_admin_frontend/src/container/logListContainer.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import LogTitle from "../components/log/logTitle"; +import LogList from "../components/log/logList"; + +const LogListContainer = () => { + return ( +
+ + +
+ ); +}; + +export default LogListContainer; diff --git a/uniro_admin_frontend/src/container/mainContainer.tsx b/uniro_admin_frontend/src/container/mainContainer.tsx new file mode 100644 index 0000000..94fb949 --- /dev/null +++ b/uniro_admin_frontend/src/container/mainContainer.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +type Props = { + children: React.ReactNode; +}; + +const MainContainer = ({ children }: Props) => { + return
{children}
; +}; + +export default MainContainer; diff --git a/uniro_admin_frontend/src/container/mapContainer.tsx b/uniro_admin_frontend/src/container/mapContainer.tsx new file mode 100644 index 0000000..e20f603 --- /dev/null +++ b/uniro_admin_frontend/src/container/mapContainer.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import Map from "../component/Map"; + +type Props = {}; + +const MapContainer = (props: Props) => { + return ( +
+
+
2025년 2월 3일 15:34
+ +
+ +
+ ); +}; + +export default MapContainer; diff --git a/uniro_admin_frontend/src/data/mock/logMockData.ts b/uniro_admin_frontend/src/data/mock/logMockData.ts new file mode 100644 index 0000000..4ad3eb9 --- /dev/null +++ b/uniro_admin_frontend/src/data/mock/logMockData.ts @@ -0,0 +1,38 @@ +export const logCardData = [ + { + date: "2025-10-20", + change: "변경 추가", + }, + { + date: "2025-10-20", + change: "변경 추가", + }, + { + date: "2025-10-20", + change: "변경 추가", + }, + { + date: "2025-10-20", + change: "변경 추가", + }, + { + date: "2025-10-20", + change: "변경 추가", + }, + { + date: "2025-10-20", + change: "변경 추가", + }, + { + date: "2025-10-20", + change: "변경 추가", + }, + { + date: "2025-10-20", + change: "변경 추가", + }, + { + date: "2025-10-20", + change: "변경 추가", + }, +]; diff --git a/uniro_admin_frontend/src/data/types/university.ts b/uniro_admin_frontend/src/data/types/university.ts new file mode 100644 index 0000000..ae33172 --- /dev/null +++ b/uniro_admin_frontend/src/data/types/university.ts @@ -0,0 +1,6 @@ +export interface UniversityLatLng { + name: string; + latlng: google.maps.LatLngLiteral; +} + +export type UniversityRecord = Record; diff --git a/uniro_admin_frontend/src/fetch/fetch.tsx b/uniro_admin_frontend/src/fetch/fetch.tsx new file mode 100644 index 0000000..e69de29 diff --git a/uniro_admin_frontend/src/hooks/useMap.tsx b/uniro_admin_frontend/src/hooks/useMap.tsx new file mode 100644 index 0000000..1d69f9c --- /dev/null +++ b/uniro_admin_frontend/src/hooks/useMap.tsx @@ -0,0 +1,45 @@ +import { useEffect, useRef, useState } from "react"; +import { initializeMap } from "../map/initializer/googleMapInitializer"; + +const useMap = (mapOptions?: google.maps.MapOptions) => { + const mapRef = useRef(null); + const [map, setMap] = useState(null); + const [overlay, setOverlay] = useState(null); + const [AdvancedMarker, setAdvancedMarker] = useState< + typeof google.maps.marker.AdvancedMarkerElement | null + >(null); + const [Polyline, setPolyline] = useState( + null + ); + const [mapLoaded, setMapLoaded] = useState(false); + + useEffect(() => { + if (!mapRef.current) return; + + const initMap = async () => { + try { + const { map, overlay, AdvancedMarkerElement, Polyline } = + await initializeMap(mapRef.current, mapOptions); + setMap(map); + setOverlay(overlay); + setAdvancedMarker(() => AdvancedMarkerElement); + setPolyline(() => Polyline); + setMapLoaded(true); + } catch (e) { + alert("Error while initializing map: " + e); + } + }; + + initMap(); + + return () => { + if (map) { + map.unbindAll(); + } + }; + }, []); + + return { mapRef, map, overlay, AdvancedMarker, Polyline, mapLoaded }; +}; + +export default useMap; diff --git a/uniro_admin_frontend/src/hooks/useUniversityRecord.tsx b/uniro_admin_frontend/src/hooks/useUniversityRecord.tsx new file mode 100644 index 0000000..a3db3a2 --- /dev/null +++ b/uniro_admin_frontend/src/hooks/useUniversityRecord.tsx @@ -0,0 +1,28 @@ +import { create } from "zustand"; +import { universityRecord } from "../constant/university"; + +interface UniversityRecordStore { + universityRecord: Record; + currentUniversity: string; + getUniversityNameList: () => string[]; + setCurrentUniversity: (newUniversity: string) => void; + setUniversityRecord: ( + newUniversityRecord: Record + ) => void; + getCurrentUniversityLngLat: () => google.maps.LatLngLiteral; +} + +const useSearchBuilding = create((set, get) => ({ + universityRecord: universityRecord, + currentUniversity: Object.keys(universityRecord)[0], + getUniversityNameList: () => Object.keys(get().universityRecord), + setUniversityRecord: (newUniversityRecord) => + set({ universityRecord: newUniversityRecord }), + setCurrentUniversity: (newUniversity) => + set({ currentUniversity: newUniversity }), + getCurrentUniversityLngLat: () => + get().universityRecord[get().currentUniversity] || + universityRecord["한양대학교"], +})); + +export default useSearchBuilding; diff --git a/uniro_admin_frontend/src/index.css b/uniro_admin_frontend/src/index.css new file mode 100644 index 0000000..44431b0 --- /dev/null +++ b/uniro_admin_frontend/src/index.css @@ -0,0 +1,152 @@ +@import "tailwindcss"; + +@font-face { + font-family: "AppleSDGothicNeo"; + font-style: normal; + font-weight: 700; + src: url("/fonts/AppleSDGothicNeo/AppleSDGothicNeoB.woff") format("woff"); + unicode-range: U+1100-11FF, U+3130-318F, U+A960-A97F, U+AC00-D7A3, U+D7B0-D7FF; +} + +@font-face { + font-family: "AppleSDGothicNeo"; + font-style: normal; + font-weight: 500; + src: url("/fonts/AppleSDGothicNeo/AppleSDGothicNeoM.woff") format("woff"); + unicode-range: U+1100-11FF, U+3130-318F, U+A960-A97F, U+AC00-D7A3, U+D7B0-D7FF; +} + +@font-face { + font-family: "AppleSDGothicNeo"; + font-style: normal; + font-weight: 400; + src: url("/fonts/AppleSDGothicNeo/AppleSDGothicNeoR.woff") format("woff"); + unicode-range: U+1100-11FF, U+3130-318F, U+A960-A97F, U+AC00-D7A3, U+D7B0-D7FF; +} + +@font-face { + font-family: "AppleSDGothicNeo"; + font-style: normal; + font-weight: 600; + src: url("/fonts/AppleSDGothicNeo/AppleSDGothicNeoSB.woff") format("woff"); + unicode-range: U+1100-11FF, U+3130-318F, U+A960-A97F, U+AC00-D7A3, U+D7B0-D7FF; +} + +@font-face { + font-family: "SF Pro Display"; + font-style: normal; + font-weight: 400; + src: url("/fonts/SF_Pro_Display/SF-Pro-Display-Regular.woff") format("woff"); +} + +* { + font-family: "SF Pro Display", "AppleSDGothicNeo" !important; + outline: none; + --webkit-scrollbar-width: none; + box-sizing: border-box; +} + +*::-webkit-scrollbar { + display: none; +} + +* { + scrollbar-width: none; +} + +@theme { + --color-primary-100: #cde1fe; + --color-primary-200: #9ac2fd; + --color-primary-300: #68a4fd; + --color-primary-400: #3585fc; + --color-primary-500: #0367fb; + --color-primary-600: #0252c9; + --color-primary-700: #023e97; + --color-primary-800: #012964; + --color-primary-900: #011532; + + --color-gray-100: #ffffff; + --color-gray-200: #f5f5f5; + --color-gray-300: #eaeaea; + --color-gray-400: #dbdbdb; + --color-gray-500: #b0b0b0; + --color-gray-600: #999999; + --color-gray-700: #808080; + --color-gray-800: #4d4d4d; + --color-gray-900: #161616; + + --color-system-black: #000000; + --color-system-red: #ff2d55; + --color-system-orange: #ff8000; + --color-system-skyblue: #f0f5fe; + --color-system-lightred: #fff5f7; + --color-system-lightorange: #fff7ef; + + --radius-100: 10px; + --radius-200: 16px; + --radius-300: 18px; + --radius-400: 20px; + --radius-500: 28px; + + --text-kor-heading1: 24px; + --text-kor-heading1--line-height: 160%; + + --text-kor-heading2: 20px; + --text-kor-heading2--line-height: 160%; + + --text-kor-body1: 18px; + --text-kor-body1--line-height: 160%; + + --text-kor-body2: 16px; + --text-kor-body2--line-height: 160%; + + --text-kor-body3: 14px; + --text-kor-body3--line-height: 160%; + + --text-kor-caption: 12px; + --text-kor-caption--line-height: 120%; + + --text-eng-heading1: 32px; + --text-eng-heading1--line-height: 70%; + + --text-eng-heading2: 24px; + --text-eng-heading2--line-height: 160%; + + --text-eng-heading3: 20px; + --text-eng-heading3--line-height: 160%; + + --text-eng-body1: 16px; + --text-eng-body1--line-height: 160%; + + --text-eng-body2: 14px; + --text-eng-body2--line-height: 160%; + + --text-eng-caption: 12px; + --text-eng-caption--line-height: 160%; +} + +@utility translate-marker { + transform: translateY(calc(100% - 10px)); +} + +@utility translate-routemarker { + transform: translateY(calc(100% - 40px)); +} + +@utility translate-waypoint { + transform: translateY(+4px); +} + +@keyframes markerAppear { + 0% { + transform: scale(0); + } + 100% { + transform: scale(1); + } +} + +.marker-appear { + animation: markerAppear 0.2s ease-in-out forwards; + transform-origin: bottom center; +} diff --git a/uniro_admin_frontend/src/main.tsx b/uniro_admin_frontend/src/main.tsx new file mode 100644 index 0000000..bef5202 --- /dev/null +++ b/uniro_admin_frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/uniro_admin_frontend/src/map/initializer/googleMapInitializer.ts b/uniro_admin_frontend/src/map/initializer/googleMapInitializer.ts new file mode 100644 index 0000000..5f2230f --- /dev/null +++ b/uniro_admin_frontend/src/map/initializer/googleMapInitializer.ts @@ -0,0 +1,47 @@ +import loadGoogleMapsLibraries from "../loader/googleMapLoader"; + +interface MapWithOverlay { + map: google.maps.Map | null; + overlay: google.maps.OverlayView | null; + AdvancedMarkerElement: typeof google.maps.marker.AdvancedMarkerElement; + Polyline: typeof google.maps.Polyline; +} + +export const initializeMap = async ( + mapElement: HTMLElement | null, + mapOptions?: google.maps.MapOptions +): Promise => { + const { Map, OverlayView, AdvancedMarkerElement, Polyline } = + await loadGoogleMapsLibraries(); + + // useMap hook에서 error을 catch 하도록 함. + if (!mapElement) { + throw new Error("mapElement is null"); + } + + const map = new Map(mapElement, { + zoom: 16, + minZoom: 15, + maxZoom: 19, + draggable: true, + scrollwheel: true, + disableDoubleClickZoom: false, + gestureHandling: "auto", + clickableIcons: false, + disableDefaultUI: true, + // restriction: { + // latLngBounds: HanyangUniversityBounds, + // strictBounds: false, + // }, + mapId: import.meta.env.VITE_REACT_APP_GOOGLE_MAP_ID, + ...mapOptions, + }); + + const overlay = new OverlayView(); + overlay.onAdd = function () {}; + overlay.draw = function () {}; + overlay.onRemove = function () {}; + overlay.setMap(map); + + return { map, overlay, AdvancedMarkerElement, Polyline }; +}; diff --git a/uniro_admin_frontend/src/map/loader/googleMapLoader.ts b/uniro_admin_frontend/src/map/loader/googleMapLoader.ts new file mode 100644 index 0000000..684e476 --- /dev/null +++ b/uniro_admin_frontend/src/map/loader/googleMapLoader.ts @@ -0,0 +1,16 @@ +import { Loader } from "@googlemaps/js-api-loader"; + +const GoogleMapsLoader = new Loader({ + apiKey: import.meta.env.VITE_REACT_APP_GOOGLE_MAPS_API_KEY, + version: "weekly", + libraries: ["maps", "marker"], +}); + +const loadGoogleMapsLibraries = async () => { + const { Map, OverlayView, Polyline } = await GoogleMapsLoader.importLibrary("maps"); + const { AdvancedMarkerElement } = await GoogleMapsLoader.importLibrary("marker"); + + return { Map, OverlayView, AdvancedMarkerElement, Polyline }; +}; + +export default loadGoogleMapsLibraries; diff --git a/uniro_admin_frontend/src/vite-env.d.ts b/uniro_admin_frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/uniro_admin_frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/uniro_admin_frontend/tsconfig.app.json b/uniro_admin_frontend/tsconfig.app.json new file mode 100644 index 0000000..358ca9b --- /dev/null +++ b/uniro_admin_frontend/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/uniro_admin_frontend/tsconfig.json b/uniro_admin_frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/uniro_admin_frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/uniro_admin_frontend/tsconfig.node.json b/uniro_admin_frontend/tsconfig.node.json new file mode 100644 index 0000000..db0becc --- /dev/null +++ b/uniro_admin_frontend/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/uniro_admin_frontend/vite.config.ts b/uniro_admin_frontend/vite.config.ts new file mode 100644 index 0000000..6b840f2 --- /dev/null +++ b/uniro_admin_frontend/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import svgr from "vite-plugin-svgr"; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react(), tailwindcss(), svgr()], +}); diff --git a/uniro_backend/.gitignore b/uniro_backend/.gitignore index c2065bc..39864c4 100644 --- a/uniro_backend/.gitignore +++ b/uniro_backend/.gitignore @@ -35,3 +35,6 @@ out/ ### VS Code ### .vscode/ + +### properties +*.properties diff --git a/uniro_backend/Dockerfile b/uniro_backend/Dockerfile new file mode 100644 index 0000000..e8f6db7 --- /dev/null +++ b/uniro_backend/Dockerfile @@ -0,0 +1,10 @@ +FROM openjdk:17 + +ARG JAR_FILE=./build/libs/*.jar +ARG SPRING_PROFILE + +COPY ${JAR_FILE} uniro-server.jar + +ENV SPRING_PROFILE=${SPRING_PROFILE} + +ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul","-Dspring.profiles.active=${SPRING_PROFILE}" ,"-jar" ,"uniro-server.jar"] \ No newline at end of file diff --git a/uniro_backend/build.gradle b/uniro_backend/build.gradle index b553cc9..ea48f8e 100644 --- a/uniro_backend/build.gradle +++ b/uniro_backend/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.4.2' + id 'org.springframework.boot' version '3.3.8' id 'io.spring.dependency-management' version '1.1.7' id 'org.hibernate.orm' version '6.3.1.Final' } @@ -62,6 +62,15 @@ dependencies { annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + //webClient + implementation 'org.springframework.boot:spring-boot-starter-webflux' + + //hibernate envers + implementation 'org.hibernate.orm:hibernate-envers:6.3.1.Final' + + // actuator + implementation 'org.springframework.boot:spring-boot-starter-actuator' } tasks.named('test') { diff --git a/uniro_backend/deploy.sh b/uniro_backend/deploy.sh new file mode 100644 index 0000000..238d9e6 --- /dev/null +++ b/uniro_backend/deploy.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +IS_GREEN=$(sudo docker ps | grep spring-green) # 현재 실행중인 App이 blue인지 확인 + +if [ -z $IS_GREEN ];then # blue라면 + + echo "### BLUE => GREEN ###" + + echo "1. get green image" + sudo docker-compose pull spring-green # green으로 이미지 내려받기 + + echo "2. green container up" + sudo docker-compose up -d spring-green # green 컨테이너 실행 + + while [ 1 = 1 ]; do + echo "3. green health check..." + sleep 3 + + REQUEST=$(curl http://127.0.0.1:8080) # green으로 request + if [ -n "$REQUEST" ]; then # 서비스 가능하면 health check 중지 + echo "health check success" + break ; + fi + done; + + echo "4. reload nginx" + cd ~/nginx-certbot/data/nginx + cp ./app.green.conf ./app.conf # 설정파일 교체 + cd ~/nginx-certbot + sudo docker-compose restart nginx + + echo "5. blue container down" + cd ~/myapp + sudo docker-compose stop spring-blue +else + echo "### GREEN => BLUE ###" + + echo "1. get blue image" + sudo docker-compose pull spring-blue + + echo "2. blue container up" + sudo docker-compose up -d spring-blue + + while [ 1 = 1 ]; do + echo "3. blue health check..." + sleep 3 + REQUEST=$(curl http://127.0.0.1:8081) # blue로 request + + if [ -n "$REQUEST" ]; then # 서비스 가능하면 health check 중지 + echo "health check success" + break ; + fi + done; + + echo "4. reload nginx" + cd ~/nginx-certbot/data/nginx + cp ./app.blue.conf ./app.conf # 설정파일 교체 + cd ~/nginx-certbot + sudo docker compose restart nginx + + echo "5. green container down" + cd ~/myapp + sudo docker-compose stop spring-green +fi \ No newline at end of file diff --git a/uniro_backend/docker-compose.yml b/uniro_backend/docker-compose.yml new file mode 100644 index 0000000..319bb0f --- /dev/null +++ b/uniro_backend/docker-compose.yml @@ -0,0 +1,28 @@ +version: '3.8' + +services: + spring-green: + image: uniro5th/uniro-docker-repo:develop + container_name: spring-green + environment: + - SPRING_PROFILES_ACTIVE=dev + restart: always + ports: + - "8080:8080" + networks: + - my_network1 + + spring-blue: + image: uniro5th/uniro-docker-repo:develop + container_name: spring-blue + environment: + - SPRING_PROFILES_ACTIVE=dev + restart: always + ports: + - "8081:8080" + networks: + - my_network1 + +networks: + my_network1: + external: true 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 new file mode 100644 index 0000000..a767ece --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/annotation/RevisionOperation.java @@ -0,0 +1,12 @@ +package com.softeer5.uniro_backend.admin.annotation; + +import com.softeer5.uniro_backend.admin.entity.RevisionOperationType; + +import java.lang.annotation.*; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface RevisionOperation { + RevisionOperationType value() default RevisionOperationType.DEFAULT; +} 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 new file mode 100644 index 0000000..86e304f --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/aspect/RevisionOperationAspect.java @@ -0,0 +1,93 @@ +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.setting.RevisionContext; +import com.softeer5.uniro_backend.route.dto.request.CreateRoutesReqDTO; +import com.softeer5.uniro_backend.route.dto.request.PostRiskReqDTO; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import static com.softeer5.uniro_backend.common.constant.UniroConst.*; + +@Aspect +@Component +@Order(BEFORE_DEFAULT_ORDER) +public class RevisionOperationAspect { + + @Around("@annotation(revisionOperation)") + public Object around(ProceedingJoinPoint joinPoint, RevisionOperation revisionOperation) throws Throwable { + RevisionOperationType opType = revisionOperation.value(); + + Object result; + switch (opType) { + case UPDATE_RISK -> result = updateRiskHandler(joinPoint); + case CREATE_ROUTE -> result = updateRouteHandler(joinPoint); + default -> result = joinPoint.proceed(); + } + + return result; + } + + private Object updateRiskHandler(ProceedingJoinPoint joinPoint) throws Throwable { + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + String[] parameterNames = signature.getParameterNames(); + Object[] args = joinPoint.getArgs(); + + Long univId = null; + String action = null; + for (int i = 0; i < args.length; i++) { + if (args[i] instanceof Long && "univId".equals(parameterNames[i])) { + univId = (Long) args[i]; + } + else if(args[i] instanceof PostRiskReqDTO postRiskReqDTO){ + int cautionSize = postRiskReqDTO.getCautionTypes().size(); + int dangerSize = postRiskReqDTO.getDangerTypes().size(); + + if (cautionSize > 0) { + action = "주의요소 업데이트"; + } else if (dangerSize > 0) { + action = "위험요소 업데이트"; + } else { + action = "위험/주의요소 해제"; + } + } + } + RevisionContext.setUnivId(univId); + RevisionContext.setAction(action); + try{ + return joinPoint.proceed(); + } + finally { + RevisionContext.clear(); + } + } + + private Object updateRouteHandler(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(); + } + } +} 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 new file mode 100644 index 0000000..e28d4d1 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/controller/AdminApi.java @@ -0,0 +1,24 @@ +package com.softeer5.uniro_backend.admin.controller; + +import com.softeer5.uniro_backend.admin.dto.RevInfoDTO; +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 org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; + +import java.util.List; + +@Tag(name = "admin 페이지 API") +public interface AdminApi { + + @Operation(summary = "모든 버전정보 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "모든 버전정보 조회 성공"), + @ApiResponse(responseCode = "400", description = "EXCEPTION(임시)", content = @Content), + }) + ResponseEntity> getAllRev(@PathVariable("univId") Long univId); + +} 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 new file mode 100644 index 0000000..1ed101a --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/controller/AdminController.java @@ -0,0 +1,23 @@ +package com.softeer5.uniro_backend.admin.controller; + +import com.softeer5.uniro_backend.admin.dto.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.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class AdminController implements AdminApi { + private final AdminService adminService; + + @Override + @GetMapping("/admin/revision/{univId}") + public ResponseEntity> getAllRev(@PathVariable("univId") Long univId) { + return ResponseEntity.ok().body(adminService.getAllRevInfo(univId)); + } +} 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/RevInfoDTO.java new file mode 100644 index 0000000..40f287c --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/dto/RevInfoDTO.java @@ -0,0 +1,25 @@ +package com.softeer5.uniro_backend.admin.dto; + +import com.softeer5.uniro_backend.admin.entity.RevInfo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +@Getter +@Schema(name = "RevInfoDTO", description = "버전 정보 조회 DTO") +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class RevInfoDTO { + @Schema(description = "버전명", example = "4") + private final Long rev; // Revision 번호 + @Schema(description = "버전 타임스탬프", example = "2025-02-04T17:56:06.832") + private final LocalDateTime revTime; // Revision 시간 + @Schema(description = "대학교 id", example = "1001") + private final Long univId; //UnivId + @Schema(description = "변경사항 설명", example = "위험요소 추가") + private final String action; // 행위 + + public static RevInfoDTO of(Long rev, LocalDateTime revTime, Long univId, String action) { + return new RevInfoDTO(rev, revTime, univId, action); + } +} \ No newline at end of file diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/entity/RevInfo.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/entity/RevInfo.java new file mode 100644 index 0000000..9685030 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/entity/RevInfo.java @@ -0,0 +1,33 @@ +package com.softeer5.uniro_backend.admin.entity; + +import java.time.LocalDateTime; + +import com.softeer5.uniro_backend.admin.setting.CustomReversionListener; +import jakarta.persistence.*; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.envers.RevisionEntity; +import org.hibernate.envers.RevisionNumber; +import org.hibernate.envers.RevisionTimestamp; + +@Entity +@RevisionEntity(CustomReversionListener.class) +@Getter +@Setter +public class RevInfo { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @RevisionNumber + private Long rev; + + @RevisionTimestamp + @Column(name = "revtstmp") + private LocalDateTime revTimeStamp; + + @Column(name = "univ_id") + @NotNull + private Long univId; + + private String action; +} 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 new file mode 100644 index 0000000..070e6ab --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/entity/RevisionOperationType.java @@ -0,0 +1,7 @@ +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/repository/RevInfoRepository.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/repository/RevInfoRepository.java new file mode 100644 index 0000000..a7ab85c --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/repository/RevInfoRepository.java @@ -0,0 +1,11 @@ +package com.softeer5.uniro_backend.admin.repository; + +import com.softeer5.uniro_backend.admin.entity.RevInfo; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + + +public interface RevInfoRepository extends JpaRepository { + List findAllByUnivId(Long univId); +} 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 new file mode 100644 index 0000000..8dbd32a --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/service/AdminService.java @@ -0,0 +1,23 @@ +package com.softeer5.uniro_backend.admin.service; + +import com.softeer5.uniro_backend.admin.dto.RevInfoDTO; +import com.softeer5.uniro_backend.admin.repository.RevInfoRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AdminService { + private final RevInfoRepository revInfoRepository; + + public List getAllRevInfo(Long univId){ + return revInfoRepository.findAllByUnivId(univId).stream().map(r -> RevInfoDTO.of(r.getRev(), + r.getRevTimeStamp(), + r.getUnivId(), + r.getAction())).toList(); + } +} 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 new file mode 100644 index 0000000..9af5ca4 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/setting/CustomReversionListener.java @@ -0,0 +1,13 @@ +package com.softeer5.uniro_backend.admin.setting; + +import com.softeer5.uniro_backend.admin.entity.RevInfo; +import org.hibernate.envers.RevisionListener; + +public class CustomReversionListener implements RevisionListener { + @Override + public void newRevision(Object revisionEntity) { + RevInfo revinfo = (RevInfo) revisionEntity; + revinfo.setUnivId(RevisionContext.getUnivId()); + revinfo.setAction(RevisionContext.getAction()); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..7bc21ee --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/admin/setting/RevisionContext.java @@ -0,0 +1,26 @@ +package com.softeer5.uniro_backend.admin.setting; + +public class RevisionContext { + private static final ThreadLocal univIdHolder = new ThreadLocal<>(); + private static final ThreadLocal actionHolder = new ThreadLocal<>(); + + public static void setAction(String action) { + actionHolder.set(action); + } + + public static String getAction() { + return actionHolder.get(); + } + + 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/common/config/CorsConfig.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/config/CorsConfig.java new file mode 100644 index 0000000..cfe1736 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/config/CorsConfig.java @@ -0,0 +1,22 @@ +package com.softeer5.uniro_backend.common.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsConfig implements WebMvcConfigurer { + @Value("${cors.allowed-origins}") + private String[] allowedOrigins; + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins(allowedOrigins) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH") + .allowedHeaders("*") + .allowCredentials(true); + } + +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/config/SwaggerConfig.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/config/SwaggerConfig.java new file mode 100644 index 0000000..f9469af --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/config/SwaggerConfig.java @@ -0,0 +1,25 @@ +package com.softeer5.uniro_backend.common.config; + +import java.util.List; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.servers.Server; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + Server devServer = new Server(); + devServer.setUrl("https://api.uniro.site"); + OpenAPI info = new OpenAPI().components(new Components()).info(new Info()); + info.setServers(List.of(devServer)); + return info; + } + +} 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 new file mode 100644 index 0000000..8142946 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/constant/UniroConst.java @@ -0,0 +1,9 @@ +package com.softeer5.uniro_backend.common.constant; + +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; +} 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 f97e66d..6ed116c 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 @@ -15,9 +15,19 @@ public enum ErrorCode { // 루트 ROUTE_NOT_FOUND(404, "루트를 찾을 수 없습니다."), CAUTION_DANGER_CANT_EXIST_SIMULTANEOUSLY(400, "위험요소와 주의요소는 동시에 존재할 수 없습니다."), + INVALID_MAP(500,"현재 지도 데이터가 제약조건에 어긋난 상태입니다."), + + //길 생성 + ELEVATION_API_ERROR(500, "구글 해발고도 API에서 오류가 발생했습니다."), // 건물 노드 BUILDING_NOT_FOUND(404, "유효한 건물을 찾을 수 없습니다."), + + // 노드 + NODE_NOT_FOUND(404, "유효한 노드를 찾을 수 없습니다."), + + // 경로 계산 + INTERSECTION_ONLY_ALLOWED_POINT(400, "기존 경로와 겹칠 수 없습니다.") ; private final int httpStatus; diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/ElevationApiException.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/ElevationApiException.java new file mode 100644 index 0000000..5f8016a --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/ElevationApiException.java @@ -0,0 +1,10 @@ +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 ElevationApiException extends CustomException { + public ElevationApiException(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 new file mode 100644 index 0000000..24c03f5 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/InvalidMapException.java @@ -0,0 +1,10 @@ +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/NodeNotFoundException.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/NodeNotFoundException.java new file mode 100644 index 0000000..5489fa9 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/NodeNotFoundException.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 NodeNotFoundException extends CustomException { + public NodeNotFoundException(String message, ErrorCode errorCode) { + super(message, errorCode); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/RouteCalculationException.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/RouteCalculationException.java new file mode 100644 index 0000000..0f8784f --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/exception/custom/RouteCalculationException.java @@ -0,0 +1,10 @@ +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 RouteCalculationException extends CustomException { + public RouteCalculationException(String message, ErrorCode errorCode) { + super(message, errorCode); + } +} 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 new file mode 100644 index 0000000..48c0898 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/logging/ExecutionLoggingAop.java @@ -0,0 +1,158 @@ +package com.softeer5.uniro_backend.common.logging; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.Enumeration; +import java.util.Objects; +import java.util.UUID; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +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; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.log4j.Log4j2; + +@Aspect +@Component +@Log4j2 +@Profile("!test") +public class ExecutionLoggingAop { + + private static final ThreadLocal userIdThreadLocal = new ThreadLocal<>(); + + @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); + userIdThreadLocal.set(userId); + } + + Object target = pjp.getTarget(); + Annotation[] declaredAnnotations = target.getClass().getDeclaredAnnotations(); + + 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); + } + } + + 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()); + } + } + } + } + + StopWatch sw = new StopWatch(); + sw.start(); + + Object result = null; + 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(); + } + } + } + + sw.stop(); + long executionTime = sw.getTotalTimeMillis(); + + log.info("[ExecutionTime] {} --> {} (ms)", task, executionTime); + log.info("🚨 [ userId = {} ] {} End", userId, className); + log.info(""); + + return result; + } + + + private void logHttpRequest(String userId) { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); + + // HTTP Request 메시지 출력 (RFC 2616 형식) + StringBuilder httpMessage = new StringBuilder(); + + // 1. Start-Line (Request Line) + String method = request.getMethod(); + String requestURI = request.getRequestURI(); + String httpVersion = "HTTP/1.1"; // 대부분의 요청은 HTTP/1.1 버전 사용 + httpMessage.append(method).append(" ").append(requestURI).append(" ").append(httpVersion).append("\r\n"); + + // 2. Field-Lines (Headers) + Enumeration headerNames = request.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + String headerValue = request.getHeader(headerName); + httpMessage.append(headerName).append(": ").append(headerValue).append("\r\n"); + } + + // 헤더 끝을 나타내는 빈 라인 추가 + httpMessage.append("\r\n"); + + // 3. Message Body (있다면 추가) + if (request.getMethod().equals("POST") || request.getMethod().equals("PUT")) { + // POST/PUT 메서드인 경우, 메시지 본문이 있을 수 있음 + // 예시로 요청 파라미터를 출력합니다. + StringBuilder body = new StringBuilder(); + request.getParameterMap().forEach((key, value) -> { + body.append(key).append("=").append(String.join(",", value)).append("&"); + }); + + // 마지막 "&" 제거 + if (body.length() > 0) { + body.deleteCharAt(body.length() - 1); + httpMessage.append(body); + } + } + + // 요청 메시지 출력 + log.info("✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅ New request"); + log.info("[ userId = "+ userId + " ] HTTP Request: \n" + httpMessage); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..f63bca1 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/utils/GeoUtils.java @@ -0,0 +1,58 @@ +package com.softeer5.uniro_backend.common.utils; + +import org.locationtech.jts.geom.*; +import org.locationtech.jts.io.WKTWriter; + +import java.util.List; + +public final class GeoUtils { + private static final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); + + private GeoUtils() { + // 인스턴스화 방지 + } + + public static GeometryFactory getInstance() { + return geometryFactory; + } + + public static Point convertDoubleToPoint(double lat, double lng) { + return geometryFactory.createPoint(new Coordinate(lat, lng)); + } + + public static String convertDoubleToPointWTK(double lat, double lng) { + return String.format("POINT(%f %f)", lat, lng); + } + + public static LineString convertDoubleToLineString(List co) { + if (co == null || co.isEmpty()) { + throw new IllegalArgumentException("coordinates can not be null or empty"); + } + + Coordinate[] coordinates = new Coordinate[co.size()]; + for (int i = 0; i < co.size(); i++) { + coordinates[i] = new Coordinate(co.get(i)[0], co.get(i)[1]); + } + + return geometryFactory.createLineString(coordinates); + } + + public static String convertDoubleToLineStringWTK(List co) { + LineString lineString = convertDoubleToLineString(co); + WKTWriter writer = new WKTWriter(); + return writer.write(lineString); + } + + public static String makeSquarePolygonString( + double leftUpLat, double leftUpLng, double rightDownLat, double rightDownLng + ) { + return String.format("Polygon((%f %f, %f %f, %f %f, %f %f, %f %f))" + , leftUpLat, leftUpLng + , rightDownLat, leftUpLng + , rightDownLat, rightDownLng + , leftUpLat, rightDownLng + , leftUpLat, leftUpLng + ); + } + +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/utils/Utils.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/utils/Utils.java deleted file mode 100644 index 17be94d..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/common/utils/Utils.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.softeer5.uniro_backend.common.utils; - -import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.geom.LineString; -import org.locationtech.jts.geom.Point; -import org.locationtech.jts.io.WKTWriter; - -import java.util.List; - -public final class Utils { - private static final GeometryFactory geometryFactory = new GeometryFactory(); - - private Utils(){ - // 인스턴스화 방지 - } - - public static Point convertDoubleToPoint(double lat, double lng) { - return geometryFactory.createPoint(new Coordinate(lat, lng)); - } - - public static String convertDoubleToPointWTK(double lat, double lng) { - return String.format("POINT(%f %f)", lat, lng); - } - - public static LineString convertDoubleToLineString(List co){ - if(co==null || co.isEmpty()){ - throw new IllegalArgumentException("coordinates can not be null or empty"); - } - - Coordinate[] coordinates = new Coordinate[co.size()]; - for (int i = 0; i < co.size(); i++) { - coordinates[i] = new Coordinate(co.get(i)[0], co.get(i)[1]); - } - - return geometryFactory.createLineString(coordinates); - } - - public static String convertDoubleToLineStringWTK(List co){ - LineString lineString = convertDoubleToLineString(co); - WKTWriter writer = new WKTWriter(); - return writer.write(lineString); - } - -} 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 new file mode 100644 index 0000000..2c16746 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/external/MapClient.java @@ -0,0 +1,9 @@ +package com.softeer5.uniro_backend.external; + +import com.softeer5.uniro_backend.node.entity.Node; + +import java.util.List; + +public interface MapClient { + void fetchHeights(List nodes); +} 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 new file mode 100644 index 0000000..6f64e59 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/external/MapClientImpl.java @@ -0,0 +1,125 @@ +package com.softeer5.uniro_backend.external; + +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 lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.util.ArrayList; +import java.util.List; + +@Service +@Slf4j +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 String SUCCESS_STATUS = "OK"; + private final WebClient webClient; + + public MapClientImpl() { + this.webClient = WebClient.builder().baseUrl(baseUrl).build(); + } + + @Getter + @Setter + public static class ElevationResponse { + private List results; + private String status; + + @Getter + @Setter + public static class Result { + private double elevation; + } + } + + + @Override + public void fetchHeights(List nodes) { + List nodesWithoutHeight = nodes.stream() + .filter(node -> node.getId() == null) + .toList(); + + if(nodesWithoutHeight.isEmpty()) return; + + List> partitions = partitionNodes(nodesWithoutHeight, MAX_BATCH_SIZE); + + List> apiCalls = partitions.stream() + .map(batch -> fetchElevationAsync(batch) + .subscribeOn(Schedulers.boundedElastic()) + .doOnNext(response -> mapElevationToNodes(response, batch)) + .then()) + .toList(); + + // 모든 비동기 호출이 완료될 때까지 대기 + Mono.when(apiCalls).block(); + } + + // 노드를 512개씩 분할 + private List> partitionNodes(List nodes, int batchSize) { + List> partitions = new ArrayList<>(); + for (int i = 0; i < nodes.size(); i += batchSize) { + partitions.add(nodes.subList(i, Math.min(i + batchSize, nodes.size()))); + } + return partitions; + } + + // 병렬로 Google Map Elevation API 호출 + private Mono fetchElevationAsync(List nodes) { + StringBuilder coordinateBuilder = convertCoordinatesToStringBuilder(nodes); + + return webClient.get() + .uri(uriBuilder -> uriBuilder + .queryParam("locations", coordinateBuilder.toString()) + .queryParam("key", apiKey) + .build()) + .retrieve() + .bodyToMono(ElevationResponse.class); + } + + private StringBuilder convertCoordinatesToStringBuilder(List nodes) { + StringBuilder result = new StringBuilder(); + for (Node node : nodes) { + result.append(node.getCoordinates().getY()) + .append(",") + .append(node.getCoordinates().getX()) + .append("|"); + } + result.setLength(result.length() - 1); + return result; + } + + // 응답결과(해발고도)를 매핑해주는 메서드 + private void mapElevationToNodes(ElevationResponse response, List batch) { + log.info("Current Thread: {}", Thread.currentThread().getName()); + + if (!response.getStatus().equals(SUCCESS_STATUS)) { + throw new ElevationApiException("Google Elevation API Fail: " + response.getStatus(), ErrorCode.ELEVATION_API_ERROR); + } + + if (response.results.size() != batch.size()) { + log.error("The number of responses does not match the number of requests. " + + "request size: {}, response size: {}", batch.size(), response.results.size()); + throw new ElevationApiException("The number of responses does not match the number of requests. " + + "request size: " + batch.size() + "response size: " + response.results.size(), ErrorCode.ELEVATION_API_ERROR); + } + + log.info("Google Elevation API Success. batch size: " + batch.size()); + + List results = response.getResults(); + for (int i = 0; i < results.size(); i++) { + batch.get(i).setHeight(results.get(i).getElevation()); + } + + } + +} 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/node/controller/NodeApi.java index 61e2874..df098fe 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/node/controller/NodeApi.java @@ -25,10 +25,10 @@ public interface NodeApi { ResponseEntity> getBuildings( @PathVariable("univId") Long univId, @RequestParam(value = "level", required = false, defaultValue = "1") int level, - @RequestParam(value = "left-up-lng") double leftUpLng, @RequestParam(value = "left-up-lat") double leftUpLat, - @RequestParam(value = "right-down-lng") double rightDownLng, - @RequestParam(value = "right-down-lat") double rightDownLat + @RequestParam(value = "left-up-lng") double leftUpLng, + @RequestParam(value = "right-down-lat") double rightDownLat, + @RequestParam(value = "right-down-lng") double rightDownLng ); @Operation(summary = "건물 노드 검색") 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/node/controller/NodeController.java index 70400ca..47344bc 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/node/controller/NodeController.java @@ -24,13 +24,13 @@ public class NodeController implements NodeApi { public ResponseEntity> getBuildings( @PathVariable("univId") Long univId, @RequestParam(value = "level", required = false, defaultValue = "1") int level, - @RequestParam(value = "left-up-lng") double leftUpLng, @RequestParam(value = "left-up-lat") double leftUpLat, - @RequestParam(value = "right-down-lng") double rightDownLng, - @RequestParam(value = "right-down-lat") double rightDownLat + @RequestParam(value = "left-up-lng") double leftUpLng, + @RequestParam(value = "right-down-lat") double rightDownLat, + @RequestParam(value = "right-down-lng") double rightDownLng ) { - List buildingResDTOS = nodeService.getBuildings(univId, level, leftUpLng, leftUpLat, - rightDownLng, rightDownLat); + List buildingResDTOS = nodeService.getBuildings(univId, level, leftUpLat, leftUpLng, + rightDownLat, rightDownLng); return ResponseEntity.ok().body(buildingResDTOS); } 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/node/dto/GetBuildingResDTO.java index f2eb7b2..e6a92b5 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/node/dto/GetBuildingResDTO.java @@ -1,7 +1,5 @@ package com.softeer5.uniro_backend.node.dto; -import java.util.Map; - import com.softeer5.uniro_backend.node.entity.Building; import com.softeer5.uniro_backend.node.entity.Node; @@ -10,17 +8,19 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; -@Schema(name = "GetBuildingResDTO", description = "건물 노드 조회 DTO") @Getter +@Schema(name = "GetBuildingResDTO", description = "건물 노드 조회 DTO") @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public class GetBuildingResDTO { @Schema(description = "노드 ID", example = "4") private final Long nodeId; - @Schema(description = "건물 노드 좌표 (위도 및 경도)", - example = "{\"lag\": 127.123456, \"lat\": 37.123456}") - private final Map node; + @Schema(description = "건물 노드 x 좌표 (위도 및 경도)", example = "127.123456") + private final double lng; + + @Schema(description = "건물 노드 y 좌표 (위도 및 경도)", example = "37.123456") + private final double lat; @Schema(description = "건물 이름", example = "공학관") private final String buildingName; @@ -38,7 +38,7 @@ public class GetBuildingResDTO { public static GetBuildingResDTO of(Building building, Node node) { - return new GetBuildingResDTO(node.getId(), node.getXY(), building.getName(), building.getImageUrl(), + return new GetBuildingResDTO(node.getId(), node.getX(), node.getY(), building.getName(), building.getImageUrl(), building.getPhoneNumber(), building.getAddress()); } 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/node/entity/Node.java index aede38b..f07c6ea 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/node/entity/Node.java @@ -1,8 +1,12 @@ package com.softeer5.uniro_backend.node.entity; +import static com.softeer5.uniro_backend.common.constant.UniroConst.*; + import java.util.Map; +import lombok.*; +import org.hibernate.envers.Audited; import org.locationtech.jts.geom.Point; import jakarta.persistence.Column; @@ -11,15 +15,14 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.validation.constraints.NotNull; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.ToString; @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +@AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter @ToString +@Audited public class Node { @Id @@ -27,6 +30,7 @@ public class Node { private Long id; @NotNull + @Column(columnDefinition = "POINT SRID 4326") private Point coordinates; private double height; @@ -42,4 +46,35 @@ public Map getXY(){ return Map.of("lat", coordinates.getY(), "lng", coordinates.getX()); } + public double getX(){ + return coordinates.getX(); + } + + public double getY(){ + return coordinates.getY(); + } + + public void setHeight(double height) { + this.height = height; + } + + public void setCoordinates(Point coordinates) { + this.coordinates = coordinates; + } + + public void setCore(boolean isCore){ + this.isCore = isCore; + } + + public String getNodeKey() { + return coordinates.getX() + NODE_KEY_DELIMITER + coordinates.getY(); + } + + @Builder + private Node(Point coordinates, double height, boolean isCore, Long univId) { + this.coordinates = coordinates; + this.height = height; + this.isCore = isCore; + this.univId = univId; + } } 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 index 9049dc7..a231d1b 100644 --- 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 @@ -18,9 +18,9 @@ public interface BuildingRepository extends JpaRepository, Build JOIN FETCH Node n ON b.nodeId = n.id WHERE b.univId = :univId AND b.level >= :level - AND ST_Within(n.coordinates, ST_MakeEnvelope(:lux, :luy, :rdx, :rdy, 4326)) + AND ST_Within(n.coordinates, ST_PolygonFromText((:polygon),4326)) """) - List findByUnivIdAndLevelWithNode(Long univId, int level, double lux , double luy, double rdx , double rdy); + List findByUnivIdAndLevelWithNode(Long univId, int level, String polygon); @Query(""" SELECT new com.softeer5.uniro_backend.node.dto.BuildingNode(b, n) 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 index 0652e1a..2a56491 100644 --- 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 @@ -9,6 +9,7 @@ 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; @@ -24,10 +25,10 @@ public class NodeService { public List getBuildings( Long univId, int level, - double leftUpLng, double leftUpLat, double rightDownLng , double rightDownLat) { + double leftUpLat, double leftUpLng, double rightDownLat, double rightDownLng) { - List buildingNodes = buildingRepository.findByUnivIdAndLevelWithNode( - univId, level, leftUpLng, leftUpLat, rightDownLng, rightDownLat); + 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())) 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/resolver/CautionListConverter.java index 9d2ee40..2feb160 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/resolver/CautionListConverter.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/resolver/CautionListConverter.java @@ -21,7 +21,7 @@ public class CautionListConverter implements AttributeConverter public String convertToDatabaseColumn(Set attribute) { try { if (attribute == null) { - return null; // List가 null일 경우, DB에 저장할 값도 null + return "[]"; // List가 null일 경우, DB에 저장할 값은 [] } return mapper.writeValueAsString(attribute); } catch (JsonProcessingException e) { 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/resolver/DangerListConverter.java index 741b004..f6246cf 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/resolver/DangerListConverter.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/resolver/DangerListConverter.java @@ -19,7 +19,7 @@ public class DangerListConverter implements AttributeConverter, public String convertToDatabaseColumn(Set attribute) { try { if (attribute == null) { - return null; // List가 null일 경우, DB에 저장할 값도 null + return "[]"; // List가 null일 경우, DB에 저장할 값은 [] } return mapper.writeValueAsString(attribute); } catch (JsonProcessingException e) { 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/route/controller/RouteApi.java index 7e370c5..5b9dfc8 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/route/controller/RouteApi.java @@ -1,6 +1,12 @@ package com.softeer5.uniro_backend.route.controller; -import com.softeer5.uniro_backend.route.dto.*; +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 org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; @@ -12,8 +18,6 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; -import java.util.List; - @Tag(name = "간선 및 위험&주의 요소 관련 Api") public interface RouteApi { @@ -22,7 +26,7 @@ public interface RouteApi { @ApiResponse(responseCode = "200", description = "모든 지도 조회 성공"), @ApiResponse(responseCode = "400", description = "EXCEPTION(임시)", content = @Content), }) - public ResponseEntity> getAllRoutesAndNodes(@PathVariable("univId") Long univId); + ResponseEntity getAllRoutesAndNodes(@PathVariable("univId") Long univId); @Operation(summary = "위험&주의 요소 조회") @ApiResponses(value = { @@ -36,11 +40,9 @@ public interface RouteApi { @ApiResponse(responseCode = "200", description = "단일 route의 위험&주의 요소 조회 성공"), @ApiResponse(responseCode = "400", description = "EXCEPTION(임시)", content = @Content), }) - ResponseEntity getRisk(@PathVariable("univId") Long univId, - @RequestParam(value = "start-lat") double startLat, - @RequestParam(value = "start-lng") double startLng, - @RequestParam(value = "end-lat") double endLat, - @RequestParam(value = "end-lng") double endLng); + ResponseEntity getRisk( + @PathVariable("univId") Long univId, + @PathVariable("routeId") Long routeId); @Operation(summary = "위험&주의 요소 제보") @ApiResponses(value = { @@ -51,6 +53,14 @@ ResponseEntity updateRisk(@PathVariable("univId") Long univId, @PathVariable("routeId") Long routeId, @RequestBody PostRiskReqDTO postRiskReqDTO); + @Operation(summary = "새로운 길 추가") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "길 추가 성공"), + @ApiResponse(responseCode = "400", description = "EXCEPTION(임시)", content = @Content), + }) + ResponseEntity createRoute (@PathVariable("univId") Long univId, + @RequestBody CreateRoutesReqDTO routes); + @Operation(summary = "빠른 길 계산") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "빠른 길 계산 성공"), 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 index 94fbdab..e1a6260 100644 --- 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 @@ -1,7 +1,14 @@ package com.softeer5.uniro_backend.route.controller; -import com.softeer5.uniro_backend.route.dto.*; +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.*; @@ -9,8 +16,6 @@ import lombok.RequiredArgsConstructor; -import java.util.List; - @RequiredArgsConstructor @RestController public class RouteController implements RouteApi { @@ -20,8 +25,8 @@ public class RouteController implements RouteApi { @Override @GetMapping("/{univId}/routes") - public ResponseEntity> getAllRoutesAndNodes(@PathVariable("univId") Long univId){ - List allRoutes = routeService.getAllRoutes(univId); + public ResponseEntity getAllRoutesAndNodes(@PathVariable("univId") Long univId){ + GetAllRoutesResDTO allRoutes = routeService.getAllRoutes(univId); return ResponseEntity.ok().body(allRoutes); } @@ -33,13 +38,10 @@ public ResponseEntity getRiskRoutes(@PathVariable("univId") } @Override - @GetMapping("/{univId}/route/risk") + @GetMapping("/{univId}/routes/{routeId}/risk") public ResponseEntity getRisk(@PathVariable("univId") Long univId, - @RequestParam(value = "start-lat") double startLat, - @RequestParam(value = "start-lng") double startLng, - @RequestParam(value = "end-lat") double endLat, - @RequestParam(value = "end-lng") double endLng){ - GetRiskResDTO riskResDTO = routeService.getRisk(univId,startLat,startLng,endLat,endLng); + @PathVariable(value = "routeId") Long routeId){ + GetRiskResDTO riskResDTO = routeService.getRisk(univId, routeId); return ResponseEntity.ok().body(riskResDTO); } @@ -52,6 +54,14 @@ public ResponseEntity updateRisk (@PathVariable("univId") Long univId, 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, diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/GetAllRoutesResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/GetAllRoutesResDTO.java deleted file mode 100644 index 51d6b41..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/GetAllRoutesResDTO.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.softeer5.uniro_backend.route.dto; - -import com.softeer5.uniro_backend.route.entity.CoreRoute; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; - -import java.util.List; -import java.util.Map; - -@Getter -@Schema(name = "GetAllRoutesResDTO", description = "모든 노드,루트 조회 DTO") -public class GetAllRoutesResDTO { - @Schema(description = "코어루트(코어노드-코어노드) id", example = "1") - private final Long coreRouteId; - @Schema(description = "node1(코어노드) id", example = "3") - private final Long node1Id; - @Schema(description = "node2(코어노드) id", example = "4") - private final Long node2Id; - @Schema(description = "길을 이루는 좌표목록", example = "") - private final List> routes; - - private GetAllRoutesResDTO(Long coreRouteId, Long node1Id, Long node2Id, List> routes){ - this.coreRouteId = coreRouteId; - this.node1Id = node1Id; - this.node2Id = node2Id; - this.routes = routes; - } - - public static GetAllRoutesResDTO of(CoreRoute coreRoute){ - return new GetAllRoutesResDTO(coreRoute.getId(), coreRoute.getNode1Id(), coreRoute.getNode2Id(), coreRoute.getPathAsList()); - } -} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/PostRiskReqDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/PostRiskReqDTO.java deleted file mode 100644 index 2ec52d7..0000000 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/PostRiskReqDTO.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.softeer5.uniro_backend.route.dto; - -import com.softeer5.uniro_backend.route.entity.CautionType; -import com.softeer5.uniro_backend.route.entity.DangerType; -import lombok.Getter; -import lombok.Setter; - -import java.util.List; - -@Getter -@Setter -public class PostRiskReqDTO { - private List cautionTypes; - private List dangerTypes; -} 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/route/dto/request/CreateRouteReqDTO.java new file mode 100644 index 0000000..ec4a021 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/request/CreateRouteReqDTO.java @@ -0,0 +1,17 @@ +package com.softeer5.uniro_backend.route.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +@Schema(name = "CreateRouteReqDTO", description = "길 생성 단일 노드 정보 DTO") +public class CreateRouteReqDTO { + + @Schema(description = "x 좌표", example = "127.123456") + private final double lng; + + @Schema(description = "y 좌표", example = "37.123456") + private final double lat; +} 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/route/dto/request/CreateRoutesReqDTO.java new file mode 100644 index 0000000..9ca0ba7 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/request/CreateRoutesReqDTO.java @@ -0,0 +1,22 @@ +package com.softeer5.uniro_backend.route.dto.request; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +@Schema(name = "CreateRoutesReqDTO", description = "길 생성 노드 목록 정보 DTO") +public class CreateRoutesReqDTO { + + @Schema(description = "시작 노드 id", example = "3") + private final Long startNodeId; + + @Schema(description = "종료 노드 id", example = "4") + private final Long endNodeId; + + @Schema(description = "노드 좌표", example = "") + 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/route/dto/request/PostRiskReqDTO.java new file mode 100644 index 0000000..3c65ab3 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/request/PostRiskReqDTO.java @@ -0,0 +1,23 @@ +package com.softeer5.uniro_backend.route.dto.request; + +import com.softeer5.uniro_backend.route.entity.CautionType; +import com.softeer5.uniro_backend.route.entity.DangerType; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@RequiredArgsConstructor +@Schema(name = "PostRiskReqDTO", description = "위험 간선 제보 DTO") +public class PostRiskReqDTO { + + @Schema(description = "주의 요소 목록", example = "[\"SLOPE\", \"CURB\"]") + private List cautionTypes; + + @Schema(description = "위험 요소 목록", example = "[\"CURB\", \"STAIRS\"]") + private List dangerTypes; +} 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/route/dto/response/CoreRouteResDTO.java new file mode 100644 index 0000000..452f909 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/CoreRouteResDTO.java @@ -0,0 +1,27 @@ +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 = "CoreRouteResDTO", description = "코어 루트 정보 DTO") +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class CoreRouteResDTO { + + @Schema(description = "코어 노드 1", example = "32") + private final Long coreNode1Id; + + @Schema(description = "코어 노드 2", example = "13") + private final Long cordNode2Id; + + @Schema(description = "간선 정보", example = "") + private final List routes; + + public static CoreRouteResDTO of(Long startNode, Long endNode, List routes){ + return new CoreRouteResDTO(startNode, endNode, routes); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/FastestRouteResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/FastestRouteResDTO.java similarity index 73% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/FastestRouteResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/FastestRouteResDTO.java index bf60130..0aa2aab 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/FastestRouteResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/FastestRouteResDTO.java @@ -1,4 +1,4 @@ -package com.softeer5.uniro_backend.route.dto; +package com.softeer5.uniro_backend.route.dto.response; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AccessLevel; @@ -8,7 +8,7 @@ import java.util.List; @Getter -@Schema(name = "GetDangerResDTO", description = "위험 요소 조회 DTO") +@Schema(name = "FastestRouteResDTO", description = "빠른 경로 조회 DTO") @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public class FastestRouteResDTO { @Schema(description = "길 찾기 결과에 위험요소가 포함되어있는지 여부", example = "true") @@ -18,15 +18,15 @@ public class FastestRouteResDTO { @Schema(description = "총 걸리는 시간(초)", example = "1050.32198432") private final double totalCost; @Schema(description = "길 찾기 결과에 포함된 모든 길", example = "") - private final List routes; + private final List routes; @Schema(description = "상세안내 관련 정보", example = "") - private final List routeDetails; + private final List routeDetails; public static FastestRouteResDTO of(boolean hasCaution, double totalDistance, double totalCost, - List routes, - List routeDetails) { + 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/dto/response/GetAllRoutesResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetAllRoutesResDTO.java new file mode 100644 index 0000000..9b0de0f --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetAllRoutesResDTO.java @@ -0,0 +1,25 @@ +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; + +import com.softeer5.uniro_backend.route.dto.response.CoreRouteResDTO; + +@Getter +@Schema(name = "GetAllRoutesResDTO", description = "모든 노드,루트 조회 DTO") +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class GetAllRoutesResDTO { + + @Schema(description = "노드 정보 (id, 좌표)", example = "") + private final List nodeInfos; + @Schema(description = "루트 정보 (id, startNodeId, endNodeId)", example = "") + private final List coreRoutes; + + public static GetAllRoutesResDTO of(List nodeInfos, List coreRoutes){ + return new GetAllRoutesResDTO(nodeInfos, coreRoutes); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/GetCautionResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetCautionResDTO.java similarity index 62% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/GetCautionResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetCautionResDTO.java index acd1f5d..8d44017 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/GetCautionResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetCautionResDTO.java @@ -1,4 +1,4 @@ -package com.softeer5.uniro_backend.route.dto; +package com.softeer5.uniro_backend.route.dto.response; import java.util.List; import java.util.Map; @@ -14,13 +14,13 @@ @RequiredArgsConstructor(access = AccessLevel.PRIVATE) @Getter -@Schema(name = "GetDangerResDTO", description = "위험 요소 조회 DTO") +@Schema(name = "GetCautionResDTO", description = "위험 요소 조회 DTO") public class GetCautionResDTO { - @Schema(description = "노드 1의 좌표", example = "{\"lag\": 127.123456, \"lat\": 37.123456}") + @Schema(description = "노드 1의 좌표", example = "{\"lng\": 127.123456, \"lat\": 37.123456}") private final Map node1; - @Schema(description = "노드 2의 좌표", example = "{\"lag\": 127.123456, \"lat\": 37.123456}") + @Schema(description = "노드 2의 좌표", example = "{\"lng\": 127.123456, \"lat\": 37.123456}") private final Map node2; @Schema(description = "간선 id", example = "3") @@ -29,7 +29,7 @@ public class GetCautionResDTO { @Schema(description = "위험 요소 타입 리스트", example = "[\"SLOPE\", \"STAIRS\"]") private final List cautionTypes; - public static GetCautionResDTO of(Node node1, Node node2, Long routeId, List cautionTypes){ - return new GetCautionResDTO(node1.getXY(), node2.getXY(), routeId, cautionTypes); + public static GetCautionResDTO of(Map node1, Map node2, Long routeId, List cautionTypes){ + return new GetCautionResDTO(node1, node2, routeId, cautionTypes); } } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/GetDangerResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetDangerResDTO.java similarity index 63% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/GetDangerResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetDangerResDTO.java index a1bd606..caecb7f 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/GetDangerResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetDangerResDTO.java @@ -1,4 +1,4 @@ -package com.softeer5.uniro_backend.route.dto; +package com.softeer5.uniro_backend.route.dto.response; import java.util.List; import java.util.Map; @@ -16,19 +16,23 @@ @Getter public class GetDangerResDTO { - @Schema(description = "노드 1의 좌표", example = "{\"lag\": 127.123456, \"lat\": 37.123456}") + @Schema(description = "노드 1의 좌표", example = "{\"lng\": 127.123456, \"lat\": 37.123456}") private final Map node1; - @Schema(description = "노드 2의 좌표", example = "{\"lag\": 127.123456, \"lat\": 37.123456}") + @Schema(description = "노드 2의 좌표", example = "{\"lng\": 127.123456, \"lat\": 37.123456}") private final Map node2; @Schema(description = "간선 id", example = "3") private final Long routeId; - @Schema(description = "위험 요소 타입 리스트", example = "[\"SLOPE\", \"STAIRS\"]") + @Schema(description = "위험 요소 타입 리스트", example = "[\"CURB\", \"STAIRS\"]") private final List dangerTypes; - public static GetDangerResDTO of(Node node1, Node node2, Long routeId, List dangerTypes){ - return new GetDangerResDTO(node1.getXY(), node2.getXY(), routeId, dangerTypes); + public static GetDangerResDTO of(Map node1, Map node2, Long routeId, List dangerTypes){ + return new GetDangerResDTO(node1, node2, routeId, dangerTypes); + } + + public List getDangerTypes() { + return dangerTypes; } } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/GetRiskResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetRiskResDTO.java similarity index 86% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/GetRiskResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetRiskResDTO.java index 5f188d5..fa980e8 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/GetRiskResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetRiskResDTO.java @@ -1,4 +1,4 @@ -package com.softeer5.uniro_backend.route.dto; +package com.softeer5.uniro_backend.route.dto.response; import com.softeer5.uniro_backend.route.entity.CautionType; import com.softeer5.uniro_backend.route.entity.DangerType; @@ -11,7 +11,7 @@ import java.util.List; @Getter -@Schema(name = "GetAllRoutesResDTO", description = "특정 간선의 주의/위험요소 조회 DTO") +@Schema(name = "GetRiskResDTO", description = "특정 간선의 주의/위험요소 조회 DTO") @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public class GetRiskResDTO { @Schema(description = "route ID", example = "3") diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/GetRiskRoutesResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetRiskRoutesResDTO.java similarity index 79% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/GetRiskRoutesResDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetRiskRoutesResDTO.java index b32ae7f..ac096ca 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/GetRiskRoutesResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/GetRiskRoutesResDTO.java @@ -1,4 +1,4 @@ -package com.softeer5.uniro_backend.route.dto; +package com.softeer5.uniro_backend.route.dto.response; import java.util.List; @@ -12,10 +12,10 @@ @Getter public class GetRiskRoutesResDTO { - @Schema(description = "위험요소 객체", example = "") + @Schema(description = "위험요소 객체") private final List dangerRoutes; - @Schema(description = "주의요소 객체", example = "") + @Schema(description = "주의요소 객체") private final List cautionRoutes; public static GetRiskRoutesResDTO of(List dangerRoutes, List cautionRoutes) { 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/route/dto/response/NodeInfoResDTO.java new file mode 100644 index 0000000..9fa9641 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/NodeInfoResDTO.java @@ -0,0 +1,26 @@ +package com.softeer5.uniro_backend.route.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) +public class NodeInfoResDTO { + + @Schema(description = "노드 ID", example = "10") + private final Long nodeId; + + @Schema(description = "x 좌표 (위도 및 경도)", example = "127.123456") + private final double lng; + + @Schema(description = "y 좌표 (위도 및 경도)", example = "37.123456") + private final double lat; + + public static NodeInfoResDTO of(Long nodeId, double lng, double lat) { + return new NodeInfoResDTO(nodeId, lng, lat); + } +} 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/route/dto/response/RouteCoordinatesInfoResDTO.java new file mode 100644 index 0000000..2771147 --- /dev/null +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/RouteCoordinatesInfoResDTO.java @@ -0,0 +1,23 @@ +package com.softeer5.uniro_backend.route.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Getter +@Schema(name = "RouteCoordinatesInfo", description = "코어 루트 정보 DTO") +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class RouteCoordinatesInfoResDTO { + + @Schema(description = "간선 id", example = "2") + private final Long routeId; + + @Schema(description = "시작 노드 id", example = "23") + private final Long startNodeId; + + @Schema(description = "종료 노드 id", example = "54") + private final Long endNodeId; + + public static RouteCoordinatesInfoResDTO of(Long routeId, Long startNodeId, Long endNodeId) { + return new RouteCoordinatesInfoResDTO(routeId, startNodeId, endNodeId); + } +} diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/RouteDetailDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/RouteDetailResDTO.java similarity index 50% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/RouteDetailDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/RouteDetailResDTO.java index c63fec7..540274e 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/RouteDetailDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/RouteDetailResDTO.java @@ -1,4 +1,4 @@ -package com.softeer5.uniro_backend.route.dto; +package com.softeer5.uniro_backend.route.dto.response; import com.softeer5.uniro_backend.route.entity.DirectionType; import io.swagger.v3.oas.annotations.media.Schema; @@ -6,15 +6,20 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; +import java.util.Map; + @Getter @RequiredArgsConstructor(access = AccessLevel.PRIVATE) -public class RouteDetailDTO { +@Schema(name = "RouteDetailResDTO", description = "상세 경로 DTO") +public class RouteDetailResDTO { @Schema(description = "다음 이정표까지의 거리", example = "17.38721484") private final double dist; @Schema(description = "좌회전, 우회전, 위험요소 등 정보", example = "RIGHT") private final DirectionType directionType; + @Schema(description = "상세 경로의 좌표", example = "{\"lng\": 127.123456, \"lat\": 37.123456}") + private final Map coordinates; - public static RouteDetailDTO of(double dist, DirectionType directionType) { - return new RouteDetailDTO(dist, directionType); + public static RouteDetailResDTO of(double dist, DirectionType directionType, Map coordinates) { + return new RouteDetailResDTO(dist, directionType, coordinates); } } diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/RouteInfoDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/RouteInfoResDTO.java similarity index 61% rename from uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/RouteInfoDTO.java rename to uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/RouteInfoResDTO.java index fdaa155..6a38995 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/RouteInfoDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/dto/response/RouteInfoResDTO.java @@ -1,5 +1,6 @@ -package com.softeer5.uniro_backend.route.dto; +package com.softeer5.uniro_backend.route.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 io.swagger.v3.oas.annotations.media.Schema; @@ -12,20 +13,21 @@ @Getter @RequiredArgsConstructor(access = AccessLevel.PRIVATE) -public class RouteInfoDTO { +@Schema(name = "RouteInfoResDTO", description = "경로 정보 DTO") +public class RouteInfoResDTO { @Schema(description = "위험요소가 있는 길의 ID", example = "2") private final Long routeId; - @Schema(description = "노드 1의 좌표", example = "{\"lag\": 127.123456, \"lat\": 37.123456}") + @Schema(description = "노드 1의 좌표", example = "{\"lng\": 127.123456, \"lat\": 37.123456}") private final Map node1; - @Schema(description = "노드 2의 좌표", example = "{\"lag\": 127.123456, \"lat\": 37.123456}") + @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 RouteInfoDTO of(Route route) { - return new RouteInfoDTO(route.getId(), - route.getNode1().getXY(), - route.getNode2().getXY(), + public static RouteInfoResDTO of(Route route, Node node1, Node node2) { + return new RouteInfoResDTO(route.getId(), + node1.getXY(), + node2.getXY(), route.getCautionFactors()); } } 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 index a14fc6b..d17113c 100644 --- 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 @@ -1,8 +1,8 @@ package com.softeer5.uniro_backend.route.entity; public enum CautionType { - SLOPE, // 경사 CURB, // 턱 - STAIRS, // 계단 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/route/entity/DangerType.java index 5897407..481bcc7 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/route/entity/DangerType.java @@ -1,8 +1,8 @@ package com.softeer5.uniro_backend.route.entity; public enum DangerType { - SLOPE, // 경사 CURB, // 턱 STAIRS, // 계단 - CRACK, // 균열 + SLOPE, // 경사 + ETC, // 기타 } 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/route/entity/Route.java index bc7e727..56ab4e3 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/route/entity/Route.java @@ -2,12 +2,15 @@ import static jakarta.persistence.FetchType.*; +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 org.hibernate.envers.Audited; +import org.hibernate.envers.RelationTargetAuditMode; import org.locationtech.jts.geom.LineString; import jakarta.persistence.Column; @@ -20,12 +23,14 @@ import jakarta.persistence.ManyToOne; import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter +@Audited public class Route { @Id @@ -34,16 +39,18 @@ public class Route { private double cost; - @Column(columnDefinition = "geometry(LineString, 4326)") // WGS84 좌표계 + @Column(columnDefinition = "LINESTRING SRID 4326") // WGS84 좌표계 private LineString path; @ManyToOne(fetch = LAZY) @JoinColumn(referencedColumnName = "id", name = "node1_id") + @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED) @NotNull private Node node1; @ManyToOne(fetch = LAZY) @JoinColumn(referencedColumnName = "id", name = "node2_id") + @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED) @NotNull private Node node2; @@ -55,11 +62,13 @@ public class Route { @Convert(converter = CautionListConverter.class) @Column(name = "caution_factors") - private Set cautionFactors; + @NotNull + private Set cautionFactors = new HashSet<>(); @Convert(converter = DangerListConverter.class) @Column(name = "danger_factors") - private Set dangerFactors; + @NotNull + private Set dangerFactors = new HashSet<>(); public List getCautionFactorsByList(){ return cautionFactors.stream().toList(); @@ -71,7 +80,7 @@ public List getDangerFactorsByList(){ public void setCautionFactors(List cautionFactors) { this.cautionFactors.clear(); - this.cautionFactors.addAll(cautionFactors); + this.cautionFactors.addAll(cautionFactors); } public void setDangerFactors(List dangerFactors) { @@ -79,4 +88,16 @@ public void setDangerFactors(List dangerFactors) { this.dangerFactors.addAll(dangerFactors); } + @Builder + private Route(double cost, LineString path, Node node1, Node node2, Long univId, Long coreRouteId, + Set cautionFactors, Set dangerFactors) { + this.cost = cost; + 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/route/repository/RouteRepository.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/route/repository/RouteRepository.java index 695a373..8925506 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/route/repository/RouteRepository.java @@ -12,20 +12,17 @@ import com.softeer5.uniro_backend.route.entity.Route; @Repository -public interface RouteRepository extends JpaRepository { +public interface RouteRepository extends JpaRepository { @EntityGraph(attributePaths = {"node1", "node2"}) @Query("SELECT r FROM Route r WHERE r.univId = :univId") List findAllRouteByUnivIdWithNodes(Long univId); - @Query("SELECT r " - + "FROM Route r " - + "JOIN FETCH r.node1 n1 " - + "JOIN FETCH r.node2 n2 " - + "WHERE r.univId = :univId " - + "AND (r.cautionFactors IS NOT NULL OR r.dangerFactors IS NOT NULL)" - ) - List findRiskRouteByUnivIdWithNode(@Param("univId") Long univId); + @Query(value = "SELECT r.* FROM route r " + + "WHERE r.univ_id = :univId " + + "AND (r.caution_factors LIKE '[\"%' OR r.danger_factors LIKE '[\"%')", + nativeQuery = true) + List findRiskRouteByUnivId(@Param("univId") Long univId); @Query(value = """ SELECT r.* FROM route r 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 index eb5428d..bcc1ce9 100644 --- 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 @@ -1,18 +1,37 @@ 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.dto.RouteDetailDTO; -import com.softeer5.uniro_backend.route.dto.RouteInfoDTO; -import com.softeer5.uniro_backend.route.dto.FastestRouteResDTO; 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; @@ -23,6 +42,8 @@ @Transactional(readOnly = true) public class RouteCalculationService { private final RouteRepository routeRepository; + private final MapClient mapClient; + private final NodeRepository nodeRepository; @AllArgsConstructor private class CostToNextNode implements Comparable { @@ -38,7 +59,7 @@ public int compareTo(CostToNextNode o) { public FastestRouteResDTO calculateFastestRoute(Long univId, Long startNodeId, Long endNodeId){ if(startNodeId.equals(endNodeId)){ - throw new SameStartAndEndPointException("Start and end nodes cannot be the same", ErrorCode.SAME_START_AND_END_POINT); + throw new SameStartAndEndPointException("Start and end nodes cannot be the same", SAME_START_AND_END_POINT); } //인접 리스트 @@ -67,8 +88,8 @@ public FastestRouteResDTO calculateFastestRoute(Long univId, Long startNodeId, L double totalCost = 0.0; double totalDistance = 0.0; - List routeInfoDTOS = new ArrayList<>(); - + List routeInfoDTOS = new ArrayList<>(); + Node currentNode = startNode; // 외부 변수를 수정해야하기 때문에 for-loop문 사용 for (Route route : shortestRoutes) { totalCost += route.getCost(); @@ -78,10 +99,18 @@ public FastestRouteResDTO calculateFastestRoute(Long univId, Long startNodeId, L hasCaution = true; } - routeInfoDTOS.add(RouteInfoDTO.of(route)); + 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); + List details = getRouteDetail(startNode, endNode, shortestRoutes); return FastestRouteResDTO.of(hasCaution, totalDistance, totalCost, routeInfoDTOS, details); } @@ -187,10 +216,12 @@ private double calculateDistance(Route route) { } // 길 상세정보를 추출하는 메서드 - private List getRouteDetail(Node startNode, Node endNode, List shortestRoutes){ - List details = new ArrayList<>(); + 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 getRouteDetail(Node startNode, Node endNode, List getRouteDetail(Node startNode, Node endNode, List getRouteDetail(Node startNode, Node endNode, List 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 index 6c8ae2c..1071e0b 100644 --- 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 @@ -1,17 +1,31 @@ package com.softeer5.uniro_backend.route.service; -import java.util.List; +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.common.utils.Utils; -import com.softeer5.uniro_backend.route.dto.*; -import com.softeer5.uniro_backend.route.entity.CoreRoute; -import com.softeer5.uniro_backend.route.repository.CoreRouteRepository; +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; @@ -22,16 +36,143 @@ @Transactional(readOnly = true) public class RouteService { private final RouteRepository routeRepository; - private final CoreRouteRepository coreRouteRepository; - public List getAllRoutes(Long univId) { - List coreRoutes = coreRouteRepository.findByUnivId(univId); - return coreRoutes.stream().map(GetAllRoutesResDTO::of).toList(); + 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.findRiskRouteByUnivIdWithNode(univId); + List riskRoutes = routeRepository.findRiskRouteByUnivId(univId); List dangerRoutes = mapRoutesToDangerDTO(riskRoutes); List cautionRoutes = mapRoutesToCautionDTO(riskRoutes); @@ -41,10 +182,10 @@ public GetRiskRoutesResDTO getRiskRoutes(Long univId) { private List mapRoutesToDangerDTO(List routes) { return routes.stream() - .filter(route -> route.getDangerFactors() != null) // 위험 요소가 있는 경로만 필터링 + .filter(route -> !route.getDangerFactors().isEmpty() && route.getCautionFactors().isEmpty()) // 위험 요소가 있는 경로만 필터링 .map(route -> GetDangerResDTO.of( - route.getNode1(), - route.getNode2(), + getPoint(route.getPath().getCoordinates()[0]), + getPoint(route.getPath().getCoordinates()[1]), route.getId(), route.getDangerFactorsByList() )).toList(); @@ -52,46 +193,34 @@ private List mapRoutesToDangerDTO(List routes) { private List mapRoutesToCautionDTO(List routes) { return routes.stream() - .filter(route -> route.getDangerFactors() == null && route.getCautionFactors() != null) + .filter(route -> route.getDangerFactors().isEmpty() && !route.getCautionFactors().isEmpty()) .map(route -> GetCautionResDTO.of( - route.getNode1(), - route.getNode2(), + getPoint(route.getPath().getCoordinates()[0]), + getPoint(route.getPath().getCoordinates()[1]), route.getId(), route.getCautionFactorsByList() )).toList(); } - - public GetRiskResDTO getRisk(Long univId, double startLat, double startLng, double endLat, double endLng) { - String startWTK = Utils.convertDoubleToPointWTK(startLat, startLng); - String endWTK = Utils.convertDoubleToPointWTK(endLat, endLng); - - Route routeWithJoin = routeRepository.findRouteByPointsAndUnivId(univId, startWTK ,endWTK) - .orElseThrow(() -> new RouteNotFoundException("Route Not Found", ErrorCode.ROUTE_NOT_FOUND)); - - /* - // LineString 사용버전 - List coordinates = Arrays.asList( - new double[]{startLat, startLng}, - new double[]{endLat, endLng} - ); - String lineStringWTK = Utils.convertDoubleToLineStringWTK(coordinates); - Collections.reverse(coordinates); - String reverseLineStringWTK = Utils.convertDoubleToLineStringWTK(coordinates); - - Route routeWithoutJoin = routeRepository.findRouteByLineStringAndUnivId(univId,lineStringWTK,reverseLineStringWTK) - .orElseThrow(() -> new RouteNotFoundException("Route Not Found", ErrorCode.ROUTE_NOT_FOUND)); - - */ + private Map getPoint(Coordinate c) { + Map point = new HashMap<>(); + point.put("lat", c.getY()); + point.put("lng", c.getX()); + return point; + } - return GetRiskResDTO.of(routeWithJoin); + 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", ErrorCode.ROUTE_NOT_FOUND)); + .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.", diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/dto/SearchUnivResDTO.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/dto/SearchUnivResDTO.java index 226fd6c..9dc32df 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/dto/SearchUnivResDTO.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/dto/SearchUnivResDTO.java @@ -7,9 +7,9 @@ import java.util.List; -@Schema(name = "SearchUnivResDTO", description = "대학 검색 DTO") @Getter @RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Schema(name = "SearchUnivResDTO", description = "대학 검색 DTO") public class SearchUnivResDTO { @Schema(description = "실제 data", example = "") diff --git a/uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/dto/UnivInfo.java b/uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/dto/UnivInfo.java index b390c46..63a813c 100644 --- a/uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/dto/UnivInfo.java +++ b/uniro_backend/src/main/java/com/softeer5/uniro_backend/univ/dto/UnivInfo.java @@ -1,13 +1,22 @@ package com.softeer5.uniro_backend.univ.dto; import com.querydsl.core.annotations.QueryProjection; + +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; @Getter +@Schema(name = "UnivInfo", description = "대학 정보 DTO") public class UnivInfo { - private Long id; - private String name; - private String imageUrl; + + @Schema(description = "대학교 id", example = "11") + private final Long id; + + @Schema(description = "대학교 이름", example = "한양대학교") + private final String name; + + @Schema(description = "대학교 로고 이미지 url", example = "www.image.com") + private final String imageUrl; @QueryProjection public UnivInfo(Long id, String name, String imageUrl) { diff --git a/uniro_backend/src/main/resources/application-dev.yml b/uniro_backend/src/main/resources/application-dev.yml new file mode 100644 index 0000000..70c6f01 --- /dev/null +++ b/uniro_backend/src/main/resources/application-dev.yml @@ -0,0 +1,44 @@ +spring: + config: + import: application.properties + datasource: + url: ${DB_URL} + username: ${DB_USER} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL8Dialect + format_sql: true + 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} + +management: + endpoints: + jmx: + exposure: + exclude: "*" + web: + exposure: + include: info, health, prometheus + prometheus: + metrics: + export: + enabled: true + +cors: + allowed-origins: ${allowed-origins} \ 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 534c39b..cec95d4 100644 --- a/uniro_backend/src/main/resources/application-local.yml +++ b/uniro_backend/src/main/resources/application-local.yml @@ -1,23 +1,43 @@ spring: datasource: - url: jdbc:h2:mem:uniro-local-db;mode=mysql - driverClassName: org.h2.Driver - username: sa + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/uniro?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC + 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 + #schema-locations: classpath:h2gis-setting.sql + mode: always h2: console: enabled: true path: /h2-console +map: + api: + key: ${google.api.key} + +management: + endpoints: + jmx: + exposure: + exclude: "*" + web: + exposure: + include: info, health, prometheus + prometheus: + metrics: + export: + enabled: true + +cors: + allowed-origins: ${allowed-origins} \ No newline at end of file diff --git a/uniro_backend/src/test/resources/application-test.yml b/uniro_backend/src/main/resources/application-test.yml similarity index 80% rename from uniro_backend/src/test/resources/application-test.yml rename to uniro_backend/src/main/resources/application-test.yml index 57fec89..b2d34ab 100644 --- a/uniro_backend/src/test/resources/application-test.yml +++ b/uniro_backend/src/main/resources/application-test.yml @@ -1,4 +1,8 @@ spring: + config: + activate: + on-profile: test + datasource: url: jdbc:h2:mem:uniro-local-db;DATABASE_TO_UPPER=FALSE;mode=mysql driverClassName: org.h2.Driver @@ -21,3 +25,9 @@ spring: console: enabled: true path: /h2-console +map: + api: + key: ${google.api.key} + +cors: + allowed-origins: ${allowed-origins} \ No newline at end of file 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 new file mode 100644 index 0000000..3f20cff --- /dev/null +++ b/uniro_backend/src/test/java/com/softeer5/uniro_backend/fixture/NodeFixture.java @@ -0,0 +1,27 @@ +package com.softeer5.uniro_backend.fixture; + +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; + +import com.softeer5.uniro_backend.common.utils.GeoUtils; +import com.softeer5.uniro_backend.node.entity.Node; + +public class NodeFixture { + static GeometryFactory geometryFactory = GeoUtils.getInstance(); + + public static Node createNode(double x, double y){ + return Node.builder() + .univId(1001L) + .isCore(false) + .coordinates(geometryFactory.createPoint(new Coordinate(x,y))) + .build(); + } + + public static Node createCoreNode(double x, double y){ + return Node.builder() + .univId(1001L) + .isCore(true) + .coordinates(geometryFactory.createPoint(new Coordinate(x,y))) + .build(); + } +} 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 new file mode 100644 index 0000000..91d6250 --- /dev/null +++ b/uniro_backend/src/test/java/com/softeer5/uniro_backend/fixture/RouteFixture.java @@ -0,0 +1,19 @@ +package com.softeer5.uniro_backend.fixture; + +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; + +public class RouteFixture { + static GeometryFactory geometryFactory = GeoUtils.getInstance(); + + public static Route createRoute(Node node1, Node node2){ + return Route.builder() + .node1(node1) + .node2(node2) + .univId(1001L) + .build(); + } +} 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/node/client/MapClientImplTest.java new file mode 100644 index 0000000..5485fc7 --- /dev/null +++ b/uniro_backend/src/test/java/com/softeer5/uniro_backend/node/client/MapClientImplTest.java @@ -0,0 +1,89 @@ +package com.softeer5.uniro_backend.node.client; + +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 org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.Point; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles("test") +public class MapClientImplTest { + @Autowired + private MapClientImpl mapClientImpl; + private final GeometryFactory geometryFactory = new GeometryFactory(); + + @Test + @DisplayName("Google Elevation API 테스트") + public void testFetchHeights() { + Point p1 = geometryFactory.createPoint(new Coordinate(-122.4194, 37.7749)); + Point p2 = geometryFactory.createPoint(new Coordinate(-74.0060, 40.7128)); + Point p3 = geometryFactory.createPoint(new Coordinate(126.9780, 37.5665)); + + Node node1 = Node.builder().build(); + node1.setCoordinates(p1); + + Node node2 = Node.builder().build(); + node2.setCoordinates(p2); + + Node node3 = Node.builder().build(); + node3.setCoordinates(p3); + + List nodes = List.of(node1, node2, node3); + + mapClientImpl.fetchHeights(nodes); + + for (Node node : nodes) { + assertThat(node.getHeight()).isNotNull(); + System.out.println("Node coordinates: " + node.getCoordinates() + ", Elevation: " + node.getHeight()); + } + } + + + @Test + @DisplayName("Google Elevation API exception 발생 테스트") + public void testFetchHeights2() { + Point p1 = geometryFactory.createPoint(new Coordinate(-122.4194, 37.7749)); + Point p2 = geometryFactory.createPoint(new Coordinate(-74.0060, 40.7128)); + Point p3 = geometryFactory.createPoint(new Coordinate(126.9780, 37.5665)); + + Node node1 = Node.builder().build(); + node1.setCoordinates(p1); + + Node node2 = Node.builder().build(); + node2.setCoordinates(p2); + + Node node3 = Node.builder().build(); + node3.setCoordinates(p3); + + List nodes = List.of(node1, node2, node3); + + // 강제로 @Value로 가져온 Google API Key를 초기화 할 수 있도록 새로운 객체 생성 + mapClientImpl = new MapClientImpl(); + //mapClientImpl.fetchHeights(nodes); + try { + mapClientImpl.fetchHeights(nodes); + Assertions.fail("예외가 발생하지 않았습니다. 예상된 ElevationApiException이 발생해야 합니다."); + } catch (ElevationApiException e) { + System.out.println("예외 발생 메시지: " + e.getMessage()); + assertThat(e.getMessage()).contains("Google Elevation API Fail"); + } + + for (Node node : nodes) { + assertThat(node.getHeight()).isNotNull(); + System.out.println("Node coordinates: " + node.getCoordinates() + ", Elevation: " + node.getHeight()); + } + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000..73caf8f --- /dev/null +++ b/uniro_backend/src/test/java/com/softeer5/uniro_backend/route/RouteCalculationServiceTest.java @@ -0,0 +1,185 @@ +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_frontend/eslint.config.js b/uniro_frontend/eslint.config.js index 3b70bfc..9681bd7 100644 --- a/uniro_frontend/eslint.config.js +++ b/uniro_frontend/eslint.config.js @@ -4,11 +4,12 @@ import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; import tseslint from "typescript-eslint"; import eslintPluginPrettier from "eslint-plugin-prettier"; +import pluginQuery from "@tanstack/eslint-plugin-query"; export default tseslint.config( { ignores: ["dist"] }, { - extends: [js.configs.recommended, ...tseslint.configs.recommended], + extends: [js.configs.recommended, ...tseslint.configs.recommended, pluginQuery.configs["flat/recommended"]], files: ["**/*.{ts,tsx}"], languageOptions: { ecmaVersion: 2020, diff --git a/uniro_frontend/index.html b/uniro_frontend/index.html index 1dc72fe..61ef53b 100644 --- a/uniro_frontend/index.html +++ b/uniro_frontend/index.html @@ -2,10 +2,10 @@ - + - Vite + React + TS + UNIRO
diff --git a/uniro_frontend/package-lock.json b/uniro_frontend/package-lock.json index eef8d56..81f3d3e 100644 --- a/uniro_frontend/package-lock.json +++ b/uniro_frontend/package-lock.json @@ -11,6 +11,8 @@ "@googlemaps/js-api-loader": "^1.16.8", "@react-spring/web": "^9.7.5", "@tailwindcss/vite": "^4.0.0", + "@tanstack/react-query": "^5.66.0", + "@tanstack/react-query-devtools": "^5.66.0", "framer-motion": "^12.0.6", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -21,6 +23,7 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@tanstack/eslint-plugin-query": "^5.66.0", "@types/google.maps": "^3.58.1", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", @@ -1874,6 +1877,76 @@ "vite": "^5.2.0 || ^6" } }, + "node_modules/@tanstack/eslint-plugin-query": { + "version": "5.66.0", + "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.66.0.tgz", + "integrity": "sha512-CzZhBxicLDuuSJbkZ4nPcuBqWnhLu72Zt9p/7qLQ93BepVnZJV6ZDlBLBuN5eg7YRACwECPLsntnwo1zuhgseQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.18.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.66.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.66.0.tgz", + "integrity": "sha512-J+JeBtthiKxrpzUu7rfIPDzhscXF2p5zE/hVdrqkACBP8Yu0M96mwJ5m/8cPPYQE9aRNvXztXHlNwIh4FEeMZw==", + "license": "MIT", + "funding": { + "type": "github", + "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", + "integrity": "sha512-z3sYixFQJe8hndFnXgWu7C79ctL+pI0KAelYyW+khaNJ1m22lWrhJU2QrsTcRKMuVPtoZvfBYrTStIdKo+x0Xw==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.66.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "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_frontend/package.json b/uniro_frontend/package.json index cedd9d5..ef0e237 100644 --- a/uniro_frontend/package.json +++ b/uniro_frontend/package.json @@ -13,6 +13,8 @@ "@googlemaps/js-api-loader": "^1.16.8", "@react-spring/web": "^9.7.5", "@tailwindcss/vite": "^4.0.0", + "@tanstack/react-query": "^5.66.0", + "@tanstack/react-query-devtools": "^5.66.0", "framer-motion": "^12.0.6", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -23,6 +25,7 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@tanstack/eslint-plugin-query": "^5.66.0", "@types/google.maps": "^3.58.1", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", diff --git a/uniro_frontend/public/loading/background.svg b/uniro_frontend/public/loading/background.svg new file mode 100644 index 0000000..0ffd172 --- /dev/null +++ b/uniro_frontend/public/loading/background.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/uniro_frontend/public/loading/spinner.gif b/uniro_frontend/public/loading/spinner.gif new file mode 100644 index 0000000..2839f3b Binary files /dev/null and b/uniro_frontend/public/loading/spinner.gif differ diff --git a/uniro_frontend/public/vite.svg b/uniro_frontend/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/uniro_frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/uniro_frontend/src/App.tsx b/uniro_frontend/src/App.tsx index 679c567..df5068e 100644 --- a/uniro_frontend/src/App.tsx +++ b/uniro_frontend/src/App.tsx @@ -2,21 +2,46 @@ import { Route, Routes } from "react-router"; import "./App.css"; import Demo from "./pages/demo"; import LandingPage from "./pages/landing"; -import UniversitySearchPage from "./pages/search"; +import UniversitySearchPage from "./pages/universitySearch"; import MapPage from "./pages/map"; import BuildingSearchPage from "./pages/buildingSearch"; import NavigationResultPage from "./pages/navigationResult"; +import ReportRoutePage from "./pages/reportRoute"; +import ReportForm from "./pages/reportForm"; +import ReportRiskPage from "./pages/reportRisk"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useDynamicSuspense } from "./hooks/useDynamicSuspense"; +import OfflinePage from "./pages/offline"; +import useNetworkStatus from "./hooks/useNetworkStatus"; +import ErrorPage from "./pages/error"; +import { Suspense } from "react"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; + +const queryClient = new QueryClient(); function App() { + const { location, fallback } = useDynamicSuspense(); + useNetworkStatus(); return ( - - } /> - } /> - } /> - } /> - } /> - } /> - + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + /** 에러 페이지 */ + } /> + } /> + + + + ); } diff --git a/uniro_frontend/src/api/nodes.ts b/uniro_frontend/src/api/nodes.ts new file mode 100644 index 0000000..e553f3b --- /dev/null +++ b/uniro_frontend/src/api/nodes.ts @@ -0,0 +1,19 @@ +import { Building } from "../data/types/node"; +import { getFetch } 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, + }); +}; diff --git a/uniro_frontend/src/api/route.ts b/uniro_frontend/src/api/route.ts new file mode 100644 index 0000000..c167382 --- /dev/null +++ b/uniro_frontend/src/api/route.ts @@ -0,0 +1,58 @@ +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); +}; diff --git a/uniro_frontend/src/api/routes.ts b/uniro_frontend/src/api/routes.ts new file mode 100644 index 0000000..6ac4143 --- /dev/null +++ b/uniro_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_frontend/src/api/search.ts b/uniro_frontend/src/api/search.ts new file mode 100644 index 0000000..d7c47e9 --- /dev/null +++ b/uniro_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_frontend/src/api/transformer/route.ts b/uniro_frontend/src/api/transformer/route.ts new file mode 100644 index 0000000..fbf6508 --- /dev/null +++ b/uniro_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_frontend/src/api/transformer/search.ts b/uniro_frontend/src/api/transformer/search.ts new file mode 100644 index 0000000..3d5fc76 --- /dev/null +++ b/uniro_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_frontend/src/api/type/request/route.d.ts b/uniro_frontend/src/api/type/request/route.d.ts new file mode 100644 index 0000000..d42feee --- /dev/null +++ b/uniro_frontend/src/api/type/request/route.d.ts @@ -0,0 +1,3 @@ +export type getAllRouteRequest = { + univId: number; +}; diff --git a/uniro_frontend/src/api/type/response/route.d.ts b/uniro_frontend/src/api/type/response/route.d.ts new file mode 100644 index 0000000..5120f82 --- /dev/null +++ b/uniro_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_frontend/src/api/type/response/search.d.ts b/uniro_frontend/src/api/type/response/search.d.ts new file mode 100644 index 0000000..0b7acde --- /dev/null +++ b/uniro_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_frontend/src/assets/error/error.svg b/uniro_frontend/src/assets/error/error.svg new file mode 100644 index 0000000..57cbfe9 --- /dev/null +++ b/uniro_frontend/src/assets/error/error.svg @@ -0,0 +1,3 @@ + + + diff --git a/uniro_frontend/src/assets/error/offline.svg b/uniro_frontend/src/assets/error/offline.svg new file mode 100644 index 0000000..d21a411 --- /dev/null +++ b/uniro_frontend/src/assets/error/offline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/uniro_frontend/src/assets/error/search-null.svg b/uniro_frontend/src/assets/error/search-null.svg new file mode 100644 index 0000000..2d8aa38 --- /dev/null +++ b/uniro_frontend/src/assets/error/search-null.svg @@ -0,0 +1,5 @@ + + + + + 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/assets/markers/university.svg b/uniro_frontend/src/assets/markers/university.svg new file mode 100644 index 0000000..9fc7685 --- /dev/null +++ b/uniro_frontend/src/assets/markers/university.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/uniro_frontend/src/component/NavgationMap.tsx b/uniro_frontend/src/component/NavgationMap.tsx index 1756403..d585c08 100644 --- a/uniro_frontend/src/component/NavgationMap.tsx +++ b/uniro_frontend/src/component/NavgationMap.tsx @@ -1,103 +1,177 @@ import { useEffect, useRef } from "react"; import useMap from "../hooks/useMap"; -import { NavigationRoute } from "../data/types/route"; +import { CautionRoute, DangerRoute, NavigationRouteList } from "../data/types/route"; import createAdvancedMarker from "../utils/markers/createAdvanedMarker"; import createMarkerElement from "../components/map/mapMarkers"; -import { Markers } from "../constant/enums"; +import { Markers } from "../constant/enum/markerEnum"; +import { Coord } from "../data/types/coord"; +import useRoutePoint from "../hooks/useRoutePoint"; +import { AdvancedMarker } from "../data/types/marker"; type MapProps = { style?: React.CSSProperties; - routes: NavigationRoute; - /** 바텀시트나 상단 UI에 의해 가려지는 영역이 있을 경우, 지도 fitBounds에 추가할 패딩값 */ + routeResult: NavigationRouteList; + risks: { + dangerRoutes: DangerRoute[]; + cautionRoutes: CautionRoute[]; + }; + isDetailView: boolean; topPadding?: number; bottomPadding?: number; }; -const NavigationMap = ({ style, routes, topPadding = 0, bottomPadding = 0 }: MapProps) => { +// TODO: useEffect로 경로가 모두 로딩된 이후에 마커가 생성되도록 수정하기 +// TODO: 경로 로딩 완료시 살짝 zoomIn 하는 부분 구현하기 + +const NavigationMap = ({ style, routeResult, risks, isDetailView, topPadding = 0, bottomPadding = 0 }: MapProps) => { const { mapRef, map, AdvancedMarker, Polyline } = useMap(); + const { origin, destination } = useRoutePoint(); const boundsRef = useRef(null); + const markersRef = useRef([]); if (!style) { style = { height: "100%", width: "100%" }; } useEffect(() => { - if (!map || !AdvancedMarker || !routes || !Polyline) return; + if (!mapRef || !map || !AdvancedMarker || !routeResult || !Polyline) return; + + if (routeResult.routes.length === 0) return; + + const cautionFactor: Coord[] = []; + + const { routes, routeDetails } = routeResult; + + // 하나의 길 완성 + const paths = [routes[0].node1, ...routes.map((el) => el.node2)]; - const { route: routeList } = routes; - if (!routeList || routeList.length === 0) return; const bounds = new google.maps.LatLngBounds(); new Polyline({ - path: [...routeList.map((edge) => edge.startNode), routeList[routeList.length - 1].endNode], + path: paths, map, strokeColor: "#000000", strokeWeight: 2.0, }); - routeList.forEach((edge, index) => { - const { startNode, endNode } = edge; - const startCoordinate = new google.maps.LatLng(startNode.lat, startNode.lng); - const endCoordinate = new google.maps.LatLng(endNode.lat, endNode.lng); - bounds.extend(startCoordinate); - bounds.extend(endCoordinate); - if (index !== 0 && index !== routeList.length - 1) { - const markerElement = createMarkerElement({ - type: Markers.WAYPOINT, - className: "translate-waypoint", - }); - if (index === 1) { - createAdvancedMarker(AdvancedMarker, map, startCoordinate, markerElement); - } - createAdvancedMarker(AdvancedMarker, map, endCoordinate, markerElement); - } + // waypoint 마커 찍기 + routeDetails.forEach((routeDetail, index) => { + const { coordinates } = routeDetail; + bounds.extend(coordinates); + const markerElement = createMarkerElement({ + type: Markers.WAYPOINT, + className: "translate-waypoint", + }); + createAdvancedMarker(AdvancedMarker, map, coordinates, markerElement); }); - const edgeRoutes = [routeList[0], routeList[routeList.length - 1]]; + // 시작 마커는 출발지 빌딩 표시 + const startMarkerElement = createMarkerElement({ + type: Markers.ORIGIN, + title: origin?.buildingName, + className: "translate-routemarker", + hasAnimation: true, + }); + const { lat: originLat, lng: originLng }: google.maps.LatLngLiteral = origin!; + const originCoord = { lat: originLat, lng: originLng }; + createAdvancedMarker(AdvancedMarker, map, originCoord, startMarkerElement); + bounds.extend(originCoord); - edgeRoutes.forEach((edge, index) => { - const { startNode, endNode } = edge; - if (index === 0) { - const startCoordinate = new google.maps.LatLng(startNode.lat, startNode.lng); - const markerElement = createMarkerElement({ - type: Markers.ORIGIN, - title: routes.originBuilding.buildingName, - className: "translate-routemarker", - }); - createAdvancedMarker(AdvancedMarker, map, startCoordinate, markerElement); - bounds.extend(startCoordinate); - } else { - const endCoordinate = new google.maps.LatLng(endNode.lat, endNode.lng); - const markerElement = createMarkerElement({ - type: Markers.DESTINATION, - title: routes.destinationBuilding.buildingName, - className: "translate-routemarker", - }); - createAdvancedMarker(AdvancedMarker, map, endCoordinate, markerElement); - bounds.extend(endCoordinate); - } + // 끝 마커는 도착지 빌딩 표시 + const endMarkerElement = createMarkerElement({ + type: Markers.DESTINATION, + title: destination?.buildingName, + className: "translate-routemarker", + 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); }); boundsRef.current = bounds; + map.fitBounds(bounds, { top: topPadding, - right: 50, + right: 30, bottom: bottomPadding, - left: 50, + left: 30, }); - }, [map, AdvancedMarker, Polyline, routes]); + }, [mapRef, map, AdvancedMarker, Polyline, routeResult]); useEffect(() => { if (!map || !boundsRef.current) return; map.fitBounds(boundsRef.current, { top: topPadding, - right: 50, + right: 30, bottom: bottomPadding, - left: 50, + left: 30, }); }, [map, bottomPadding, topPadding]); + useEffect(() => { + if (!AdvancedMarker || !map) return; + + if (isDetailView) { + const { routeDetails } = routeResult; + markersRef.current = []; + + routeDetails.forEach((routeDetail, index) => { + const { coordinates } = routeDetail; + const markerElement = createMarkerElement({ + type: Markers.NUMBERED_WAYPOINT, + number: index + 1, + hasAnimation: true, + }); + + const marker = createAdvancedMarker(AdvancedMarker, map, coordinates, markerElement); + + markersRef.current.push(marker); + }); + } + + return () => { + markersRef.current.forEach((marker) => { + const markerElement = marker.content as HTMLElement; + if (markerElement) { + markerElement.classList.add("fade-out"); + setTimeout(() => { + marker.map = null; + }, 300); + } else { + marker.map = null; + } + }); + markersRef.current = []; + }; + }, [isDetailView, AdvancedMarker, map]); + return
; }; diff --git a/uniro_frontend/src/components/error/Error.tsx b/uniro_frontend/src/components/error/Error.tsx new file mode 100644 index 0000000..d2dfb1b --- /dev/null +++ b/uniro_frontend/src/components/error/Error.tsx @@ -0,0 +1,15 @@ +import ErrorIcon from "../../assets/error/error.svg?react"; + +export default function Error() { + return ( +
+ +
+

+ 일시적인 오류로
데이터를 불러올 수 없습니다. +

+

잠시 후 다시 시도해 주세요.

+
+
+ ); +} diff --git a/uniro_frontend/src/components/error/Offline.tsx b/uniro_frontend/src/components/error/Offline.tsx new file mode 100644 index 0000000..e48ec8a --- /dev/null +++ b/uniro_frontend/src/components/error/Offline.tsx @@ -0,0 +1,13 @@ +import OfflineIcon from "../../assets/error/offline.svg?react"; + +export default function Offline() { + return ( +
+ +
+

오프라인 상태입니다.

+

네트워크에 연결됐는지 확인해 주세요.

+
+
+ ); +} diff --git a/uniro_frontend/src/components/error/SearchNull.tsx b/uniro_frontend/src/components/error/SearchNull.tsx new file mode 100644 index 0000000..fbf5772 --- /dev/null +++ b/uniro_frontend/src/components/error/SearchNull.tsx @@ -0,0 +1,13 @@ +import NullIcon from "../../assets/error/search-null.svg?react"; + +export default function SearchNull({ message }: { message: string }) { + return ( +
+ +
+

검색 결과가 없습니다.

+

{message}

+
+
+ ); +} diff --git a/uniro_frontend/src/components/loading/loading.tsx b/uniro_frontend/src/components/loading/loading.tsx new file mode 100644 index 0000000..4af3061 --- /dev/null +++ b/uniro_frontend/src/components/loading/loading.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import useUniversityInfo from "../../hooks/useUniversityInfo"; +type Props = { + isLoading: boolean; + loadingContent?: string; +}; + +const svgModules = import.meta.glob("/src/assets/university/*.svg", { eager: true }); + +const Loading = ({ isLoading, loadingContent }: Props) => { + const { university } = useUniversityInfo(); + const svgPath = (svgModules[`/src/assets/university/${university}.svg`] as { default: string })?.default; + return ( + + {isLoading && ( + +
+ +

{university?.name}

+
+

{loadingContent}

+ +
+ )} +
+ ); +}; + +export default Loading; diff --git a/uniro_frontend/src/components/map/TopSheet.tsx b/uniro_frontend/src/components/map/TopSheet.tsx index 3d478d1..a7c7b61 100644 --- a/uniro_frontend/src/components/map/TopSheet.tsx +++ b/uniro_frontend/src/components/map/TopSheet.tsx @@ -7,7 +7,7 @@ import { Link } from "react-router"; import { motion } from "framer-motion"; import useRoutePoint from "../../hooks/useRoutePoint"; import useSearchBuilding from "../../hooks/useSearchBuilding"; -import { RoutePoint } from "../../constant/enums"; +import { RoutePoint } from "../../constant/enum/routeEnum"; export default function TopSheet({ open }: { open: boolean }) { const { origin, setOrigin, destination, setDestination, switchBuilding } = useRoutePoint(); diff --git a/uniro_frontend/src/components/map/backButton.tsx b/uniro_frontend/src/components/map/backButton.tsx new file mode 100644 index 0000000..892025d --- /dev/null +++ b/uniro_frontend/src/components/map/backButton.tsx @@ -0,0 +1,21 @@ +import { useNavigate } from "react-router"; +import ChevronLeft from "../../../public/icons/chevron-left.svg?react"; + +interface BackButtonProps { + className?: string; + onClick?: () => void +} + +export default function BackButton({ className = "", onClick }: BackButtonProps) { + const navigate = useNavigate(); + + const handleBack = () => { + navigate(-1); + } + + return ( + + ) +} diff --git a/uniro_frontend/src/components/map/mapBottomSheet.tsx b/uniro_frontend/src/components/map/mapBottomSheet.tsx index cafa43a..aa376b2 100644 --- a/uniro_frontend/src/components/map/mapBottomSheet.tsx +++ b/uniro_frontend/src/components/map/mapBottomSheet.tsx @@ -12,7 +12,7 @@ interface MapBottomSheetFromListProps { export function MapBottomSheetFromList({ building, buttonText, onClick }: MapBottomSheetFromListProps) { if (building.property === undefined) return; - const { id, lng, lat, isCore, buildingName, buildingImageUrl, phoneNumber, address } = building.property; + const { nodeId, lng, lat, buildingName, buildingImageUrl, phoneNumber, address } = building.property; return (
@@ -46,7 +46,7 @@ interface MapBottomSheetProps { export function MapBottomSheetFromMarker({ building, onClickLeft, onClickRight }: MapBottomSheetProps) { if (building.property === undefined) return; - const { id, lng, lat, isCore, buildingName, buildingImageUrl, phoneNumber, address } = building.property; + const { nodeId, lng, lat, buildingName, buildingImageUrl, phoneNumber, address } = building.property; return (
diff --git a/uniro_frontend/src/components/map/mapMarkers.tsx b/uniro_frontend/src/components/map/mapMarkers.tsx index cf4ed47..b44877f 100644 --- a/uniro_frontend/src/components/map/mapMarkers.tsx +++ b/uniro_frontend/src/components/map/mapMarkers.tsx @@ -1,5 +1,5 @@ -import { MarkerTypes } from "../../data/types/marker"; -import { Markers } from "../../constant/enums"; +import { Markers } from "../../constant/enum/markerEnum"; +import { MarkerTypes } from "../../data/types/enum"; const markerImages = import.meta.glob("/src/assets/markers/*.svg", { eager: true }); @@ -54,17 +54,60 @@ function createContainerElement(className?: string) { return container; } +function attachAnimation(container: HTMLElement, hasAnimation: boolean) { + if (hasAnimation) { + const outerContainer = document.createElement("div"); + outerContainer.className = "marker-appear"; + outerContainer.appendChild(container); + return outerContainer; + } + + return container; +} + +function createNumberMarkerElement({ + number, + className, + hasAnimation = false, +}: { + number: number | string; + className?: string; + hasAnimation?: boolean; +}): HTMLElement { + const container = createContainerElement(className); + const numberText = document.createElement("p"); + numberText.innerText = `${number}`; + numberText.className = + "h-[17px] w-[17px] flex items-center justify-center text-white text-kor-body3 text-[10.25px] font-bold bg-[#161616] rounded-full"; + const markerWrapper = document.createElement("div"); + markerWrapper.className = "relative flex items-center justify-center"; + markerWrapper.style.transform = "translateY(8.5px)"; + markerWrapper.appendChild(numberText); + + container.appendChild(markerWrapper); + + return attachAnimation(container, hasAnimation); +} + export default function createMarkerElement({ type, title, className, hasTopContent = false, + hasAnimation = false, + number = 0, }: { type: MarkerTypes; className?: string; title?: string; hasTopContent?: boolean; + hasAnimation?: boolean; + number?: number; }): HTMLElement { + if (number && type === Markers.NUMBERED_WAYPOINT) { + return createNumberMarkerElement({ number, className, hasAnimation }); + } + const container = createContainerElement(className); const markerImage = createImageElement(type); @@ -74,14 +117,15 @@ export default function createMarkerElement({ if (hasTopContent) { container.appendChild(markerTitle); container.appendChild(markerImage); - return container; + return attachAnimation(container, hasAnimation); } container.appendChild(markerImage); container.appendChild(markerTitle); - return container; + return attachAnimation(container, hasAnimation); } container.appendChild(markerImage); - return container; + + return attachAnimation(container, hasAnimation); } diff --git a/uniro_frontend/src/components/map/reportButton.tsx b/uniro_frontend/src/components/map/reportButton.tsx index 5944561..f23a3a6 100644 --- a/uniro_frontend/src/components/map/reportButton.tsx +++ b/uniro_frontend/src/components/map/reportButton.tsx @@ -1,14 +1,14 @@ -import { Link } from "react-router"; import ReportIcon from "../../assets/report.svg?react"; +import { ButtonHTMLAttributes } from "react"; -export default function ReportButton() { +export default function ReportButton({ ...rest }: ButtonHTMLAttributes) { return ( - 제보하기 - + ); } diff --git a/uniro_frontend/src/components/map/reportModal.tsx b/uniro_frontend/src/components/map/reportModal.tsx new file mode 100644 index 0000000..af79ee6 --- /dev/null +++ b/uniro_frontend/src/components/map/reportModal.tsx @@ -0,0 +1,31 @@ +import { Link } from "react-router"; +import ChevronRight from "../../../public/icons/chevron-right.svg?react"; + +interface ReportModalProps { + close: () => void; +} + +export default function ReportModal({ close }: ReportModalProps) { + return ( +
+ +

새로운 길 제보

+ + + + +

불편한 길 제보

+ + +
+ ); +} diff --git a/uniro_frontend/src/components/navigation/navigationDescription.tsx b/uniro_frontend/src/components/navigation/navigationDescription.tsx index e3ab1e0..85ad9ee 100644 --- a/uniro_frontend/src/components/navigation/navigationDescription.tsx +++ b/uniro_frontend/src/components/navigation/navigationDescription.tsx @@ -1,56 +1,65 @@ -import React, { useState } from "react"; +import React from "react"; import Cancel from "../../assets/icon/close.svg?react"; import CautionIcon from "../../assets/icon/cautionText.svg?react"; import SafeIcon from "../../assets/icon/safeText.svg?react"; import DestinationIcon from "../../assets/icon/destination.svg?react"; import OriginIcon from "../../assets/icon/start.svg?react"; import ResultDivider from "../../assets/icon/resultDivider.svg?react"; -import { NavigationRoute } from "../../data/types/route"; -import { mockNavigationRoute } from "../../data/mock/hanyangRoute"; +import { NavigationRouteList } from "../../data/types/route"; +import useRoutePoint from "../../hooks/useRoutePoint"; +import { formatDistance } from "../../utils/navigation/formatDistance"; +import { Link } from "react-router"; + const TITLE = "전동휠체어 예상소요시간"; type TopBarProps = { isDetailView: boolean; + navigationRoute: NavigationRouteList; }; -const NavigationDescription = ({ isDetailView }: TopBarProps) => { - const [route, _] = useState(mockNavigationRoute); +const NavigationDescription = ({ isDetailView, navigationRoute }: TopBarProps) => { + const { origin, destination } = useRoutePoint(); + const { totalCost, totalDistance, hasCaution } = navigationRoute; return (
{TITLE} - {!isDetailView && } + {!isDetailView && ( + + + + )}
- {route.totalCost} + {Math.floor(totalCost / 60)}
-
- {`${route.totalDistance}m`} +
+ {formatDistance(totalDistance)}
- {route.hasCaution ? : } + {navigationRoute.hasCaution ? : } - 가는 길에 주의 요소가 {route.hasCaution ? "있어요" : "없어요"} + 가는 길에 주의 요소가 {hasCaution ? "있어요" : "없어요"}
- {route.originBuilding.buildingName} + {origin?.buildingName}
- {route.destinationBuilding.buildingName} + {destination?.buildingName}
diff --git a/uniro_frontend/src/components/navigation/route/routeCard.tsx b/uniro_frontend/src/components/navigation/route/routeCard.tsx index d00f175..2ea7487 100644 --- a/uniro_frontend/src/components/navigation/route/routeCard.tsx +++ b/uniro_frontend/src/components/navigation/route/routeCard.tsx @@ -4,8 +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 { RouteEdge } from "../../../data/types/edge"; import { Building } from "../../../data/types/node"; +import { RouteDetail } from "../../../data/types/route"; +import useRoutePoint from "../../../hooks/useRoutePoint"; +import { formatDistance } from "../../../utils/navigation/formatDistance"; const NumberIcon = ({ index }: { index: number }) => { return ( @@ -22,17 +24,20 @@ export const RouteCard = ({ destinationBuilding, }: { index: number; - route: RouteEdge; + route: RouteDetail; originBuilding: Building; destinationBuilding: Building; }) => { - switch (route.direction) { + const { dist: distance, directionType } = route; + const formattedDistance = formatDistance(distance); + const { origin, destination } = useRoutePoint(); + switch (directionType.toLocaleLowerCase()) { case "straight": return (
-
{route.distance}m
+
{formattedDistance}
@@ -45,7 +50,20 @@ export const RouteCard = ({
-
{route.distance}m
+
{formattedDistance}
+
+
+ +
우회전
+
+
+ ); + case "sharp_right": + return ( +
+
+ +
{formattedDistance}
@@ -58,7 +76,20 @@ export const RouteCard = ({
-
{route.distance}m
+
{formattedDistance}
+
+
+ +
좌회전
+
+
+ ); + case "sharp_left": + return ( +
+
+ +
{formattedDistance}
@@ -71,11 +102,11 @@ export const RouteCard = ({
-
{route.distance}m
+
{formattedDistance}
-
U턴
+
유턴
); @@ -87,12 +118,12 @@ export const RouteCard = ({
출발
-
{originBuilding.buildingName}
-
{originBuilding.address}
+
{origin?.buildingName}
+
{origin?.address}
); - case "destination": + case "finish": return (
@@ -100,8 +131,8 @@ export const RouteCard = ({
도착
-
{destinationBuilding.buildingName}
-
{destinationBuilding.address}
+
{destination?.buildingName}
+
{destination?.address}
); @@ -110,11 +141,11 @@ export const RouteCard = ({
-
{route.distance}m
+
{formattedDistance}
-
턱이 있어요
+
); diff --git a/uniro_frontend/src/components/navigation/route/routeList.tsx b/uniro_frontend/src/components/navigation/route/routeList.tsx index 6c27124..39e8963 100644 --- a/uniro_frontend/src/components/navigation/route/routeList.tsx +++ b/uniro_frontend/src/components/navigation/route/routeList.tsx @@ -1,28 +1,42 @@ -import { Fragment } from "react"; -import { RouteEdge } from "../../../data/types/edge"; -import { Building } from "../../../data/types/node"; +import { Fragment, useEffect } from "react"; import { RouteCard } from "./routeCard"; +import useRoutePoint from "../../../hooks/useRoutePoint"; +import { RouteDetail } from "../../../data/types/route"; +import { Direction } from "../../../data/types/route"; type RouteListProps = { - routes: RouteEdge[]; - originBuilding: Building; - destinationBuilding: Building; + routes: RouteDetail[]; }; +// export type RouteDetail = { +// dist: number; +// directionType: Direction; +// coordinates: Coord; +// }; + const Divider = () =>
; -const RouteList = ({ routes, originBuilding, destinationBuilding }: RouteListProps) => { +const RouteList = ({ routes }: RouteListProps) => { + const { origin, destination } = useRoutePoint(); + return (
- {routes.map((route, index) => ( - - -
+ {[ + { + dist: 0, + directionType: "origin" as Direction, + coordinates: { lat: origin!.lat, lng: origin!.lng }, + }, + ...routes, + ].map((route, index) => ( + + +
diff --git a/uniro_frontend/src/components/report/formTitle.tsx b/uniro_frontend/src/components/report/formTitle.tsx new file mode 100644 index 0000000..379875b --- /dev/null +++ b/uniro_frontend/src/components/report/formTitle.tsx @@ -0,0 +1,28 @@ +import { PassableStatus } from "../../constant/enum/reportEnum"; +import { ReportModeType } from "../../data/types/report"; + +type FormTitleProps = { + isPrimary: boolean; + reportMode: ReportModeType; + passableStatus: PassableStatus; +}; + +const createTitleString = ({ isPrimary, reportMode, passableStatus }: FormTitleProps) => { + const mainTitle = isPrimary + ? "통행 가능 여부" + : `통행 ${passableStatus === PassableStatus.DANGER ? "불가" : "주의"} 불편 요소 선택`; + const subTitle = isPrimary + ? "통행 불가능한 길은 추천 여부에서 제외됩니다." + : `불편했던 요소를 ${reportMode === "create" ? "선택" : "수정"}해주세요.`; + return { mainTitle, subTitle }; +}; + +export const FormTitle = ({ isPrimary, reportMode, passableStatus }: FormTitleProps) => { + const { mainTitle, subTitle } = createTitleString({ isPrimary, reportMode, passableStatus }); + return ( +
+
{mainTitle}
+
{subTitle}
+
+ ); +}; diff --git a/uniro_frontend/src/components/report/primaryButton.tsx b/uniro_frontend/src/components/report/primaryButton.tsx new file mode 100644 index 0000000..caef3c8 --- /dev/null +++ b/uniro_frontend/src/components/report/primaryButton.tsx @@ -0,0 +1,25 @@ +import { useState } from "react"; +import { getThemeByPassableStatus } from "../../utils/report/getThemeByPassableStatus"; +import { PassableStatus } from "../../constant/enum/reportEnum"; + +export const PrimaryFormButton = ({ + onClick, + formPassableStatus, + passableStatus, + content, +}: { + onClick: (status: PassableStatus) => void; + formPassableStatus: PassableStatus; + passableStatus: PassableStatus; + content: string; +}) => { + const [selectedThemeString, _] = useState(getThemeByPassableStatus(passableStatus)); + return ( + + ); +}; diff --git a/uniro_frontend/src/components/report/primaryForm.tsx b/uniro_frontend/src/components/report/primaryForm.tsx new file mode 100644 index 0000000..a12f32e --- /dev/null +++ b/uniro_frontend/src/components/report/primaryForm.tsx @@ -0,0 +1,39 @@ +import { PassableStatus } from "../../constant/enum/reportEnum"; +import { PrimaryQuestionButton, ReportModeType } from "../../data/types/report"; + +import { FormTitle } from "./formTitle"; +import { PrimaryFormButton } from "./primaryButton"; + +const buttonConfig = [ + { content: "통행이 불가능해요", passableStatus: PassableStatus.DANGER }, + { content: "통행이 가능하지만 주의가 필요해요", passableStatus: PassableStatus.CAUTION }, + { content: "통행이 가능해졌어요", passableStatus: PassableStatus.RESTORED, mode: "update" }, +] as PrimaryQuestionButton[]; + +interface PrimaryFormProps { + passableStatus: PassableStatus; + handlePrimarySelect: (status: PassableStatus) => void; + reportMode: ReportModeType; +} + +export const PrimaryForm = ({ passableStatus, handlePrimarySelect, reportMode }: PrimaryFormProps) => { + return ( + <> + +
+ {buttonConfig.map((button, index) => { + if (button.mode && button.mode !== reportMode) return null; + return ( + + ); + })} +
+ + ); +}; diff --git a/uniro_frontend/src/components/report/reportDivider.tsx b/uniro_frontend/src/components/report/reportDivider.tsx new file mode 100644 index 0000000..db9bce0 --- /dev/null +++ b/uniro_frontend/src/components/report/reportDivider.tsx @@ -0,0 +1,3 @@ +export const ReportDivider = () => { + return
; +}; diff --git a/uniro_frontend/src/components/report/reportTitle.tsx b/uniro_frontend/src/components/report/reportTitle.tsx new file mode 100644 index 0000000..6845853 --- /dev/null +++ b/uniro_frontend/src/components/report/reportTitle.tsx @@ -0,0 +1,10 @@ +import { ReportModeType } from "../../data/types/report"; +export const ReportTitle = ({ reportMode }: { reportMode: ReportModeType }) => { + const CREATE_TITLE = "불편한 길을 알려주세요."; + const UPDATE_TITLE = "불편한 길을 수정해주세요."; + return ( +
+ {reportMode === "create" ? CREATE_TITLE : UPDATE_TITLE} +
+ ); +}; diff --git a/uniro_frontend/src/components/report/secondaryButton.tsx b/uniro_frontend/src/components/report/secondaryButton.tsx new file mode 100644 index 0000000..3985d79 --- /dev/null +++ b/uniro_frontend/src/components/report/secondaryButton.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { getThemeByPassableStatus } from "../../utils/report/getThemeByPassableStatus"; +import { CautionIssue, DangerIssue, IssueTypeKey, PassableStatus } from "../../constant/enum/reportEnum"; +import { CautionIssueType, DangerIssueType } from "../../data/types/enum"; + +export const SecondaryFormButton = ({ + onClick, + formPassableStatus, + isSelected, + contentKey, +}: { + onClick: (answer: IssueTypeKey) => void; + formPassableStatus: PassableStatus; + isSelected: boolean; + contentKey: IssueTypeKey; +}) => { + return ( + + ); +}; diff --git a/uniro_frontend/src/components/report/secondaryForm.tsx b/uniro_frontend/src/components/report/secondaryForm.tsx new file mode 100644 index 0000000..b95296c --- /dev/null +++ b/uniro_frontend/src/components/report/secondaryForm.tsx @@ -0,0 +1,51 @@ +import { IssueTypeKey, PassableStatus } from "../../constant/enum/reportEnum"; +import { IssueQuestionButtons, ReportFormData, ReportModeType } from "../../data/types/report"; + +import { FormTitle } from "./formTitle"; +import { SecondaryFormButton } from "./secondaryButton"; + +const buttonConfig = { + danger: [IssueTypeKey.CURB, IssueTypeKey.STAIRS, IssueTypeKey.SLOPE, IssueTypeKey.ETC], + caution: [IssueTypeKey.CURB, IssueTypeKey.CRACK, IssueTypeKey.SLOPE, IssueTypeKey.ETC], +} as IssueQuestionButtons; + +type SecondaryFormProps = { + reportMode: ReportModeType; + formData: ReportFormData; + handleSecondarySelect: (answer: IssueTypeKey) => void; +}; + +export const SecondaryForm = ({ formData, handleSecondarySelect, reportMode }: SecondaryFormProps) => { + return ( + <> + {(formData.passableStatus === PassableStatus.CAUTION || + formData.passableStatus === PassableStatus.DANGER) && ( + + )} +
+ {formData.passableStatus === PassableStatus.CAUTION && + buttonConfig.caution.map((button, index) => { + return ( + + ); + })} + {formData.passableStatus === PassableStatus.DANGER && + buttonConfig.danger.map((button, index) => { + return ( + + ); + })} +
+ + ); +}; diff --git a/uniro_frontend/src/components/universityButton.tsx b/uniro_frontend/src/components/universityButton.tsx index 2dd99bf..8822ba1 100644 --- a/uniro_frontend/src/components/universityButton.tsx +++ b/uniro_frontend/src/components/universityButton.tsx @@ -7,23 +7,19 @@ interface UniversityButtonProps { onClick: () => void; } -const svgModules = import.meta.glob("/src/assets/university/*.svg", { eager: true }); - export default function UniversityButton({ name, img, selected, onClick }: UniversityButtonProps) { const handleClick = (e: MouseEvent) => { e.stopPropagation(); onClick(); }; - const svgPath = (svgModules[`/src/assets/university/${img}`] as { default: string })?.default; - return (
  • diff --git a/uniro_frontend/src/constant/edge.ts b/uniro_frontend/src/constant/edge.ts new file mode 100644 index 0000000..d55001d --- /dev/null +++ b/uniro_frontend/src/constant/edge.ts @@ -0,0 +1 @@ +export const EDGE_LENGTH = 3; diff --git a/uniro_frontend/src/constant/enums.ts b/uniro_frontend/src/constant/enum/markerEnum.ts similarity index 75% rename from uniro_frontend/src/constant/enums.ts rename to uniro_frontend/src/constant/enum/markerEnum.ts index e816ddb..9487ca9 100644 --- a/uniro_frontend/src/constant/enums.ts +++ b/uniro_frontend/src/constant/enum/markerEnum.ts @@ -7,9 +7,5 @@ export const enum Markers { SELECTED_BUILDING = "selectedBuilding", WAYPOINT = "waypoint", NUMBERED_WAYPOINT = "numberedWayPoint", -} - -export const enum RoutePoint { - ORIGIN = "origin", - DESTINATION = "destination", + 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..c2f249d --- /dev/null +++ b/uniro_frontend/src/constant/enum/messageEnum.ts @@ -0,0 +1,6 @@ +export enum ReportRiskMessage { + DEFAULT = "선 위를 눌러 제보할 지점을 선택하세요", + CREATE = "이 지점으로 새로운 제보를 진행할까요?", + UPDATE = "이 지점에 제보된 기존 정보를 바꿀까요?", + ERROR = "선 위에서만 선택 가능해요", +} diff --git a/uniro_frontend/src/constant/enum/reportEnum.ts b/uniro_frontend/src/constant/enum/reportEnum.ts new file mode 100644 index 0000000..04ccce6 --- /dev/null +++ b/uniro_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_frontend/src/constant/enum/routeEnum.ts b/uniro_frontend/src/constant/enum/routeEnum.ts new file mode 100644 index 0000000..aeff459 --- /dev/null +++ b/uniro_frontend/src/constant/enum/routeEnum.ts @@ -0,0 +1,4 @@ +export const enum RoutePoint { + ORIGIN = "origin", + DESTINATION = "destination", +} diff --git a/uniro_frontend/src/constant/fallback.tsx b/uniro_frontend/src/constant/fallback.tsx new file mode 100644 index 0000000..adf8c73 --- /dev/null +++ b/uniro_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_frontend/src/constant/reportTheme.ts b/uniro_frontend/src/constant/reportTheme.ts new file mode 100644 index 0000000..8e8d6ac --- /dev/null +++ b/uniro_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_frontend/src/container/animatedSheetContainer.tsx b/uniro_frontend/src/container/animatedSheetContainer.tsx new file mode 100644 index 0000000..bbb5dd8 --- /dev/null +++ b/uniro_frontend/src/container/animatedSheetContainer.tsx @@ -0,0 +1,47 @@ +// container/animatedSheetContainer.tsx + +import React from "react"; +import { AnimatePresence, motion, MotionProps } from "framer-motion"; + +type AnimatedSheetContainerProps = { + isVisible: boolean; + height: number; + children: React.ReactNode; + className?: string; + transition?: MotionProps["transition"]; + motionProps?: MotionProps; +}; + +const AnimatedSheetContainer = ({ + isVisible, + height, + children, + className = "", + transition = { duration: 0.3, type: "tween" }, + motionProps = {}, +}: AnimatedSheetContainerProps) => { + return ( + + {isVisible && ( + + {children} + + )} + + ); +}; + +export default AnimatedSheetContainer; diff --git a/uniro_frontend/src/data/factory/edgeFactory.ts b/uniro_frontend/src/data/factory/edgeFactory.ts index 77ca341..ccb0549 100644 --- a/uniro_frontend/src/data/factory/edgeFactory.ts +++ b/uniro_frontend/src/data/factory/edgeFactory.ts @@ -1,4 +1,4 @@ -import { Direction, HazardEdge, RouteEdge } from "../types/edge"; +import { Direction, HazardEdge, RouteEdge } from "../types/route"; import { CautionFactor, DangerFactor } from "../types/factor"; import { CustomNode } from "../types/node"; diff --git a/uniro_frontend/src/data/factory/navigationFactory.ts b/uniro_frontend/src/data/factory/navigationFactory.ts index d62f342..37ff378 100644 --- a/uniro_frontend/src/data/factory/navigationFactory.ts +++ b/uniro_frontend/src/data/factory/navigationFactory.ts @@ -1,5 +1,5 @@ import { hanyangBuildings } from "../mock/hanyangBuildings"; -import { RouteEdge } from "../types/edge"; +import { RouteEdge } from "../types/route"; import { NavigationRoute } from "../types/route"; // TODO: Distance를 m-> km로 자동 변환해주는 util diff --git a/uniro_frontend/src/data/mock/hanyangHazardEdge.ts b/uniro_frontend/src/data/mock/hanyangHazardEdge.ts index 0cd4e67..fc54068 100644 --- a/uniro_frontend/src/data/mock/hanyangHazardEdge.ts +++ b/uniro_frontend/src/data/mock/hanyangHazardEdge.ts @@ -1,6 +1,6 @@ import { createHazardEdge } from "../factory/edgeFactory"; import { createNode } from "../factory/nodeFactory"; -import { HazardEdge } from "../types/edge"; +import { HazardEdge } from "../types/route"; import { CustomNode } from "../types/node"; const nodes: CustomNode[] = [ diff --git a/uniro_frontend/src/data/mock/hanyangRoute.ts b/uniro_frontend/src/data/mock/hanyangRoute.ts index 40c9594..ebe71b8 100644 --- a/uniro_frontend/src/data/mock/hanyangRoute.ts +++ b/uniro_frontend/src/data/mock/hanyangRoute.ts @@ -1,7 +1,7 @@ import { createHazardEdge, createRouteEdges } from "../factory/edgeFactory"; import { createNavigationRoute } from "../factory/navigationFactory"; import { createNode } from "../factory/nodeFactory"; -import { HazardEdge } from "../types/edge"; +import { HazardEdge } from "../types/route"; import { CustomNode } from "../types/node"; const nodes: CustomNode[] = [ @@ -17,8 +17,8 @@ const edges: HazardEdge[] = [ createHazardEdge("route2", nodes[1], nodes[2], ["도로에 균열이 있어요"]), createHazardEdge("route3", nodes[2], nodes[3]), createHazardEdge("route4", nodes[3], nodes[4]), - createHazardEdge("route5", nodes[3], nodes[4]), - createHazardEdge("route6", nodes[3], nodes[4]), + // createHazardEdge("route5", nodes[3], nodes[4]), + // createHazardEdge("route6", nodes[3], nodes[4]), ]; export const mockNavigationRoute = createNavigationRoute(createRouteEdges(edges)); diff --git a/uniro_frontend/src/data/types/coord.d.ts b/uniro_frontend/src/data/types/coord.d.ts new file mode 100644 index 0000000..625eadd --- /dev/null +++ b/uniro_frontend/src/data/types/coord.d.ts @@ -0,0 +1 @@ +export type Coord = google.maps.LatLngLiteral; diff --git a/uniro_frontend/src/data/types/edge.d.ts b/uniro_frontend/src/data/types/edge.d.ts deleted file mode 100644 index bf2271f..0000000 --- a/uniro_frontend/src/data/types/edge.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CautionFactor, DangerFactor } from "./factor"; -import { CustomNode } from "./node"; - -export interface Edge { - id: string; - startNode: CustomNode; - endNode: CustomNode; -} - -export type Direction = "origin" | "right" | "straight" | "left" | "uturn" | "destination" | "caution"; - -// 위험 요소 & 주의 요소 -// 마커를 표시하거나, 길 찾기 결과의 경로를 그릴 때 사용 -export interface HazardEdge extends Edge { - dangerFactors?: DangerFactor[]; - cautionFactors?: CautionFactor[]; -} - -export interface RouteEdge extends HazardEdge { - distance: number; - direction: Direction; -} diff --git a/uniro_frontend/src/data/types/enum.d.ts b/uniro_frontend/src/data/types/enum.d.ts new file mode 100644 index 0000000..ab2cc3f --- /dev/null +++ b/uniro_frontend/src/data/types/enum.d.ts @@ -0,0 +1,6 @@ +import { CautionIssue, DangerIssue } from "../../constant/enum/reportEnum"; +import { Markers } from "../../constant/enum/markerEnum"; + +export type DangerIssueType = keyof typeof DangerIssue; +export type CautionIssueType = keyof typeof CautionIssue; +export type MarkerTypes = (typeof Markers)[keyof typeof Markers]; diff --git a/uniro_frontend/src/data/types/event.d.ts b/uniro_frontend/src/data/types/event.d.ts new file mode 100644 index 0000000..36cf7f4 --- /dev/null +++ b/uniro_frontend/src/data/types/event.d.ts @@ -0,0 +1,4 @@ +export type ClickEvent = { + latLng: google.maps.LatLng; + domEvent: MouseEvent; +}; diff --git a/uniro_frontend/src/data/types/marker.d.ts b/uniro_frontend/src/data/types/marker.d.ts index 866d06c..fb5d960 100644 --- a/uniro_frontend/src/data/types/marker.d.ts +++ b/uniro_frontend/src/data/types/marker.d.ts @@ -1,13 +1,8 @@ -import { Markers } from "../../constant/enums"; +import { Markers } from "../../constant/enum/markerEnum"; export type AdvancedMarker = google.maps.marker.AdvancedMarkerElement; -export type MarkerTypes = - | Markers.BUILDING - | Markers.CAUTION - | Markers.DANGER - | Markers.DESTINATION - | Markers.ORIGIN - | Markers.NUMBERED_WAYPOINT - | Markers.WAYPOINT - | Markers.SELECTED_BUILDING; +export type MarkerTypesWithElement = { + type: MarkerTypes; + element: AdvancedMarker; +}; diff --git a/uniro_frontend/src/data/types/node.d.ts b/uniro_frontend/src/data/types/node.d.ts index 52d7ec5..87a812a 100644 --- a/uniro_frontend/src/data/types/node.d.ts +++ b/uniro_frontend/src/data/types/node.d.ts @@ -1,14 +1,14 @@ -export interface CustomNode { - id?: string; - lng: number; - lat: number; - isCore?: boolean; +import { Coord } from "./coord"; + +export type NodeId = number; + +export interface Node extends Coord { + nodeId: NodeId; } -// 건물 노드의 정보를 담고 있음 -export interface Building extends CustomNode { +export interface Building extends Node { buildingName: string; - buildingImageUrl?: string; + buildingImageUrl: string; phoneNumber: string; address: string; } diff --git a/uniro_frontend/src/data/types/report.d.ts b/uniro_frontend/src/data/types/report.d.ts new file mode 100644 index 0000000..b8b7a45 --- /dev/null +++ b/uniro_frontend/src/data/types/report.d.ts @@ -0,0 +1,20 @@ +import { CautionIssue, DangerIssue, PassableStatus } from "../../constant/enum/reportEnum"; + +export type ReportModeType = "create" | "update"; + +export interface PrimaryQuestionButton { + content: string; + passableStatus: PassableStatus; + mode?: ReportModeType; +} + +export interface IssueQuestionButtons { + danger: DangerIssue[]; + caution: CautionIssue[]; +} +export interface ReportFormData { + passableStatus: PassableStatus; + dangerIssues: IssueTypeKey[]; + cautionIssues: IssueTypeKey[]; + +} diff --git a/uniro_frontend/src/data/types/route.d.ts b/uniro_frontend/src/data/types/route.d.ts index 665455b..1d8ac84 100644 --- a/uniro_frontend/src/data/types/route.d.ts +++ b/uniro_frontend/src/data/types/route.d.ts @@ -1,17 +1,58 @@ -import { RoutePoint } from "../../constant/enums"; -import { RouteEdge } from "./edge"; -import { Building } from "./node"; +import { Markers } from "../../constant/enum/markerEnum"; +import { CautionIssueType, DangerIssueType } from "../../constant/enum/reportEnum"; +import { RoutePoint } from "../../constant/enum/routeEnum"; +import { Coord } from "./coord"; +import { MarkerTypes } from "./marker"; +import { Node } from "./node"; -export interface Route { - route: RouteEdge[]; +export type RouteId = number; + +export type Route = { + routeId: RouteId; + node1: Coord; + node2: Coord; +}; + +export type Direction = "origin" | "right" | "straight" | "left" | "uturn" | "destination" | "caution"; + +export interface CautionRoute extends Route { + cautionTypes: CautionIssueType[]; +} + +export interface DangerRoute extends Route { + dangerTypes: DangerIssueType[]; } export interface NavigationRoute extends Route { - hasCaution: boolean; - totalDistance: number; - totalCost: number; - originBuilding: Building; - destinationBuilding: Building; + cautionTypes: CautionIssueType[]; +} + +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; +}; + +export type NavigationRouteList = { + hasCaution: boolean; + totalDistance: number; + totalCost: number; + routes: NavigationRoute[]; + routeDetails: RouteDetail[]; +}; diff --git a/uniro_frontend/src/data/types/university.d.ts b/uniro_frontend/src/data/types/university.d.ts new file mode 100644 index 0000000..8f41ad0 --- /dev/null +++ b/uniro_frontend/src/data/types/university.d.ts @@ -0,0 +1,5 @@ +export type University = { + name: string; + imageUrl: string; + id: number; +}; diff --git a/uniro_frontend/src/hooks/useDynamicSuspense.tsx b/uniro_frontend/src/hooks/useDynamicSuspense.tsx new file mode 100644 index 0000000..9b21075 --- /dev/null +++ b/uniro_frontend/src/hooks/useDynamicSuspense.tsx @@ -0,0 +1,16 @@ +import { useLocation } from "react-router"; +import { useFallbackStore } from "./useFallbackStore"; +import { useEffect } from "react"; +import { fallbackConfig } from "../constant/fallback"; + +export const useDynamicSuspense = () => { + const location = useLocation(); + const { fallback, setFallback } = useFallbackStore(); + + useEffect(() => { + const newFallback = fallbackConfig[location.pathname] || fallbackConfig["/"]; + setFallback(newFallback); + }, [location.pathname, setFallback]); + + return { location, fallback }; +}; diff --git a/uniro_frontend/src/hooks/useFallbackStore.tsx b/uniro_frontend/src/hooks/useFallbackStore.tsx new file mode 100644 index 0000000..10b5875 --- /dev/null +++ b/uniro_frontend/src/hooks/useFallbackStore.tsx @@ -0,0 +1,14 @@ +import { create } from "zustand"; +import { fallbackConfig } from "../constant/fallback"; + +interface FallbackStore { + fallback: React.ReactNode; + setFallback: (fallback: React.ReactNode) => void; +} + +export const useFallbackStore = create((set) => { + return { + fallback: fallbackConfig["/"], + setFallback: (f) => set({ fallback: f }), + }; +}); diff --git a/uniro_frontend/src/hooks/useLoading.tsx b/uniro_frontend/src/hooks/useLoading.tsx new file mode 100644 index 0000000..088f1a1 --- /dev/null +++ b/uniro_frontend/src/hooks/useLoading.tsx @@ -0,0 +1,17 @@ +import React, { memo, useCallback, useState } from "react"; + +const useLoading = (): [boolean, () => void, () => void] => { + const [isLoading, setIsLoading] = useState(false); + + const show = useCallback(() => { + setIsLoading(true); + }, []); + + const hide = useCallback(() => { + setIsLoading(false); + }, []); + + return [isLoading, show, hide]; +}; + +export default useLoading; diff --git a/uniro_frontend/src/hooks/useMap.tsx b/uniro_frontend/src/hooks/useMap.tsx index 8f643cf..c8d9934 100644 --- a/uniro_frontend/src/hooks/useMap.tsx +++ b/uniro_frontend/src/hooks/useMap.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { initializeMap } from "../map/initializer/googleMapInitializer"; -const useMap = () => { +const useMap = (mapOptions?: google.maps.MapOptions) => { const mapRef = useRef(null); const [map, setMap] = useState(null); const [overlay, setOverlay] = useState(null); @@ -14,7 +14,10 @@ const useMap = () => { const initMap = async () => { try { - const { map, overlay, AdvancedMarkerElement, Polyline } = await initializeMap(mapRef.current); + const { map, overlay, AdvancedMarkerElement, Polyline } = await initializeMap( + mapRef.current, + mapOptions, + ); setMap(map); setOverlay(overlay); setAdvancedMarker(() => AdvancedMarkerElement); diff --git a/uniro_frontend/src/hooks/useModal.tsx b/uniro_frontend/src/hooks/useModal.tsx index b48ce40..2033f7b 100644 --- a/uniro_frontend/src/hooks/useModal.tsx +++ b/uniro_frontend/src/hooks/useModal.tsx @@ -1,6 +1,8 @@ import React, { ReactNode, useCallback, useState } from "react"; -export default function useModal(): [React.FC<{ children: ReactNode }>, boolean, () => void, () => void] { +export default function useModal( + onClose?: () => void, +): [React.FC<{ children: ReactNode }>, boolean, () => void, () => void] { const [isOpen, setIsOpen] = useState(false); const open = useCallback(() => { @@ -8,6 +10,9 @@ export default function useModal(): [React.FC<{ children: ReactNode }>, boolean, }, []); const close = useCallback(() => { + if (onClose) { + onClose(); + } setIsOpen(false); }, []); diff --git a/uniro_frontend/src/hooks/useNetworkStatus.tsx b/uniro_frontend/src/hooks/useNetworkStatus.tsx new file mode 100644 index 0000000..4e42695 --- /dev/null +++ b/uniro_frontend/src/hooks/useNetworkStatus.tsx @@ -0,0 +1,27 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router"; + +export default function useNetworkStatus() { + const [isOffline, setIsOffline] = useState(false); + const navigate = useNavigate(); + + const handleOffline = () => { + setIsOffline(true); + navigate("/error/offline"); + }; + + const handleOnline = () => { + setIsOffline(false); + navigate(-1); + }; + + useEffect(() => { + window.addEventListener("offline", handleOffline); + window.addEventListener("online", handleOnline); + + return () => { + window.removeEventListener("offline", handleOffline); + window.removeEventListener("online", handleOnline); + }; + }); +} diff --git a/uniro_frontend/src/hooks/useRedirectUndefined.tsx b/uniro_frontend/src/hooks/useRedirectUndefined.tsx new file mode 100644 index 0000000..ec4bd18 --- /dev/null +++ b/uniro_frontend/src/hooks/useRedirectUndefined.tsx @@ -0,0 +1,12 @@ +import { useEffect } from "react"; +import { useNavigate } from "react-router"; + +export default function useRedirectUndefined(deps: T[], url: string = "/") { + const navigate = useNavigate(); + + useEffect(() => { + if (deps.some((dep) => dep === undefined)) { + navigate(url); + } + }, [...deps]); +} diff --git a/uniro_frontend/src/hooks/useReportRisk.ts b/uniro_frontend/src/hooks/useReportRisk.ts new file mode 100644 index 0000000..b4e77a9 --- /dev/null +++ b/uniro_frontend/src/hooks/useReportRisk.ts @@ -0,0 +1,14 @@ +import { create } from "zustand"; +import { RouteId } from "../data/types/route"; + +interface ReportedRiskRoute { + reportRouteId: RouteId | undefined; + setReportRouteId: (selectedRouteId: RouteId) => void; +} + +const useReportRisk = create((set) => ({ + reportRouteId: undefined, + setReportRouteId: (newRouteId: RouteId) => set({ reportRouteId: newRouteId }), +})); + +export default useReportRisk; diff --git a/uniro_frontend/src/hooks/useRoutePoint.ts b/uniro_frontend/src/hooks/useRoutePoint.ts index 50b6d88..2dd5389 100644 --- a/uniro_frontend/src/hooks/useRoutePoint.ts +++ b/uniro_frontend/src/hooks/useRoutePoint.ts @@ -1,19 +1,46 @@ 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 | undefined; + origin: Building; setOrigin: (origin: Building | undefined) => void; - destination: Building | undefined; + destination: Building; + setDemoBuildingInfo: (building: Building) => void; setDestination: (destination: Building | undefined) => void; + setOriginCoord: (lng: number, lat: number) => void; + setDestinationCoord: (lng: number, lat: number) => void; switchBuilding: () => void; } /** 출발지, 도착지 관리 전역 상태 */ const useRoutePoint = create((set) => ({ - origin: undefined, + origin: originMockInfo, setOrigin: (newOrigin: Building | undefined) => set(() => ({ origin: newOrigin })), - destination: undefined, + destination: destMockInfo, + setDemoBuildingInfo: (building: Building) => set(() => ({ origin: building, destination: building })), + setOriginCoord: (lng: number, lat: number) => set(({ origin }) => ({ origin: { ...origin, lng, lat } })), + setDestinationCoord: (lng: number, lat: number) => + set(({ destination }) => ({ destination: { ...destination, lng, lat } })), setDestination: (newDestination: Building | undefined) => set(() => ({ destination: newDestination })), switchBuilding: () => set(({ origin, destination }) => ({ origin: destination, destination: origin })), })); diff --git a/uniro_frontend/src/hooks/useSearchBuilding.ts b/uniro_frontend/src/hooks/useSearchBuilding.ts index b397f85..8ab44f2 100644 --- a/uniro_frontend/src/hooks/useSearchBuilding.ts +++ b/uniro_frontend/src/hooks/useSearchBuilding.ts @@ -1,7 +1,7 @@ import { create } from "zustand"; import { Building } from "../data/types/node"; import { RoutePointType } from "../data/types/route"; -import { RoutePoint } from "../constant/enums"; +import { RoutePoint } from "../constant/enum/routeEnum"; interface SearchModeStore { mode: RoutePointType; diff --git a/uniro_frontend/src/hooks/useSuspenseMap.tsx b/uniro_frontend/src/hooks/useSuspenseMap.tsx new file mode 100644 index 0000000..86aee45 --- /dev/null +++ b/uniro_frontend/src/hooks/useSuspenseMap.tsx @@ -0,0 +1,38 @@ +import { useCallback, useEffect, useState } from "react"; +import { createMapResource } from "../map/createMapResource"; + +interface MapResource { + map: google.maps.Map | null; + AdvancedMarkerElement: typeof google.maps.marker.AdvancedMarkerElement | null; + Polyline: typeof google.maps.Polyline | null; +} + +export function useSuspenseMap(mapOptions?: google.maps.MapOptions) { + const [mapElement, setMapElement] = useState(null); + + const [resource, setResource] = useState<{ read(): MapResource }>(() => createMapResource(null, mapOptions)); + + const mapRef = useCallback((node: HTMLDivElement | null) => { + setMapElement(node); + }, []); + + useEffect(() => { + setResource(createMapResource(mapElement, mapOptions)); + return () => { + if (mapElement) { + mapElement.innerHTML = ""; + } + }; + }, [mapElement, mapOptions]); + + const { map, AdvancedMarkerElement, Polyline } = resource.read(); + + return { + mapRef, + map, + AdvancedMarker: AdvancedMarkerElement, + Polyline, + }; +} + +export default useSuspenseMap; diff --git a/uniro_frontend/src/hooks/useUniversityInfo.tsx b/uniro_frontend/src/hooks/useUniversityInfo.tsx new file mode 100644 index 0000000..4097718 --- /dev/null +++ b/uniro_frontend/src/hooks/useUniversityInfo.tsx @@ -0,0 +1,26 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { University } from "../data/types/university"; + +interface UniversityInfoStore { + university: University | undefined; + setUniversity: (university: University) => void; + resetUniversity: () => void; +} +const useUniversityInfo = create( + persist( + (set) => ({ + university: undefined, + setUniversity: (newUniversity: University) => { + set(() => ({ university: newUniversity })); + }, + resetUniversity: () => { + set(() => ({ university: undefined })); + }, + }), + { + name: "university-info", + }, + ), +); +export default useUniversityInfo; diff --git a/uniro_frontend/src/index.css b/uniro_frontend/src/index.css index 3508d62..a65ec74 100644 --- a/uniro_frontend/src/index.css +++ b/uniro_frontend/src/index.css @@ -136,3 +136,32 @@ @utility translate-waypoint { transform: translateY(+4px); } + +@keyframes markerAppear { + 0% { + transform: scale(0); + } + 100% { + transform: scale(1); + } +} + +.marker-appear { + animation: markerAppear 0.2s ease-in-out forwards; + transform-origin: bottom center; +} + +.fade-out { + animation: fadeOut 0.3s forwards ease-in; +} + +@keyframes fadeOut { + 0% { + opacity: 1; + transform: scale(1); + } + 100% { + opacity: 0; + transform: scale(0); + } +} diff --git a/uniro_frontend/src/map/createMapResource.ts b/uniro_frontend/src/map/createMapResource.ts new file mode 100644 index 0000000..ca2cbf8 --- /dev/null +++ b/uniro_frontend/src/map/createMapResource.ts @@ -0,0 +1,50 @@ +import { initializeMap } from "./initializer/googleMapInitializer"; + +export interface MapResource { + map: google.maps.Map | null; + AdvancedMarkerElement: typeof google.maps.marker.AdvancedMarkerElement | null; + Polyline: typeof google.maps.Polyline | null; +} + +const dummyResource = { + read() { + return { + map: null, + AdvancedMarkerElement: null, + Polyline: null, + } as MapResource; + }, +}; + +export function createMapResource( + mapElement: HTMLDivElement | null, + mapOptions?: google.maps.MapOptions, +): { read(): MapResource } { + if (!mapElement) { + return dummyResource; + } + + let status = "pending"; + let result: MapResource; + let suspender = initializeMap(mapElement, mapOptions) + .then((res) => { + status = "success"; + result = res; + }) + .catch((e) => { + status = "error"; + result = e; + }); + + return { + read() { + if (status === "error") { + throw result; + } else if (status === "pending") { + throw suspender; + } else { + return result; + } + }, + }; +} diff --git a/uniro_frontend/src/map/initializer/googleMapInitializer.ts b/uniro_frontend/src/map/initializer/googleMapInitializer.ts index 32d559c..7707924 100644 --- a/uniro_frontend/src/map/initializer/googleMapInitializer.ts +++ b/uniro_frontend/src/map/initializer/googleMapInitializer.ts @@ -9,12 +9,14 @@ interface MapWithOverlay { Polyline: typeof google.maps.Polyline; } -export const initializeMap = async (mapElement: HTMLElement | null): Promise => { +export const initializeMap = async ( + mapElement: HTMLElement | null, + mapOptions?: google.maps.MapOptions, +): Promise => { const { Map, OverlayView, AdvancedMarkerElement, Polyline } = await loadGoogleMapsLibraries(); - // useMap hook에서 error을 catch 하도록 함. if (!mapElement) { - throw new Error("mapElement is null"); + throw new Error("Map Element is not provided"); } const map = new Map(mapElement, { @@ -33,6 +35,7 @@ export const initializeMap = async (mapElement: HTMLElement | null): Promise([university]); + return ( -
    +
    - { }} handleVoiceInput={() => { }} placeholder="" /> + {}} handleVoiceInput={() => {}} placeholder="" />
      diff --git a/uniro_frontend/src/pages/demo.tsx b/uniro_frontend/src/pages/demo.tsx index d082b02..7a47634 100644 --- a/uniro_frontend/src/pages/demo.tsx +++ b/uniro_frontend/src/pages/demo.tsx @@ -6,18 +6,57 @@ import Map from "../component/Map"; import RouteInput from "../components/map/routeSearchInput"; import OriginIcon from "../assets/map/origin.svg?react"; import DestinationIcon from "../assets/map/destination.svg?react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import ReportButton from "../components/map/reportButton"; import { CautionToggleButton, DangerToggleButton } from "../components/map/floatingButtons"; +import { useMutation, useQuery, useSuspenseQuery } from "@tanstack/react-query"; +import { getFetch, postFetch, putFetch } from "../utils/fetch/fetch"; +import SuspenseMapComponent from "../component/SuspenseMap"; +import { getMockTest } from "../utils/fetch/mockFetch"; +import SearchNull from "../components/error/SearchNull"; +import Offline from "../components/error/Offline"; +import Error from "../components/error/Error"; + +const getTest = () => { + /** https://jsonplaceholder.typicode.com/comments?postId=1 */ + return getFetch<{ postId: string }>("/comments", { + postId: 1, + }); +}; + +const postTest = (): Promise<{ id: string }> => { + return postFetch<{ id: string }, string>("/posts", { id: "test" }); +}; + +const putTest = (): Promise<{ id: string }> => { + return putFetch<{ id: string }, string>("/posts/1", { id: "test" }); +}; export default function Demo() { const [FailModal, isFailOpen, openFail, closeFail] = useModal(); const [SuccessModal, isSuccessOpen, openSuccess, closeSuccess] = useModal(); const [destination, setDestination] = useState("역사관"); + const { data, status } = useSuspenseQuery({ + queryKey: ["test"], + queryFn: getMockTest, + }); + + const { data: postData, mutate: mutatePost } = useMutation<{ id: string }>({ + mutationFn: postTest, + }); + + const { data: putData, mutate: mutatePut } = useMutation<{ id: string }>({ + mutationFn: putTest, + }); + + useEffect(() => { + console.log(data); + }, [status]); + return ( <> -
      +
      -
      - +
      + +
      +
      + + +
      + + +
      +
      + + +
      +

      Click on the Vite and React logos to learn more

      diff --git a/uniro_frontend/src/pages/error.tsx b/uniro_frontend/src/pages/error.tsx new file mode 100644 index 0000000..10f51cd --- /dev/null +++ b/uniro_frontend/src/pages/error.tsx @@ -0,0 +1,9 @@ +import Error from "../components/error/Error"; + +export default function ErrorPage() { + return ( +
      + +
      + ); +} diff --git a/uniro_frontend/src/pages/landing.tsx b/uniro_frontend/src/pages/landing.tsx index ce05879..495a0bb 100644 --- a/uniro_frontend/src/pages/landing.tsx +++ b/uniro_frontend/src/pages/landing.tsx @@ -5,7 +5,7 @@ import { Link } from "react-router"; export default function LandingPage() { return ( -
      +

      어디든 갈 수 있는 캠퍼스.

      쉽고 빠르게 이동하세요.

      diff --git a/uniro_frontend/src/pages/map.tsx b/uniro_frontend/src/pages/map.tsx index f93ad44..e19882e 100644 --- a/uniro_frontend/src/pages/map.tsx +++ b/uniro_frontend/src/pages/map.tsx @@ -1,10 +1,8 @@ import { useEffect, useRef, useState } from "react"; import useMap from "../hooks/useMap"; -import { hanyangBuildings } from "../data/mock/hanyangBuildings"; import createMarkerElement from "../components/map/mapMarkers"; -import { mockHazardEdges } from "../data/mock/hanyangHazardEdge"; import { BottomSheet, BottomSheetRef } from "react-spring-bottom-sheet"; -import { Building } from "../data/types/node"; +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"; @@ -13,66 +11,122 @@ import ReportButton from "../components/map/reportButton"; import useRoutePoint from "../hooks/useRoutePoint"; import useSearchBuilding from "../hooks/useSearchBuilding"; import Button from "../components/customButton"; -import { AdvancedMarker, MarkerTypes } from "../data/types/marker"; -import { RoutePointType } from "../data/types/route"; -import { RoutePoint } from "../constant/enums"; -import { Markers } from "../constant/enums"; -import createAdvancedMarker from "../utils/markers/createAdvanedMarker"; +import { AdvancedMarker } from "../data/types/marker"; +import { RouteId, RoutePointType } from "../data/types/route"; +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 useModal from "../hooks/useModal"; +import ReportModal from "../components/map/reportModal"; +import useUniversityInfo from "../hooks/useUniversityInfo"; +import useRedirectUndefined from "../hooks/useRedirectUndefined"; +import { HanyangUniversity } from "../constant/university"; +import { University } from "../data/types/university"; +import { CautionIssueType, DangerIssueType, MarkerTypes } from "../data/types/enum"; +import { CautionIssue, DangerIssue } from "../constant/enum/reportEnum"; + +/** API 호출 */ +import { useSuspenseQueries } from "@tanstack/react-query"; +import { getAllRisks } from "../api/routes"; +import { getAllBuildings } from "../api/nodes"; export type SelectedMarkerTypes = { type: MarkerTypes; + id: NodeId | RouteId; element: AdvancedMarker; property?: Building; + factors?: DangerIssueType[] | CautionIssueType[]; from: "Marker" | "List"; }; export default function MapPage() { const { mapRef, map, AdvancedMarker } = useMap(); + const [zoom, setZoom] = useState(16); + const prevZoom = useRef(16); + const [selectedMarker, setSelectedMarker] = useState(); const bottomSheetRef = useRef(null); const [sheetOpen, setSheetOpen] = useState(false); - const [buildingMarkers, setBuildingMarkers] = useState<{ element: AdvancedMarker; id: string }[]>([]); - const [dangerMarkers, setDangerMarkers] = useState([]); - const [isDangerAcitve, setIsDangerActive] = useState(true); + const [buildingMarkers, setBuildingMarkers] = useState<{ element: AdvancedMarker; nodeId: NodeId }[]>([]); + + const [dangerMarkers, setDangerMarkers] = useState<{ element: AdvancedMarker, routeId: RouteId }[]>([]); + const [isDangerAcitve, setIsDangerActive] = useState(false); + + const [cautionMarkers, setCautionMarkers] = useState<{ element: AdvancedMarker, routeId: RouteId }[]>([]); + const [isCautionAcitve, setIsCautionActive] = useState(false); - const [cautionMarkers, setCautionMarkers] = useState([]); - const [isCautionAcitve, setIsCautionActive] = useState(true); + const [universityMarker, setUniversityMarker] = useState(); const { origin, setOrigin, destination, setDestination } = useRoutePoint(); const { mode, building: selectedBuilding } = useSearchBuilding(); + const [_, isOpen, open, close] = useModal(); + + const { university } = useUniversityInfo(); + useRedirectUndefined([university]); + + if (!university) return; + + const results = useSuspenseQueries({ + queries: [ + { queryKey: [university.id, 'risks'], queryFn: () => getAllRisks(university.id) }, + { + queryKey: [university.id, 'buildings'], queryFn: () => getAllBuildings(university.id, { + leftUpLat: 38, + leftUpLng: 127, + rightDownLat: 37, + rightDownLng: 128 + }) + } + ] + }); + + const [risks, buildings] = results; + const initMap = () => { - if (map === null) return; + if (map === null || !AdvancedMarker) return; map.addListener("click", (e: unknown) => { setSheetOpen(false); - setSelectedMarker((marker) => { - if (marker) { - const { type, element } = marker; - - if (type === Markers.BUILDING) return undefined; + setSelectedMarker(undefined); + }); + map.addListener("zoom_changed", () => { + setZoom((prev) => { + const curZoom = map.getZoom() as number; + prevZoom.current = prev; - element.content = createMarkerElement({ type, }); - } - return undefined; + return curZoom }); - }); + }) + + const centerMarker = createUniversityMarker( + AdvancedMarker, + map, + HanyangUniversity, + university ? university.name : "", + ) + setUniversityMarker(centerMarker); }; const addBuildings = () => { if (AdvancedMarker === null || map === null) return; - const markersWithId: { id: string; element: AdvancedMarker }[] = []; - for (const building of hanyangBuildings) { - const { id, lat, lng, buildingName } = building; + const buildingList = buildings.data; + const buildingMarkersWithID: { nodeId: NodeId; element: AdvancedMarker }[] = []; + + for (const building of buildingList) { + const { nodeId, lat, lng, buildingName } = building; const buildingMarker = createAdvancedMarker( AdvancedMarker, - map, + null, new google.maps.LatLng(lat, lng), createMarkerElement({ type: Markers.BUILDING, title: buildingName, className: "translate-marker" }), () => { setSelectedMarker({ + id: nodeId, type: Markers.BUILDING, element: buildingMarker, property: building, @@ -81,67 +135,110 @@ export default function MapPage() { }, ); - markersWithId.push({ id: id ? id : "", element: buildingMarker }); + buildingMarkersWithID.push({ nodeId: nodeId ? nodeId : -1, element: buildingMarker }); } - setBuildingMarkers(markersWithId); + setBuildingMarkers(buildingMarkersWithID); }; - const addHazardMarker = () => { + const addRiskMarker = () => { if (AdvancedMarker === null || map === null) return; - for (const edge of mockHazardEdges) { - const { id, startNode, endNode, dangerFactors, cautionFactors } = edge; - const hazardMarker = createAdvancedMarker( + const { dangerRoutes, cautionRoutes } = risks.data; + + /** 위험 마커 생성 */ + const dangerMarkersWithId: { routeId: RouteId; element: AdvancedMarker }[] = []; + + for (const route of dangerRoutes) { + const { routeId, node1, node2, dangerTypes } = route; + const type = Markers.DANGER; + + const dangerMarker = createAdvancedMarker( AdvancedMarker, - map, + null, new google.maps.LatLng({ - lat: (startNode.lat + endNode.lat) / 2, - lng: (startNode.lng + endNode.lng) / 2, + lat: (node1.lat + node2.lat) / 2, + lng: (node1.lng + node2.lng) / 2, }), - createMarkerElement({ type: dangerFactors ? Markers.DANGER : Markers.CAUTION }), + createMarkerElement({ type }), () => { - hazardMarker.content = createMarkerElement({ - type: dangerFactors ? Markers.DANGER : Markers.CAUTION, - title: dangerFactors ? dangerFactors[0] : cautionFactors && cautionFactors[0], - hasTopContent: true, - }); - setSelectedMarker({ - type: dangerFactors ? Markers.DANGER : Markers.CAUTION, - element: hazardMarker, - from: "Marker", - }); + setSelectedMarker((prevMarker) => { + if (prevMarker && prevMarker.id === routeId) { + return undefined; + } + return { + id: routeId, + type: type, + element: dangerMarker, + factors: dangerTypes, + from: "Marker", + } + }) }, ); - if (dangerFactors) { - setDangerMarkers((prevMarkers) => [...prevMarkers, hazardMarker]); - } else { - setCautionMarkers((prevMarkers) => [...prevMarkers, hazardMarker]); - } + + dangerMarkersWithId.push({ routeId, element: dangerMarker }); } - }; + setDangerMarkers(dangerMarkersWithId); - /** Marker 보이기 안보이기 토글 */ - const toggleMarkers = (isActive: boolean, markers: AdvancedMarker[]) => { - if (isActive) { - for (const marker of markers) { - marker.map = map; - } - } else { - for (const marker of markers) { - marker.map = null; - } + /** 주의 마커 생성 */ + const cautionMarkersWithId: { routeId: RouteId; element: AdvancedMarker }[] = []; + + for (const route of cautionRoutes) { + const { routeId, node1, node2, cautionTypes } = route; + const type = Markers.CAUTION; + + const cautionMarker = createAdvancedMarker( + AdvancedMarker, + null, + new google.maps.LatLng({ + lat: (node1.lat + node2.lat) / 2, + lng: (node1.lng + node2.lng) / 2, + }), + createMarkerElement({ type }), + () => { + setSelectedMarker((prevMarker) => { + if (prevMarker && prevMarker.id === routeId) { + return undefined; + } + return { + id: routeId, + type: type, + element: cautionMarker, + factors: cautionTypes, + from: "Marker", + } + }) + }, + ); + cautionMarkersWithId.push({ routeId, element: cautionMarker }); } + + setCautionMarkers(cautionMarkersWithId); }; const toggleCautionButton = () => { + if (!map) return; + if (zoom <= 16) { + map.setOptions({ + zoom: 17, + center: HanyangUniversity, + }); + } setIsCautionActive((isActive) => { - toggleMarkers(!isActive, cautionMarkers); + toggleMarkers(!isActive, cautionMarkers.map(marker => marker.element), map); return !isActive; }); }; const toggleDangerButton = () => { + if (!map) return; + if (zoom <= 16) { + map.setOptions({ + zoom: 17, + center: HanyangUniversity, + }); + } setIsDangerActive((isActive) => { - toggleMarkers(!isActive, dangerMarkers); + toggleMarkers(!isActive, dangerMarkers.map(marker => marker.element), map); return !isActive; }); }; @@ -153,9 +250,11 @@ export default function MapPage() { if (selectedMarker.from === "Marker" && type) { switch (type) { case RoutePoint.ORIGIN: + if (selectedMarker.id === destination?.nodeId) setDestination(undefined); setOrigin(selectedMarker.property); break; case RoutePoint.DESTINATION: + if (selectedMarker.id === origin?.nodeId) setOrigin(undefined); setDestination(selectedMarker.property); break; } @@ -169,32 +268,88 @@ export default function MapPage() { }; /** isSelect(Marker 선택 시) Marker Content 변경, 지도 이동, BottomSheet 열기 */ - const changeMarkerStyle = (marker: AdvancedMarker, isSelect: boolean) => { - if (!map || !selectedMarker || selectedMarker.type !== Markers.BUILDING || !selectedMarker.property) return; + 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", + }); + } + + else if (marker.id === destination?.nodeId) { + marker.element.content = createMarkerElement({ + type: Markers.DESTINATION, + title: destination.buildingName, + className: "translate-routemarker", + }); - if (isSelect) { - marker.content = createMarkerElement({ - type: Markers.SELECTED_BUILDING, - title: selectedMarker.property.buildingName, + } + + marker.element.content = createMarkerElement({ + type: Markers.BUILDING, + title: marker.property.buildingName, className: "translate-marker", }); - map.setOptions({ - center: { lat: selectedMarker.property.lat, lng: selectedMarker.property.lng }, - zoom: 19, - }); - setSheetOpen(true); + } 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], + hasTopContent: true, + }); + } + else if (marker.type === Markers.CAUTION) { + const key = marker.factors && marker.factors[0] as CautionIssueType; + marker.element.content = createMarkerElement({ + type: marker.type, + title: key && CautionIssue[key], + hasTopContent: true, + }); + } + return; + } - return; + marker.element.content = createMarkerElement({ + type: marker.type, + }); } - marker.content = createMarkerElement({ - type: Markers.BUILDING, - title: selectedMarker.property.buildingName, - className: "translate-marker", - }); }; - const findBuildingMarker = (id: string): AdvancedMarker | undefined => { - const matchedMarker = buildingMarkers.find((el) => el.id === id)?.element; + const findBuildingMarker = (id: NodeId): AdvancedMarker | undefined => { + const matchedMarker = buildingMarkers.find((el) => el.nodeId === id)?.element; return matchedMarker; }; @@ -203,26 +358,26 @@ export default function MapPage() { useEffect(() => { initMap(); addBuildings(); - addHazardMarker(); + addRiskMarker(); }, [map]); /** 선택된 마커가 있는 경우 */ useEffect(() => { - if (selectedMarker === undefined) return; - changeMarkerStyle(selectedMarker.element, true); + changeMarkerStyle(selectedMarker, true); return () => { - changeMarkerStyle(selectedMarker.element, false); + changeMarkerStyle(selectedMarker, false); }; }, [selectedMarker]); /** 빌딩 리스트에서 넘어온 경우, 일치하는 BuildingMarkerElement를 탐색 */ useEffect(() => { - if (buildingMarkers.length === 0 || !selectedBuilding || !selectedBuilding.id) return; + if (buildingMarkers.length === 0 || !selectedBuilding || !selectedBuilding.nodeId) return; - const matchedMarker = findBuildingMarker(selectedBuilding.id); + const matchedMarker = findBuildingMarker(selectedBuilding.nodeId); if (matchedMarker) setSelectedMarker({ + id: selectedBuilding.nodeId, type: Markers.BUILDING, element: matchedMarker, from: "List", @@ -232,9 +387,9 @@ export default function MapPage() { /** 출발지 결정 시, Marker Content 변경 */ useEffect(() => { - if (!origin || !origin.id) return; + if (!origin || !origin.nodeId) return; - const originMarker = findBuildingMarker(origin.id); + const originMarker = findBuildingMarker(origin.nodeId); if (!originMarker) return; originMarker.content = createMarkerElement({ @@ -247,16 +402,16 @@ export default function MapPage() { originMarker.content = createMarkerElement({ type: Markers.BUILDING, title: origin.buildingName, - className: "translate-routemarker", + className: "translate-marker", }); }; }, [origin]); /** 도착지 결정 시, Marker Content 변경 */ useEffect(() => { - if (!destination || !destination.id) return; + if (!destination || !destination.nodeId) return; - const destinationMarker = findBuildingMarker(destination.id); + const destinationMarker = findBuildingMarker(destination.nodeId); if (!destinationMarker) return; destinationMarker.content = createMarkerElement({ @@ -269,13 +424,37 @@ export default function MapPage() { destinationMarker.content = createMarkerElement({ type: Markers.BUILDING, title: destination.buildingName, - className: "translate-routemarker", + className: "translate-marker", }); }; }, [destination]); + useEffect(() => { + if (!map) return; + + const _buildingMarkers = buildingMarkers.map(buildingMarker => buildingMarker.element); + + if (prevZoom.current >= 17 && zoom <= 16) { + if (isCautionAcitve) { + setIsCautionActive(false); + toggleMarkers(false, cautionMarkers.map(marker => marker.element), map); + } + if (isDangerAcitve) { + setIsDangerActive(false); + 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, universityMarker ? [universityMarker] : [], map); + toggleMarkers(true, _buildingMarkers, map); + } + }, [map, zoom]) + return ( -
      +
      ))} - {origin && destination && origin.id !== destination.id ? ( + {origin && destination && origin.nodeId !== destination.nodeId ? ( /** 출발지랑 도착지가 존재하는 경우 길찾기 버튼 보이기 */ -
      + -
      + ) : ( /** 출발지랑 도착지가 존재하지 않거나, 같은 경우 기존 Button UI 보이기 */ <>
      - +
      @@ -318,6 +497,7 @@ export default function MapPage() {
      )} + {isOpen && }
      ); } diff --git a/uniro_frontend/src/pages/navigationResult.tsx b/uniro_frontend/src/pages/navigationResult.tsx index 73a0c8d..4c99c1b 100644 --- a/uniro_frontend/src/pages/navigationResult.tsx +++ b/uniro_frontend/src/pages/navigationResult.tsx @@ -1,20 +1,27 @@ -import React, { useCallback, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { PanInfo, useDragControls } from "framer-motion"; +import { useSuspenseQueries } from "@tanstack/react-query"; + import Button from "../components/customButton"; -import GoBack from "../assets/icon/goBack.svg?react"; import RouteList from "../components/navigation/route/routeList"; - -import { mockNavigationRoute } from "../data/mock/hanyangRoute"; -import { NavigationRoute } from "../data/types/route"; -import useScrollControl from "../hooks/useScrollControl"; -import AnimatedContainer from "../container/animatedContainer"; -import NavigationMap from "../component/NavgationMap"; import NavigationDescription from "../components/navigation/navigationDescription"; import BottomSheetHandle from "../components/navigation/bottomSheet/bottomSheetHandle"; +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"; -// 1. 돌아가면 위치 reset ✅ -// 2. 상세경로 scroll 끝까지 가능하게 하기 ❎ -// 3. 코드 리팩토링 하기 +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"; const MAX_SHEET_HEIGHT = window.innerHeight * 0.7; const MIN_SHEET_HEIGHT = window.innerHeight * 0.35; @@ -22,19 +29,66 @@ const CLOSED_SHEET_HEIGHT = 0; const INITIAL_TOP_BAR_HEIGHT = 143; const BOTTOM_SHEET_HANDLE_HEIGHT = 40; - 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 { university } = useUniversityInfo(); + const { origin, destination, setOriginCoord, setDestinationCoord } = useRoutePoint(); + + // TEST용 Link + const { search } = location; + const params = new URLSearchParams(search); + + const originId = params.get("node1Id"); + const destinationId = params.get("node2Id"); + + useRedirectUndefined([university, origin, destination]); useScrollControl(); + // Cache를 위한 Key를 지정하기 위해서 추가한 코드 + const requestOriginId = originId ? Number(originId) : origin?.nodeId; + const requestDestinationId = destinationId ? Number(destinationId) : destination?.nodeId; + + const result = useSuspenseQueries({ + queries: [ + { + queryKey: ["fastRoute", university?.id, requestOriginId, requestDestinationId], + queryFn: async () => { + try { + const response = await getNavigationResult( + university?.id ?? 1001, + 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, + ); + return response; + } catch (e) { + return null; + } + }, + retry: 1, + staleTime: 0, + }, + { + queryKey: [university?.id, "risks"], + queryFn: () => getAllRisks(university?.id ?? 1001), + retry: 1, + staleTime: 0, + }, + ], + }); + const dragControls = useDragControls(); const showDetailView = () => { @@ -42,24 +96,32 @@ const NavigationResultPage = () => { setTopBarHeight(PADDING_FOR_MAP_BOUNDARY); setIsDetailView(true); }; + const hideDetailView = () => { setSheetHeight(CLOSED_SHEET_HEIGHT); setTopBarHeight(INITIAL_TOP_BAR_HEIGHT); setIsDetailView(false); }; - const handleDrag = useCallback( - (event: Event, info: PanInfo) => { - setSheetHeight((prev) => { - const newHeight = prev - info.delta.y; - return Math.min(Math.max(newHeight, MIN_SHEET_HEIGHT), MAX_SHEET_HEIGHT); - }); - }, - [setSheetHeight, MAX_SHEET_HEIGHT, MIN_SHEET_HEIGHT], - ); + 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 ( -
      +
      + {/* 지도 영역 */} + + { isTop={true} transition={{ type: "spring", damping: 20, duration: 0.3 }} > - + - + - + { positionDelta={60} isTop={true} > - + - +
      - - + +
      -
      +
      ); }; diff --git a/uniro_frontend/src/pages/offline.tsx b/uniro_frontend/src/pages/offline.tsx new file mode 100644 index 0000000..bb182f0 --- /dev/null +++ b/uniro_frontend/src/pages/offline.tsx @@ -0,0 +1,9 @@ +import Offline from "../components/error/Offline"; + +export default function OfflinePage() { + return ( +
      + +
      + ); +} diff --git a/uniro_frontend/src/pages/reportForm.tsx b/uniro_frontend/src/pages/reportForm.tsx new file mode 100644 index 0000000..552f03d --- /dev/null +++ b/uniro_frontend/src/pages/reportForm.tsx @@ -0,0 +1,184 @@ +import React, { useEffect, useState } from "react"; + +import { PassableStatus, IssueTypeKey } from "../constant/enum/reportEnum"; +import { ReportModeType, ReportFormData } from "../data/types/report"; + +import { ReportTitle } from "../components/report/reportTitle"; +import { ReportDivider } from "../components/report/reportDivider"; +import { PrimaryForm } from "../components/report/primaryForm"; +import { SecondaryForm } from "../components/report/secondaryForm"; +import Button from "../components/customButton"; + +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 { 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"; + +const ReportForm = () => { + useScrollControl(); + + const navigate = useNavigate(); + const redirectToMap = () => navigate("/map"); + const queryClient = useQueryClient(); + + const [disabled, setDisabled] = useState(true); + + const [FailModal, isFailOpen, openFail, closeFail] = useModal(redirectToMap); + const [SuccessModal, isSuccessOpen, openSuccess, closeSuccess] = useModal(redirectToMap); + + const [errorTitle, setErrorTitle] = useState(""); + + const { university } = useUniversityInfo(); + const { reportRouteId: routeId } = useReportRisk(); + + useRedirectUndefined([university, routeId]); + + console.log(routeId) + + if (!routeId) return; + + const { data } = useSuspenseQuery({ + queryKey: ["report", university?.id ?? 1001, routeId], + queryFn: async () => { + try { + const data = await getSingleRouteRisk(university?.id ?? 1001, routeId); + return data; + } catch (e) { + return { + routeId: -1, + dangerTypes: [], + cautionTypes: [], + }; + } + }, + 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", + ); + + const [formData, setFormData] = useState({ + passableStatus: + reportMode === "create" + ? PassableStatus.INITIAL + : data.cautionTypes.length > 0 + ? PassableStatus.CAUTION + : PassableStatus.DANGER, + dangerIssues: data.dangerTypes, + cautionIssues: data.cautionTypes, + }); + + useEffect(() => { + if ( + formData.passableStatus === PassableStatus.INITIAL || + (formData.passableStatus === PassableStatus.DANGER && formData.dangerIssues.length === 0) || + (formData.passableStatus === PassableStatus.CAUTION && formData.cautionIssues.length === 0) + ) { + setDisabled(true); + return; + } + setDisabled(false); + }, [formData]); + + const handlePrimarySelect = (status: PassableStatus) => { + setFormData((prev) => ({ + passableStatus: status === prev.passableStatus ? PassableStatus.INITIAL : status, + dangerIssues: status === prev.passableStatus ? prev.dangerIssues : [], + cautionIssues: status === prev.passableStatus ? prev.cautionIssues : [], + })); + }; + + const handleSecondarySelect = (answerType: IssueTypeKey) => { + if (formData.passableStatus === PassableStatus.DANGER) { + setFormData((prev) => ({ + ...prev, + dangerIssues: prev.dangerIssues.includes(answerType) + ? prev.dangerIssues.filter((issue) => issue !== answerType) + : [...prev.dangerIssues, answerType], + })); + } else if (formData.passableStatus === PassableStatus.CAUTION) { + setFormData((prev) => ({ + ...prev, + cautionIssues: prev.cautionIssues.includes(answerType) + ? prev.cautionIssues.filter((issue) => issue !== answerType) + : [...prev.cautionIssues, answerType], + })); + } + }; + + 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(); + }, + onError: () => { + setErrorTitle("제보에 실패하였습니다"); + openFail(); + }, + }); + + return ( +
      + + +
      + + +
      +
      + +
      + +

      불편한 길 제보를 완료했어요!

      +
      +

      제보는 바로 반영되지만,

      +

      + 더 정확한 정보를 위해 추후 수정될 수 있어요. +

      +
      +
      + +

      {errorTitle}

      +
      +

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

      +

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

      +
      +
      +
      + ); +}; + +export default ReportForm; diff --git a/uniro_frontend/src/pages/reportRisk.tsx b/uniro_frontend/src/pages/reportRisk.tsx new file mode 100644 index 0000000..ab99561 --- /dev/null +++ b/uniro_frontend/src/pages/reportRisk.tsx @@ -0,0 +1,305 @@ +import { MouseEvent, useEffect, useState } from "react"; +import useMap from "../hooks/useMap"; +import createAdvancedMarker from "../utils/markers/createAdvanedMarker"; +import createMarkerElement from "../components/map/mapMarkers"; +import { CoreRoute, CoreRoutesList, RouteId } from "../data/types/route"; +import { Markers } from "../constant/enum/markerEnum"; +import { ClickEvent } from "../data/types/event"; +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 { ReportRiskMessage } from "../constant/enum/messageEnum"; +import { motion } from "framer-motion"; +import AnimatedContainer from "../container/animatedContainer"; + +import BackButton from "../components/map/backButton"; + +import useUniversityInfo from "../hooks/useUniversityInfo"; +import useRedirectUndefined from "../hooks/useRedirectUndefined"; +import { University } from "../data/types/university"; +import { useSuspenseQueries } from "@tanstack/react-query"; +import { getAllRoutes } from "../api/route"; +import { getAllRisks } from "../api/routes"; +import useReportRisk from "../hooks/useReportRisk"; +import { CautionIssueType, DangerIssueType } from "../data/types/enum"; +import { CautionIssue, DangerIssue } from "../constant/enum/reportEnum"; + +interface reportMarkerTypes extends MarkerTypesWithElement { + route: RouteId; + factors?: DangerIssueType[] | CautionIssueType[]; +} + +export default function ReportRiskPage() { + const { map, mapRef, AdvancedMarker, Polyline } = useMap({ zoom: 18, minZoom: 17 }); + + const [reportMarker, setReportMarker] = useState(); + + const [message, setMessage] = useState(ReportRiskMessage.DEFAULT); + const { setReportRouteId } = useReportRisk(); + const { university } = useUniversityInfo(); + + useRedirectUndefined([university]); + + if (!university) return; + + const result = useSuspenseQueries({ + queries: [ + { + queryKey: ["routes", university.id], + queryFn: () => getAllRoutes(university.id), + }, + { queryKey: [university.id, 'risks'], queryFn: () => getAllRisks(university.id) }, + ] + }); + + const [routes, risks] = result; + + const reportRisk = (e: MouseEvent) => { + if (!reportMarker) { + e.preventDefault(); + return; + } + + setReportRouteId(reportMarker.route) + } + + const resetMarker = (prevMarker: MarkerTypesWithElement) => { + if (prevMarker.type === Markers.REPORT) { + prevMarker.element.map = null; + return; + } else { + prevMarker.element.content = createMarkerElement({ type: prevMarker.type }); + } + }; + + const addRiskMarker = () => { + if (AdvancedMarker === null || map === null) return; + const { dangerRoutes, cautionRoutes } = risks.data; + + for (const route of dangerRoutes) { + const { routeId, node1, node2, dangerTypes } = route; + const type = Markers.DANGER; + + const dangerMarker = createAdvancedMarker( + AdvancedMarker, + map, + new google.maps.LatLng({ + lat: (node1.lat + node2.lat) / 2, + lng: (node1.lng + node2.lng) / 2, + }), + createMarkerElement({ type }), + () => { + setReportMarker((prevMarker) => { + if (prevMarker) resetMarker(prevMarker); + + return { + type: Markers.DANGER, + element: dangerMarker, + route: routeId, + factors: dangerTypes, + } + }) + }, + ); + } + + for (const route of cautionRoutes) { + const { routeId, node1, node2, cautionTypes } = route; + const type = Markers.CAUTION; + + const cautionMarker = createAdvancedMarker( + AdvancedMarker, + map, + new google.maps.LatLng({ + lat: (node1.lat + node2.lat) / 2, + lng: (node1.lng + node2.lng) / 2, + }), + createMarkerElement({ type }), + () => { + setReportMarker((prevMarker) => { + if (prevMarker) resetMarker(prevMarker); + + return { + type: Markers.CAUTION, + element: cautionMarker, + route: routeId, + factors: cautionTypes, + } + }) + }, + ); + } + }; + + 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; + + createAdvancedMarker( + AdvancedMarker, + map, + endNode, + createMarkerElement({ type: Markers.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: ClickEvent) => { + const edges: CoreRoute[] = subRoutes.map(({ routeId, node1, node2 }) => { + return { routeId, node1, node2 }; + }); + + const point = LatLngToLiteral(e.latLng); + const { edge: nearestEdge, point: nearestPoint } = findNearestSubEdge(edges, point); + + const newReportMarker = createAdvancedMarker( + AdvancedMarker, + map, + centerCoordinate(nearestEdge.node1, nearestEdge.node2), + createMarkerElement({ + type: Markers.REPORT, + className: "translate-routemarker", + hasAnimation: true, + }), + ); + + setReportMarker((prevMarker) => { + if (prevMarker) resetMarker(prevMarker); + + return { + type: Markers.REPORT, + element: newReportMarker, + route: nearestEdge.routeId + } + }) + + }); + + const startNode = subRoutes[0].node1; + + createAdvancedMarker( + AdvancedMarker, + map, + startNode, + createMarkerElement({ type: Markers.WAYPOINT, className: "translate-waypoint" }), + ); + } + }; + + + useEffect(() => { + drawRoute(routes.data); + addRiskMarker(); + + if (map) { + map.addListener("click", () => { + setReportMarker((prevMarker) => { + if (prevMarker) { + setMessage(ReportRiskMessage.DEFAULT); + resetMarker(prevMarker); + } else setMessage(ReportRiskMessage.ERROR); + + return undefined; + }); + }); + + } + }, [map, AdvancedMarker, Polyline]); + + useEffect(() => { + if (message === ReportRiskMessage.ERROR) { + setTimeout(() => { + setMessage(ReportRiskMessage.DEFAULT); + }, 1000); + } + }, [message]); + + /** isSelect(Marker 선택 시) Marker Content 변경, 지도 이동, BottomSheet 열기 */ + 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], + hasTopContent: true, + }); + } + else if (marker.type === Markers.CAUTION) { + const key = marker.factors && marker.factors[0] as CautionIssueType; + marker.element.content = createMarkerElement({ + type: marker.type, + title: key && CautionIssue[key], + hasTopContent: true, + }); + } + return; + } + + marker.element.content = createMarkerElement({ + type: marker.type, + }); + }; + + /** 선택된 마커가 있는 경우 */ + 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) + else setMessage(ReportRiskMessage.DEFAULT); + + changeMarkerStyle(reportMarker, true); + return () => { + changeMarkerStyle(reportMarker, false); + }; + }, [reportMarker]); + + return ( +
      +
      + + {message} + +
      + +
      + + + + + +
      + ); +} diff --git a/uniro_frontend/src/pages/reportRoute.tsx b/uniro_frontend/src/pages/reportRoute.tsx new file mode 100644 index 0000000..bd1fed0 --- /dev/null +++ b/uniro_frontend/src/pages/reportRoute.tsx @@ -0,0 +1,445 @@ +import { useEffect, useRef, useState } from "react"; +import createMarkerElement from "../components/map/mapMarkers"; +import { Markers } from "../constant/enum/markerEnum"; +import useMap from "../hooks/useMap"; +import createAdvancedMarker from "../utils/markers/createAdvanedMarker"; +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 { AdvancedMarker } from "../data/types/marker"; +import Button from "../components/customButton"; +import { CautionToggleButton, DangerToggleButton } 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 { getAllRoutes, postReportRoute } from "../api/route"; +import { CoreRoute, CoreRoutesList, Route, RouteId } from "../data/types/route"; +import { Node, NodeId } from "../data/types/node"; +import { Coord } from "../data/types/coord"; +import useModal from "../hooks/useModal"; +import { useNavigate } from "react-router"; +import { getAllRisks } from "../api/routes"; +import { CautionIssueType, DangerIssueType } from "../data/types/enum"; +import { CautionIssue, DangerIssue } from "../constant/enum/reportEnum"; + +type SelectedMarkerTypes = { + type: Markers.CAUTION | Markers.DANGER; + id: RouteId; + element: AdvancedMarker; + factors: DangerIssueType[] | CautionIssueType[]; +}; + +export default function ReportRoutePage() { + const navigate = useNavigate(); + const queryClient = useQueryClient(); + const { map, mapRef, AdvancedMarker, Polyline } = useMap({ zoom: 18, minZoom: 17 }); + const originPoint = useRef<{ point: Node; element: AdvancedMarker } | undefined>(); + const [newPoints, setNewPoints] = useState<{ element: AdvancedMarker | null; coords: (Coord | Node)[] }>({ + element: null, + coords: [], + }); + const newPolyLine = useRef(); + const [isActive, setIsActive] = useState(false); + + const [dangerMarkers, setDangerMarkers] = useState<{ element: AdvancedMarker, routeId: RouteId }[]>([]); + const [isDangerAcitve, setIsDangerActive] = useState(false); + + 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(); + + if (!university) return; + + const result = useSuspenseQueries({ + queries: [ + { + queryKey: ["routes", university.id], + queryFn: () => getAllRoutes(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], }); + }, + 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([]); + }, + }); + + const addRiskMarker = () => { + if (AdvancedMarker === null || map === null) return; + const { dangerRoutes, cautionRoutes } = risks.data; + + /** 위험 마커 생성 */ + const dangerMarkersWithId: { routeId: RouteId; element: AdvancedMarker }[] = []; + + for (const route of dangerRoutes) { + const { routeId, node1, node2, dangerTypes } = route; + const type = Markers.DANGER; + + const dangerMarker = createAdvancedMarker( + AdvancedMarker, + null, + new google.maps.LatLng({ + lat: (node1.lat + node2.lat) / 2, + lng: (node1.lng + node2.lng) / 2, + }), + createMarkerElement({ type }), + () => { + setSelectedMarker((prevMarker) => { + if (prevMarker?.id === routeId) return undefined; + return { + type: Markers.DANGER, + element: dangerMarker, + factors: dangerTypes, + id: routeId + } + }) + }, + ); + + dangerMarkersWithId.push({ routeId, element: dangerMarker }); + } + setDangerMarkers(dangerMarkersWithId); + + /** 주의 마커 생성 */ + const cautionMarkersWithId: { routeId: RouteId; element: AdvancedMarker }[] = []; + + for (const route of cautionRoutes) { + const { routeId, node1, node2, cautionTypes } = route; + const type = Markers.CAUTION; + + const cautionMarker = createAdvancedMarker( + AdvancedMarker, + null, + new google.maps.LatLng({ + lat: (node1.lat + node2.lat) / 2, + lng: (node1.lng + node2.lng) / 2, + }), + createMarkerElement({ type }), + () => { + setSelectedMarker((prevMarker) => { + if (prevMarker?.id === routeId) return undefined; + return { + type: Markers.CAUTION, + element: cautionMarker, + factors: cautionTypes, + id: routeId + } + }) + }, + ); + cautionMarkersWithId.push({ routeId, element: cautionMarker }); + } + + setCautionMarkers(cautionMarkersWithId); + }; + + const toggleCautionButton = () => { + if (!map) return; + setIsCautionActive((isActive) => { + 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); + return !isActive; + }); + }; + + const reportNewRoute = () => { + if (!newPolyLine.current || !Polyline) return; + + const subNodes = []; + const edges = newPoints.coords.map((node, idx) => [node, newPoints.coords[idx + 1]]).slice(0, -1); + + const lastPoint = newPoints.coords[newPoints.coords.length - 1] as Node | Coord; + + for (const edge of edges) { + const subNode = createSubNodes(new Polyline({ path: edge })).slice(0, -1); + subNodes.push(...subNode); + } + + subNodes.push(lastPoint); + + 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 }); + }; + + 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; + + createAdvancedMarker( + AdvancedMarker, + map, + endNode, + createMarkerElement({ type: Markers.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: ClickEvent) => { + const edges: CoreRoute[] = subRoutes.map(({ routeId, node1, node2 }) => { + return { routeId, node1, node2 }; + }); + + const point = LatLngToLiteral(e.latLng); + const { edge: nearestEdge, point: nearestPoint } = findNearestSubEdge(edges, point); + + const tempWaypointMarker = createAdvancedMarker( + AdvancedMarker, + map, + nearestPoint, + createMarkerElement({ + type: Markers.WAYPOINT, + className: "translate-waypoint", + }), + ); + + if (originPoint.current) { + setNewPoints((prevPoints) => { + if (prevPoints.element) { + prevPoints.element.position = nearestPoint; + return { + ...prevPoints, + coords: [...prevPoints.coords, nearestPoint], + }; + } else { + setIsActive(true); + return { + element: new AdvancedMarker({ + map: map, + position: nearestPoint, + content: createMarkerElement({ + type: Markers.DESTINATION, + }), + }), + coords: [...prevPoints.coords, nearestPoint], + }; + } + }); + } else { + const originMarker = new AdvancedMarker({ + map: map, + position: nearestPoint, + content: createMarkerElement({ type: Markers.ORIGIN }), + }); + originPoint.current = { + point: nearestPoint, + element: originMarker, + }; + setNewPoints({ + element: null, + coords: [nearestPoint], + }); + } + }); + + const startNode = subRoutes[0].node1; + + createAdvancedMarker( + AdvancedMarker, + map, + startNode, + createMarkerElement({ type: Markers.WAYPOINT, className: "translate-waypoint" }), + ); + } + }; + + useEffect(() => { + if (newPolyLine.current) { + newPolyLine.current.setPath(newPoints.coords); + } + }, [newPoints]); + + useEffect(() => { + drawRoute(routes.data); + addRiskMarker(); + if (Polyline) { + newPolyLine.current = new Polyline({ map: map, path: [], strokeColor: "#0367FB" }); + } + + if (map && AdvancedMarker) { + map.addListener("click", (e: ClickEvent) => { + setSelectedMarker(undefined) + if (originPoint.current) { + const point = LatLngToLiteral(e.latLng); + setNewPoints((prevPoints) => { + if (prevPoints.element) { + prevPoints.element.position = point; + return { + ...prevPoints, + coords: [...prevPoints.coords, point], + }; + } else { + setIsActive(true); + return { + element: new AdvancedMarker({ + map: map, + position: point, + content: createMarkerElement({ + type: Markers.DESTINATION, + }), + }), + coords: [...prevPoints.coords, point], + }; + } + }); + } + }); + } + }, [map]); + + /** isSelect(Marker 선택 시) Marker Content 변경, 지도 이동, BottomSheet 열기 */ + 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; + marker.element.content = createMarkerElement({ + type: marker.type, + title: key && DangerIssue[key], + hasTopContent: true, + }); + } + else if (marker.type === Markers.CAUTION) { + const key = marker.factors[0] as CautionIssueType; + marker.element.content = createMarkerElement({ + type: marker.type, + title: key && CautionIssue[key], + hasTopContent: true, + }); + } + return; + } + + marker.element.content = createMarkerElement({ + type: marker.type, + }); + }; + + /** 선택된 마커가 있는 경우 */ + useEffect(() => { + changeMarkerStyle(selectedMarker, true); + return () => { + changeMarkerStyle(selectedMarker, false); + }; + }, [selectedMarker]); + + return ( +
      +
      +

      + 선 위 또는 기존 지점을 선택하세요 +

      +
      + +
      + {isActive && ( +
      + +
      + )} +
      + + +
      + {isSuccessOpen && ( + +

      새로운 길 제보를 완료했어요!

      +
      +

      제보는 바로 반영되지만,

      +

      + 더 정확한 정보를 위해 추후 수정될 수 있어요. +

      +
      +
      + )} + {isFailOpen && ( + +

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

      +
      +

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

      +

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

      +
      +
      + )} +
      + ); +} diff --git a/uniro_frontend/src/pages/search.tsx b/uniro_frontend/src/pages/search.tsx deleted file mode 100644 index cc749d2..0000000 --- a/uniro_frontend/src/pages/search.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { useState } from "react"; -import Input from "../components/customInput"; -import UniversityButton from "../components/universityButton"; -import { UniversityList } from "../constant/university"; -import Button from "../components/customButton"; -import { Link } from "react-router"; - -export default function UniversitySearchPage() { - const [selectedUniv, setSelectedUniv] = useState(""); - - return ( -
      -
      - {}} placeholder="우리 학교를 검색해보세요" handleVoiceInput={() => {}} /> -
      -
      -
        { - setSelectedUniv(""); - }} - > - {UniversityList.map(({ name, img }) => ( - { - setSelectedUniv(name); - }} - name={name} - img={img} - /> - ))} -
      -
      -
      - {selectedUniv !== "" && ( - - - - )} -
      -
      - ); -} diff --git a/uniro_frontend/src/pages/universitySearch.tsx b/uniro_frontend/src/pages/universitySearch.tsx new file mode 100644 index 0000000..a3d15fd --- /dev/null +++ b/uniro_frontend/src/pages/universitySearch.tsx @@ -0,0 +1,55 @@ +import { useState } from "react"; +import Input from "../components/customInput"; +import UniversityButton from "../components/universityButton"; +import Button from "../components/customButton"; +import { Link } from "react-router"; +import useUniversityInfo from "../hooks/useUniversityInfo"; +import { useQuery } from "@tanstack/react-query"; +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(); + + return ( +
      +
      + {}} placeholder="우리 학교를 검색해보세요" handleVoiceInput={() => {}} /> +
      +
      +
        { + setSelectedUniv(undefined); + }} + > + {universityList && + universityList.map((univ) => ( + { + setSelectedUniv(univ); + }} + name={univ.name} + img={univ.imageUrl} + /> + ))} +
      +
      +
      + {selectedUniv && ( + setUniversity(selectedUniv)}> + + + )} +
      +
      + ); +} diff --git a/uniro_frontend/src/utils/coordinates/centerCoordinate.ts b/uniro_frontend/src/utils/coordinates/centerCoordinate.ts new file mode 100644 index 0000000..1e16b47 --- /dev/null +++ b/uniro_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_frontend/src/utils/coordinates/coordinateTransform.ts b/uniro_frontend/src/utils/coordinates/coordinateTransform.ts new file mode 100644 index 0000000..088eab8 --- /dev/null +++ b/uniro_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_frontend/src/utils/coordinates/distance.ts b/uniro_frontend/src/utils/coordinates/distance.ts new file mode 100644 index 0000000..44bad78 --- /dev/null +++ b/uniro_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_frontend/src/utils/fetch/fetch.ts b/uniro_frontend/src/utils/fetch/fetch.ts new file mode 100644 index 0000000..b31bbf4 --- /dev/null +++ b/uniro_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_frontend/src/utils/fetch/mockFetch.ts b/uniro_frontend/src/utils/fetch/mockFetch.ts new file mode 100644 index 0000000..d6780aa --- /dev/null +++ b/uniro_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_frontend/src/utils/markers/createAdvanedMarker.ts b/uniro_frontend/src/utils/markers/createAdvanedMarker.ts index bdbcbd7..28fb887 100644 --- a/uniro_frontend/src/utils/markers/createAdvanedMarker.ts +++ b/uniro_frontend/src/utils/markers/createAdvanedMarker.ts @@ -1,7 +1,7 @@ export default function createAdvancedMarker( AdvancedMarker: typeof google.maps.marker.AdvancedMarkerElement, - map: google.maps.Map, - position: google.maps.LatLng, + map: google.maps.Map | null, + position: google.maps.LatLng | google.maps.LatLngLiteral, content: HTMLElement, onClick?: () => void, ) { @@ -15,3 +15,33 @@ export default function createAdvancedMarker( 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_frontend/src/utils/markers/toggleMarkers.ts b/uniro_frontend/src/utils/markers/toggleMarkers.ts new file mode 100644 index 0000000..bcfc52a --- /dev/null +++ b/uniro_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_frontend/src/utils/navigation/formatDistance.ts b/uniro_frontend/src/utils/navigation/formatDistance.ts new file mode 100644 index 0000000..f2e81c1 --- /dev/null +++ b/uniro_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_frontend/src/utils/polylines/createSubnodes.ts b/uniro_frontend/src/utils/polylines/createSubnodes.ts new file mode 100644 index 0000000..4560fd3 --- /dev/null +++ b/uniro_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_frontend/src/utils/polylines/findNearestEdge.ts b/uniro_frontend/src/utils/polylines/findNearestEdge.ts new file mode 100644 index 0000000..64ccc99 --- /dev/null +++ b/uniro_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_frontend/src/utils/report/getThemeByPassableStatus.ts b/uniro_frontend/src/utils/report/getThemeByPassableStatus.ts new file mode 100644 index 0000000..f58b320 --- /dev/null +++ b/uniro_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]; +};