diff --git a/.github/workflows/be_cd-production.yml b/.github/workflows/be_cd-production.yml new file mode 100644 index 00000000..71737961 --- /dev/null +++ b/.github/workflows/be_cd-production.yml @@ -0,0 +1,174 @@ +name: Coduo Backend Production Server CD + +on: + push: + branches: [ "production" ] + +jobs: + build: + environment: production + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./backend + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'corretto' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + with: + cache-write-only: true + + - name: Grant Execute Permission For Gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew bootJar + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_HUB_ID }} + password: ${{ secrets.DOCKER_HUB_PASSWORD }} + + - name: Image build and push + run: | + docker build --build-arg PROFILE=prod --build-arg DEPENDENCY=build/dependency -t ${{ secrets.DOCKER_REPO_NAME }}/springboot-app:test-latest --platform linux/arm64 . + docker push ${{ secrets.DOCKER_REPO_NAME }}/springboot-app:test-latest + + - name: Upload docker-compose yaml script to artifact + uses: actions/upload-artifact@v4 + with: + name: docker-compose + path: ${{ github.workspace }}/backend/be_app-docker-compose.yml + + deployA: + environment: production + runs-on: production-ec2-A-runner + needs: build + defaults: + run: + working-directory: ./backend + steps: + - name: Set docker-compose YAML script to runner + uses: actions/download-artifact@v4 + with: + name: docker-compose + path: ${{ github.workspace }}/backend + + - name: Move docker-compose YAML + run: | + sudo mv be_app-docker-compose.yml ${{ secrets.DOCKER_COMPOSE_YAML_PATH }}/ + + - name: Extract secrets as .be_app-env file + run: | + cat < ${{ secrets.DOCKER_COMPOSE_YAML_PATH }}/.be_app-env + + # Docker Hub info from Github Secrets + DOCKER_REPO_NAME=${{ secrets.DOCKER_REPO_NAME }} + + # DB Configuration secrets info from Github Secrets + MYSQL_DB_NAME=${{ secrets.MYSQL_DB_NAME }} + MYSQL_TIME_ZONE=${{ secrets.MYSQL_TIME_ZONE }} + DB_BINDING_PORT=${{ secrets.DB_BINDING_PORT }} + DOCKER_DATA_PATH=${{ secrets.DOCKER_DATA_PATH }} + MASTER_DB_URL=${{ secrets.MASTER_DB_URL }} + MASTER_DB_USERNAME=${{ secrets.MASTER_DB_USERNAME }} + MASTER_DB_PASSWORD=${{ secrets.MASTER_DB_PASSWORD }} + SLAVE_DB_URL=${{ secrets.SLAVE_DB_URL }} + SLAVE_DB_USERNAME=${{ secrets.SLAVE_DB_USERNAME }} + SLAVE_DB_PASSWORD=${{ secrets.SLAVE_DB_PASSWORD }} + DDL_AUTO=${{ secrets.DDL_AUTO }} + + # OAUTH & JWT + CLIENT_ID=${{ secrets.CLIENT_ID }} + CLIENT_SECRET=${{ secrets.CLIENT_SECRET }} + CLIENT_REDIRECT_URI=${{ secrets.CLIENT_REDIRECT_URI }} + JWT_KEY=${{ secrets.JWT_KEY}} + + # INSTANCE NAME + INSTANCE_NAME=${{ secrets.INSTANCE_A_NAME }} + + # Server App + SERVER_BINDING_PORT=${{ secrets.PRODUCTION_SERVER_BINDING_PORT }} + SERVER_LOGS_PATH=${{ secrets.SERVER_LOGS_PATH }} + EOF + + - name: Stop and remove existing containers + run: | + sudo docker-compose --env-file ${{ secrets.DOCKER_COMPOSE_YAML_PATH }}/.be_app-env -f ${{ secrets.DOCKER_COMPOSE_YAML_PATH }}/be_app-docker-compose.yml down --rmi all + - name: Deploy docker container + run: | + sudo docker-compose --env-file ${{ secrets.DOCKER_COMPOSE_YAML_PATH }}/.be_app-env -f ${{ secrets.DOCKER_COMPOSE_YAML_PATH }}/be_app-docker-compose.yml up -d + + deployB: + environment: production + runs-on: production-ec2-B-runner + needs: deployA + defaults: + run: + working-directory: ./backend + steps: + - name: Set docker-compose YAML script to runner + uses: actions/download-artifact@v4 + with: + name: docker-compose + path: ${{ github.workspace }}/backend + + - name: Move docker-compose YAML + run: | + sudo mv be_app-docker-compose.yml ${{ secrets.DOCKER_COMPOSE_YAML_PATH }}/ + + - name: waiting deployA complete + run: | + sudo ${{ secrets.DOCKER_COMPOSE_YAML_PATH }}/scheduler.sh + + - name: Extract secrets as .be_app-env file + run: | + cat < ${{ secrets.DOCKER_COMPOSE_YAML_PATH }}/.be_app-env + + # Docker Hub info from Github Secrets + DOCKER_REPO_NAME=${{ secrets.DOCKER_REPO_NAME }} + + # DB Configuration secrets info from Github Secrets + MYSQL_DB_NAME=${{ secrets.MYSQL_DB_NAME }} + MYSQL_TIME_ZONE=${{ secrets.MYSQL_TIME_ZONE }} + DB_BINDING_PORT=${{ secrets.DB_BINDING_PORT }} + DOCKER_DATA_PATH=${{ secrets.DOCKER_DATA_PATH }} + MASTER_DB_URL=${{ secrets.MASTER_DB_URL }} + MASTER_DB_USERNAME=${{ secrets.MASTER_DB_USERNAME }} + MASTER_DB_PASSWORD=${{ secrets.MASTER_DB_PASSWORD }} + SLAVE_DB_URL=${{ secrets.SLAVE_DB_URL }} + SLAVE_DB_USERNAME=${{ secrets.SLAVE_DB_USERNAME }} + SLAVE_DB_PASSWORD=${{ secrets.SLAVE_DB_PASSWORD }} + DDL_AUTO=${{ secrets.DDL_AUTO }} + + + # OAUTH & JWT + CLIENT_ID=${{ secrets.CLIENT_ID }} + CLIENT_SECRET=${{ secrets.CLIENT_SECRET }} + CLIENT_REDIRECT_URI=${{ secrets.CLIENT_REDIRECT_URI }} + JWT_KEY=${{ secrets.JWT_KEY}} + + # INSTANCE NAME + INSTANCE_NAME=${{ secrets.INSTANCE_B_NAME }} + + + # Server App + SERVER_BINDING_PORT=${{ secrets.PRODUCTION_SERVER_BINDING_PORT }} + SERVER_LOGS_PATH=${{ secrets.SERVER_LOGS_PATH }} + EOF + + - name: Stop and remove existing containers + run: | + sudo docker-compose --env-file ${{ secrets.DOCKER_COMPOSE_YAML_PATH }}/.be_app-env -f ${{ secrets.DOCKER_COMPOSE_YAML_PATH }}/be_app-docker-compose.yml down --rmi all + - name: Deploy docker container + run: | + sudo docker-compose --env-file ${{ secrets.DOCKER_COMPOSE_YAML_PATH }}/.be_app-env -f ${{ secrets.DOCKER_COMPOSE_YAML_PATH }}/be_app-docker-compose.yml up -d diff --git a/.github/workflows/be_cd-test.yml b/.github/workflows/be_cd-test.yml index a3ccd66c..0eec28b8 100644 --- a/.github/workflows/be_cd-test.yml +++ b/.github/workflows/be_cd-test.yml @@ -51,7 +51,7 @@ jobs: deploy: environment: test - runs-on: coduo_runner_test-server + runs-on: test-ec2-runner needs: build defaults: run: @@ -75,21 +75,24 @@ jobs: DOCKER_REPO_NAME=${{ secrets.DOCKER_REPO_NAME }} # DB Configuration secrets info from Github Secrets - MYSQL_DB_NAME=${{ secrets.MYSQL_DB_NAME }} - MYSQL_TIME_ZONE=${{ secrets.MYSQL_TIME_ZONE }} - DB_BINDING_PORT=${{ secrets.DB_BINDING_PORT }} - DOCKER_DATA_PATH=${{ secrets.DOCKER_DATA_PATH }} - DB_URL=${{ secrets.DB_URL }} - DB_USERNAME=${{ secrets.DB_USERNAME }} - DB_PASSWORD=${{ secrets.DB_PASSWORD }} + MASTER_DB_URL=${{ secrets.TEST_SERVER_DB_URL }} + MASTER_DB_USERNAME=${{ secrets.TEST_SERVER_DB_USERNAME }} + MASTER_DB_PASSWORD=${{ secrets.TEST_SERVER_DB_PASSWORD }} + + SLAVE_DB_URL=${{ secrets.TEST_SERVER_DB_URL }} + SLAVE_DB_USERNAME=${{ secrets.TEST_SERVER_DB_USERNAME }} + SLAVE_DB_PASSWORD=${{ secrets.TEST_SERVER_DB_PASSWORD }} DDL_AUTO=${{ secrets.DDL_AUTO }} # OAUTH & JWT - CLIENT_ID=${{ secrets.CLIENT_ID }} - CLIENT_SECRET=${{ secrets.CLIENT_SECRET }} - CLIENT_REDIRECT_URI=${{ secrets.CLIENT_REDIRECT_URI }} + CLIENT_ID=${{ secrets.TEST_CLIENT_ID }} + CLIENT_SECRET=${{ secrets.TEST_CLIENT_SECRET }} + CLIENT_REDIRECT_URI=${{ secrets.TEST_CLIENT_REDIRECT_URI }} JWT_KEY=${{ secrets.JWT_KEY}} - + + # Sticky Synchronize + INSTANCE_NAME=${{ secrets.INSTANCE_A_NAME }} + # Server App SERVER_BINDING_PORT=${{ secrets.SERVER_BINDING_PORT }} SERVER_LOGS_PATH=${{ secrets.SERVER_LOGS_PATH }} diff --git a/.github/workflows/be_ci.yml b/.github/workflows/be_ci.yml index 717a838e..53f7304d 100644 --- a/.github/workflows/be_ci.yml +++ b/.github/workflows/be_ci.yml @@ -1,8 +1,8 @@ -name: Coduo Backend Test Server CI +name: Coduo Backend Production Server CI on: pull_request: - branches: [ "BE/dev", "BE/test" ] + branches: [ "BE/dev", "BE/test", "production" ] jobs: build-and-test: diff --git a/.github/workflows/fe-CI.yml b/.github/workflows/fe-CI.yml new file mode 100644 index 00000000..f5b2e6dd --- /dev/null +++ b/.github/workflows/fe-CI.yml @@ -0,0 +1,63 @@ +name: Coduo Frontend CI + +on: + pull_request: + branches: [FE/dev] + +jobs: + test-and-lint: + runs-on: ubuntu-latest + defaults: + run: + shell: bash + working-directory: ./frontend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22.4.1" + + - name: Install dependencies + run: yarn install + + - name: Clear test cache + run: yarn jest --clearCache + + - name: Run tests + run: yarn test + + - name: Run lint + run: yarn lint + + - name: Run lint:css + run: yarn lint:css + + - name: Run build + run: yarn build + + publish-storybook: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./frontend + needs: test-and-lint + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22.4.1" + + - name: Install dependencies + run: yarn install + + - name: Publish storybook + run: npx chromatic --project-token=${{ secrets.CHROMATIC_PROJECT_TOKEN }} --storybook-config-dir=.storybook diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 553927ba..bd43b5cd 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -1,6 +1,6 @@ -name: PULL REQUEST +name: pull request -on: +on: pull_request: types: [opened, reopened] @@ -11,11 +11,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 with: - ref: ${{ vars.SLACK_BRANCH}} - + ref: ${{vars.SLACK_BRANCH}} + - name: Install jq run: sudo apt-get install jq - + - name: Decode base64 and parse JSON run: | echo ${{secrets.SLACK_CREW_INFO_JSON}} | base64 --decode > decoded.json @@ -38,11 +38,11 @@ jobs: } console.log(`::set-output name=names::${outputString.trim()}`); console.log(`::set-output name=requester::${requester.name}`); - + - name: pull_request_noifiy id: pull_request_notify uses: slackapi/slack-github-action@v1.26.0 - with: + with: channel-id: ${{secrets.SLACK_BE_REVIEW_CHANNEL}} payload: | { @@ -52,7 +52,7 @@ jobs: "type": "section", "text": { "type": "mrkdwn", - "text": "*πŸ“¬${{steps.extract_receiver_list.outputs.requester}}λ‹˜μ˜ PR이 λ„μ°©ν–ˆμ–΄μš”.πŸ“¬*\n\n\n⚑⚑⚑⚑⚑⚑⚑⚑⚑\n*⚑<${{github.event.pull_request.html_url}} | [${{steps.extract_receiver_list.outputs.requester}}] 혼ꡬ멍 λ‚΄λŸ¬κ°€κΈ°~>⚑* \n⚑⚑⚑⚑⚑⚑⚑⚑⚑\n\n _ν˜Όλ‚΄μ€„ μ‚¬λžŒλ“€: ${{steps.extract_receiver_list.outputs.names}}_ \n" + "text": "*πŸ“¬${{steps.extract_receiver_list.outputs.requester}}λ‹˜μ˜ PR이 λ„μ°©ν–ˆμ–΄μš”.πŸ“¬*\n\n\n⚑⚑⚑⚑⚑⚑⚑⚑⚑\n*⚑<${{github.event.pull_request.html_url}}| [${{steps.extract_receiver_list.outputs.requester}}] 혼ꡬ멍 λ‚΄λŸ¬κ°€κΈ°~>⚑* \n⚑⚑⚑⚑⚑⚑⚑⚑⚑\n\n _ν˜Όλ‚΄μ€„ μ‚¬λžŒλ“€: ${{steps.extract_receiver_list.outputs.names}}_ \n" } }, { @@ -62,7 +62,7 @@ jobs: } env: SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} - + - name: Append Thread info to JSON run: | NEW_THREAD_JSON=$(jq -n \ @@ -73,20 +73,20 @@ jobs: mkdir -p .github/logs cd .github/logs - - if [ -f thread_be.json ]; then - jq --argjson new "$NEW_THREAD_JSON" '. += [$new]' thread_be.json > thread_be.tmp.json - mv thread_be.tmp.json thread_be.json + + if [ -f thread_fe.json ]; then + jq --argjson new "$NEW_THREAD_JSON" '. += [$new]' thread_fe.json > thread_fe.tmp.json + mv thread_fe.tmp.json thread_fe.json else - echo "[$NEW_THREAD_JSON]" > thread_be.json + echo "[$NEW_THREAD_JSON]" > thread_fe.json fi - - name: Commit updated thread_be.json + - name: Commit updated thread_fe.json run: | git config --global user.email "action@github.com" git config --global user.name "GitHub Actions" - git add .github/logs/thread_be.json - git commit -m "Update thread_be.json with new thread data" + git add .github/logs/thread_fe.json + git commit -m "Update thread_fe.json with new thread data" git push origin ${{ vars.SLACK_BRANCH }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/review_submit_notification.yml b/.github/workflows/review_submit_notification.yml index 3ff18adc..d594ccc6 100644 --- a/.github/workflows/review_submit_notification.yml +++ b/.github/workflows/review_submit_notification.yml @@ -1,9 +1,9 @@ name: review submitted notification -on: +on: pull_request_review: types: [submitted] - + jobs: notify: runs-on: ubuntu-latest @@ -15,30 +15,29 @@ jobs: - name: Install jq run: sudo apt-get install jq - + - name: Read Thead JSON and Set Env Variables run: | TARGET_URL=${{ github.event.pull_request.html_url }} - JSON_CONTENT=$(cat .github/logs/thread_be.json) + JSON_CONTENT=$(cat .github/logs/thread_fe.json) TARGET_OBJECT=$(echo "$JSON_CONTENT" | jq --arg url "$TARGET_URL" '.[] | select(.pr_url == $url)') - + PR_URL=$(echo "$TARGET_OBJECT" | jq -r '.pr_url') AUTHOR=$(echo "$TARGET_OBJECT" | jq -r '.author') THREAD_TS=$(echo "$TARGET_OBJECT" | jq -r '.thread_ts') - + echo "PR_URL=${PR_URL}" >> $GITHUB_ENV echo "PR_AUTHOR=${AUTHOR}" >> $GITHUB_ENV echo "REVIEW_AUTHOR=${{ github.event.review.user.login }}" >> $GITHUB_ENV echo "THREAD_TS=${THREAD_TS}" >> $GITHUB_ENV - - name: Decode base64 and parse CREW JSON run: | echo ${{secrets.SLACK_CREW_INFO_JSON}} | base64 --decode > decoded.json JSON_CONTENT=$(cat decoded.json | jq -c .) echo "CREW_INFO_JSON=$JSON_CONTENT" >> $GITHUB_ENV - - name: Extract member + - name: Extract member id: extract_member uses: actions/github-script@v7.0.1 with: @@ -48,12 +47,10 @@ jobs: const reviewer = members.find(entry => entry.githubId === process.env.REVIEW_AUTHOR); console.log(`::set-output name=reviewee_slack_id::${reviewee.slackId}`); console.log(`::set-output name=reviewer_slack_name::${reviewer.name}`); - - - name: Send Slack notification uses: slackapi/slack-github-action@v1.22.0 with: - channel-id: ${{secrets.SLACK_BE_REVIEW_CHANNEL}} + channel-id: ${{secrets.SLACK_FE_REVIEW_CHANNEL}} payload: | { "text": "리뷰가 λ„μ°©ν–ˆμ–΄μš”βœ‰οΈ", diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..f80a334a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "editor.codeActionsOnSave": { + "source.fixAll.stylelint": "explicit" + }, + "stylelint.enable": true, + "stylelint.validate": [ + "css", + "scss", + "postcss", + "typescript", + "typescriptreact" + ], + "stylelint.customSyntax": "postcss-styled-syntax", + "diffEditor.ignoreTrimWhitespace": false +} diff --git a/README.md b/README.md index 5442cd84..533e3aca 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,13 @@ # 2024-coduo -ν™”λͺ©ν•œ λ°±μ—”λ“œ... + +## ν•¨κ»˜ ν•˜λ©΄ 쒋은 μ½”λ”©, μ½”λ”©ν•΄λ“€μ˜€ πŸ€ + +νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ— λŒ€ν•΄ κ°œλ…μ€ μ•Œκ³  μžˆμ§€λ§Œ μ–΄λ–»κ²Œ μ‹œμž‘ν•΄μ•Ό 할지 λ§‰λ§‰ν–ˆλ˜ κ²½ν—˜μ΄ μžˆμœΌμ‹ κ°€μš”? ν˜Ήμ€ νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ„ ν•΄λ΄€μ§€λ§Œ μ œλŒ€λ‘œ ν•΄λ³Έ 건지 ν˜Όλž€μŠ€λŸ¬μ› λ˜ 적이 μžˆμœΌμ‹ κ°€μš”? λ§Žμ€ μ‚¬λžŒλ“€μ΄ νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ„ μ‹œλ„ν•΄ λ³΄λ €λŠ” λ§ˆμŒμ€ μžˆμ§€λ§Œ, μ‹€μ œλ‘œ μ–Όλ§ˆλ‚˜ μ‹œκ°„μ„ νˆ¬μžν•΄μ•Ό 할지, μ–΄λ–€ μˆœμ„œλ‘œ 진행해야 할지, 의미 μžˆλŠ” κ²½ν—˜μ„ μ–»κΈ° μœ„ν•΄μ„œλŠ” μ–΄λ–»κ²Œ ν•΄μ•Ό 할지에 λŒ€ν•œ 고민이 λ§ŽμŠ΅λ‹ˆλ‹€. + +이 λͺ¨λ“  고민을 μ½”λ”©ν•΄λ“€μ˜€κ°€ ν•œ λ²ˆμ— ν•΄κ²°ν•΄ λ“œλ¦½λ‹ˆλ‹€. νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ° κ³ μˆ˜κ°€ 되고 μ‹Άλ‹€λ©΄, μ§€κΈˆ λ°”λ‘œ μ½”λ”©ν•΄λ“€μ˜€λ₯Ό μ΄μš©ν•΄ λ³΄μ„Έμš”. + +μ½”λ”©ν•΄λ“€μ˜€λŠ” νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ„ 효과적으둜 μ§„ν–‰ν•˜κΈ° μœ„ν•œ λ‹€μ–‘ν•œ κ°€μ΄λ“œλ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€. λ˜ν•œ, 타이머 μ•ŒλžŒκ³Ό λΈŒλΌμš°μ € νŒμ—…μœΌλ‘œ κ΅λŒ€ 타이밍을 μžλ™μœΌλ‘œ μ•Œλ €μ£Όμ–΄ λ³„λ„μ˜ 타이머λ₯Ό μ‚¬μš©ν•  ν•„μš”κ°€ μ—†μŠ΅λ‹ˆλ‹€. λ‹€μ‹œ 보고 싢은 λ ˆνΌλŸ°μŠ€λŠ” μ½”λ”©ν•΄λ“€μ˜€κ°€ ν•œ 곳에 λͺ¨μ•„ κΉ”λ”ν•˜κ²Œ 관리해 μ€λ‹ˆλ‹€. + +μ½”λ”©ν•΄λ“€μ˜€λŠ” λ‹Ήμ‹ μ˜ μ„±μž₯을 도와쀄 졜고의 λ„κ΅¬μž…λ‹ˆλ‹€. ν•¨κ»˜ μ„±μž₯ν•˜λ©° 더 λ‚˜μ€ κ°œλ°œμžκ°€ 되기 μœ„ν•œ 여정을 μ½”λ”©ν•΄λ“€μ˜€κ°€ μ‘μ›ν•©λ‹ˆλ‹€. + +[μ½”λ”©ν•΄λ“€μ˜€ μ‚¬μ΄νŠΈ λ°”λ‘œκ°€κΈ° πŸš€](https://coduo.site) diff --git a/backend/.gitignore b/backend/.gitignore index b9c756f1..5c5cf065 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -204,5 +204,6 @@ gradle-app.setting ### application.yml src/main/resources/application-ci.yml src/main/resources/application-local.yml +src/main/resources/application.yml # End of https://www.toptal.com/developers/gitignore/api/java,intellij+all,gradle,macos,windows diff --git a/backend/be_app-docker-compose.yml b/backend/be_app-docker-compose.yml index 69e68a2c..e2a14e20 100644 --- a/backend/be_app-docker-compose.yml +++ b/backend/be_app-docker-compose.yml @@ -1,23 +1,4 @@ services: - mysql: - container_name: coduo_mysql - image: mysql:8.0 - restart: unless-stopped - ports: - - ${DB_BINDING_PORT} - environment: - MYSQL_DATABASE: ${MYSQL_DB_NAME} - MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} - TZ: ${MYSQL_TIME_ZONE} - volumes: - - ${DOCKER_DATA_PATH}/db/mysql/conf.d:/etc/mysql/conf.d - - ${DOCKER_DATA_PATH}/db/mysql/data:/var/lib/mysql - - ${DOCKER_DATA_PATH}/db/mysql/init:/docker_entrypoint-initdb.d - command: - - '--character-set-server=utf8mb4' - - '--collation-server=utf8mb4_0900_ai_ci' - networks: - - coduo_net springboot: container_name: coduo_springboot-app @@ -29,19 +10,21 @@ services: - ${SERVER_LOGS_PATH}/springboot-app:/logs - ${SERVER_LOGS_PATH}/springboot-app/backup:/logs/backup environment: - SPRING_DATASOURCE_URL: ${DB_URL} - SPRING_DATASOURCE_USERNAME: ${DB_USERNAME} - SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD} + SPRING_DATASOURCE_REPLICA_MASTER_JDBC-URL: ${MASTER_DB_URL} + SPRING_DATASOURCE_REPLICA_MASTER_USERNAME: ${MASTER_DB_USERNAME} + SPRING_DATASOURCE_REPLICA_MASTER_PASSWORD: ${MASTER_DB_PASSWORD} + SPRING_DATASOURCE_REPLICA_SLAVE_JDBC-URL: ${SLAVE_DB_URL} + SPRING_DATASOURCE_REPLICA_SLAVE_USERNAME: ${SLAVE_DB_USERNAME} + SPRING_DATASOURCE_REPLICA_SLAVE_PASSWORD: ${SLAVE_DB_PASSWORD} SPRING_JPA_HIBERNATE_DDL-AUTO: ${DDL_AUTO} OAUTH_GITHUB_CLIENT_ID: ${CLIENT_ID} OAUTH_GITHUB_CLIENT_SECRET: ${CLIENT_SECRET} OAUTH_GITHUB_REDIRECT_URI: ${CLIENT_REDIRECT_URI} JWT_SIGN_KEY: ${JWT_KEY} + EC2_PREFIX: ${INSTANCE_NAME} restart: on-failure networks: - coduo_net - depends_on: - - mysql networks: coduo_net: diff --git a/backend/src/main/java/site/coduo/common/config/storage/DataSourceConfig.java b/backend/src/main/java/site/coduo/common/config/storage/DataSourceConfig.java new file mode 100644 index 00000000..d74c8811 --- /dev/null +++ b/backend/src/main/java/site/coduo/common/config/storage/DataSourceConfig.java @@ -0,0 +1,55 @@ +package site.coduo.common.config.storage; + +import java.util.HashMap; + +import javax.sql.DataSource; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; + +@Configuration +public class DataSourceConfig { + + @Bean + @ConfigurationProperties(prefix = "spring.datasource.replica.master") + public DataSource masterDataSource() { + return DataSourceBuilder.create() + .build(); + } + + @Bean + @ConfigurationProperties(prefix = "spring.datasource.replica.slave") + public DataSource slaveDataSource() { + return DataSourceBuilder.create() + .build(); + } + + @Bean + @DependsOn({"masterDataSource", "slaveDataSource"}) + public DataSource routingDataSource() { + DataSourceRouter dataSourceRouter = new DataSourceRouter(); + DataSource writeDataSource = masterDataSource(); + DataSource readDataSource = slaveDataSource(); + HashMap dataSourceMap = new HashMap<>(); + dataSourceMap.put(DataSourceRouter.MASTER_TAG, writeDataSource); + dataSourceMap.put(DataSourceRouter.SLAVE_TAG, readDataSource); + + dataSourceRouter.setTargetDataSources(dataSourceMap); + dataSourceRouter.setDefaultTargetDataSource(writeDataSource); + + return dataSourceRouter; + } + + @Bean + @Primary + @DependsOn({"routingDataSource"}) + public DataSource dataSource() { + final DataSource determinedDataSource = routingDataSource(); + return new LazyConnectionDataSourceProxy(determinedDataSource); + } +} diff --git a/backend/src/main/java/site/coduo/common/config/storage/DataSourceRouter.java b/backend/src/main/java/site/coduo/common/config/storage/DataSourceRouter.java new file mode 100644 index 00000000..5342e56f --- /dev/null +++ b/backend/src/main/java/site/coduo/common/config/storage/DataSourceRouter.java @@ -0,0 +1,15 @@ +package site.coduo.common.config.storage; + +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +public class DataSourceRouter extends AbstractRoutingDataSource { + + public static final String MASTER_TAG = "writer"; + public static final String SLAVE_TAG = "reader"; + + @Override + protected Object determineCurrentLookupKey() { + return TransactionSynchronizationManager.isCurrentTransactionReadOnly() ? SLAVE_TAG : MASTER_TAG; + } +} diff --git a/backend/src/main/java/site/coduo/common/config/web/FilterConfig.java b/backend/src/main/java/site/coduo/common/config/web/FilterConfig.java new file mode 100644 index 00000000..0e1d3fed --- /dev/null +++ b/backend/src/main/java/site/coduo/common/config/web/FilterConfig.java @@ -0,0 +1,44 @@ +package site.coduo.common.config.web; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; +import site.coduo.common.config.web.filter.AccessTokenCookieFilter; +import site.coduo.common.config.web.filter.AuthFailHandlerFilter; +import site.coduo.common.config.web.filter.SignInCookieFilter; +import site.coduo.member.infrastructure.security.JwtProvider; + +@RequiredArgsConstructor +@Configuration +public class FilterConfig { + + @Bean + public FilterRegistrationBean accessTokenSessionFilter(final JwtProvider jwtProvider) { + final FilterRegistrationBean bean = new FilterRegistrationBean<>(); + bean.setFilter(new AccessTokenCookieFilter(jwtProvider)); + bean.addUrlPatterns("/api/sign-up", "/api/sign-in/callback"); + bean.setOrder(2); + return bean; + } + + @Bean + public FilterRegistrationBean signInCookieFilter(final JwtProvider jwtProvider) { + final FilterRegistrationBean bean = new FilterRegistrationBean<>(); + bean.setFilter(new SignInCookieFilter(jwtProvider)); + bean.addUrlPatterns("/api/sign-out", "/api/member"); + bean.setOrder(1); + return bean; + } + + @Bean + public FilterRegistrationBean authFailHandlerFilter(final ObjectMapper objectMapper) { + final FilterRegistrationBean bean = new FilterRegistrationBean<>(); + bean.setFilter(new AuthFailHandlerFilter(objectMapper)); + bean.setOrder(0); + return bean; + } +} diff --git a/backend/src/main/java/site/coduo/common/config/WebConfig.java b/backend/src/main/java/site/coduo/common/config/web/WebMvcConfig.java similarity index 74% rename from backend/src/main/java/site/coduo/common/config/WebConfig.java rename to backend/src/main/java/site/coduo/common/config/web/WebMvcConfig.java index 64305244..871f7014 100644 --- a/backend/src/main/java/site/coduo/common/config/WebConfig.java +++ b/backend/src/main/java/site/coduo/common/config/web/WebMvcConfig.java @@ -1,4 +1,4 @@ -package site.coduo.common.config; +package site.coduo.common.config.web; import org.springframework.context.annotation.Configuration; import org.springframework.web.bind.annotation.RestController; @@ -11,7 +11,7 @@ @RequiredArgsConstructor @Configuration -public class WebConfig implements WebMvcConfigurer { +public class WebMvcConfig implements WebMvcConfigurer { @Override public void configurePathMatch(final PathMatchConfigurer configurer) { @@ -21,8 +21,9 @@ public void configurePathMatch(final PathMatchConfigurer configurer) { @Override public void addCorsMappings(final CorsRegistry registry) { registry.addMapping("/**") - .allowedMethods("*") - .allowedOrigins("http://localhost:3000", "https://coduo.site", "http://coduo.site:443") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "HEAD") + .allowedOrigins("http://localhost:3000", "https://coduo.site", "https://test.coduo.site", + "https://api-test.coduo.site", "https://api.coduo.site") .allowCredentials(true); } } diff --git a/backend/src/main/java/site/coduo/common/config/web/filter/AccessTokenCookieFilter.java b/backend/src/main/java/site/coduo/common/config/web/filter/AccessTokenCookieFilter.java new file mode 100644 index 00000000..8a88de63 --- /dev/null +++ b/backend/src/main/java/site/coduo/common/config/web/filter/AccessTokenCookieFilter.java @@ -0,0 +1,56 @@ +package site.coduo.common.config.web.filter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; + +import lombok.RequiredArgsConstructor; +import site.coduo.member.exception.AuthenticationException; +import site.coduo.member.infrastructure.security.JwtProvider; + +@RequiredArgsConstructor +public class AccessTokenCookieFilter implements Filter { + + public static final String TEMPORARY_ACCESS_TOKEN_COOKIE_NAME = "temp_access"; + + private final JwtProvider jwtProvider; + + @Override + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) + throws IOException, ServletException { + final HttpServletRequest httpRequest = (HttpServletRequest) request; + if (httpRequest.getMethod().equalsIgnoreCase("OPTIONS")) { + chain.doFilter(request, response); + return; + } + validate(parseSignInCookie(httpRequest)); + chain.doFilter(request, response); + } + + private Cookie parseSignInCookie(final HttpServletRequest request) { + final Cookie[] cookies = request.getCookies(); + if (Objects.isNull(cookies)) { + throw new AuthenticationException("μΏ ν‚€ 값이 λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€."); + } + + return Arrays.stream(cookies) + .filter(cookie -> cookie.getName().equals(TEMPORARY_ACCESS_TOKEN_COOKIE_NAME)) + .findAny() + .orElseThrow(() -> new AuthenticationException("μž„μ‹œ μ—‘μ„ΈμŠ€ 토큰 μΏ ν‚€λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")); + } + + private void validate(final Cookie cookie) { + if (jwtProvider.isValid(cookie.getValue())) { + return; + } + throw new AuthenticationException("μž„μ‹œ μ—‘μ„ΈμŠ€ 토큰 μΏ ν‚€ 값이 μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + } +} diff --git a/backend/src/main/java/site/coduo/common/config/web/filter/AccessTokenSessionFilter.java b/backend/src/main/java/site/coduo/common/config/web/filter/AccessTokenSessionFilter.java new file mode 100644 index 00000000..cb5c9fce --- /dev/null +++ b/backend/src/main/java/site/coduo/common/config/web/filter/AccessTokenSessionFilter.java @@ -0,0 +1,63 @@ +package site.coduo.common.config.web.filter; + +import java.io.IOException; +import java.util.Objects; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import org.springframework.http.HttpHeaders; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import site.coduo.member.exception.AuthenticationException; + +@Slf4j +@RequiredArgsConstructor +public class AccessTokenSessionFilter implements SessionFilter { + + public static final String ACCESS_TOKEN_SESSION_NAME = "access token"; + + @Override + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) + throws IOException, ServletException { + template((HttpServletRequest) request, (HttpServletResponse) response); + chain.doFilter(request, response); + } + + @Override + public String getStoreSession(final HttpServletRequest request) { + final HttpSession session = request.getSession(); + final String sessionState = (String) session.getAttribute(ACCESS_TOKEN_SESSION_NAME); + if (Objects.isNull(sessionState)) { + throw new AuthenticationException("μ„Έμ…˜μ—μ„œ Access token의 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + return sessionState; + } + + @Override + public void removeSession(final HttpServletRequest request, final HttpServletResponse response) { + final String setCookie = response.getHeader(HttpHeaders.SET_COOKIE); + final HttpSession session = request.getSession(); + if (hasSignInCookie(setCookie)) { + session.invalidate(); + } + } + + private boolean hasSignInCookie(final String setCookie) { + return !Objects.isNull(setCookie) && !setCookie.isBlank() && setCookie.startsWith( + SignInCookieFilter.SIGN_IN_COOKIE_NAME); + } + + @Override + public void validate(final HttpServletRequest request, final String storeSession) { + if (storeSession.isBlank()) { + throw new AuthenticationException("μ„Έμ…˜μ˜ Access token이 λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€."); + } + } +} diff --git a/backend/src/main/java/site/coduo/common/config/web/filter/AuthFailHandlerFilter.java b/backend/src/main/java/site/coduo/common/config/web/filter/AuthFailHandlerFilter.java new file mode 100644 index 00000000..c7984134 --- /dev/null +++ b/backend/src/main/java/site/coduo/common/config/web/filter/AuthFailHandlerFilter.java @@ -0,0 +1,41 @@ +package site.coduo.common.config.web.filter; + +import java.io.IOException; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.web.filter.OncePerRequestFilter; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import site.coduo.common.controller.response.ApiErrorResponse; +import site.coduo.member.controller.error.MemberApiError; +import site.coduo.member.exception.AuthenticationException; + +@Slf4j +@RequiredArgsConstructor +public class AuthFailHandlerFilter extends OncePerRequestFilter { + + private final ObjectMapper objectMapper; + + @Override + protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, + final FilterChain filterChain) throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (final AuthenticationException e) { + log.warn(e.getMessage()); + + response.setStatus(MemberApiError.AUTHENTICATION_ERROR.getHttpStatus().value()); + response.setContentType("application/json;charset=utf-8"); + response.getWriter() + .write(objectMapper.writeValueAsString( + new ApiErrorResponse(MemberApiError.AUTHENTICATION_ERROR.getMessage()))); + } + } +} diff --git a/backend/src/main/java/site/coduo/common/config/web/filter/SessionFilter.java b/backend/src/main/java/site/coduo/common/config/web/filter/SessionFilter.java new file mode 100644 index 00000000..b629e24f --- /dev/null +++ b/backend/src/main/java/site/coduo/common/config/web/filter/SessionFilter.java @@ -0,0 +1,23 @@ +package site.coduo.common.config.web.filter; + +import jakarta.servlet.Filter; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public interface SessionFilter extends Filter { + + default void template(HttpServletRequest request, HttpServletResponse response) { + if (request.getMethod().equalsIgnoreCase("OPTIONS")) { + return; + } + final String storeSession = getStoreSession(request); + validate(request, storeSession); + removeSession(request, response); + } + + String getStoreSession(HttpServletRequest request); + + void removeSession(HttpServletRequest request, HttpServletResponse response); + + void validate(HttpServletRequest request, String storeSession); +} diff --git a/backend/src/main/java/site/coduo/common/config/web/filter/SignInCookieFilter.java b/backend/src/main/java/site/coduo/common/config/web/filter/SignInCookieFilter.java new file mode 100644 index 00000000..98367615 --- /dev/null +++ b/backend/src/main/java/site/coduo/common/config/web/filter/SignInCookieFilter.java @@ -0,0 +1,56 @@ +package site.coduo.common.config.web.filter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; + +import lombok.RequiredArgsConstructor; +import site.coduo.member.exception.AuthenticationException; +import site.coduo.member.infrastructure.security.JwtProvider; + +@RequiredArgsConstructor +public class SignInCookieFilter implements Filter { + + public static final String SIGN_IN_COOKIE_NAME = "coduo_whoami"; + + private final JwtProvider jwtProvider; + + @Override + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) + throws IOException, ServletException { + final HttpServletRequest httpRequest = (HttpServletRequest) request; + if (httpRequest.getMethod().equalsIgnoreCase("OPTIONS")) { + chain.doFilter(request, response); + return; + } + validate(parseSignInCookie(httpRequest)); + chain.doFilter(request, response); + } + + private Cookie parseSignInCookie(final HttpServletRequest request) { + final Cookie[] cookies = request.getCookies(); + if (Objects.isNull(cookies)) { + throw new AuthenticationException("μΏ ν‚€ 값이 λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€."); + } + + return Arrays.stream(cookies) + .filter(cookie -> cookie.getName().equals(SIGN_IN_COOKIE_NAME)) + .findAny() + .orElseThrow(() -> new AuthenticationException("둜그인 μΏ ν‚€λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")); + } + + private void validate(final Cookie cookie) { + if (jwtProvider.isValid(cookie.getValue())) { + return; + } + throw new AuthenticationException("둜그인 μΏ ν‚€ 값이 μœ νš¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + } +} diff --git a/backend/src/main/java/site/coduo/common/config/web/filter/StateSessionFilter.java b/backend/src/main/java/site/coduo/common/config/web/filter/StateSessionFilter.java new file mode 100644 index 00000000..c927e5fb --- /dev/null +++ b/backend/src/main/java/site/coduo/common/config/web/filter/StateSessionFilter.java @@ -0,0 +1,64 @@ +package site.coduo.common.config.web.filter; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpSession; + +import lombok.extern.slf4j.Slf4j; +import site.coduo.member.exception.AuthenticationException; +import site.coduo.member.service.dto.oauth.State; + +@Slf4j +public class StateSessionFilter implements SessionFilter { + + public static final String STATE_SESSION_NAME = "state"; + public static final int STATE_SESSION_EXPIRE_IN_SECOND = 30; + + @Override + public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) + throws ServletException, IOException { + template((HttpServletRequest) request, (HttpServletResponse) response); + chain.doFilter(request, response); + } + + @Override + public String getStoreSession(final HttpServletRequest request) { + final HttpSession session = request.getSession(); + final String sessionState = (String) session.getAttribute(STATE_SESSION_NAME); + if (Objects.isNull(sessionState)) { + throw new AuthenticationException("μ„Έμ…˜μ—μ„œ state 정보λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + return sessionState; + } + + @Override + public void removeSession(final HttpServletRequest request, final HttpServletResponse response) { + final HttpSession session = request.getSession(); + session.removeAttribute(STATE_SESSION_NAME); + } + + @Override + public void validate(final HttpServletRequest request, final String storeSession) { + final State storedState = new State(storeSession); + final State requestedState = new State(getRequestSession(request)); + storedState.validate(requestedState); + } + + private String getRequestSession(final HttpServletRequest request) { + final String query = request.getQueryString(); + + return Arrays.stream(query.split("&")) + .filter(queryKey -> queryKey.startsWith(STATE_SESSION_NAME)) + .map(stateQuery -> stateQuery.split("=")[1]) + .findAny() + .orElseThrow(() -> new AuthenticationException("Http μš”μ²­ Query Stringμ—μ„œ stateλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")); + } +} diff --git a/backend/src/main/java/site/coduo/common/controller/CommonErrorController.java b/backend/src/main/java/site/coduo/common/controller/CommonExceptionHandler.java similarity index 62% rename from backend/src/main/java/site/coduo/common/controller/CommonErrorController.java rename to backend/src/main/java/site/coduo/common/controller/CommonExceptionHandler.java index b65e8cad..919d871a 100644 --- a/backend/src/main/java/site/coduo/common/controller/CommonErrorController.java +++ b/backend/src/main/java/site/coduo/common/controller/CommonExceptionHandler.java @@ -1,9 +1,13 @@ package site.coduo.common.controller; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import org.springframework.web.servlet.resource.NoResourceFoundException; import lombok.extern.slf4j.Slf4j; @@ -12,19 +16,27 @@ @Slf4j @RestControllerAdvice -public class CommonErrorController { - - @ExceptionHandler(NoResourceFoundException.class) - public ResponseEntity handleNoResourceFoundException(final NoResourceFoundException e) { +public class CommonExceptionHandler extends ResponseEntityExceptionHandler { + + @Override + protected ResponseEntity handleNoResourceFoundException( + final NoResourceFoundException e, + final HttpHeaders headers, + final HttpStatusCode status, + final WebRequest request + ) { log.warn(e.getMessage()); return ResponseEntity.status(CommonApiError.DATA_NOT_FOUND_ERROR.getHttpStatus()) .body(new ApiErrorResponse(CommonApiError.DATA_NOT_FOUND_ERROR.getMessage())); } - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleMethodArgumentNotValidException( - final MethodArgumentNotValidException e + @Override + protected ResponseEntity handleMethodArgumentNotValid( + final MethodArgumentNotValidException e, + final HttpHeaders headers, + final HttpStatusCode status, + final WebRequest request ) { log.warn(e.getMessage()); diff --git a/backend/src/main/java/site/coduo/member/client/GithubApiClient.java b/backend/src/main/java/site/coduo/member/client/GithubApiClient.java index 1cfe7d91..354ffb38 100644 --- a/backend/src/main/java/site/coduo/member/client/GithubApiClient.java +++ b/backend/src/main/java/site/coduo/member/client/GithubApiClient.java @@ -1,13 +1,18 @@ package site.coduo.member.client; +import org.springframework.http.HttpStatusCode; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.ResponseSpec.ErrorHandler; +import lombok.extern.slf4j.Slf4j; import site.coduo.member.client.dto.GithubUserRequest; import site.coduo.member.client.dto.GithubUserResponse; +import site.coduo.member.exception.ExternalApiCallException; +@Slf4j @Component public class GithubApiClient { @@ -28,6 +33,10 @@ public GithubApiClient() { .build(); } + GithubApiClient(final RestClient restClient) { + this.client = restClient; + } + public GithubUserResponse getUser(final GithubUserRequest request) { return client.get() @@ -35,6 +44,18 @@ public GithubUserResponse getUser(final GithubUserRequest request) { .accept() .headers(httpHeaders -> httpHeaders.addAll(request.getHeaders())) .retrieve() + .onStatus(HttpStatusCode::isError, getErrorHandler()) .body(GithubUserResponse.class); } + + private ErrorHandler getErrorHandler() { + return (request, response) -> { + if (response.getStatusCode().is4xxClientError()) { + throw new ExternalApiCallException("Github API ν˜ΈμΆœμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); + } + if (response.getStatusCode().is5xxServerError()) { + throw new ExternalApiCallException("Github API 호좜 κ³Όμ • 쀑 μ„œλ²„ λ‚΄λΆ€μ—μ„œ 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."); + } + }; + } } diff --git a/backend/src/main/java/site/coduo/member/client/GithubOAuthClient.java b/backend/src/main/java/site/coduo/member/client/GithubOAuthClient.java index 2b8e6d03..985ac3f0 100644 --- a/backend/src/main/java/site/coduo/member/client/GithubOAuthClient.java +++ b/backend/src/main/java/site/coduo/member/client/GithubOAuthClient.java @@ -2,14 +2,17 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.ResponseSpec.ErrorHandler; import site.coduo.member.client.dto.TokenRequest; import site.coduo.member.client.dto.TokenResponse; +import site.coduo.member.exception.ExternalApiCallException; import site.coduo.member.infrastructure.http.Basic; @Component @@ -50,6 +53,7 @@ public TokenResponse grant(final TokenRequest request) { .header(HttpHeaders.AUTHORIZATION, new Basic(oAuthClientId, oAuthClientSecret).getValue()) .body(request.toQueryParams()) .retrieve() + .onStatus(HttpStatusCode::isError, getErrorHandler()) .body(TokenResponse.class); } @@ -60,4 +64,15 @@ public String getOAuthClientId() { public String getOAuthRedirectUri() { return oAuthRedirectUri; } + + private ErrorHandler getErrorHandler() { + return (request, response) -> { + if (response.getStatusCode().is4xxClientError()) { + throw new ExternalApiCallException("Github OAuth API ν˜ΈμΆœμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); + } + if (response.getStatusCode().is5xxServerError()) { + throw new ExternalApiCallException("Github OAuth κ³Όμ • 쀑 μ„œλ²„ λ‚΄λΆ€μ—μ„œ 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€."); + } + }; + } } diff --git a/backend/src/main/java/site/coduo/member/controller/AuthController.java b/backend/src/main/java/site/coduo/member/controller/AuthController.java index 11431687..3451221e 100644 --- a/backend/src/main/java/site/coduo/member/controller/AuthController.java +++ b/backend/src/main/java/site/coduo/member/controller/AuthController.java @@ -1,6 +1,7 @@ package site.coduo.member.controller; -import static site.coduo.member.controller.GithubOAuthController.ACCESS_TOKEN_SESSION_NAME; +import static site.coduo.common.config.web.filter.AccessTokenCookieFilter.TEMPORARY_ACCESS_TOKEN_COOKIE_NAME; +import static site.coduo.common.config.web.filter.SignInCookieFilter.SIGN_IN_COOKIE_NAME; import java.net.URI; @@ -13,40 +14,39 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.bind.annotation.SessionAttribute; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import site.coduo.member.controller.dto.auth.SignInCheckResponse; -import site.coduo.member.controller.dto.auth.SignInCookie; -import site.coduo.member.controller.dto.auth.SignInWebResponse; -import site.coduo.member.controller.dto.auth.SignUpRequest; +import site.coduo.member.controller.docs.AuthControllerDocs; import site.coduo.member.service.AuthService; import site.coduo.member.service.MemberService; import site.coduo.member.service.dto.SignInServiceResponse; +import site.coduo.member.service.dto.auth.SignInCheckResponse; +import site.coduo.member.service.dto.auth.SignInCookie; +import site.coduo.member.service.dto.auth.SignInWebResponse; +import site.coduo.member.service.dto.auth.SignUpRequest; -@Slf4j @RequiredArgsConstructor @RestController -public class AuthController { +public class AuthController implements AuthControllerDocs { + + public static final String PRODUCT_DOMAIN = ".coduo.site"; private final AuthService authService; private final MemberService memberService; @GetMapping("/sign-out") - public ResponseEntity signOut(@CookieValue(name = SignInCookie.SIGN_IN_COOKIE_NAME) String signInToken) { - final SignInCookie cookie = new SignInCookie(signInToken); - + public ResponseEntity signOut(@CookieValue(name = SIGN_IN_COOKIE_NAME) final String signInToken) { + final ResponseCookie expire = SignInCookie.expire(PRODUCT_DOMAIN); return ResponseEntity.ok() - .header(HttpHeaders.SET_COOKIE, cookie.expire().toString()) + .header(HttpHeaders.SET_COOKIE, expire.toString()) .build(); } @PostMapping("/sign-up") public ResponseEntity signUp(@RequestBody final SignUpRequest request, - @SessionAttribute(name = ACCESS_TOKEN_SESSION_NAME, required = false) final String accessToken + @CookieValue(name = TEMPORARY_ACCESS_TOKEN_COOKIE_NAME) final String encryptedAccessToken ) { - memberService.createMember(request.username(), accessToken); + memberService.createMember(request.username(), encryptedAccessToken); return ResponseEntity.status(HttpStatus.FOUND) .location(URI.create("/api/sign-in/callback")) @@ -55,19 +55,19 @@ public ResponseEntity signUp(@RequestBody final SignUpRequest request, @GetMapping("/sign-in/callback") public ResponseEntity signInCallback( - @SessionAttribute(name = ACCESS_TOKEN_SESSION_NAME, required = false) final String accessToken + @CookieValue(name = TEMPORARY_ACCESS_TOKEN_COOKIE_NAME) final String encryptedAccessToken ) { - final SignInServiceResponse serviceResponse = authService.createSignInToken(accessToken); - final ResponseCookie cookie = new SignInCookie(serviceResponse.token()).generate(); + final SignInServiceResponse serviceResponse = authService.createSignInToken(encryptedAccessToken); + final ResponseCookie signInCookie = new SignInCookie(serviceResponse.token()).generate(PRODUCT_DOMAIN); return ResponseEntity.ok() - .header(HttpHeaders.SET_COOKIE, cookie.toString()) + .header(HttpHeaders.SET_COOKIE, signInCookie.toString()) .body(SignInWebResponse.of(serviceResponse)); } @GetMapping("/sign-in/check") public ResponseEntity signInCheck( - @CookieValue(value = SignInCookie.SIGN_IN_COOKIE_NAME) final String signInToken + @CookieValue(name = SIGN_IN_COOKIE_NAME, required = false) final String signInToken ) { final boolean signedIn = authService.isSignedIn(signInToken); final SignInCheckResponse response = new SignInCheckResponse(signedIn); diff --git a/backend/src/main/java/site/coduo/member/controller/GithubOAuthController.java b/backend/src/main/java/site/coduo/member/controller/GithubOAuthController.java index c4a9b016..8319d7ce 100644 --- a/backend/src/main/java/site/coduo/member/controller/GithubOAuthController.java +++ b/backend/src/main/java/site/coduo/member/controller/GithubOAuthController.java @@ -1,66 +1,64 @@ package site.coduo.member.controller; + +import static site.coduo.common.config.web.filter.StateSessionFilter.STATE_SESSION_EXPIRE_IN_SECOND; +import static site.coduo.common.config.web.filter.StateSessionFilter.STATE_SESSION_NAME; +import static site.coduo.member.controller.AuthController.PRODUCT_DOMAIN; + import java.net.URI; import jakarta.servlet.http.HttpSession; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.bind.annotation.SessionAttribute; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import site.coduo.member.client.dto.TokenResponse; -import site.coduo.member.controller.dto.oauth.GithubAuthQuery; -import site.coduo.member.controller.dto.oauth.GithubAuthUri; -import site.coduo.member.controller.dto.oauth.GithubCallbackQuery; -import site.coduo.member.controller.dto.oauth.GithubOAuthEndpoint; -import site.coduo.member.controller.dto.oauth.State; +import site.coduo.member.controller.docs.GithubOAuthControllerDocs; import site.coduo.member.service.GithubOAuthService; +import site.coduo.member.service.dto.auth.AccessTokenCookie; +import site.coduo.member.service.dto.oauth.GithubAuthQuery; +import site.coduo.member.service.dto.oauth.GithubAuthUri; +import site.coduo.member.service.dto.oauth.GithubCallbackQuery; +import site.coduo.member.service.dto.oauth.GithubOAuthEndpoint; @Slf4j @RequiredArgsConstructor @RestController -public class GithubOAuthController { - - public static final String ACCESS_TOKEN_SESSION_NAME = "access token"; - - private static final String STATE_SESSION_NAME = "state"; - private static final int STATE_SESSION_EXPIRE_IN = 30; - private static final int ACCESS_TOKEN_EXPIRE_IN = 600; +public class GithubOAuthController implements GithubOAuthControllerDocs { private final GithubOAuthService githubOAuthService; + @Value("${front.url}") + private String frontUrl; + @GetMapping("/sign-in/oauth/github") public ResponseEntity getGithubAuthCode(final HttpSession session) { final GithubAuthQuery query = githubOAuthService.createAuthorizationContent(); final GithubAuthUri githubAuthUri = new GithubAuthUri(query); session.setAttribute(STATE_SESSION_NAME, query.state()); - session.setMaxInactiveInterval(STATE_SESSION_EXPIRE_IN); - + session.setMaxInactiveInterval(STATE_SESSION_EXPIRE_IN_SECOND); return ResponseEntity.ok() .body(new GithubOAuthEndpoint(githubAuthUri.toPlainText())); } @GetMapping("/github/callback") public ResponseEntity getAccessToken(@ModelAttribute final GithubCallbackQuery query, - @SessionAttribute(name = STATE_SESSION_NAME) final String state, final HttpSession session) { + final String encryptedAccessToken = githubOAuthService.invokeOAuthCallback(query.code()); + final AccessTokenCookie cookie = new AccessTokenCookie(encryptedAccessToken); + final ResponseCookie responseCookie = cookie.generate(PRODUCT_DOMAIN); - State savedState = new State(state); - savedState.validate(new State(query.state())); - final TokenResponse tokenResponse = githubOAuthService.invokeOAuthCallback(query.code()); - - session.removeAttribute(STATE_SESSION_NAME); - session.setAttribute(ACCESS_TOKEN_SESSION_NAME, tokenResponse.accessToken()); - session.setMaxInactiveInterval(ACCESS_TOKEN_EXPIRE_IN); - - return ResponseEntity.status(HttpStatus.FOUND) - .location(URI.create("https://coduo.site/callback")) + return ResponseEntity.status(HttpStatus.TEMPORARY_REDIRECT) + .header(HttpHeaders.SET_COOKIE, responseCookie.toString()) + .location(URI.create("https://" + frontUrl + "/callback")) .build(); } } diff --git a/backend/src/main/java/site/coduo/member/controller/MemberController.java b/backend/src/main/java/site/coduo/member/controller/MemberController.java new file mode 100644 index 00000000..e0cafb98 --- /dev/null +++ b/backend/src/main/java/site/coduo/member/controller/MemberController.java @@ -0,0 +1,36 @@ +package site.coduo.member.controller; + +import static site.coduo.common.config.web.filter.SignInCookieFilter.SIGN_IN_COOKIE_NAME; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import site.coduo.member.controller.docs.MemberControllerDocs; +import site.coduo.member.service.MemberService; +import site.coduo.member.service.dto.member.MemberReadResponse; + +@RequiredArgsConstructor +@RestController +public class MemberController implements MemberControllerDocs { + + private final MemberService memberService; + + @GetMapping("/member") + public ResponseEntity getMember(@CookieValue(SIGN_IN_COOKIE_NAME) final String token) { + final MemberReadResponse response = memberService.findMemberNameByCredential(token); + + return ResponseEntity.ok(response); + } + + @DeleteMapping("/member") + public ResponseEntity deleteMember(@CookieValue(SIGN_IN_COOKIE_NAME) final String token) { + memberService.deleteMember(token); + + return ResponseEntity.noContent() + .build(); + } +} diff --git a/backend/src/main/java/site/coduo/member/controller/MemberErrorController.java b/backend/src/main/java/site/coduo/member/controller/MemberErrorController.java deleted file mode 100644 index 777de3bf..00000000 --- a/backend/src/main/java/site/coduo/member/controller/MemberErrorController.java +++ /dev/null @@ -1,45 +0,0 @@ -package site.coduo.member.controller; - -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.ServletRequestBindingException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -import lombok.extern.slf4j.Slf4j; -import site.coduo.common.controller.response.ApiErrorResponse; -import site.coduo.member.controller.error.MemberApiError; -import site.coduo.member.exception.AuthenticationException; -import site.coduo.member.exception.AuthorizationException; - -@Slf4j -@RestControllerAdvice -@Order(Ordered.HIGHEST_PRECEDENCE) -public class MemberErrorController { - - @ExceptionHandler(ServletRequestBindingException.class) - public ResponseEntity handleServletRequestBindingException( - final ServletRequestBindingException e) { - log.warn(e.getMessage()); - - return ResponseEntity.status(MemberApiError.AUTHENTICATION_ERROR.getHttpStatus()) - .body(new ApiErrorResponse(MemberApiError.AUTHENTICATION_ERROR.getMessage())); - } - - @ExceptionHandler(AuthenticationException.class) - public ResponseEntity handleAuthenticationException(final AuthenticationException e) { - log.warn("인증 μ˜ˆμ™Έ: {}", e.getMessage()); - - return ResponseEntity.status(MemberApiError.AUTHENTICATION_ERROR.getHttpStatus()) - .body(new ApiErrorResponse(MemberApiError.AUTHENTICATION_ERROR.getMessage())); - } - - @ExceptionHandler(AuthorizationException.class) - public ResponseEntity handleAuthorizationException(final AuthorizationException e) { - log.warn("인가 μ˜ˆμ™Έ: {}", e.getMessage()); - - return ResponseEntity.status(MemberApiError.AUTHORIZATION_ERROR.getHttpStatus()) - .body(new ApiErrorResponse(MemberApiError.AUTHORIZATION_ERROR.getMessage())); - } -} diff --git a/backend/src/main/java/site/coduo/member/controller/MemberExceptionHandler.java b/backend/src/main/java/site/coduo/member/controller/MemberExceptionHandler.java new file mode 100644 index 00000000..d0a24459 --- /dev/null +++ b/backend/src/main/java/site/coduo/member/controller/MemberExceptionHandler.java @@ -0,0 +1,35 @@ +package site.coduo.member.controller; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import lombok.extern.slf4j.Slf4j; +import site.coduo.common.controller.response.ApiErrorResponse; +import site.coduo.member.controller.error.MemberApiError; +import site.coduo.member.exception.ExternalApiCallException; +import site.coduo.member.exception.MemberNotFoundException; + +@Slf4j +@RestControllerAdvice +@Order(Ordered.HIGHEST_PRECEDENCE) +public class MemberExceptionHandler { + + @ExceptionHandler(MemberNotFoundException.class) + public ResponseEntity handlerMemberNotFoundException(final MemberNotFoundException e) { + log.warn(e.getMessage()); + + return ResponseEntity.status(MemberApiError.MEMBER_NOT_FOUND_ERROR.getHttpStatus()) + .body(new ApiErrorResponse(MemberApiError.MEMBER_NOT_FOUND_ERROR.getMessage())); + } + + @ExceptionHandler(ExternalApiCallException.class) + public ResponseEntity handlerExternalApiCallFailureException(final ExternalApiCallException e) { + log.error(e.getMessage()); + + return ResponseEntity.status(MemberApiError.API_CALL_FAILURE_ERROR.getHttpStatus()) + .body(new ApiErrorResponse(MemberApiError.MEMBER_NOT_FOUND_ERROR.getMessage())); + } +} diff --git a/backend/src/main/java/site/coduo/member/controller/docs/AuthControllerDocs.java b/backend/src/main/java/site/coduo/member/controller/docs/AuthControllerDocs.java index f4f4c307..e5ce6adc 100644 --- a/backend/src/main/java/site/coduo/member/controller/docs/AuthControllerDocs.java +++ b/backend/src/main/java/site/coduo/member/controller/docs/AuthControllerDocs.java @@ -4,24 +4,35 @@ import org.springframework.http.ResponseEntity; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import site.coduo.common.controller.response.ApiErrorResponse; -import site.coduo.member.controller.dto.auth.SignInWebResponse; -import site.coduo.member.controller.dto.auth.SignUpRequest; +import site.coduo.member.service.dto.auth.SignInCheckResponse; +import site.coduo.member.service.dto.auth.SignInWebResponse; +import site.coduo.member.service.dto.auth.SignUpRequest; @Tag(name = "인증/인가 API") public interface AuthControllerDocs { - @Operation(summary = "λ‘œκ·Έμ•„μ›ƒμ„ μš”μ²­ν•œλ‹€.") - @ApiResponse(responseCode = "200", description = "둜그인 μΏ ν‚€ μ‚­μ œ") - @ApiResponse(responseCode = "403", description = "인가 μ‹€νŒ¨", + @Operation(summary = "λ‘œκ·Έμ•„μ›ƒ μš”μ²­μ„ ν•œλ‹€..") + @ApiResponse(responseCode = "200", description = "νšŒμ› 정보(μœ μ €μ΄λ¦„)을 λ“±λ‘ν•œλ‹€.", content + = @Content(schema = @Schema(contentMediaType = MediaType.APPLICATION_JSON_VALUE))) + @ApiResponse(responseCode = "401", description = "인증 μ‹€νŒ¨", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))) - ResponseEntity signOut(); + ResponseEntity signOut( + @Parameter( + in = ParameterIn.COOKIE, + name = "coduo_whoami", + description = "μ‚¬μš©μžκ°€ 인증에 μ„±κ³΅ν•˜λ©΄ μ„œλ²„μ—μ„œ λ°œκΈ‰ν•˜λŠ” μΏ ν‚€", + schema = @Schema(type = "string") + ) + String signInToken); @Operation(summary = "νšŒμ›κ°€μž… μš”μ²­ν•œλ‹€.", requestBody = @RequestBody( @@ -37,5 +48,51 @@ public interface AuthControllerDocs { @ApiResponse(responseCode = "401", description = "인증 μ‹€νŒ¨", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))) - ResponseEntity signUp(SignUpRequest request, String accessToken); + ResponseEntity signUp( + @Parameter(description = "νšŒμ› 정보", required = true) + SignUpRequest request, + + @Parameter( + in = ParameterIn.COOKIE, + name = "JSESSIONID", + description = "OAuth 인가 κ³Όμ •μ—μ„œ μ‚¬μš©μž μ„Έμ…˜μ„ μœ μ§€ν•˜κΈ° μœ„ν•œ μΏ ν‚€", + schema = @Schema(type = "string") + ) + String accessToken); + + @Operation(summary = "둜그인 μƒνƒœλ₯Ό ν™•μΈν•œλ‹€.", + responses = { + @ApiResponse(responseCode = "200", description = "둜그인 μ‹œ μ˜ˆμ‹œ", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = SignInCheckResponse.class))), + @ApiResponse(responseCode = "401", description = "인증 μ‹€νŒ¨", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ApiErrorResponse.class))) + } + ) + ResponseEntity signInCheck( + @Parameter( + in = ParameterIn.COOKIE, + name = "coduo_whoami", + description = "μ‚¬μš©μžκ°€ 인증에 μ„±κ³΅ν•˜λ©΄ μ„œλ²„μ—μ„œ λ°œκΈ‰ν•˜λŠ” μΏ ν‚€", + schema = @Schema(type = "string") + ) + String signInToken); + + @Operation(summary = "둜그인 인증 μš”μ²­ 콜백", + responses = { + @ApiResponse(responseCode = "200", description = "인증에 μ„±κ³΅ν•˜λ©΄ μΏ ν‚€λ₯Ό λ°œκΈ‰ν•œλ‹€.", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = SignInWebResponse.class))), + @ApiResponse(responseCode = "401", description = "인증 μ‹€νŒ¨", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ApiErrorResponse.class))) + } + ) + ResponseEntity signInCallback( + @Parameter( + in = ParameterIn.COOKIE, + name = "JSESSIONID", + description = "OAuth 인가 κ³Όμ •μ—μ„œ μ‚¬μš©μž μ„Έμ…˜μ„ μœ μ§€ν•˜κΈ° μœ„ν•œ μΏ ν‚€", + schema = @Schema(type = "string") + ) + String accessToken); } diff --git a/backend/src/main/java/site/coduo/member/controller/docs/GithubOAuthControllerDocs.java b/backend/src/main/java/site/coduo/member/controller/docs/GithubOAuthControllerDocs.java index dede598c..b3d1a69a 100644 --- a/backend/src/main/java/site/coduo/member/controller/docs/GithubOAuthControllerDocs.java +++ b/backend/src/main/java/site/coduo/member/controller/docs/GithubOAuthControllerDocs.java @@ -5,20 +5,41 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import site.coduo.common.controller.response.ApiErrorResponse; -import site.coduo.member.controller.dto.oauth.GithubOAuthEndpoint; +import site.coduo.member.service.dto.oauth.GithubCallbackQuery; +import site.coduo.member.service.dto.oauth.GithubOAuthEndpoint; -@Tag(name = "인증/인가 API") +@Tag(name = "κΉƒν—ˆλΈŒ OAuth API") public interface GithubOAuthControllerDocs { - @ApiResponse(responseCode = "302", description = "νšŒμ› 정보(μœ μ €μ΄λ¦„)을 λ“±λ‘ν•œλ‹€.", content - = @Content(schema = @Schema(contentMediaType = MediaType.APPLICATION_JSON_VALUE))) + @Operation(summary = "κΉƒν—ˆλΈŒ μΈκ°€μ—”λ“œ 포인트 URI 호좜", + responses = { + @ApiResponse(responseCode = "200", description = "κΉƒν—ˆλΈŒ μΈ‘ 인가 μ—”λ“œν¬μΈνŠΈ 및 κ΄€λ ¨ Query 담은 URI", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = GithubOAuthEndpoint.class))), + @ApiResponse(responseCode = "401", description = "인증 μ‹€νŒ¨", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ApiErrorResponse.class))) + } + ) + ResponseEntity getGithubAuthCode(@Parameter(hidden = true) HttpSession session); + + @ApiResponse(responseCode = "200") @ApiResponse(responseCode = "401", description = "인증 μ‹€νŒ¨", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))) - ResponseEntity getGithubAuthCode(HttpSession session); + ResponseEntity getAccessToken( + GithubCallbackQuery query, + @Parameter( + in = ParameterIn.COOKIE, + name = "JSESSIONID", + description = "OAuth 인가 κ³Όμ •μ—μ„œ μ‚¬μš©μž μ„Έμ…˜μ„ μœ μ§€ν•˜κΈ° μœ„ν•œ μΏ ν‚€", + schema = @Schema(type = "string") + ) HttpSession session); } diff --git a/backend/src/main/java/site/coduo/member/controller/docs/MemberControllerDocs.java b/backend/src/main/java/site/coduo/member/controller/docs/MemberControllerDocs.java new file mode 100644 index 00000000..de75df8f --- /dev/null +++ b/backend/src/main/java/site/coduo/member/controller/docs/MemberControllerDocs.java @@ -0,0 +1,42 @@ +package site.coduo.member.controller.docs; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; + +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import site.coduo.common.controller.response.ApiErrorResponse; +import site.coduo.member.service.dto.member.MemberReadResponse; + +@Tag(name = "νšŒμ› API") +public interface MemberControllerDocs { + + @ApiResponse(responseCode = "200", description = "νšŒμ› 등둝 정보λ₯Ό μ‘°νšŒν•œλ‹€.", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = MemberReadResponse.class))) + @ApiResponse(responseCode = "401", description = "인증 μ‹€νŒ¨", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = ApiErrorResponse.class))) + ResponseEntity getMember( + @Parameter( + in = ParameterIn.COOKIE, + name = "coduo_whoami", + description = "μ‚¬μš©μžκ°€ 인증에 μ„±κ³΅ν•˜λ©΄ μ„œλ²„μ—μ„œ λ°œκΈ‰ν•˜λŠ” μΏ ν‚€", + schema = @Schema(type = "string") + ) + String token); + + @ApiResponse(responseCode = "204", description = "νšŒμ› 정보λ₯Ό μ‚­μ œν•œλ‹€.", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE)) + ResponseEntity deleteMember( + @Parameter( + in = ParameterIn.COOKIE, + name = "coduo_whoami", + description = "μ‚¬μš©μžκ°€ 인증에 μ„±κ³΅ν•˜λ©΄ μ„œλ²„μ—μ„œ λ°œκΈ‰ν•˜λŠ” μΏ ν‚€", + schema = @Schema(type = "string") + ) String token); +} diff --git a/backend/src/main/java/site/coduo/member/controller/dto/auth/SignInCheckResponse.java b/backend/src/main/java/site/coduo/member/controller/dto/auth/SignInCheckResponse.java deleted file mode 100644 index cb9dc1dc..00000000 --- a/backend/src/main/java/site/coduo/member/controller/dto/auth/SignInCheckResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package site.coduo.member.controller.dto.auth; - -public record SignInCheckResponse(boolean signedIn) { -} diff --git a/backend/src/main/java/site/coduo/member/controller/dto/auth/SignUpRequest.java b/backend/src/main/java/site/coduo/member/controller/dto/auth/SignUpRequest.java deleted file mode 100644 index 62552895..00000000 --- a/backend/src/main/java/site/coduo/member/controller/dto/auth/SignUpRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package site.coduo.member.controller.dto.auth; - -public record SignUpRequest(String username) { -} diff --git a/backend/src/main/java/site/coduo/member/controller/dto/oauth/GithubOAuthEndpoint.java b/backend/src/main/java/site/coduo/member/controller/dto/oauth/GithubOAuthEndpoint.java deleted file mode 100644 index d8fee0b6..00000000 --- a/backend/src/main/java/site/coduo/member/controller/dto/oauth/GithubOAuthEndpoint.java +++ /dev/null @@ -1,4 +0,0 @@ -package site.coduo.member.controller.dto.oauth; - -public record GithubOAuthEndpoint(String endpoint) { -} diff --git a/backend/src/main/java/site/coduo/member/controller/error/MemberApiError.java b/backend/src/main/java/site/coduo/member/controller/error/MemberApiError.java index 59b72b35..8e0cedc2 100644 --- a/backend/src/main/java/site/coduo/member/controller/error/MemberApiError.java +++ b/backend/src/main/java/site/coduo/member/controller/error/MemberApiError.java @@ -8,8 +8,10 @@ @Getter @RequiredArgsConstructor public enum MemberApiError { + AUTHENTICATION_ERROR(HttpStatus.UNAUTHORIZED, "μΈμ¦λ˜μ§€ μ•Šμ€ μ ‘κ·Όμž…λ‹ˆλ‹€."), - AUTHORIZATION_ERROR(HttpStatus.FORBIDDEN, "κΆŒν•œ λ°– μ ‘κ·Όμž…λ‹ˆλ‹€."); + MEMBER_NOT_FOUND_ERROR(HttpStatus.NOT_FOUND, "μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” νšŒμ›μž…λ‹ˆλ‹€."), + API_CALL_FAILURE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "μ™ΈλΆ€ API와 μƒν˜Έμž‘μš© 쀑 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); private final HttpStatus httpStatus; private final String message; diff --git a/backend/src/main/java/site/coduo/member/domain/Member.java b/backend/src/main/java/site/coduo/member/domain/Member.java index 3e77188b..81bbb23a 100644 --- a/backend/src/main/java/site/coduo/member/domain/Member.java +++ b/backend/src/main/java/site/coduo/member/domain/Member.java @@ -1,5 +1,6 @@ package site.coduo.member.domain; +import java.time.LocalDateTime; import java.util.Objects; import jakarta.persistence.Column; @@ -9,6 +10,8 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; +import org.springframework.format.annotation.DateTimeFormat; + import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; @@ -26,13 +29,13 @@ public class Member extends BaseTimeEntity { @Column(name = "ID", nullable = false) private Long id; - @Column(name = "ACCESS_TOKEN", nullable = false) + @Column(name = "ACCESS_TOKEN", nullable = false, unique = true) private String accessToken; @Column(name = "PROVIDER_LOGIN_ID", nullable = false) private String loginId; - @Column(name = "PROVIDER_USER_ID", nullable = false) + @Column(name = "PROVIDER_USER_ID", nullable = false, unique = true) private String userId; @Column(name = "PROFILE_IMAGE") @@ -41,14 +44,19 @@ public class Member extends BaseTimeEntity { @Column(name = "USER_NAME") private String username; + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) + @Column(name = "DELETED_AT") + private LocalDateTime deletedAt; + @Builder private Member(final String accessToken, final String loginId, final String userId, final String profileImage, - final String username) { + final String username, final LocalDateTime deletedAt) { this.accessToken = accessToken; this.loginId = loginId; this.userId = userId; this.profileImage = profileImage; this.username = username; + this.deletedAt = deletedAt; } public void update(final Member other) { @@ -57,6 +65,15 @@ public void update(final Member other) { this.userId = other.userId; this.profileImage = other.profileImage; this.username = other.username; + this.deletedAt = other.deletedAt; + } + + public void delete() { + this.deletedAt = LocalDateTime.now(); + } + + public boolean isDeleted() { + return deletedAt != null; } @Override @@ -76,4 +93,3 @@ public int hashCode() { return Objects.hash(id); } } - diff --git a/backend/src/main/java/site/coduo/member/domain/repository/MemberRepository.java b/backend/src/main/java/site/coduo/member/domain/repository/MemberRepository.java index 0e1f72ea..af500af5 100644 --- a/backend/src/main/java/site/coduo/member/domain/repository/MemberRepository.java +++ b/backend/src/main/java/site/coduo/member/domain/repository/MemberRepository.java @@ -1,15 +1,33 @@ package site.coduo.member.domain.repository; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import site.coduo.member.domain.Member; +import site.coduo.member.exception.MemberNotFoundException; public interface MemberRepository extends JpaRepository { Optional findByUserId(String userId); + List findByDeletedAtIsNull(); + + @Override + default List findAll() { + return findByDeletedAtIsNull(); + } + + default Member fetchByUserId(final String userId) { + final Member member = findByUserId(userId) + .orElseThrow(() -> new MemberNotFoundException(String.format("%sλŠ” 찾을 수 μ—†λŠ” νšŒμ› μ•„μ΄λ””μž…λ‹ˆλ‹€.", userId))); + if (member.isDeleted()) { + throw new MemberNotFoundException(String.format("%sλŠ” μ‚­μ œλœ νšŒμ›μž…λ‹ˆλ‹€.", userId)); + } + return member; + } + boolean existsByUserId(String userId); } diff --git a/backend/src/main/java/site/coduo/member/exception/AuthorizationException.java b/backend/src/main/java/site/coduo/member/exception/AuthorizationException.java deleted file mode 100644 index c89da0a5..00000000 --- a/backend/src/main/java/site/coduo/member/exception/AuthorizationException.java +++ /dev/null @@ -1,8 +0,0 @@ -package site.coduo.member.exception; - -public class AuthorizationException extends MemberException { - - public AuthorizationException(final String message) { - super(message); - } -} diff --git a/backend/src/main/java/site/coduo/member/exception/ExternalApiCallException.java b/backend/src/main/java/site/coduo/member/exception/ExternalApiCallException.java new file mode 100644 index 00000000..2a8554c7 --- /dev/null +++ b/backend/src/main/java/site/coduo/member/exception/ExternalApiCallException.java @@ -0,0 +1,8 @@ +package site.coduo.member.exception; + +public class ExternalApiCallException extends MemberException { + + public ExternalApiCallException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/member/exception/MemberNotFoundException.java b/backend/src/main/java/site/coduo/member/exception/MemberNotFoundException.java new file mode 100644 index 00000000..b6304292 --- /dev/null +++ b/backend/src/main/java/site/coduo/member/exception/MemberNotFoundException.java @@ -0,0 +1,8 @@ +package site.coduo.member.exception; + +public class MemberNotFoundException extends MemberException { + + public MemberNotFoundException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/member/infrastructure/security/JwtProvider.java b/backend/src/main/java/site/coduo/member/infrastructure/security/JwtProvider.java index 68703203..74de0bbc 100644 --- a/backend/src/main/java/site/coduo/member/infrastructure/security/JwtProvider.java +++ b/backend/src/main/java/site/coduo/member/infrastructure/security/JwtProvider.java @@ -5,7 +5,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; @@ -36,7 +35,7 @@ public boolean isValid(final String token) { try { verify(token); return true; - } catch (final JwtException e) { + } catch (final Exception e) { return false; } } diff --git a/backend/src/main/java/site/coduo/member/infrastructure/security/NanceProvider.java b/backend/src/main/java/site/coduo/member/infrastructure/security/NonceProvider.java similarity index 70% rename from backend/src/main/java/site/coduo/member/infrastructure/security/NanceProvider.java rename to backend/src/main/java/site/coduo/member/infrastructure/security/NonceProvider.java index a8ea1e7d..52c9d9f9 100644 --- a/backend/src/main/java/site/coduo/member/infrastructure/security/NanceProvider.java +++ b/backend/src/main/java/site/coduo/member/infrastructure/security/NonceProvider.java @@ -1,6 +1,6 @@ package site.coduo.member.infrastructure.security; -public interface NanceProvider { +public interface NonceProvider { String generate(); } diff --git a/backend/src/main/java/site/coduo/member/infrastructure/security/UUIDNanceProvider.java b/backend/src/main/java/site/coduo/member/infrastructure/security/UUIDNonceProvider.java similarity index 80% rename from backend/src/main/java/site/coduo/member/infrastructure/security/UUIDNanceProvider.java rename to backend/src/main/java/site/coduo/member/infrastructure/security/UUIDNonceProvider.java index 6f08ff79..5d591799 100644 --- a/backend/src/main/java/site/coduo/member/infrastructure/security/UUIDNanceProvider.java +++ b/backend/src/main/java/site/coduo/member/infrastructure/security/UUIDNonceProvider.java @@ -5,7 +5,7 @@ import org.springframework.stereotype.Component; @Component -public class UUIDNanceProvider implements NanceProvider { +public class UUIDNonceProvider implements NonceProvider { @Override public String generate() { diff --git a/backend/src/main/java/site/coduo/member/service/AuthService.java b/backend/src/main/java/site/coduo/member/service/AuthService.java index b371b19c..176acecb 100644 --- a/backend/src/main/java/site/coduo/member/service/AuthService.java +++ b/backend/src/main/java/site/coduo/member/service/AuthService.java @@ -22,18 +22,21 @@ public class AuthService { private final JwtProvider jwtProvider; @Transactional - public SignInServiceResponse createSignInToken(final String accessToken) { + public SignInServiceResponse createSignInToken(final String encryptedAccessToken) { + final String accessToken = jwtProvider.extractSubject(encryptedAccessToken); final GithubUserResponse userResponse = githubApiClient.getUser(new GithubUserRequest(accessToken)); memberRepository.findByUserId(userResponse.userId()) .ifPresent(member -> new MemberUpdate(member).update(accessToken)); - final String signInToken = jwtProvider.sign(userResponse.userId()); return new SignInServiceResponse(memberRepository.existsByUserId(userResponse.userId()), signInToken); } public boolean isSignedIn(final String signInToken) { + if (signInToken == null) { + return false; + } return jwtProvider.isValid(signInToken); } } diff --git a/backend/src/main/java/site/coduo/member/service/GithubOAuthService.java b/backend/src/main/java/site/coduo/member/service/GithubOAuthService.java index 55b39922..30ffda46 100644 --- a/backend/src/main/java/site/coduo/member/service/GithubOAuthService.java +++ b/backend/src/main/java/site/coduo/member/service/GithubOAuthService.java @@ -7,8 +7,9 @@ import site.coduo.member.client.GithubOAuthClient; import site.coduo.member.client.dto.TokenRequest; import site.coduo.member.client.dto.TokenResponse; -import site.coduo.member.controller.dto.oauth.GithubAuthQuery; -import site.coduo.member.infrastructure.security.NanceProvider; +import site.coduo.member.infrastructure.security.JwtProvider; +import site.coduo.member.infrastructure.security.NonceProvider; +import site.coduo.member.service.dto.oauth.GithubAuthQuery; @RequiredArgsConstructor @Transactional(readOnly = true) @@ -16,19 +17,21 @@ public class GithubOAuthService { private final GithubOAuthClient oAuthClient; - private final NanceProvider nanceProvider; + private final NonceProvider nonceProvider; + private final JwtProvider jwtProvider; public GithubAuthQuery createAuthorizationContent() { return new GithubAuthQuery( oAuthClient.getOAuthClientId(), oAuthClient.getOAuthRedirectUri(), - nanceProvider.generate() + nonceProvider.generate() ); } - public TokenResponse invokeOAuthCallback(final String code) { + public String invokeOAuthCallback(final String code) { String redirectUri = oAuthClient.getOAuthRedirectUri(); - return oAuthClient.grant(new TokenRequest(code, redirectUri)); + final TokenResponse tokenResponse = oAuthClient.grant(new TokenRequest(code, redirectUri)); + return jwtProvider.sign(tokenResponse.accessToken()); } } diff --git a/backend/src/main/java/site/coduo/member/service/MemberService.java b/backend/src/main/java/site/coduo/member/service/MemberService.java index 0a9517d2..f56b345b 100644 --- a/backend/src/main/java/site/coduo/member/service/MemberService.java +++ b/backend/src/main/java/site/coduo/member/service/MemberService.java @@ -11,6 +11,8 @@ import site.coduo.member.domain.Member; import site.coduo.member.domain.repository.MemberRepository; import site.coduo.member.infrastructure.http.Bearer; +import site.coduo.member.infrastructure.security.JwtProvider; +import site.coduo.member.service.dto.member.MemberReadResponse; @Slf4j @RequiredArgsConstructor @@ -20,13 +22,35 @@ public class MemberService { private final MemberRepository memberRepository; private final GithubApiClient githubClient; + private final JwtProvider jwtProvider; @Transactional - public void createMember(final String username, final String accessToken) { + public void createMember(final String username, final String encryptedAccessToken) { + final String accessToken = jwtProvider.extractSubject(encryptedAccessToken); final Bearer bearer = new Bearer(accessToken); final GithubUserResponse userResponse = githubClient.getUser(new GithubUserRequest(bearer)); final Member member = userResponse.toDomain(bearer, username); - memberRepository.save(member); } + + public MemberReadResponse findMemberNameByCredential(final String token) { + final String userId = jwtProvider.extractSubject(token); + final Member member = memberRepository.fetchByUserId(userId); + + return new MemberReadResponse(member.getUsername()); + } + + public Member findMemberByCredential(final String token) { + final String userId = jwtProvider.extractSubject(token); + + return memberRepository.fetchByUserId(userId); + } + + @Transactional + public void deleteMember(final String token) { + final String userId = jwtProvider.extractSubject(token); + final Member member = memberRepository.fetchByUserId(userId); + + member.delete(); + } } diff --git a/backend/src/main/java/site/coduo/member/service/dto/auth/AccessTokenCookie.java b/backend/src/main/java/site/coduo/member/service/dto/auth/AccessTokenCookie.java new file mode 100644 index 00000000..f3313c99 --- /dev/null +++ b/backend/src/main/java/site/coduo/member/service/dto/auth/AccessTokenCookie.java @@ -0,0 +1,29 @@ +package site.coduo.member.service.dto.auth; + +import static site.coduo.common.config.web.filter.AccessTokenCookieFilter.TEMPORARY_ACCESS_TOKEN_COOKIE_NAME; + +import java.time.Duration; + +import org.springframework.http.ResponseCookie; + +public record AccessTokenCookie(String accessToken) { + + public static ResponseCookie expire(final String domain) { + return ResponseCookie.from(TEMPORARY_ACCESS_TOKEN_COOKIE_NAME) + .maxAge(Duration.ZERO) + .domain(domain) + .path("/") + .build(); + } + + public ResponseCookie generate(final String domain) { + return ResponseCookie.from(TEMPORARY_ACCESS_TOKEN_COOKIE_NAME) + .value(accessToken) + .maxAge(Duration.ofMinutes(10)) + .httpOnly(true) + .secure(true) + .domain(domain) + .path("/") + .build(); + } +} diff --git a/backend/src/main/java/site/coduo/member/service/dto/auth/SignInCheckResponse.java b/backend/src/main/java/site/coduo/member/service/dto/auth/SignInCheckResponse.java new file mode 100644 index 00000000..93955f46 --- /dev/null +++ b/backend/src/main/java/site/coduo/member/service/dto/auth/SignInCheckResponse.java @@ -0,0 +1,7 @@ +package site.coduo.member.service.dto.auth; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "둜그인 μ—¬λΆ€ 확인") +public record SignInCheckResponse(@Schema(description = "둜그인 μ—¬λΆ€", example = "true") boolean signedIn) { +} diff --git a/backend/src/main/java/site/coduo/member/controller/dto/auth/SignInCookie.java b/backend/src/main/java/site/coduo/member/service/dto/auth/SignInCookie.java similarity index 58% rename from backend/src/main/java/site/coduo/member/controller/dto/auth/SignInCookie.java rename to backend/src/main/java/site/coduo/member/service/dto/auth/SignInCookie.java index 3cb9aa2d..099076c2 100644 --- a/backend/src/main/java/site/coduo/member/controller/dto/auth/SignInCookie.java +++ b/backend/src/main/java/site/coduo/member/service/dto/auth/SignInCookie.java @@ -1,4 +1,6 @@ -package site.coduo.member.controller.dto.auth; +package site.coduo.member.service.dto.auth; + +import static site.coduo.common.config.web.filter.SignInCookieFilter.SIGN_IN_COOKIE_NAME; import java.time.Duration; @@ -6,23 +8,20 @@ public record SignInCookie(String credential) { - public static final String SIGN_IN_COOKIE_NAME = "coduo_whoami"; - private static final String SERVICE_DOMAIN_NAME = "coduo.site"; - - public ResponseCookie generate() { + public static ResponseCookie expire(final String domain) { return ResponseCookie.from(SIGN_IN_COOKIE_NAME) - .value(credential) - .httpOnly(true) - .secure(true) - .domain(SERVICE_DOMAIN_NAME) + .maxAge(Duration.ZERO) + .domain(domain) .path("/") .build(); } - public ResponseCookie expire() { + public ResponseCookie generate(final String domain) { return ResponseCookie.from(SIGN_IN_COOKIE_NAME) - .maxAge(Duration.ZERO) - .domain(SERVICE_DOMAIN_NAME) + .value(credential) + .httpOnly(true) + .secure(true) + .domain(domain) .path("/") .build(); } diff --git a/backend/src/main/java/site/coduo/member/controller/dto/auth/SignInWebResponse.java b/backend/src/main/java/site/coduo/member/service/dto/auth/SignInWebResponse.java similarity index 79% rename from backend/src/main/java/site/coduo/member/controller/dto/auth/SignInWebResponse.java rename to backend/src/main/java/site/coduo/member/service/dto/auth/SignInWebResponse.java index dc0f30b2..b130fe5a 100644 --- a/backend/src/main/java/site/coduo/member/controller/dto/auth/SignInWebResponse.java +++ b/backend/src/main/java/site/coduo/member/service/dto/auth/SignInWebResponse.java @@ -1,9 +1,9 @@ -package site.coduo.member.controller.dto.auth; +package site.coduo.member.service.dto.auth; import io.swagger.v3.oas.annotations.media.Schema; import site.coduo.member.service.dto.SignInServiceResponse; -@Schema(description = "νšŒμ›κ°€μž… μš”μ²­") +@Schema(description = "νšŒμ›λ“±λ‘ μ—¬λΆ€ 확인") public record SignInWebResponse(@Schema(description = "νšŒμ› 등둝 μ—¬λΆ€", example = "true") boolean signedUp) { public static SignInWebResponse of(final SignInServiceResponse response) { diff --git a/backend/src/main/java/site/coduo/member/service/dto/auth/SignUpRequest.java b/backend/src/main/java/site/coduo/member/service/dto/auth/SignUpRequest.java new file mode 100644 index 00000000..c621b3f9 --- /dev/null +++ b/backend/src/main/java/site/coduo/member/service/dto/auth/SignUpRequest.java @@ -0,0 +1,7 @@ +package site.coduo.member.service.dto.auth; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "νšŒμ›λ“±λ‘") +public record SignUpRequest(@Schema(description = "νšŒμ› 등둝 이름", example = "true") String username) { +} diff --git a/backend/src/main/java/site/coduo/member/service/dto/member/MemberReadResponse.java b/backend/src/main/java/site/coduo/member/service/dto/member/MemberReadResponse.java new file mode 100644 index 00000000..af5f8f73 --- /dev/null +++ b/backend/src/main/java/site/coduo/member/service/dto/member/MemberReadResponse.java @@ -0,0 +1,7 @@ +package site.coduo.member.service.dto.member; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "νšŒμ› 정보") +public record MemberReadResponse(@Schema(description = "νšŒμ› 등둝 이름", example = "fram98") String username) { +} diff --git a/backend/src/main/java/site/coduo/member/controller/dto/oauth/GithubAuthQuery.java b/backend/src/main/java/site/coduo/member/service/dto/oauth/GithubAuthQuery.java similarity index 90% rename from backend/src/main/java/site/coduo/member/controller/dto/oauth/GithubAuthQuery.java rename to backend/src/main/java/site/coduo/member/service/dto/oauth/GithubAuthQuery.java index 2fba0beb..42a90dce 100644 --- a/backend/src/main/java/site/coduo/member/controller/dto/oauth/GithubAuthQuery.java +++ b/backend/src/main/java/site/coduo/member/service/dto/oauth/GithubAuthQuery.java @@ -1,4 +1,4 @@ -package site.coduo.member.controller.dto.oauth; +package site.coduo.member.service.dto.oauth; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; diff --git a/backend/src/main/java/site/coduo/member/controller/dto/oauth/GithubAuthUri.java b/backend/src/main/java/site/coduo/member/service/dto/oauth/GithubAuthUri.java similarity index 91% rename from backend/src/main/java/site/coduo/member/controller/dto/oauth/GithubAuthUri.java rename to backend/src/main/java/site/coduo/member/service/dto/oauth/GithubAuthUri.java index 188b0eab..20e69a07 100644 --- a/backend/src/main/java/site/coduo/member/controller/dto/oauth/GithubAuthUri.java +++ b/backend/src/main/java/site/coduo/member/service/dto/oauth/GithubAuthUri.java @@ -1,4 +1,4 @@ -package site.coduo.member.controller.dto.oauth; +package site.coduo.member.service.dto.oauth; import java.net.URI; diff --git a/backend/src/main/java/site/coduo/member/controller/dto/oauth/GithubCallbackQuery.java b/backend/src/main/java/site/coduo/member/service/dto/oauth/GithubCallbackQuery.java similarity index 57% rename from backend/src/main/java/site/coduo/member/controller/dto/oauth/GithubCallbackQuery.java rename to backend/src/main/java/site/coduo/member/service/dto/oauth/GithubCallbackQuery.java index 1b8e0d08..7e7be332 100644 --- a/backend/src/main/java/site/coduo/member/controller/dto/oauth/GithubCallbackQuery.java +++ b/backend/src/main/java/site/coduo/member/service/dto/oauth/GithubCallbackQuery.java @@ -1,4 +1,4 @@ -package site.coduo.member.controller.dto.oauth; +package site.coduo.member.service.dto.oauth; public record GithubCallbackQuery(String code, String state) { } diff --git a/backend/src/main/java/site/coduo/member/service/dto/oauth/GithubOAuthEndpoint.java b/backend/src/main/java/site/coduo/member/service/dto/oauth/GithubOAuthEndpoint.java new file mode 100644 index 00000000..39617b3a --- /dev/null +++ b/backend/src/main/java/site/coduo/member/service/dto/oauth/GithubOAuthEndpoint.java @@ -0,0 +1,7 @@ +package site.coduo.member.service.dto.oauth; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record GithubOAuthEndpoint( + @Schema(description = "Github 인가 λ“œν¬μΈνŠΈ", example = "https://www.github.com/login/oauth/authorize?client_id=test&state=random%20number&redirect_uri=http://test.test") String endpoint) { +} diff --git a/backend/src/main/java/site/coduo/member/controller/dto/oauth/State.java b/backend/src/main/java/site/coduo/member/service/dto/oauth/State.java similarity index 84% rename from backend/src/main/java/site/coduo/member/controller/dto/oauth/State.java rename to backend/src/main/java/site/coduo/member/service/dto/oauth/State.java index 2cd33b14..20fd2872 100644 --- a/backend/src/main/java/site/coduo/member/controller/dto/oauth/State.java +++ b/backend/src/main/java/site/coduo/member/service/dto/oauth/State.java @@ -1,4 +1,4 @@ -package site.coduo.member.controller.dto.oauth; +package site.coduo.member.service.dto.oauth; import site.coduo.member.exception.AuthenticationException; diff --git a/backend/src/main/java/site/coduo/pairroom/controller/PairRoomController.java b/backend/src/main/java/site/coduo/pairroom/controller/PairRoomController.java index 115e95ba..3d953dcb 100644 --- a/backend/src/main/java/site/coduo/pairroom/controller/PairRoomController.java +++ b/backend/src/main/java/site/coduo/pairroom/controller/PairRoomController.java @@ -1,28 +1,36 @@ package site.coduo.pairroom.controller; +import static site.coduo.common.config.web.filter.SignInCookieFilter.SIGN_IN_COOKIE_NAME; + import java.net.URI; +import java.util.List; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import site.coduo.pairroom.controller.docs.PairRoomDocs; -import site.coduo.pairroom.dto.PairRoomCreateRequest; -import site.coduo.pairroom.dto.PairRoomCreateResponse; -import site.coduo.pairroom.dto.PairRoomDeleteRequest; -import site.coduo.pairroom.dto.PairRoomReadRequest; -import site.coduo.pairroom.dto.PairRoomReadResponse; -import site.coduo.pairroom.dto.TimerDurationCreateRequest; import site.coduo.pairroom.service.PairRoomService; +import site.coduo.pairroom.service.dto.PairRoomCreateRequest; +import site.coduo.pairroom.service.dto.PairRoomCreateResponse; +import site.coduo.pairroom.service.dto.PairRoomExistResponse; +import site.coduo.pairroom.service.dto.PairRoomMemberResponse; +import site.coduo.pairroom.service.dto.PairRoomReadRequest; +import site.coduo.pairroom.service.dto.PairRoomReadResponse; +import site.coduo.pairroom.service.dto.PairRoomStatusUpdateRequest; +@Slf4j @RequiredArgsConstructor @RestController public class PairRoomController implements PairRoomDocs { @@ -31,23 +39,31 @@ public class PairRoomController implements PairRoomDocs { @PostMapping("/pair-room") public ResponseEntity createPairRoom( - @Valid @RequestBody final PairRoomCreateRequest request + @Valid @RequestBody final PairRoomCreateRequest request, + @CookieValue(value = SIGN_IN_COOKIE_NAME, required = false) final String token ) { - final PairRoomCreateResponse response = new PairRoomCreateResponse( - pairRoomService.savePairNameAndAccessCode(request)); - + final String accessCode = pairRoomService.savePairRoom(request, token); + final PairRoomCreateResponse response = new PairRoomCreateResponse(accessCode); return ResponseEntity.created(URI.create("/")) .body(response); } - @PatchMapping("/pair-room/{accessCode}/timer") - public ResponseEntity updateTimerDuration( + @PatchMapping("/pair-room/{accessCode}/pair-swap") + public ResponseEntity updatePairRole(@PathVariable("accessCode") final String accessCode) { + pairRoomService.updateNavigatorWithDriver(accessCode); + + return ResponseEntity.noContent() + .build(); + } + + @PatchMapping("/pair-room/{accessCode}/status") + public ResponseEntity updatePairRoomStatus( @PathVariable("accessCode") final String accessCode, - @Valid @RequestBody final TimerDurationCreateRequest request + @Valid @RequestBody final PairRoomStatusUpdateRequest request ) { - pairRoomService.saveTimerDuration(accessCode, request); + pairRoomService.updatePairRoomStatus(accessCode, request.status()); - return ResponseEntity.created(URI.create("/")) + return ResponseEntity.noContent() .build(); } @@ -55,19 +71,32 @@ public ResponseEntity updateTimerDuration( public ResponseEntity getPairRoom( @Valid @PathVariable("accessCode") final PairRoomReadRequest request ) { - final PairRoomReadResponse response = PairRoomReadResponse.from( - pairRoomService.findByAccessCode(request.accessCode())); + final PairRoomReadResponse response = pairRoomService.findPairRoomAndTimer(request.accessCode()); return ResponseEntity.ok(response); } - @DeleteMapping("/pair-room/{accessCode}") - public ResponseEntity deletePairRoom( - @Valid @PathVariable("accessCode") final PairRoomDeleteRequest request + @GetMapping("/my-pair-rooms") + public ResponseEntity> getPairRooms( + @CookieValue(SIGN_IN_COOKIE_NAME) final String token ) { - pairRoomService.deletePairRoom(request.accessCode()); + final List pairRooms = pairRoomService.findPairRooms(token); - return ResponseEntity.noContent() - .build(); + return ResponseEntity.ok() + .body(pairRooms); + } + + @GetMapping("/pair-room/exists") + public ResponseEntity pairRoomExists(@RequestParam("access_code") final String accessCode) { + final PairRoomExistResponse response = new PairRoomExistResponse( + pairRoomService.existsByAccessCode(accessCode)); + + return ResponseEntity.ok(response); + } + + @DeleteMapping("/pair-room/{accessCode}") + public ResponseEntity deletePairRoom(@PathVariable("accessCode") final String accessCode) { + pairRoomService.deletePairRoom(accessCode); + return ResponseEntity.noContent().build(); } } diff --git a/backend/src/main/java/site/coduo/pairroom/controller/PairRoomErrorController.java b/backend/src/main/java/site/coduo/pairroom/controller/PairRoomExceptionHandler.java similarity index 57% rename from backend/src/main/java/site/coduo/pairroom/controller/PairRoomErrorController.java rename to backend/src/main/java/site/coduo/pairroom/controller/PairRoomExceptionHandler.java index 6470872a..9882e5a7 100644 --- a/backend/src/main/java/site/coduo/pairroom/controller/PairRoomErrorController.java +++ b/backend/src/main/java/site/coduo/pairroom/controller/PairRoomExceptionHandler.java @@ -11,14 +11,33 @@ import lombok.extern.slf4j.Slf4j; import site.coduo.common.controller.response.ApiErrorResponse; import site.coduo.pairroom.controller.error.PairRoomApiError; +import site.coduo.pairroom.exception.DuplicatePairNameException; +import site.coduo.pairroom.exception.InvalidAccessCodeException; import site.coduo.pairroom.exception.InvalidNameFormatException; import site.coduo.pairroom.exception.PairRoomException; import site.coduo.pairroom.exception.PairRoomNotFoundException; +import site.coduo.pairroom.exception.InvalidPairRoomStatusException; @Slf4j @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) -public class PairRoomErrorController { +public class PairRoomExceptionHandler { + + @ExceptionHandler(DuplicatePairNameException.class) + public ResponseEntity handleDuplicatePairNameException(final DuplicatePairNameException e) { + log.warn(e.getMessage()); + + return ResponseEntity.status(PairRoomApiError.INVALID_PAIR_NAME.getHttpStatus()) + .body(new ApiErrorResponse(PairRoomApiError.INVALID_PAIR_NAME.getMessage())); + } + + @ExceptionHandler(InvalidAccessCodeException.class) + public ResponseEntity handleInvalidAccessCodeException(final InvalidAccessCodeException e) { + log.warn(e.getMessage()); + + return ResponseEntity.status(PairRoomApiError.INVALID_ACCESS_CODE.getHttpStatus()) + .body(new ApiErrorResponse(PairRoomApiError.INVALID_ACCESS_CODE.getMessage())); + } @ExceptionHandler(InvalidNameFormatException.class) public ResponseEntity handleInvalidPropertiesFormatException( @@ -29,6 +48,14 @@ public ResponseEntity handleInvalidPropertiesFormatException( .body(new ApiErrorResponse(PairRoomApiError.INVALID_PROPERTIES_FORMAT.getMessage())); } + @ExceptionHandler(InvalidPairRoomStatusException.class) + public ResponseEntity handlePairRoomStatusNotFoundException(final InvalidPairRoomStatusException e) { + log.warn(e.getMessage()); + + return ResponseEntity.status(PairRoomApiError.INVALID_PROPERTIES_FORMAT.getHttpStatus()) + .body(new ApiErrorResponse(PairRoomApiError.INVALID_PROPERTIES_FORMAT.getMessage())); + } + @ExceptionHandler(PairRoomNotFoundException.class) public ResponseEntity handlePairRoomNotFoundException(final PairRoomNotFoundException e) { log.warn(e.getMessage()); diff --git a/backend/src/main/java/site/coduo/pairroom/controller/docs/PairRoomDocs.java b/backend/src/main/java/site/coduo/pairroom/controller/docs/PairRoomDocs.java index fa88f041..cfc0a3a4 100644 --- a/backend/src/main/java/site/coduo/pairroom/controller/docs/PairRoomDocs.java +++ b/backend/src/main/java/site/coduo/pairroom/controller/docs/PairRoomDocs.java @@ -1,49 +1,84 @@ package site.coduo.pairroom.controller.docs; +import java.util.List; + +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import site.coduo.pairroom.dto.PairRoomCreateRequest; -import site.coduo.pairroom.dto.PairRoomCreateResponse; -import site.coduo.pairroom.dto.PairRoomDeleteRequest; -import site.coduo.pairroom.dto.PairRoomReadRequest; -import site.coduo.pairroom.dto.PairRoomReadResponse; -import site.coduo.pairroom.dto.TimerDurationCreateRequest; +import site.coduo.pairroom.service.dto.PairRoomCreateRequest; +import site.coduo.pairroom.service.dto.PairRoomCreateResponse; +import site.coduo.pairroom.service.dto.PairRoomExistResponse; +import site.coduo.pairroom.service.dto.PairRoomMemberResponse; +import site.coduo.pairroom.service.dto.PairRoomReadRequest; +import site.coduo.pairroom.service.dto.PairRoomReadResponse; +import site.coduo.pairroom.service.dto.PairRoomStatusUpdateRequest; @Tag(name = "νŽ˜μ–΄λ£Έ API") public interface PairRoomDocs { - @Operation(summary = "νŽ˜μ–΄λ£Έμ„ μ‘°νšŒν•œλ‹€.") - @ApiResponse(responseCode = "200", description = "νŽ˜μ–΄λ£Έ 쑰회 성곡") + @Operation(summary = "νŽ˜μ–΄λ£Έκ³Ό ν•΄λ‹Ή νŽ˜μ–΄λ£Έμ˜ 타이머 정보λ₯Ό ν•¨κ»˜ μ‘°νšŒν•œλ‹€.") + @ApiResponse(responseCode = "200", description = "νŽ˜μ–΄λ£Έ & 타이머 쑰회 성곡", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = PairRoomReadResponse.class))) ResponseEntity getPairRoom( @Parameter(description = "νŽ˜μ–΄λ£Έ μ ‘κ·Ό μ½”λ“œ", required = true) PairRoomReadRequest request ); @Operation(summary = "νŽ˜μ–΄λ£Έμ„ μƒμ„±ν•œλ‹€.") - @ApiResponse(responseCode = "201", description = "νŽ˜μ–΄λ£Έ μ €μž₯ 성곡") + @ApiResponse(responseCode = "201", description = "νŽ˜μ–΄λ£Έ μ €μž₯ 성곡", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = PairRoomCreateResponse.class))) ResponseEntity createPairRoom( - @Parameter(description = "νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ— μ°Έμ—¬ν•˜λŠ” νŽ˜μ–΄ A의 이름, νŽ˜μ–΄ B의 이름", required = true) - PairRoomCreateRequest pairRoomCreateRequest + @Parameter(description = "νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ— μ°Έμ—¬ν•˜λŠ” λ“œλΌμ΄λ²„ 이름, λ‚΄λΉ„κ²Œμ΄ν„° 이름, 타이머 μ‹œκ°„, 타이머 남은 μ‹œκ°„, λ―Έμ…˜ 리포지토리 링크", required = true) + PairRoomCreateRequest pairRoomCreateRequest, + @Parameter(description = "둜그인 μœ μ € 토큰") + String token + ); + + @Operation(summary = "λ“œλΌμ΄λ²„ λ‚΄λΉ„κ²Œμ΄ν„° 역할을 λ°”κΎΌλ‹€.") + @ApiResponse(responseCode = "204", description = "νŽ˜μ–΄λ£Έ μ—­ν•  μŠ€μ™‘ 성곡") + ResponseEntity updatePairRole( + @Parameter(description = "νŽ˜μ–΄λ£Έ μ ‘κ·Ό μ½”λ“œ") + String accessCode ); - @Operation(summary = "타이머 μ‹œκ°„μ„ μ €μž₯ν•œλ‹€.") - @ApiResponse(responseCode = "201", description = "타이머 μ‹œκ°„ μ €μž₯ 성곡") - ResponseEntity updateTimerDuration( + @Operation(summary = "νŽ˜μ–΄λ£Έμ˜ μƒνƒœλ₯Ό λ³€κ²½ν•œλ‹€.") + @ApiResponse(responseCode = "204", description = "νŽ˜μ–΄λ£Έ μƒνƒœ λ³€κ²½ 성곡") + ResponseEntity updatePairRoomStatus( @Parameter(description = "νŽ˜μ–΄λ£Έ μ ‘κ·Ό μ½”λ“œ", required = true) String accessCode, - - @Parameter(description = "타이머 μ‹œκ°„ μ €μž₯ μš”μ²­ λ°”λ””", required = true) - TimerDurationCreateRequest request + @Parameter(description = "λ³€κ²½ν•  νŽ˜μ–΄λ£Έ μƒνƒœ", required = true) + PairRoomStatusUpdateRequest request ); + @Operation(summary = "μžμ‹ μ˜ νŽ˜μ–΄λ£Έμ„ μ‘°νšŒν•œλ‹€.") + @ApiResponse(responseCode = "200", description = "νŽ˜μ–΄λ£Έ 쑰회 성곡", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = PairRoomMemberResponse.class))) + ResponseEntity> getPairRooms( + @Parameter( + in = ParameterIn.COOKIE, + name = "coduo_whoami", + description = "μ‚¬μš©μžκ°€ 인증에 μ„±κ³΅ν•˜λ©΄ μ„œλ²„μ—μ„œ λ°œκΈ‰ν•˜λŠ” μΏ ν‚€", + schema = @Schema(type = "string"), + required = true + ) + String signInToken); + + @Operation(summary = "μ•‘μ„ΈμŠ€ μ½”λ“œλ‘œ νŽ˜μ–΄λ£Έμ΄ μ‘΄μž¬ν•˜λŠ”μ§€ μ‘°νšŒν•œλ‹€.") + @ApiResponse(responseCode = "200", description = "νŽ˜μ–΄λ£Έ 쑴재 μ—¬λΆ€", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = PairRoomExistResponse.class))) + ResponseEntity pairRoomExists(String accessCode); + @Operation(summary = "νŽ˜μ–΄λ£Έμ„ μ‚­μ œν•œλ‹€.") @ApiResponse(responseCode = "204", description = "νŽ˜μ–΄λ£Έ μ‚­μ œ 성곡") - @ApiResponse(responseCode = "404", description = "νŽ˜μ–΄λ£Έ μ‚­μ œ μ‹€νŒ¨") ResponseEntity deletePairRoom( - @Parameter(description = "νŽ˜μ–΄λ£Έ μ ‘κ·Ό μ½”λ“œ", required = true) PairRoomDeleteRequest request + @Parameter(description = "νŽ˜μ–΄λ£Έ μ ‘κ·Ό μ½”λ“œ", required = true) + String accessCode ); } diff --git a/backend/src/main/java/site/coduo/pairroom/controller/error/PairRoomApiError.java b/backend/src/main/java/site/coduo/pairroom/controller/error/PairRoomApiError.java index 94b04452..2c7c0fb9 100644 --- a/backend/src/main/java/site/coduo/pairroom/controller/error/PairRoomApiError.java +++ b/backend/src/main/java/site/coduo/pairroom/controller/error/PairRoomApiError.java @@ -10,6 +10,8 @@ public enum PairRoomApiError { INVALID_REQUEST(HttpStatus.BAD_REQUEST, "μœ νš¨ν•˜μ§€ μ•Šμ€ μš”μ²­μž…λ‹ˆλ‹€."), + INVALID_PAIR_NAME(HttpStatus.BAD_REQUEST, "μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ νŽ˜μ–΄ μ΄λ¦„μž…λ‹ˆλ‹€."), + INVALID_ACCESS_CODE(HttpStatus.BAD_REQUEST, "μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ μ ‘κ·Ό μ½”λ“œμž…λ‹ˆλ‹€."), PAIR_ROOM_NOT_FOUND(HttpStatus.NOT_FOUND, "νŽ˜μ–΄λ£Έμ΄ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), INVALID_PROPERTIES_FORMAT(HttpStatus.BAD_REQUEST, "μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 데이터 ν˜•μ‹μž…λ‹ˆλ‹€."); diff --git a/backend/src/main/java/site/coduo/pairroom/domain/MissionUrl.java b/backend/src/main/java/site/coduo/pairroom/domain/MissionUrl.java new file mode 100644 index 00000000..214221d6 --- /dev/null +++ b/backend/src/main/java/site/coduo/pairroom/domain/MissionUrl.java @@ -0,0 +1,13 @@ +package site.coduo.pairroom.domain; + +import lombok.Getter; + +@Getter +public class MissionUrl { + + private final String value; + + public MissionUrl(final String value) { + this.value = value; + } +} diff --git a/backend/src/main/java/site/coduo/pairroom/domain/Pair.java b/backend/src/main/java/site/coduo/pairroom/domain/Pair.java index f8087628..2f8565c3 100644 --- a/backend/src/main/java/site/coduo/pairroom/domain/Pair.java +++ b/backend/src/main/java/site/coduo/pairroom/domain/Pair.java @@ -2,45 +2,38 @@ import java.util.Objects; -import jakarta.persistence.AttributeOverride; -import jakarta.persistence.Column; -import jakarta.persistence.Embeddable; -import jakarta.persistence.Embedded; - -import lombok.Getter; -import lombok.NoArgsConstructor; import site.coduo.pairroom.exception.DuplicatePairNameException; -@Embeddable -@NoArgsConstructor -@Getter public class Pair { - @Embedded - @AttributeOverride(name = "value", column = @Column(name = "FIRST_PAIR", nullable = false)) - private PairName firstPair; - - @Embedded - @AttributeOverride(name = "value", column = @Column(name = "SECOND_PAIR", nullable = false)) - private PairName secondPair; + private final PairName navigator; + private final PairName driver; - public Pair(final PairName firstPair, final PairName secondPair) { - validate(firstPair, secondPair); - this.firstPair = firstPair; - this.secondPair = secondPair; + public Pair(final PairName navigator, final PairName driver) { + validate(navigator, driver); + this.navigator = navigator; + this.driver = driver; } - public void validate(final PairName firstPair, final PairName secondPair) { - if (Objects.equals(firstPair, secondPair)) { + public void validate(final PairName navigator, final PairName driver) { + if (Objects.equals(navigator, driver)) { throw new DuplicatePairNameException("νŽ˜μ–΄μ˜ 이름이 μ€‘λ³΅λ˜μ—ˆμŠ΅λ‹ˆλ‹€."); } } + public String getNavigatorName() { + return navigator.getValue(); + } + + public String getDriverName() { + return driver.getValue(); + } + @Override public String toString() { return "Pair{" + - "firstPair=" + firstPair + - ", secondPair=" + secondPair + + "navigator=" + navigator + + ", driver=" + driver + '}'; } } diff --git a/backend/src/main/java/site/coduo/pairroom/domain/PairName.java b/backend/src/main/java/site/coduo/pairroom/domain/PairName.java index 3db1f5d7..1849d1d4 100644 --- a/backend/src/main/java/site/coduo/pairroom/domain/PairName.java +++ b/backend/src/main/java/site/coduo/pairroom/domain/PairName.java @@ -2,21 +2,16 @@ import java.util.Objects; -import jakarta.persistence.Column; -import jakarta.persistence.Embeddable; - import org.apache.commons.lang3.StringUtils; import lombok.Getter; import site.coduo.pairroom.exception.InvalidNameFormatException; @Getter -@Embeddable public class PairName { private static final int MAX_LENGTH = 10; - @Column(length = MAX_LENGTH, nullable = false) private final String value; protected PairName() { diff --git a/backend/src/main/java/site/coduo/pairroom/domain/PairRoom.java b/backend/src/main/java/site/coduo/pairroom/domain/PairRoom.java index 4dc321b1..95db9853 100644 --- a/backend/src/main/java/site/coduo/pairroom/domain/PairRoom.java +++ b/backend/src/main/java/site/coduo/pairroom/domain/PairRoom.java @@ -1,62 +1,60 @@ package site.coduo.pairroom.domain; -import jakarta.persistence.Column; -import jakarta.persistence.Embedded; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.Table; - -import lombok.AccessLevel; +import java.util.Objects; + import lombok.Getter; -import lombok.NoArgsConstructor; -import site.coduo.common.infrastructure.audit.entity.BaseTimeEntity; +import lombok.RequiredArgsConstructor; import site.coduo.pairroom.domain.accesscode.AccessCode; @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "PAIR_ROOM") -@Entity -public class PairRoom extends BaseTimeEntity { +@RequiredArgsConstructor +public class PairRoom { - @Id - @Column(name = "ID", nullable = false) - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; + private final PairRoomStatus status; + private final Pair pair; + private final MissionUrl missionUrl; + private final AccessCode accessCode; - @Embedded - private Pair pair; + public String getAccessCodeText() { + return accessCode.getValue(); + } - @Embedded - @Column(name = "ACCESS_CODE", nullable = false) - private AccessCode accessCode; + public String getNavigatorName() { + return pair.getNavigatorName(); + } - @Column(name = "TIMER_DURATION", nullable = true) - private Long timerDuration; + public String getDriverName() { + return pair.getDriverName(); + } - public PairRoom(final Pair pair, final AccessCode accessCode) { - this.pair = pair; - this.accessCode = accessCode; + public String getMissionUrl() { + return missionUrl.getValue(); } - public PairRoom(final Long id, final Pair pair, final AccessCode accessCode) { - this.id = id; - this.pair = pair; - this.accessCode = accessCode; + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof final PairRoom pairRoom)) { + return false; + } + return status == pairRoom.status && Objects.equals(pair, pairRoom.pair) && Objects.equals( + missionUrl, pairRoom.missionUrl) && Objects.equals(accessCode, pairRoom.accessCode); } - public String getAccessCodeText() { - return accessCode.getValue(); + @Override + public int hashCode() { + return Objects.hash(status, pair, missionUrl, accessCode); } @Override public String toString() { return "PairRoom{" + - "id=" + id + + "status=" + status + ", pair=" + pair + + ", missionUrl=" + missionUrl + ", accessCode=" + accessCode + - ", timerDuration=" + timerDuration + '}'; } } diff --git a/backend/src/main/java/site/coduo/pairroom/domain/PairRoomStatus.java b/backend/src/main/java/site/coduo/pairroom/domain/PairRoomStatus.java new file mode 100644 index 00000000..06a6d5b4 --- /dev/null +++ b/backend/src/main/java/site/coduo/pairroom/domain/PairRoomStatus.java @@ -0,0 +1,27 @@ +package site.coduo.pairroom.domain; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import lombok.Getter; +import site.coduo.pairroom.exception.InvalidPairRoomStatusException; + +@Getter +public enum PairRoomStatus { + + IN_PROGRESS, + COMPLETED, + DELETED; + + private static final Map STATUS = Arrays.stream(values()) + .collect(Collectors.toMap(PairRoomStatus::name, Function.identity())); + + public static PairRoomStatus findByName(String value) { + if (STATUS.containsKey(value)) { + return STATUS.get(value); + } + throw new InvalidPairRoomStatusException("νŽ˜μ–΄λ£Έ μƒνƒœκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + } +} diff --git a/backend/src/main/java/site/coduo/pairroom/domain/accesscode/AccessCode.java b/backend/src/main/java/site/coduo/pairroom/domain/accesscode/AccessCode.java index ae6a1937..72aadb4a 100644 --- a/backend/src/main/java/site/coduo/pairroom/domain/accesscode/AccessCode.java +++ b/backend/src/main/java/site/coduo/pairroom/domain/accesscode/AccessCode.java @@ -1,21 +1,14 @@ package site.coduo.pairroom.domain.accesscode; -import static site.coduo.pairroom.domain.accesscode.AccessCodeStrategy.ACCESS_CODE_LENGTH; - import java.util.Objects; -import jakarta.persistence.Column; -import jakarta.persistence.Embeddable; - import lombok.Getter; import lombok.RequiredArgsConstructor; @Getter -@Embeddable @RequiredArgsConstructor public class AccessCode { - @Column(name = "ACCESS_CODE", length = ACCESS_CODE_LENGTH, nullable = false) private final String value; protected AccessCode() { diff --git a/backend/src/main/java/site/coduo/pairroom/domain/accesscode/AccessCodeFactory.java b/backend/src/main/java/site/coduo/pairroom/domain/accesscode/AccessCodeFactory.java deleted file mode 100644 index 01fddca8..00000000 --- a/backend/src/main/java/site/coduo/pairroom/domain/accesscode/AccessCodeFactory.java +++ /dev/null @@ -1,19 +0,0 @@ -package site.coduo.pairroom.domain.accesscode; - -import java.util.List; - -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class AccessCodeFactory { - - private final AccessCodeStrategy strategy; - - public AccessCode generateWithoutDuplication(final List accessCodes) { - final AccessCode accessCode = new AccessCode(strategy.generateAccessCode()); - if (accessCodes.contains(accessCode)) { - return generateWithoutDuplication(accessCodes); - } - return accessCode; - } -} diff --git a/backend/src/main/java/site/coduo/pairroom/domain/accesscode/AccessCodeStrategy.java b/backend/src/main/java/site/coduo/pairroom/domain/accesscode/AccessCodeStrategy.java deleted file mode 100644 index 2b0679c2..00000000 --- a/backend/src/main/java/site/coduo/pairroom/domain/accesscode/AccessCodeStrategy.java +++ /dev/null @@ -1,8 +0,0 @@ -package site.coduo.pairroom.domain.accesscode; - -public interface AccessCodeStrategy { - - int ACCESS_CODE_LENGTH = 6; - - String generateAccessCode(); -} diff --git a/backend/src/main/java/site/coduo/pairroom/domain/accesscode/UUIDAccessCodeGenerator.java b/backend/src/main/java/site/coduo/pairroom/domain/accesscode/UUIDAccessCodeGenerator.java new file mode 100644 index 00000000..3badffe6 --- /dev/null +++ b/backend/src/main/java/site/coduo/pairroom/domain/accesscode/UUIDAccessCodeGenerator.java @@ -0,0 +1,21 @@ +package site.coduo.pairroom.domain.accesscode; + +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class UUIDAccessCodeGenerator { + + private static final int ACCESS_CODE_LENGTH = 9; + + @Value("${ec2.prefix}") + private String prefix; + + public String generate() { + return prefix + UUID.randomUUID() + .toString() + .substring(0, ACCESS_CODE_LENGTH); + } +} diff --git a/backend/src/main/java/site/coduo/pairroom/domain/accesscode/UUIDAccessCodeStrategy.java b/backend/src/main/java/site/coduo/pairroom/domain/accesscode/UUIDAccessCodeStrategy.java deleted file mode 100644 index 271c7509..00000000 --- a/backend/src/main/java/site/coduo/pairroom/domain/accesscode/UUIDAccessCodeStrategy.java +++ /dev/null @@ -1,13 +0,0 @@ -package site.coduo.pairroom.domain.accesscode; - -import java.util.UUID; - -public class UUIDAccessCodeStrategy implements AccessCodeStrategy { - - @Override - public String generateAccessCode() { - return UUID.randomUUID() - .toString() - .substring(0, ACCESS_CODE_LENGTH); - } -} diff --git a/backend/src/main/java/site/coduo/pairroom/dto/PairRoomCreateRequest.java b/backend/src/main/java/site/coduo/pairroom/dto/PairRoomCreateRequest.java deleted file mode 100644 index 2ac8428c..00000000 --- a/backend/src/main/java/site/coduo/pairroom/dto/PairRoomCreateRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package site.coduo.pairroom.dto; - -import jakarta.validation.constraints.NotBlank; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "νŽ˜μ–΄λ£Έ 생성 μš”μ²­ λ°”λ””") -public record PairRoomCreateRequest( - @Schema(description = "첫 번째 νŽ˜μ–΄μ˜ 이름") - @NotBlank - String firstPair, - - @Schema(description = "두 번째 νŽ˜μ–΄μ˜ 이름") - @NotBlank - String secondPair -) { -} diff --git a/backend/src/main/java/site/coduo/pairroom/dto/PairRoomReadResponse.java b/backend/src/main/java/site/coduo/pairroom/dto/PairRoomReadResponse.java deleted file mode 100644 index 17721dff..00000000 --- a/backend/src/main/java/site/coduo/pairroom/dto/PairRoomReadResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -package site.coduo.pairroom.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import site.coduo.pairroom.domain.PairRoom; - -@Schema(description = "νŽ˜μ–΄λ£Έ 쑰회 응닡 λ°”λ””") -public record PairRoomReadResponse( - @Schema(description = "νŽ˜μ–΄λ£Έ id", example = "1") - long id, - - @Schema(description = "첫 번째 νŽ˜μ–΄μ˜ 이름", example = "ν•΄μ‹œ") - String firstPair, - - @Schema(description = "두 번째 νŽ˜μ–΄μ˜ 이름", example = "파슬리") - String secondPair, - - @Schema(description = "타이머 μ‹œκ°„. millisecond κΈ°μ€€μœΌλ‘œ μ €μž₯ν•œλ‹€. λ§Œμ•½ 타이머 μ‹œκ°„μ΄ μ €μž₯λ˜μ§€ μ•Šμ•˜λ‹€λ©΄ null이 λ°˜ν™˜λœλ‹€.", example = "60000") - Long timerDuration -) { - - public static PairRoomReadResponse from(final PairRoom pairRoom) { - return new PairRoomReadResponse( - pairRoom.getId(), - pairRoom.getPair().getFirstPair().getValue(), - pairRoom.getPair().getSecondPair().getValue(), - pairRoom.getTimerDuration() - ); - } -} diff --git a/backend/src/main/java/site/coduo/pairroom/exception/DeletePairRoomException.java b/backend/src/main/java/site/coduo/pairroom/exception/DeletePairRoomException.java new file mode 100644 index 00000000..a470d3ed --- /dev/null +++ b/backend/src/main/java/site/coduo/pairroom/exception/DeletePairRoomException.java @@ -0,0 +1,8 @@ +package site.coduo.pairroom.exception; + +public class DeletePairRoomException extends PairRoomException { + + public DeletePairRoomException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/pairroom/exception/InvalidAccessCodeException.java b/backend/src/main/java/site/coduo/pairroom/exception/InvalidAccessCodeException.java new file mode 100644 index 00000000..3e95fff8 --- /dev/null +++ b/backend/src/main/java/site/coduo/pairroom/exception/InvalidAccessCodeException.java @@ -0,0 +1,8 @@ +package site.coduo.pairroom.exception; + +public class InvalidAccessCodeException extends PairRoomException { + + public InvalidAccessCodeException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/pairroom/exception/InvalidPairRoomStatusException.java b/backend/src/main/java/site/coduo/pairroom/exception/InvalidPairRoomStatusException.java new file mode 100644 index 00000000..884a37fc --- /dev/null +++ b/backend/src/main/java/site/coduo/pairroom/exception/InvalidPairRoomStatusException.java @@ -0,0 +1,8 @@ +package site.coduo.pairroom.exception; + +public class InvalidPairRoomStatusException extends PairRoomException { + + public InvalidPairRoomStatusException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/pairroom/exception/InvalidTimerDurationException.java b/backend/src/main/java/site/coduo/pairroom/exception/InvalidTimerDurationException.java deleted file mode 100644 index 9e4d710a..00000000 --- a/backend/src/main/java/site/coduo/pairroom/exception/InvalidTimerDurationException.java +++ /dev/null @@ -1,8 +0,0 @@ -package site.coduo.pairroom.exception; - -public class InvalidTimerDurationException extends PairRoomException { - - public InvalidTimerDurationException(final String message) { - super(message); - } -} diff --git a/backend/src/main/java/site/coduo/pairroom/repository/PairRoomEntity.java b/backend/src/main/java/site/coduo/pairroom/repository/PairRoomEntity.java new file mode 100644 index 00000000..55ca232c --- /dev/null +++ b/backend/src/main/java/site/coduo/pairroom/repository/PairRoomEntity.java @@ -0,0 +1,130 @@ +package site.coduo.pairroom.repository; + +import java.util.Objects; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import site.coduo.common.infrastructure.audit.entity.BaseTimeEntity; +import site.coduo.pairroom.domain.MissionUrl; +import site.coduo.pairroom.domain.Pair; +import site.coduo.pairroom.domain.PairName; +import site.coduo.pairroom.domain.PairRoom; +import site.coduo.pairroom.domain.PairRoomStatus; +import site.coduo.pairroom.domain.accesscode.AccessCode; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "PAIR_ROOM") +@Entity +public class PairRoomEntity extends BaseTimeEntity { + + @Id + @Column(name = "ID", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(value = EnumType.STRING) + @Column(name = "STATUS", nullable = false) + private PairRoomStatus status; + + @Column(name = "NAVIGATOR", nullable = false) + private String navigator; + + @Column(name = "DRIVER", nullable = false) + private String driver; + + @Column(name = "MISSION_URL", nullable = false) + private String missionUrl; + + @Column(name = "ACCESS_CODE", nullable = false, unique = true) + private String accessCode; + + @Builder + private PairRoomEntity(final Long id, + final PairRoomStatus status, + final String navigator, + final String driver, + final String missionUrl, + final String accessCode) { + this.id = id; + this.status = status; + this.navigator = navigator; + this.driver = driver; + this.missionUrl = missionUrl; + this.accessCode = accessCode; + } + + public static PairRoomEntity from(final PairRoom pairRoom) { + return new PairRoomEntity( + null, + pairRoom.getStatus(), + pairRoom.getNavigatorName(), + pairRoom.getDriverName(), + pairRoom.getMissionUrl(), + pairRoom.getAccessCodeText() + ); + } + + public PairRoom toDomain() { + return new PairRoom( + status, + new Pair(new PairName(navigator), new PairName(driver)), + new MissionUrl(missionUrl), + new AccessCode(accessCode) + ); + } + + public void updateStatus(final PairRoomStatus status) { + this.status = status; + } + + public void swapNavigatorWithDriver() { + final String temp = this.navigator; + this.navigator = this.driver; + this.driver = temp; + } + + public boolean isDelete() { + return status == PairRoomStatus.DELETED; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final PairRoomEntity that = (PairRoomEntity) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "PairRoomEntity{" + + "id=" + id + + ", status=" + status + + ", navigator='" + navigator + '\'' + + ", driver='" + driver + '\'' + + ", missionUrl='" + missionUrl + '\'' + + ", accessCode='" + accessCode + '\'' + + '}'; + } +} diff --git a/backend/src/main/java/site/coduo/pairroom/repository/PairRoomMemberEntity.java b/backend/src/main/java/site/coduo/pairroom/repository/PairRoomMemberEntity.java new file mode 100644 index 00000000..fa504719 --- /dev/null +++ b/backend/src/main/java/site/coduo/pairroom/repository/PairRoomMemberEntity.java @@ -0,0 +1,56 @@ +package site.coduo.pairroom.repository; + +import java.util.Objects; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import site.coduo.member.domain.Member; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "PAIR_ROOM_MEMBER") +@Entity +public class PairRoomMemberEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "PAIR_ROOM_ID") + private PairRoomEntity pairRoom; + + @ManyToOne + @JoinColumn(name = "MEMBER_ID") + private Member member; + + public PairRoomMemberEntity(final PairRoomEntity pairRoom, final Member member) { + this.pairRoom = pairRoom; + this.member = member; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof final PairRoomMemberEntity that)) { + return false; + } + return Objects.equals(getId(), that.getId()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getId()); + } +} diff --git a/backend/src/main/java/site/coduo/pairroom/repository/PairRoomMemberRepository.java b/backend/src/main/java/site/coduo/pairroom/repository/PairRoomMemberRepository.java new file mode 100644 index 00000000..6c74e1f3 --- /dev/null +++ b/backend/src/main/java/site/coduo/pairroom/repository/PairRoomMemberRepository.java @@ -0,0 +1,12 @@ +package site.coduo.pairroom.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import site.coduo.member.domain.Member; + +public interface PairRoomMemberRepository extends JpaRepository { + + List findByMember(Member member); +} diff --git a/backend/src/main/java/site/coduo/pairroom/repository/PairRoomRepository.java b/backend/src/main/java/site/coduo/pairroom/repository/PairRoomRepository.java index f082b067..9f1bec18 100644 --- a/backend/src/main/java/site/coduo/pairroom/repository/PairRoomRepository.java +++ b/backend/src/main/java/site/coduo/pairroom/repository/PairRoomRepository.java @@ -3,24 +3,26 @@ import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import site.coduo.pairroom.domain.PairRoom; +import site.coduo.pairroom.domain.PairRoomStatus; import site.coduo.pairroom.domain.accesscode.AccessCode; import site.coduo.pairroom.exception.PairRoomNotFoundException; -public interface PairRoomRepository extends JpaRepository { +public interface PairRoomRepository extends JpaRepository { - Optional findByAccessCode(AccessCode accessCode); + Optional findByAccessCode(String accessCode); - default PairRoom fetchByAccessCode(AccessCode accessCode) { - return findByAccessCode(accessCode) + default PairRoomEntity fetchByAccessCode(String accessCodeText) { + return findByAccessCode(accessCodeText) .orElseThrow(() -> new PairRoomNotFoundException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” νŽ˜μ–΄λ£Έ μ ‘κ·Ό μ½”λ“œμž…λ‹ˆλ‹€.")); } - @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("UPDATE PairRoom pr SET pr.timerDuration = :newTimerDuration WHERE pr.id = :id") - int updateTimerDuration(@Param("id") long id, @Param("newTimerDuration") long newTimerDuration); + default PairRoomEntity fetchByAccessCode(AccessCode accessCode) { + return findByAccessCode(accessCode.getValue()) + .orElseThrow(() -> new PairRoomNotFoundException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” νŽ˜μ–΄λ£Έ μ ‘κ·Ό μ½”λ“œμž…λ‹ˆλ‹€.")); + } + + boolean existsByAccessCode(String generatedAccessCode); + + boolean existsByAccessCodeAndStatusNot(String accessCode, PairRoomStatus status); } diff --git a/backend/src/main/java/site/coduo/pairroom/repository/ProdPairRoomRepository.java b/backend/src/main/java/site/coduo/pairroom/repository/ProdPairRoomRepository.java deleted file mode 100644 index de1a82f7..00000000 --- a/backend/src/main/java/site/coduo/pairroom/repository/ProdPairRoomRepository.java +++ /dev/null @@ -1,31 +0,0 @@ -package site.coduo.pairroom.repository; - -import java.util.Optional; - -import org.springframework.stereotype.Repository; - -import lombok.RequiredArgsConstructor; -import site.coduo.pairroom.domain.PairRoom; -import site.coduo.pairroom.domain.accesscode.AccessCode; - -@RequiredArgsConstructor -@Repository -public class ProdPairRoomRepository implements site.coduo.pairroom.service.port.PairRoomRepository { - - private final PairRoomRepository pairRoomJpaRepository; - - @Override - public Optional findById(final Long id) { - return pairRoomJpaRepository.findById(id); - } - - @Override - public Optional findByAccessCode(final AccessCode accessCode) { - return pairRoomJpaRepository.findByAccessCode(accessCode); - } - - @Override - public PairRoom save(final PairRoom pairRoom) { - return pairRoomJpaRepository.save(pairRoom); - } -} diff --git a/backend/src/main/java/site/coduo/pairroom/service/PairRoomService.java b/backend/src/main/java/site/coduo/pairroom/service/PairRoomService.java index e36470a5..b2c78353 100644 --- a/backend/src/main/java/site/coduo/pairroom/service/PairRoomService.java +++ b/backend/src/main/java/site/coduo/pairroom/service/PairRoomService.java @@ -2,62 +2,127 @@ import java.util.List; +import jakarta.annotation.Nullable; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import site.coduo.member.domain.Member; +import site.coduo.member.service.MemberService; +import site.coduo.pairroom.domain.MissionUrl; import site.coduo.pairroom.domain.Pair; import site.coduo.pairroom.domain.PairName; import site.coduo.pairroom.domain.PairRoom; +import site.coduo.pairroom.domain.PairRoomStatus; import site.coduo.pairroom.domain.accesscode.AccessCode; -import site.coduo.pairroom.domain.accesscode.AccessCodeFactory; -import site.coduo.pairroom.domain.accesscode.UUIDAccessCodeStrategy; -import site.coduo.pairroom.dto.PairRoomCreateRequest; -import site.coduo.pairroom.dto.TimerDurationCreateRequest; -import site.coduo.pairroom.exception.InvalidTimerDurationException; +import site.coduo.pairroom.domain.accesscode.UUIDAccessCodeGenerator; +import site.coduo.pairroom.exception.DeletePairRoomException; +import site.coduo.pairroom.repository.PairRoomEntity; +import site.coduo.pairroom.repository.PairRoomMemberEntity; +import site.coduo.pairroom.repository.PairRoomMemberRepository; import site.coduo.pairroom.repository.PairRoomRepository; +import site.coduo.pairroom.service.dto.PairRoomCreateRequest; +import site.coduo.pairroom.service.dto.PairRoomMemberResponse; +import site.coduo.pairroom.service.dto.PairRoomReadResponse; +import site.coduo.timer.domain.Timer; +import site.coduo.timer.repository.TimerEntity; +import site.coduo.timer.repository.TimerRepository; +@Slf4j @RequiredArgsConstructor @Transactional(readOnly = true) @Service public class PairRoomService { - private static final int UPDATED_ROW_COUNT = 1; - private final PairRoomRepository pairRoomRepository; + private final TimerRepository timerRepository; + private final PairRoomMemberRepository pairRoomMemberRepository; + private final MemberService memberService; + private final UUIDAccessCodeGenerator uuidAccessCodeGenerator; @Transactional - public String savePairNameAndAccessCode(final PairRoomCreateRequest request) { - final Pair pair = new Pair(new PairName(request.firstPair()), new PairName(request.secondPair())); - final List accessCodes = pairRoomRepository.findAll() - .stream() - .map(PairRoom::getAccessCode) - .toList(); + public String savePairRoom(final PairRoomCreateRequest request, @Nullable final String token) { + final PairRoom pairRoom = createPairRoom(request); + final PairRoomEntity pairRoomEntity = pairRoomRepository.save(PairRoomEntity.from(pairRoom)); - final AccessCodeFactory accessCodeFactory = new AccessCodeFactory(new UUIDAccessCodeStrategy()); - final PairRoom pairRoom = new PairRoom(pair, accessCodeFactory.generateWithoutDuplication(accessCodes)); - - pairRoomRepository.save(pairRoom); + final Timer timer = new Timer(pairRoom.getAccessCode(), request.timerDuration(), request.timerRemainingTime()); + timerRepository.save(new TimerEntity(timer, pairRoomEntity)); + if (token != null) { + final Member member = memberService.findMemberByCredential(token); + pairRoomMemberRepository.save(new PairRoomMemberEntity(pairRoomEntity, member)); + } return pairRoom.getAccessCodeText(); } + public boolean existsByAccessCode(final String accessCode) { + return pairRoomRepository.existsByAccessCodeAndStatusNot(accessCode, PairRoomStatus.DELETED); + } + + private PairRoom createPairRoom(final PairRoomCreateRequest request) { + final AccessCode accessCode = generateAccessCode(); + final PairRoomStatus status = PairRoomStatus.findByName(request.status()); + final Pair pair = new Pair(new PairName(request.navigator()), new PairName(request.driver())); + final MissionUrl missionUrl = new MissionUrl(request.missionUrl()); + return new PairRoom(status, pair, missionUrl, accessCode); + } + + private AccessCode generateAccessCode() { + final String generatedAccessCode = uuidAccessCodeGenerator.generate(); + if (pairRoomRepository.existsByAccessCode(generatedAccessCode)) { + return generateAccessCode(); + } + return new AccessCode(generatedAccessCode); + } + @Transactional - public void saveTimerDuration(final String accessCode, final TimerDurationCreateRequest request) { - final PairRoom pairRoom = pairRoomRepository.fetchByAccessCode(new AccessCode(accessCode)); + public void updateNavigatorWithDriver(final String accessCode) { + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + checkDeletePairRoom(pairRoomEntity); + pairRoomEntity.swapNavigatorWithDriver(); + } - if (pairRoomRepository.updateTimerDuration(pairRoom.getId(), request.timerDuration()) != UPDATED_ROW_COUNT) { - throw new InvalidTimerDurationException("타이머 μ‹œκ°„μ„ μ €μž₯ν•˜λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); + private void checkDeletePairRoom(final PairRoomEntity pairRoomEntity) { + if (pairRoomEntity.isDelete()) { + throw new DeletePairRoomException("μ‚­μ œλœ νŽ˜μ–΄λ£Έμž…λ‹ˆλ‹€."); } } - public PairRoom findByAccessCode(final String accessCode) { - return pairRoomRepository.fetchByAccessCode(new AccessCode(accessCode)); + @Transactional + public void updatePairRoomStatus(final String accessCode, final String statusName) { + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + checkDeletePairRoom(pairRoomEntity); + final PairRoomStatus status = PairRoomStatus.findByName(statusName); + pairRoomEntity.updateStatus(status); + } + + public PairRoomReadResponse findPairRoomAndTimer(final String accessCode) { + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + checkDeletePairRoom(pairRoomEntity); + final TimerEntity timerEntity = timerRepository.fetchTimerByPairRoomEntity(pairRoomEntity); + return PairRoomReadResponse.of(pairRoomEntity.toDomain(), timerEntity.toDomain()); + } + + public List findPairRooms(final String token) { + final Member member = memberService.findMemberByCredential(token); + + final List pairRooms = pairRoomMemberRepository.findByMember(member); + final List pairRoomEntities = pairRooms.stream() + .map(PairRoomMemberEntity::getPairRoom) + .filter(pairRoomEntity -> !pairRoomEntity.isDelete()) + .toList(); + + return pairRoomEntities.stream() + .map(PairRoomMemberResponse::from) + .toList(); } @Transactional public void deletePairRoom(final String accessCode) { - final PairRoom pairRoom = pairRoomRepository.fetchByAccessCode(new AccessCode(accessCode)); - pairRoomRepository.delete(pairRoom); + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + checkDeletePairRoom(pairRoomEntity); + pairRoomEntity.updateStatus(PairRoomStatus.DELETED); } } diff --git a/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomCreateRequest.java b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomCreateRequest.java new file mode 100644 index 00000000..68ac00b8 --- /dev/null +++ b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomCreateRequest.java @@ -0,0 +1,35 @@ +package site.coduo.pairroom.service.dto; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "νŽ˜μ–΄λ£Έ 생성 μš”μ²­ λ°”λ””") +public record PairRoomCreateRequest( + @Schema(description = "λ‚΄λΉ„κ²Œμ΄ν„° 이름") + @NotBlank + String navigator, + + @Schema(description = "λ“œλΌμ΄λ²„ 이름") + @NotBlank + String driver, + + @Schema(description = "타이머 μ‹œκ°„") + @Min(value = 1, message = "타이머 μ‹œκ°„μ€ 0보닀 μ»€μ•Όν•©λ‹ˆλ‹€.") + long timerDuration, + + @Schema(description = "타이머 남은 μ‹œκ°„") + @Min(value = 1, message = "타이머 남은 μ‹œκ°„μ€ 0보닀 μ»€μ•Όν•©λ‹ˆλ‹€.") + long timerRemainingTime, + + @Schema(description = "λ―Έμ…˜ 리포지토리 링크. 'κ·Έλƒ₯ μ‹œμž‘ν• λž˜μš”'둜 μƒμ„±ν•˜λ©΄ 빈 λ¬Έμžμ—΄") + @NotNull + String missionUrl, + + @Schema(description = "νŽ˜μ–΄λ£Έμ˜ μƒνƒœ", example = "IN_PROGRESS") + @NotBlank + String status +) { +} diff --git a/backend/src/main/java/site/coduo/pairroom/dto/PairRoomCreateResponse.java b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomCreateResponse.java similarity index 85% rename from backend/src/main/java/site/coduo/pairroom/dto/PairRoomCreateResponse.java rename to backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomCreateResponse.java index 11dc5389..45ef4672 100644 --- a/backend/src/main/java/site/coduo/pairroom/dto/PairRoomCreateResponse.java +++ b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomCreateResponse.java @@ -1,4 +1,4 @@ -package site.coduo.pairroom.dto; +package site.coduo.pairroom.service.dto; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/backend/src/main/java/site/coduo/pairroom/dto/PairRoomDeleteRequest.java b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomDeleteRequest.java similarity index 88% rename from backend/src/main/java/site/coduo/pairroom/dto/PairRoomDeleteRequest.java rename to backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomDeleteRequest.java index 78f2e41c..2b38d5ba 100644 --- a/backend/src/main/java/site/coduo/pairroom/dto/PairRoomDeleteRequest.java +++ b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomDeleteRequest.java @@ -1,4 +1,4 @@ -package site.coduo.pairroom.dto; +package site.coduo.pairroom.service.dto; import jakarta.validation.constraints.NotBlank; diff --git a/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomExistResponse.java b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomExistResponse.java new file mode 100644 index 00000000..ccbb5968 --- /dev/null +++ b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomExistResponse.java @@ -0,0 +1,7 @@ +package site.coduo.pairroom.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "νŽ˜μ–΄λ£Έ 쑴재 μ—¬λΆ€ 확인 응닡 λ°”λ””") +public record PairRoomExistResponse(@Schema(description = "νŽ˜μ–΄λ£Έ 쑴재 확인") boolean exists) { +} diff --git a/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomMemberResponse.java b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomMemberResponse.java new file mode 100644 index 00000000..a24b01a4 --- /dev/null +++ b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomMemberResponse.java @@ -0,0 +1,17 @@ +package site.coduo.pairroom.service.dto; + +import site.coduo.pairroom.repository.PairRoomEntity; + +public record PairRoomMemberResponse( + Long id, + String status, + String navigator, + String driver, + String accessCode +) { + + public static PairRoomMemberResponse from(final PairRoomEntity pairRoom) { + return new PairRoomMemberResponse(pairRoom.getId(), pairRoom.getStatus().name(), pairRoom.getNavigator(), + pairRoom.getDriver(), pairRoom.getAccessCode()); + } +} diff --git a/backend/src/main/java/site/coduo/pairroom/dto/PairRoomReadRequest.java b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomReadRequest.java similarity index 88% rename from backend/src/main/java/site/coduo/pairroom/dto/PairRoomReadRequest.java rename to backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomReadRequest.java index 7daf93c5..e74940f9 100644 --- a/backend/src/main/java/site/coduo/pairroom/dto/PairRoomReadRequest.java +++ b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomReadRequest.java @@ -1,4 +1,4 @@ -package site.coduo.pairroom.dto; +package site.coduo.pairroom.service.dto; import jakarta.validation.constraints.NotBlank; diff --git a/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomReadResponse.java b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomReadResponse.java new file mode 100644 index 00000000..0b70198e --- /dev/null +++ b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomReadResponse.java @@ -0,0 +1,38 @@ +package site.coduo.pairroom.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import site.coduo.pairroom.domain.PairRoom; +import site.coduo.timer.domain.Timer; + +@Schema(description = "νŽ˜μ–΄λ£Έ 쑰회 응닡 λ°”λ””") +public record PairRoomReadResponse( + @Schema(description = "첫 번째 νŽ˜μ–΄μ˜ 이름", example = "ν•΄μ‹œ") + String navigator, + + @Schema(description = "두 번째 νŽ˜μ–΄μ˜ 이름", example = "파슬리") + String driver, + + @Schema(description = "νŽ˜μ–΄λ£Έμ˜ μƒνƒœ", example = "IN_PROGRESS") + String status, + + @Schema(description = "타이머 μ‹œκ°„ (millisecond κΈ°μ€€)", example = "10000") + long duration, + + @Schema(description = "타이머 남은 μ‹œκ°„ (millisecond κΈ°μ€€)", example = "5000") + long remainingTime, + + @Schema(description = "λ―Έμ…˜ 리포지토리 링크", example = "https://github.com/coduo-missions/coduo-javascript-rps") + String missionUrl +) { + + public static PairRoomReadResponse of(final PairRoom pairRoom, final Timer timer) { + return new PairRoomReadResponse( + pairRoom.getNavigatorName(), + pairRoom.getDriverName(), + pairRoom.getStatus().name(), + timer.getDuration(), + timer.getRemainingTime(), + pairRoom.getMissionUrl() + ); + } +} diff --git a/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomStatusUpdateRequest.java b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomStatusUpdateRequest.java new file mode 100644 index 00000000..a15948e2 --- /dev/null +++ b/backend/src/main/java/site/coduo/pairroom/service/dto/PairRoomStatusUpdateRequest.java @@ -0,0 +1,13 @@ +package site.coduo.pairroom.service.dto; + +import jakarta.validation.constraints.NotBlank; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "νŽ˜μ–΄λ£Έ μƒνƒœ λ³€κ²½ μš”μ²­ λ°”λ””") +public record PairRoomStatusUpdateRequest( + @Schema(description = "λ³€κ²½ν•  νŽ˜μ–΄λ£Έ μƒνƒœ", example = "IN_PROGRESS") + @NotBlank + String status +) { +} diff --git a/backend/src/main/java/site/coduo/pairroom/dto/TimerDurationCreateRequest.java b/backend/src/main/java/site/coduo/pairroom/service/dto/TimerDurationCreateRequest.java similarity index 87% rename from backend/src/main/java/site/coduo/pairroom/dto/TimerDurationCreateRequest.java rename to backend/src/main/java/site/coduo/pairroom/service/dto/TimerDurationCreateRequest.java index 1de4b67d..7944a417 100644 --- a/backend/src/main/java/site/coduo/pairroom/dto/TimerDurationCreateRequest.java +++ b/backend/src/main/java/site/coduo/pairroom/service/dto/TimerDurationCreateRequest.java @@ -1,4 +1,4 @@ -package site.coduo.pairroom.dto; +package site.coduo.pairroom.service.dto; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/backend/src/main/java/site/coduo/pairroom/service/port/PairRoomRepository.java b/backend/src/main/java/site/coduo/pairroom/service/port/PairRoomRepository.java deleted file mode 100644 index e1bdfccc..00000000 --- a/backend/src/main/java/site/coduo/pairroom/service/port/PairRoomRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package site.coduo.pairroom.service.port; - -import java.util.Optional; - -import site.coduo.pairroom.domain.PairRoom; -import site.coduo.pairroom.domain.accesscode.AccessCode; - -public interface PairRoomRepository { - - Optional findById(Long id); - - Optional findByAccessCode(AccessCode accessCode); - - PairRoom save(PairRoom pairRoom); -} diff --git a/backend/src/main/java/site/coduo/pairroomhistory/controller/PairRoomHistoryController.java b/backend/src/main/java/site/coduo/pairroomhistory/controller/PairRoomHistoryController.java deleted file mode 100644 index a8dbb088..00000000 --- a/backend/src/main/java/site/coduo/pairroomhistory/controller/PairRoomHistoryController.java +++ /dev/null @@ -1,58 +0,0 @@ -package site.coduo.pairroomhistory.controller; - -import java.net.URI; - -import jakarta.validation.Valid; - -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - -import lombok.RequiredArgsConstructor; -import site.coduo.pairroomhistory.controller.docs.PairRoomHistoryDocs; -import site.coduo.pairroomhistory.dto.PairRoomHistoryCreateRequest; -import site.coduo.pairroomhistory.dto.PairRoomHistoryReadResponse; -import site.coduo.pairroomhistory.dto.PairRoomHistoryUpdateRequest; -import site.coduo.pairroomhistory.service.PairRoomHistoryService; - -@RequiredArgsConstructor -@RestController -public class PairRoomHistoryController implements PairRoomHistoryDocs { - - private final PairRoomHistoryService pairRoomHistoryService; - - @PostMapping("/{accessCode}/history") - public ResponseEntity createPairRoomHistory( - @PathVariable("accessCode") final String accessCode, - @Valid @RequestBody final PairRoomHistoryCreateRequest request - ) { - pairRoomHistoryService.createPairRoomHistory(accessCode, request); - - return ResponseEntity.created(URI.create("/")) - .build(); - } - - @PatchMapping("/{accessCode}/history/latest/timer-remaining-time") - public ResponseEntity updateTimerRemainingTime( - @PathVariable("accessCode") final String accessCode, - @Valid @RequestBody final PairRoomHistoryUpdateRequest request - ) { - pairRoomHistoryService.updateTimerRemainingTimeHistory(accessCode, request); - - return ResponseEntity.ok() - .build(); - } - - @GetMapping("/{accessCode}/history/latest") - public ResponseEntity getPairRoomHistory( - @PathVariable("accessCode") final String accessCode - ) { - final PairRoomHistoryReadResponse response = pairRoomHistoryService.readLatestPairRoomHistory(accessCode); - - return ResponseEntity.ok(response); - } -} diff --git a/backend/src/main/java/site/coduo/pairroomhistory/controller/docs/PairRoomHistoryDocs.java b/backend/src/main/java/site/coduo/pairroomhistory/controller/docs/PairRoomHistoryDocs.java deleted file mode 100644 index 0a8e308c..00000000 --- a/backend/src/main/java/site/coduo/pairroomhistory/controller/docs/PairRoomHistoryDocs.java +++ /dev/null @@ -1,44 +0,0 @@ -package site.coduo.pairroomhistory.controller.docs; - -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import site.coduo.common.controller.response.ApiErrorResponse; -import site.coduo.pairroomhistory.dto.PairRoomHistoryCreateRequest; -import site.coduo.pairroomhistory.dto.PairRoomHistoryReadResponse; -import site.coduo.pairroomhistory.dto.PairRoomHistoryUpdateRequest; - -@Tag(name = "νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬ API") -public interface PairRoomHistoryDocs { - - @Operation(summary = "νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬λ₯Ό μƒμ„±ν•œλ‹€.") - @ApiResponse(responseCode = "201", description = "νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬ 생성 성곡", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE)) - @ApiResponse(responseCode = "4xx", description = "νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬ 생성 μ‹€νŒ¨", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))) - ResponseEntity createPairRoomHistory( - String accessCode, - @Parameter(description = "νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬ 생성 μš”μ²­", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE), required = true) - PairRoomHistoryCreateRequest request - ); - - @Operation(summary = "μ΅œμ‹  νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬μ˜ 타이머 남은 μ‹œκ°„μ„ μ—…λ°μ΄νŠΈν•œλ‹€.") - @ApiResponse(responseCode = "200", description = "νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬ 타이머 남은 μ‹œκ°„ μ—…λ°μ΄νŠΈ 성곡", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE)) - @ApiResponse(responseCode = "4xx", description = "νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬ 타이머 남은 μ‹œκ°„ μ—…λ°μ΄νŠΈ μ‹€νŒ¨", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))) - ResponseEntity updateTimerRemainingTime( - String accessCode, - @Parameter(description = "νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬ 타이머 남은 μ‹œκ°„ μ—…λ°μ΄νŠΈ μš”μ²­", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE), required = true) - PairRoomHistoryUpdateRequest request - ); - - @Operation(summary = "μ΅œμ‹  νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬λ₯Ό μ‘°νšŒν•œλ‹€.") - @ApiResponse(responseCode = "200", description = "νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬ 쑰회 성곡", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = PairRoomHistoryReadResponse.class))) - @ApiResponse(responseCode = "4xx", description = "νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬ 생성 μ‹€νŒ¨", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))) - ResponseEntity getPairRoomHistory( - String accessCode - ); -} diff --git a/backend/src/main/java/site/coduo/pairroomhistory/domain/PairRoomHistory.java b/backend/src/main/java/site/coduo/pairroomhistory/domain/PairRoomHistory.java deleted file mode 100644 index 06ffe4be..00000000 --- a/backend/src/main/java/site/coduo/pairroomhistory/domain/PairRoomHistory.java +++ /dev/null @@ -1,51 +0,0 @@ -package site.coduo.pairroomhistory.domain; - -import java.util.Objects; - -import lombok.Builder; -import lombok.Getter; -import site.coduo.pairroom.domain.PairRoom; - -@Getter -public class PairRoomHistory { - - private final PairRoom pairRoom; - private final String driver; - private final String navigator; - private final int timerRound; - private final long timerRemainingTime; - - @Builder - private PairRoomHistory( - final PairRoom pairRoom, - final String driver, - final String navigator, - final int timerRound, - final long timerRemainingTime - ) { - this.pairRoom = pairRoom; - this.driver = driver; - this.navigator = navigator; - this.timerRound = timerRound; - this.timerRemainingTime = timerRemainingTime; - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final PairRoomHistory that = (PairRoomHistory) o; - return timerRound == that.timerRound && timerRemainingTime == that.timerRemainingTime && Objects.equals( - pairRoom, that.pairRoom) && Objects.equals(driver, that.driver) && Objects.equals( - navigator, that.navigator); - } - - @Override - public int hashCode() { - return Objects.hash(pairRoom, driver, navigator, timerRound, timerRemainingTime); - } -} diff --git a/backend/src/main/java/site/coduo/pairroomhistory/dto/PairRoomHistoryCreateRequest.java b/backend/src/main/java/site/coduo/pairroomhistory/dto/PairRoomHistoryCreateRequest.java deleted file mode 100644 index 60d5e680..00000000 --- a/backend/src/main/java/site/coduo/pairroomhistory/dto/PairRoomHistoryCreateRequest.java +++ /dev/null @@ -1,26 +0,0 @@ -package site.coduo.pairroomhistory.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬ 생성 μš”μ²­ λ°”λ””") -public record PairRoomHistoryCreateRequest( - @Schema(description = "λ“œλΌμ΄λ²„ 이름", example = "레λͺ¨λ„€") - @NotBlank - String driver, - - @Schema(description = "λ„€λΉ„κ²Œμ΄ν„° 이름", example = "ν•΄μ‹œ") - @NotBlank - String navigator, - - @Schema(description = "타이머가 λͺ‡ 번 λ°˜λ³΅λ˜μ—ˆλŠ”μ§€ λ‚˜νƒ€λ‚΄λŠ” 타이머 λΌμš΄λ“œ", example = "1") - @NotNull - int timerRound, - - @Schema(description = "타이머가 μ’…λ£Œλ˜κΈ°κΉŒμ§€ 남은 μ‹œκ°„. μ‹œκ°„μ€ millisecond κΈ°μ€€μœΌλ‘œ μ €μž₯ν•œλ‹€.", example = "60000") - @NotNull - long timerRemainingTime -) { -} diff --git a/backend/src/main/java/site/coduo/pairroomhistory/dto/PairRoomHistoryReadResponse.java b/backend/src/main/java/site/coduo/pairroomhistory/dto/PairRoomHistoryReadResponse.java deleted file mode 100644 index 3df9d071..00000000 --- a/backend/src/main/java/site/coduo/pairroomhistory/dto/PairRoomHistoryReadResponse.java +++ /dev/null @@ -1,41 +0,0 @@ -package site.coduo.pairroomhistory.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; - -import io.swagger.v3.oas.annotations.media.Schema; -import site.coduo.pairroomhistory.domain.PairRoomHistory; - -@Schema(description = "νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬ 쑰회 응닡 λ°”λ””") -public record PairRoomHistoryReadResponse( - @Schema(description = "νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬ μ‹λ³„μž", example = "1") - @NotNull - long id, - - @Schema(description = "λ“œλΌμ΄λ²„ 이름", example = "νŒŒλž€") - @NotBlank - String driver, - - @Schema(description = "λ„€λΉ„κ²Œμ΄ν„° 이름", example = "파슬리") - @NotBlank - String navigator, - - @Schema(description = "타이머가 λͺ‡ 번 λ°˜λ³΅λ˜μ—ˆλŠ”μ§€ λ‚˜νƒ€λ‚΄λŠ” 타이머 λΌμš΄λ“œ", example = "1") - @NotNull - int timerRound, - - @Schema(description = "타이머가 μ’…λ£Œλ˜κΈ°κΉŒμ§€ 남은 μ‹œκ°„. μ‹œκ°„μ€ millisecond κΈ°μ€€μœΌλ‘œ μ €μž₯ν•œλ‹€.", example = "60000") - @NotNull - long timerRemainingTime -) { - - public static PairRoomHistoryReadResponse of(final long id, final PairRoomHistory pairRoomHistory) { - return new PairRoomHistoryReadResponse( - id, - pairRoomHistory.getDriver(), - pairRoomHistory.getNavigator(), - pairRoomHistory.getTimerRound(), - pairRoomHistory.getTimerRemainingTime() - ); - } -} diff --git a/backend/src/main/java/site/coduo/pairroomhistory/dto/PairRoomHistoryUpdateRequest.java b/backend/src/main/java/site/coduo/pairroomhistory/dto/PairRoomHistoryUpdateRequest.java deleted file mode 100644 index 305ae0d9..00000000 --- a/backend/src/main/java/site/coduo/pairroomhistory/dto/PairRoomHistoryUpdateRequest.java +++ /dev/null @@ -1,13 +0,0 @@ -package site.coduo.pairroomhistory.dto; - -import jakarta.validation.constraints.NotNull; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "νŽ˜μ–΄λ£Έ 졜근 νžˆμŠ€ν† λ¦¬ 타이머 남은 μ‹œκ°„ μ—…λ°μ΄νŠΈ μš”μ²­ λ°”λ””") -public record PairRoomHistoryUpdateRequest( - @Schema(description = "타이머가 μ’…λ£Œλ˜κΈ°κΉŒμ§€ 남은 μ‹œκ°„. 이 μ‹œκ°„μœΌλ‘œ νŽ˜μ–΄λ£Έ 졜근 νžˆμŠ€ν† λ¦¬κ°€ μ—…λ°μ΄νŠΈ λœλ‹€.", example = "60000") - @NotNull - long timerRemainingTime -) { -} diff --git a/backend/src/main/java/site/coduo/pairroomhistory/exception/PairRoomHistoryException.java b/backend/src/main/java/site/coduo/pairroomhistory/exception/PairRoomHistoryException.java deleted file mode 100644 index b580595e..00000000 --- a/backend/src/main/java/site/coduo/pairroomhistory/exception/PairRoomHistoryException.java +++ /dev/null @@ -1,8 +0,0 @@ -package site.coduo.pairroomhistory.exception; - -public class PairRoomHistoryException extends RuntimeException { - - public PairRoomHistoryException(final String message) { - super(message); - } -} diff --git a/backend/src/main/java/site/coduo/pairroomhistory/exception/PairRoomHistoryNotFoundException.java b/backend/src/main/java/site/coduo/pairroomhistory/exception/PairRoomHistoryNotFoundException.java deleted file mode 100644 index 6eeaebb5..00000000 --- a/backend/src/main/java/site/coduo/pairroomhistory/exception/PairRoomHistoryNotFoundException.java +++ /dev/null @@ -1,8 +0,0 @@ -package site.coduo.pairroomhistory.exception; - -public class PairRoomHistoryNotFoundException extends PairRoomHistoryException { - - public PairRoomHistoryNotFoundException(final String message) { - super(message); - } -} diff --git a/backend/src/main/java/site/coduo/pairroomhistory/repository/PairRoomHistoryEntity.java b/backend/src/main/java/site/coduo/pairroomhistory/repository/PairRoomHistoryEntity.java deleted file mode 100644 index f7617bae..00000000 --- a/backend/src/main/java/site/coduo/pairroomhistory/repository/PairRoomHistoryEntity.java +++ /dev/null @@ -1,81 +0,0 @@ -package site.coduo.pairroomhistory.repository; - -import java.util.Objects; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; - -import lombok.Getter; -import lombok.NoArgsConstructor; -import site.coduo.common.infrastructure.audit.entity.BaseTimeEntity; -import site.coduo.pairroom.domain.PairRoom; -import site.coduo.pairroomhistory.domain.PairRoomHistory; - -@Getter -@NoArgsConstructor -@Table(name = "PAIR_ROOM_HISTORY") -@Entity -public class PairRoomHistoryEntity extends BaseTimeEntity { - - @Id - @Column(name = "ID", nullable = false) - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "PAIR_ROOM_ID", nullable = false) - private PairRoom pairRoom; - - @Column(name = "DRIVER", nullable = false) - private String driver; - - @Column(name = "NAVIGATOR", nullable = false) - private String navigator; - - @Column(name = "TIMER_ROUND", nullable = false) - private int timerRound; - - @Column(name = "TIMER_REMAINING_TIME", nullable = false) - private long timerRemainingTime; - - public PairRoomHistoryEntity(final PairRoomHistory pairRoomHistory) { - this.pairRoom = pairRoomHistory.getPairRoom(); - this.driver = pairRoomHistory.getDriver(); - this.navigator = pairRoomHistory.getNavigator(); - this.timerRound = pairRoomHistory.getTimerRound(); - this.timerRemainingTime = pairRoomHistory.getTimerRemainingTime(); - } - - public PairRoomHistory toDomain() { - return PairRoomHistory.builder() - .pairRoom(pairRoom) - .driver(driver) - .navigator(navigator) - .timerRound(timerRound) - .timerRemainingTime(timerRemainingTime) - .build(); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final PairRoomHistoryEntity that = (PairRoomHistoryEntity) o; - return Objects.equals(id, that.id); - } - - @Override - public int hashCode() { - return Objects.hash(id); - } -} diff --git a/backend/src/main/java/site/coduo/pairroomhistory/repository/PairRoomHistoryRepository.java b/backend/src/main/java/site/coduo/pairroomhistory/repository/PairRoomHistoryRepository.java deleted file mode 100644 index adc15aa3..00000000 --- a/backend/src/main/java/site/coduo/pairroomhistory/repository/PairRoomHistoryRepository.java +++ /dev/null @@ -1,30 +0,0 @@ -package site.coduo.pairroomhistory.repository; - -import java.util.Optional; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import site.coduo.pairroomhistory.exception.PairRoomHistoryNotFoundException; - -public interface PairRoomHistoryRepository extends JpaRepository { - - default PairRoomHistoryEntity fetchTopByPairRoomIdOrderByCreatedAtDesc(long pairRoomId) { - return findTopByPairRoomIdOrderByCreatedAtDesc(pairRoomId) - .orElseThrow(() -> new PairRoomHistoryNotFoundException("ν•΄λ‹Ή νŽ˜μ–΄λ£Έμ˜ νžˆμŠ€ν† λ¦¬κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")); - } - - Optional findTopByPairRoomIdOrderByCreatedAtDesc(long pairRoomId); - - default void updateByPairRoomIdLatestTimerRemainingTime(long pairRoomId, long timerRemainingTime) { - PairRoomHistoryEntity pairRoomHistoryEntity = fetchTopByPairRoomIdOrderByCreatedAtDesc(pairRoomId); - updateByIdTimerRemainingTime(pairRoomHistoryEntity.getId(), timerRemainingTime); - } - - @Modifying(clearAutomatically = true, flushAutomatically = true) - @Query("UPDATE PairRoomHistoryEntity prh SET prh.timerRemainingTime = :timerRemainingTime " + - "WHERE prh.id = :id") - void updateByIdTimerRemainingTime(@Param("id") long id, @Param("timerRemainingTime") long timerRemainingTime); -} diff --git a/backend/src/main/java/site/coduo/pairroomhistory/service/PairRoomHistoryService.java b/backend/src/main/java/site/coduo/pairroomhistory/service/PairRoomHistoryService.java deleted file mode 100644 index b6cf7852..00000000 --- a/backend/src/main/java/site/coduo/pairroomhistory/service/PairRoomHistoryService.java +++ /dev/null @@ -1,50 +0,0 @@ -package site.coduo.pairroomhistory.service; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import lombok.RequiredArgsConstructor; -import site.coduo.pairroom.domain.PairRoom; -import site.coduo.pairroom.service.PairRoomService; -import site.coduo.pairroomhistory.domain.PairRoomHistory; -import site.coduo.pairroomhistory.dto.PairRoomHistoryCreateRequest; -import site.coduo.pairroomhistory.dto.PairRoomHistoryReadResponse; -import site.coduo.pairroomhistory.dto.PairRoomHistoryUpdateRequest; -import site.coduo.pairroomhistory.repository.PairRoomHistoryEntity; -import site.coduo.pairroomhistory.repository.PairRoomHistoryRepository; - -@Transactional -@RequiredArgsConstructor -@Service -public class PairRoomHistoryService { - - private final PairRoomHistoryRepository pairRoomHistoryRepository; - private final PairRoomService pairRoomService; - - public void createPairRoomHistory(final String accessCode, final PairRoomHistoryCreateRequest request) { - final PairRoom pairRoom = pairRoomService.findByAccessCode(accessCode); - final PairRoomHistory pairRoomHistory = PairRoomHistory.builder() - .pairRoom(pairRoom) - .driver(request.driver()) - .navigator(request.navigator()) - .timerRound(request.timerRound()) - .timerRemainingTime(request.timerRemainingTime()) - .build(); - - pairRoomHistoryRepository.save(new PairRoomHistoryEntity(pairRoomHistory)); - } - - public PairRoomHistoryReadResponse readLatestPairRoomHistory(final String accessCode) { - final PairRoom pairRoom = pairRoomService.findByAccessCode(accessCode); - final PairRoomHistoryEntity pairRoomHistoryEntity = - pairRoomHistoryRepository.fetchTopByPairRoomIdOrderByCreatedAtDesc(pairRoom.getId()); - - return PairRoomHistoryReadResponse.of(pairRoomHistoryEntity.getId(), pairRoomHistoryEntity.toDomain()); - } - - public void updateTimerRemainingTimeHistory(final String accessCode, final PairRoomHistoryUpdateRequest request) { - final PairRoom pairRoom = pairRoomService.findByAccessCode(accessCode); - pairRoomHistoryRepository - .updateByPairRoomIdLatestTimerRemainingTime(pairRoom.getId(), request.timerRemainingTime()); - } -} diff --git a/backend/src/main/java/site/coduo/referencelink/controller/CategoryController.java b/backend/src/main/java/site/coduo/referencelink/controller/CategoryController.java index b3691355..19a9d73b 100644 --- a/backend/src/main/java/site/coduo/referencelink/controller/CategoryController.java +++ b/backend/src/main/java/site/coduo/referencelink/controller/CategoryController.java @@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.RestController; import lombok.RequiredArgsConstructor; +import site.coduo.referencelink.controller.docs.CategoryDocs; import site.coduo.referencelink.service.CategoryService; import site.coduo.referencelink.service.dto.CategoryCreateRequest; import site.coduo.referencelink.service.dto.CategoryCreateResponse; @@ -24,7 +25,7 @@ @RestController @RequiredArgsConstructor -public class CategoryController { +public class CategoryController implements CategoryDocs { private final CategoryService categoryService; @@ -57,12 +58,12 @@ public ResponseEntity updateCategory( return ResponseEntity.ok(response); } - @DeleteMapping("/{accessCode}/category/{categoryName}") + @DeleteMapping("/{accessCode}/category/{categoryId}") public ResponseEntity deleteCategory( @PathVariable("accessCode") String accessCode, - @PathVariable("categoryName") String categoryName + @PathVariable("categoryId") Long categoryId ) { - categoryService.deleteCategory(accessCode, categoryName); + categoryService.deleteCategory(accessCode, categoryId); return ResponseEntity.noContent() .build(); diff --git a/backend/src/main/java/site/coduo/referencelink/controller/ReferenceLinkController.java b/backend/src/main/java/site/coduo/referencelink/controller/ReferenceLinkController.java index 7ea529bb..40f6d31f 100644 --- a/backend/src/main/java/site/coduo/referencelink/controller/ReferenceLinkController.java +++ b/backend/src/main/java/site/coduo/referencelink/controller/ReferenceLinkController.java @@ -15,11 +15,13 @@ import org.springframework.web.bind.annotation.RestController; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import site.coduo.referencelink.controller.docs.ReferenceLinkDocs; import site.coduo.referencelink.service.ReferenceLinkService; import site.coduo.referencelink.service.dto.ReferenceLinkCreateRequest; import site.coduo.referencelink.service.dto.ReferenceLinkResponse; +@Slf4j @RequiredArgsConstructor @RestController public class ReferenceLinkController implements ReferenceLinkDocs { @@ -47,13 +49,13 @@ public ResponseEntity> getReferenceLinks( return ResponseEntity.ok(responses); } - @GetMapping(value = "/{accessCode}/reference-link", params = "categoryName") + @GetMapping(value = "/{accessCode}/reference-link", params = "categoryId") public ResponseEntity> getReferenceLinksOfCategory( @PathVariable("accessCode") final String accessCodeText, - @RequestParam(value = "categoryName") final String categoryName + @RequestParam(value = "categoryId") final Long categoryId ) { - final List responses = referenceLinkService.findReferenceLinksByCategory(accessCodeText, - categoryName); + final List responses = referenceLinkService + .findReferenceLinksByCategory(accessCodeText, categoryId); return ResponseEntity.ok(responses); } @@ -63,7 +65,7 @@ public ResponseEntity deleteReferenceLink( @PathVariable("accessCode") final String accessCodeText, @PathVariable("id") final long id ) { - referenceLinkService.deleteReferenceLink(id); + referenceLinkService.deleteReferenceLink(accessCodeText, id); return ResponseEntity.noContent() .build(); diff --git a/backend/src/main/java/site/coduo/referencelink/controller/ReferenceLinkErrorController.java b/backend/src/main/java/site/coduo/referencelink/controller/ReferenceLinkExceptionHandler.java similarity index 97% rename from backend/src/main/java/site/coduo/referencelink/controller/ReferenceLinkErrorController.java rename to backend/src/main/java/site/coduo/referencelink/controller/ReferenceLinkExceptionHandler.java index d64e2fd9..411b7963 100644 --- a/backend/src/main/java/site/coduo/referencelink/controller/ReferenceLinkErrorController.java +++ b/backend/src/main/java/site/coduo/referencelink/controller/ReferenceLinkExceptionHandler.java @@ -15,7 +15,7 @@ @Slf4j @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) -public class ReferenceLinkErrorController { +public class ReferenceLinkExceptionHandler { @ExceptionHandler(InvalidUrlFormatException.class) public ResponseEntity handleInvalidUrlFormatException(final InvalidUrlFormatException e) { diff --git a/backend/src/main/java/site/coduo/referencelink/controller/docs/CategoryDocs.java b/backend/src/main/java/site/coduo/referencelink/controller/docs/CategoryDocs.java index 9a1f1d75..ba2591b3 100644 --- a/backend/src/main/java/site/coduo/referencelink/controller/docs/CategoryDocs.java +++ b/backend/src/main/java/site/coduo/referencelink/controller/docs/CategoryDocs.java @@ -11,6 +11,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import site.coduo.common.controller.response.ApiErrorResponse; import site.coduo.referencelink.service.dto.CategoryCreateRequest; import site.coduo.referencelink.service.dto.CategoryCreateResponse; @@ -18,6 +19,7 @@ import site.coduo.referencelink.service.dto.CategoryUpdateRequest; import site.coduo.referencelink.service.dto.CategoryUpdateResponse; +@Tag(name = "μΉ΄ν…Œκ³ λ¦¬ API") public interface CategoryDocs { @Operation(summary = "λͺ¨λ“  μΉ΄ν…Œκ³ λ¦¬λ₯Ό μ‘°νšŒν•œλ‹€.") @@ -40,5 +42,5 @@ ResponseEntity updateCategory(@PathVariable("accessCode" @Operation(summary = "μΉ΄ν…Œκ³ λ¦¬λ₯Ό μ‚­μ œν•œλ‹€.") @ApiResponse(responseCode = "204", description = "μΉ΄ν…Œκ³ λ¦¬ 링크 μ‚­μ œ 성곡", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE)) @ApiResponse(responseCode = "4xx", description = "μΉ΄ν…Œκ³ λ¦¬ 링크 μ‚­μ œ μ‹€νŒ¨", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))) - ResponseEntity deleteCategory(String accessCode, String categoryName); + ResponseEntity deleteCategory(String accessCode, Long categoryName); } diff --git a/backend/src/main/java/site/coduo/referencelink/controller/docs/ReferenceLinkDocs.java b/backend/src/main/java/site/coduo/referencelink/controller/docs/ReferenceLinkDocs.java index 476d71e8..8c47316c 100644 --- a/backend/src/main/java/site/coduo/referencelink/controller/docs/ReferenceLinkDocs.java +++ b/backend/src/main/java/site/coduo/referencelink/controller/docs/ReferenceLinkDocs.java @@ -34,7 +34,7 @@ ResponseEntity createReferenceLink( @Operation(summary = "μΉ΄ν…Œκ³ λ¦¬λ‘œ ν•„ν„°λ§λœ 레퍼런슀 링크λ₯Ό μ‘°νšŒν•œλ‹€.") @ApiResponse(responseCode = "200", description = "레퍼런슀 링크 쑰회 성곡", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ReferenceLinkResponse.class))) @ApiResponse(responseCode = "4xx", description = "레퍼런슀 링크 쑰회 μ‹€νŒ¨", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))) - ResponseEntity> getReferenceLinksOfCategory(String accessCodeText, String categoryName); + ResponseEntity> getReferenceLinksOfCategory(String accessCodeText, Long categoryId); @Operation(summary = "레퍼런슀 링크λ₯Ό μ‚­μ œν•œλ‹€.") @ApiResponse(responseCode = "204", description = "레퍼런슀 링크 μ‚­μ œ 성곡", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE)) diff --git a/backend/src/main/java/site/coduo/referencelink/domain/Category.java b/backend/src/main/java/site/coduo/referencelink/domain/Category.java index 948fde7c..a309ae8d 100644 --- a/backend/src/main/java/site/coduo/referencelink/domain/Category.java +++ b/backend/src/main/java/site/coduo/referencelink/domain/Category.java @@ -8,18 +8,22 @@ @Getter public class Category { - private static final int CATEGORY_NAME_MAX_LENGTH = 15; + private static final int CATEGORY_NAME_MAX_LENGTH = 10; private final String value; public Category(final String value) { validate(value); - this.value = value; + this.value = value.trim(); } private void validate(final String value) { - if (value.length() > CATEGORY_NAME_MAX_LENGTH) { - throw new InvalidCategoryException("μΉ΄ν…Œκ³ λ¦¬ κΈΈμ΄λŠ” " + CATEGORY_NAME_MAX_LENGTH + "자 μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€."); + if (value == null || value.isBlank()) { + throw new InvalidCategoryException("μΉ΄ν…Œκ³ λ¦¬ 값이 λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€."); + } + + if (value.trim().length() > CATEGORY_NAME_MAX_LENGTH) { + throw new InvalidCategoryException(String.format("μΉ΄ν…Œκ³ λ¦¬ κΈΈμ΄λŠ” %d자 μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€.", CATEGORY_NAME_MAX_LENGTH)); } } diff --git a/backend/src/main/java/site/coduo/referencelink/domain/OpenGraph.java b/backend/src/main/java/site/coduo/referencelink/domain/OpenGraph.java index c5717781..cb3b6690 100644 --- a/backend/src/main/java/site/coduo/referencelink/domain/OpenGraph.java +++ b/backend/src/main/java/site/coduo/referencelink/domain/OpenGraph.java @@ -1,5 +1,7 @@ package site.coduo.referencelink.domain; +import java.net.URL; + import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; @@ -45,18 +47,18 @@ public static OpenGraph from(final Document document) { .build(); } - public static OpenGraph of(final Document document, final String domain) { + public static OpenGraph of(final Document document, final URL url) { return OpenGraph.builder() - .headTitle(domain) + .headTitle(url.getHost()) .openGraphTitle(findMetaTag(document, "title")) .description(findMetaTag(document, "description")) .image(findMetaTag(document, "image")) .build(); } - public static OpenGraph from(final String domain) { + public static OpenGraph from(final URL url) { return OpenGraph.builder() - .headTitle(domain) + .headTitle(url.getHost()) .openGraphTitle(DEFAULT_VALUE) .description(DEFAULT_VALUE) .image(DEFAULT_VALUE) diff --git a/backend/src/main/java/site/coduo/referencelink/domain/ReferenceLink.java b/backend/src/main/java/site/coduo/referencelink/domain/ReferenceLink.java index 90dece14..c6df6448 100644 --- a/backend/src/main/java/site/coduo/referencelink/domain/ReferenceLink.java +++ b/backend/src/main/java/site/coduo/referencelink/domain/ReferenceLink.java @@ -1,20 +1,22 @@ package site.coduo.referencelink.domain; +import java.net.URL; + import lombok.Getter; import site.coduo.pairroom.domain.accesscode.AccessCode; @Getter public class ReferenceLink { - private final Url url; + private final URL url; private final AccessCode accessCode; - public ReferenceLink(final Url url, final AccessCode accessCode) { + public ReferenceLink(final URL url, final AccessCode accessCode) { this.url = url; this.accessCode = accessCode; } public String getUrlText() { - return url.getValue(); + return url.toExternalForm(); } } diff --git a/backend/src/main/java/site/coduo/referencelink/domain/Url.java b/backend/src/main/java/site/coduo/referencelink/domain/Url.java deleted file mode 100644 index f9bb29b8..00000000 --- a/backend/src/main/java/site/coduo/referencelink/domain/Url.java +++ /dev/null @@ -1,56 +0,0 @@ -package site.coduo.referencelink.domain; - -import java.io.IOException; -import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; - -import lombok.Getter; -import site.coduo.referencelink.exception.DocumentAccessException; -import site.coduo.referencelink.exception.InvalidUrlFormatException; - -@Getter -public class Url { - - private static final Pattern VALID_REGEX = Pattern.compile( - "https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#()?&//=]*)"); - private static final String DOMAIN_REGEX = "^(?:https?://)?(?:www\\.)?([^:/\\s]+)"; - - private final String value; - - public Url(final String value) { - validate(value); - this.value = value; - } - - private void validate(final String value) { - if (Objects.isNull(value)) { - throw new InvalidUrlFormatException("URL λΉ„μ–΄μžˆμŠ΅λ‹ˆλ‹€."); - } - - if (VALID_REGEX.matcher(value).matches()) { - return; - } - throw new InvalidUrlFormatException("URL이 정해진 μ •κ·œ ν‘œν˜„μ‹κ³Ό λ‹€λ¦…λ‹ˆλ‹€."); - } - - public Document getDocument() { - try { - return Jsoup.connect(value).get(); - } catch (final IOException e) { - throw new DocumentAccessException("URL에 λŒ€ν•œ Documentλ₯Ό 뢈러올 수 μ—†μŠ΅λ‹ˆλ‹€."); - } - } - - public String extractDomain() { - final Pattern pattern = Pattern.compile(DOMAIN_REGEX); - final Matcher matcher = pattern.matcher(value); - if (matcher.find()) { - return matcher.group(1); - } - return OpenGraph.DEFAULT_VALUE; - } -} diff --git a/backend/src/main/java/site/coduo/referencelink/repository/CategoryEntity.java b/backend/src/main/java/site/coduo/referencelink/repository/CategoryEntity.java index 7089d39a..3d059d5a 100644 --- a/backend/src/main/java/site/coduo/referencelink/repository/CategoryEntity.java +++ b/backend/src/main/java/site/coduo/referencelink/repository/CategoryEntity.java @@ -15,7 +15,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import site.coduo.common.infrastructure.audit.entity.BaseTimeEntity; -import site.coduo.pairroom.domain.PairRoom; +import site.coduo.pairroom.repository.PairRoomEntity; import site.coduo.referencelink.domain.Category; @Getter @@ -32,11 +32,11 @@ public class CategoryEntity extends BaseTimeEntity { private String categoryName; @ManyToOne - @JoinColumn(name = "PAIR_ROOM", nullable = false) - private PairRoom pairRoom; + @JoinColumn(name = "PAIR_ROOM_ID", referencedColumnName = "ID", nullable = false) + private PairRoomEntity pairRoomEntity; - public CategoryEntity(final PairRoom pairRoom, final Category category) { - this.pairRoom = pairRoom; + public CategoryEntity(final PairRoomEntity pairRoomEntity, final Category category) { + this.pairRoomEntity = pairRoomEntity; this.categoryName = category.getValue(); } @@ -60,4 +60,13 @@ public boolean equals(final Object o) { public int hashCode() { return Objects.hashCode(getId()); } + + @Override + public String toString() { + return "CategoryEntity{" + + "id=" + id + + ", categoryId='" + categoryName + '\'' + + ", pairRoom=" + pairRoomEntity + + '}'; + } } diff --git a/backend/src/main/java/site/coduo/referencelink/repository/CategoryRepository.java b/backend/src/main/java/site/coduo/referencelink/repository/CategoryRepository.java index 7a95e37a..ed276772 100644 --- a/backend/src/main/java/site/coduo/referencelink/repository/CategoryRepository.java +++ b/backend/src/main/java/site/coduo/referencelink/repository/CategoryRepository.java @@ -5,21 +5,23 @@ import org.springframework.data.jpa.repository.JpaRepository; -import site.coduo.pairroom.domain.PairRoom; +import site.coduo.pairroom.repository.PairRoomEntity; import site.coduo.referencelink.exception.CategoryNotFoundException; public interface CategoryRepository extends JpaRepository { - Optional findByPairRoomAndCategoryName(PairRoom pairRoom, String categoryName); + Optional findByPairRoomEntityAndId(PairRoomEntity pairRoomEntity, Long id); - default CategoryEntity fetchByPairRoomAndCategoryName(PairRoom pairRoom, String categoryName) { - return findByPairRoomAndCategoryName(pairRoom, categoryName) + default CategoryEntity fetchByPairRoomAndCategoryId(PairRoomEntity pairRoomEntity, Long id) { + return findByPairRoomEntityAndId(pairRoomEntity, id) .orElseThrow(() -> new CategoryNotFoundException("μ‘΄μž¬ν•˜μ§€ μ•Šμ€ μΉ΄ν…Œκ³ λ¦¬μž…λ‹ˆλ‹€.")); } - List findAllByPairRoom(PairRoom pairRoom); + List findAllByPairRoomEntity(PairRoomEntity pairRoomEntity); - boolean existsByCategoryNameAndPairRoom(String categoryName, PairRoom pairRoom); + boolean existsByCategoryNameAndPairRoomEntity(String categoryName, PairRoomEntity pairRoomEntity); - void deleteCategoryByPairRoomAndCategoryName(PairRoom pairRoom, String categoryName); + boolean existsByIdAndPairRoomEntity(Long id, PairRoomEntity pairRoomEntity); + + void deleteCategoryByPairRoomEntityAndId(PairRoomEntity pairRoomEntity, Long id); } diff --git a/backend/src/main/java/site/coduo/referencelink/repository/OpenGraphRepository.java b/backend/src/main/java/site/coduo/referencelink/repository/OpenGraphRepository.java index 883e6b69..e1df1a33 100644 --- a/backend/src/main/java/site/coduo/referencelink/repository/OpenGraphRepository.java +++ b/backend/src/main/java/site/coduo/referencelink/repository/OpenGraphRepository.java @@ -6,7 +6,7 @@ public interface OpenGraphRepository extends JpaRepository { - void deleteByReferenceLinkEntityId(Long referenceLinkEntityId); + void deleteByReferenceLinkEntity(ReferenceLinkEntity referenceLinkEntity); Optional findByReferenceLinkEntityId(Long id); } diff --git a/backend/src/main/java/site/coduo/referencelink/repository/ReferenceLinkEntity.java b/backend/src/main/java/site/coduo/referencelink/repository/ReferenceLinkEntity.java index 61b00a93..b30fc0c2 100644 --- a/backend/src/main/java/site/coduo/referencelink/repository/ReferenceLinkEntity.java +++ b/backend/src/main/java/site/coduo/referencelink/repository/ReferenceLinkEntity.java @@ -15,8 +15,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; import site.coduo.common.infrastructure.audit.entity.BaseTimeEntity; -import site.coduo.pairroom.domain.PairRoom; import site.coduo.pairroom.domain.accesscode.AccessCode; +import site.coduo.pairroom.repository.PairRoomEntity; import site.coduo.referencelink.domain.Category; import site.coduo.referencelink.domain.ReferenceLink; @@ -40,24 +40,24 @@ public class ReferenceLinkEntity extends BaseTimeEntity { @JoinColumn(name = "PAIR_ROOM_ID", referencedColumnName = "ID", nullable = false) @ManyToOne - private PairRoom pairRoom; + private PairRoomEntity pairRoomEntity; public ReferenceLinkEntity(final ReferenceLink referenceLink, final CategoryEntity categoryEntity, - final PairRoom pairRoom) { + final PairRoomEntity pairRoomEntity) { this.url = referenceLink.getUrlText(); this.categoryEntity = categoryEntity; - this.pairRoom = pairRoom; + this.pairRoomEntity = pairRoomEntity; } - public ReferenceLinkEntity(final ReferenceLink referenceLink, final PairRoom pairRoom) { + public ReferenceLinkEntity(final ReferenceLink referenceLink, final PairRoomEntity pairRoomEntity) { this.url = referenceLink.getUrlText(); this.categoryEntity = null; - this.pairRoom = pairRoom; + this.pairRoomEntity = pairRoomEntity; } public boolean isSameAccessCode(final AccessCode accessCode) { - return pairRoom.getAccessCode() - .equals(accessCode); + return pairRoomEntity.getAccessCode() + .equals(accessCode.getValue()); } public boolean isSameCategory(final Category category) { diff --git a/backend/src/main/java/site/coduo/referencelink/repository/ReferenceLinkRepository.java b/backend/src/main/java/site/coduo/referencelink/repository/ReferenceLinkRepository.java index 666ea5dc..9721e5a7 100644 --- a/backend/src/main/java/site/coduo/referencelink/repository/ReferenceLinkRepository.java +++ b/backend/src/main/java/site/coduo/referencelink/repository/ReferenceLinkRepository.java @@ -1,7 +1,18 @@ package site.coduo.referencelink.repository; +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; +import site.coduo.pairroom.repository.PairRoomEntity; +import site.coduo.referencelink.exception.ReferenceLinkException; + public interface ReferenceLinkRepository extends JpaRepository { + List findByPairRoomEntity(PairRoomEntity pairRoomEntity); + + default ReferenceLinkEntity fetchById(long id) { + return findById(id) + .orElseThrow(() -> new ReferenceLinkException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” λ§ν¬μž…λ‹ˆλ‹€.")); + } } diff --git a/backend/src/main/java/site/coduo/referencelink/service/CategoryService.java b/backend/src/main/java/site/coduo/referencelink/service/CategoryService.java index 7663c47d..666125ca 100644 --- a/backend/src/main/java/site/coduo/referencelink/service/CategoryService.java +++ b/backend/src/main/java/site/coduo/referencelink/service/CategoryService.java @@ -6,8 +6,8 @@ import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; -import site.coduo.pairroom.domain.PairRoom; import site.coduo.pairroom.domain.accesscode.AccessCode; +import site.coduo.pairroom.repository.PairRoomEntity; import site.coduo.pairroom.repository.PairRoomRepository; import site.coduo.referencelink.domain.Category; import site.coduo.referencelink.exception.InvalidCategoryException; @@ -31,46 +31,45 @@ public class CategoryService { @Transactional(readOnly = true) public List findAllByPairRoomAccessCode(final String accessCode) { - final PairRoom pairRoom = pairRoomRepository.fetchByAccessCode(new AccessCode(accessCode)); + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(new AccessCode(accessCode)); - return categoryRepository.findAllByPairRoom(pairRoom) + return categoryRepository.findAllByPairRoomEntity(pairRoomEntity) .stream() .map(CategoryReadResponse::from) .toList(); } public CategoryCreateResponse createCategory(final String accessCode, final CategoryCreateRequest request) { - final PairRoom pairRoom = pairRoomRepository.fetchByAccessCode(new AccessCode(accessCode)); - validateDuplicated(request.value(), pairRoom); - final CategoryEntity saved = categoryRepository.save( - new CategoryEntity(pairRoom, new Category(request.value()))); + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(new AccessCode(accessCode)); + validateDuplicated(request.value(), pairRoomEntity); + final CategoryEntity categoryEntity = categoryRepository.save( + new CategoryEntity(pairRoomEntity, new Category(request.value()))); - return new CategoryCreateResponse(saved.getId(), saved.getCategoryName()); + return CategoryCreateResponse.from(categoryEntity); } - private void validateDuplicated(final String categoryName, final PairRoom pairRoom) { - if (categoryRepository.existsByCategoryNameAndPairRoom(categoryName, pairRoom)) { + private void validateDuplicated(final String categoryName, final PairRoomEntity pairRoomEntity) { + if (categoryRepository.existsByCategoryNameAndPairRoomEntity(categoryName, pairRoomEntity)) { throw new InvalidCategoryException("μ€‘λ³΅λœ μ΄λ¦„μ˜ μΉ΄ν…Œκ³ λ¦¬κ°€ 이미 μ‘΄μž¬ν•©λ‹ˆλ‹€."); } } - public CategoryUpdateResponse updateCategoryName(final String accessCode, - final CategoryUpdateRequest request) { - final PairRoom pairRoom = pairRoomRepository.fetchByAccessCode(new AccessCode(accessCode)); - validateDuplicated(request.updatedCategoryName(), pairRoom); - final CategoryEntity category = categoryRepository.fetchByPairRoomAndCategoryName(pairRoom, - request.previousCategoryName()); + public CategoryUpdateResponse updateCategoryName(final String accessCode, final CategoryUpdateRequest request) { + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(new AccessCode(accessCode)); + validateDuplicated(request.updatedCategoryName(), pairRoomEntity); + final CategoryEntity category = categoryRepository.fetchByPairRoomAndCategoryId(pairRoomEntity, + request.categoryId()); category.updateCategoryName(request.updatedCategoryName()); return new CategoryUpdateResponse(category.getCategoryName()); } - public void deleteCategory(final String accessCode, final String categoryName) { - final PairRoom pairRoom = pairRoomRepository.fetchByAccessCode(new AccessCode(accessCode)); - if (categoryRepository.existsByCategoryNameAndPairRoom(categoryName, pairRoom)) { + public void deleteCategory(final String accessCode, final Long categoryId) { + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(new AccessCode(accessCode)); + if (categoryRepository.existsByIdAndPairRoomEntity(categoryId, pairRoomEntity)) { final List referenceLinks = referenceLinkService.findReferenceLinksEntityByCategory( - accessCode, categoryName); + accessCode, categoryId); referenceLinks.forEach(ReferenceLinkEntity::updateCategoryToNull); - categoryRepository.deleteCategoryByPairRoomAndCategoryName(pairRoom, categoryName); + categoryRepository.deleteCategoryByPairRoomEntityAndId(pairRoomEntity, categoryId); } } } diff --git a/backend/src/main/java/site/coduo/referencelink/service/OpenGraphService.java b/backend/src/main/java/site/coduo/referencelink/service/OpenGraphService.java index d487f3fa..5f360fc2 100644 --- a/backend/src/main/java/site/coduo/referencelink/service/OpenGraphService.java +++ b/backend/src/main/java/site/coduo/referencelink/service/OpenGraphService.java @@ -1,7 +1,10 @@ package site.coduo.referencelink.service; +import java.io.IOException; +import java.net.URL; import java.util.Optional; +import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.springframework.stereotype.Service; @@ -9,7 +12,6 @@ import lombok.RequiredArgsConstructor; import site.coduo.referencelink.domain.OpenGraph; -import site.coduo.referencelink.domain.Url; import site.coduo.referencelink.exception.DocumentAccessException; import site.coduo.referencelink.repository.OpenGraphEntity; import site.coduo.referencelink.repository.OpenGraphRepository; @@ -22,26 +24,33 @@ public class OpenGraphService { private final OpenGraphRepository openGraphRepository; - public OpenGraph createOpenGraph(final ReferenceLinkEntity referenceLinkEntity) { - final OpenGraph openGraph = getOpenGraph(referenceLinkEntity); + public OpenGraph createOpenGraph(final ReferenceLinkEntity referenceLinkEntity, final URL url) { + final OpenGraph openGraph = getOpenGraph(url); final OpenGraphEntity openGraphEntity = new OpenGraphEntity(openGraph, referenceLinkEntity); return openGraphRepository.save(openGraphEntity) .toDomain(); } - private OpenGraph getOpenGraph(final ReferenceLinkEntity referenceLinkEntity) { - final Url url = new Url(referenceLinkEntity.getUrl()); + private OpenGraph getOpenGraph(final URL url) { try { - final Document document = url.getDocument(); + final Document document = getDocumentFromUrl(url); return getOpenGraphFromDocument(document, url); } catch (final DocumentAccessException e) { - return OpenGraph.from(url.extractDomain()); + return OpenGraph.from(url); } } - private OpenGraph getOpenGraphFromDocument(final Document document, final Url url) { + private Document getDocumentFromUrl(final URL url) { + try { + return Jsoup.connect(url.toExternalForm()).get(); + } catch (final IOException e) { + throw new DocumentAccessException("URL에 λŒ€ν•œ Documentλ₯Ό 뢈러올 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + } + + private OpenGraph getOpenGraphFromDocument(final Document document, final URL url) { if (hasNoTitle(document)) { - return OpenGraph.of(document, url.extractDomain()); + return OpenGraph.of(document, url); } return OpenGraph.from(document); } @@ -60,7 +69,7 @@ public OpenGraph findOpenGraph(final Long id) { return new OpenGraph(); } - public void deleteByReferenceLinkId(final long referenceLinkEntityId) { - openGraphRepository.deleteByReferenceLinkEntityId(referenceLinkEntityId); + public void deleteByReferenceLink(final ReferenceLinkEntity referenceLinkEntity) { + openGraphRepository.deleteByReferenceLinkEntity(referenceLinkEntity); } } diff --git a/backend/src/main/java/site/coduo/referencelink/service/ReferenceLinkService.java b/backend/src/main/java/site/coduo/referencelink/service/ReferenceLinkService.java index b92b2630..68eec979 100644 --- a/backend/src/main/java/site/coduo/referencelink/service/ReferenceLinkService.java +++ b/backend/src/main/java/site/coduo/referencelink/service/ReferenceLinkService.java @@ -1,18 +1,21 @@ package site.coduo.referencelink.service; +import java.net.MalformedURLException; +import java.net.URL; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; -import site.coduo.pairroom.domain.PairRoom; +import lombok.extern.slf4j.Slf4j; import site.coduo.pairroom.domain.accesscode.AccessCode; +import site.coduo.pairroom.repository.PairRoomEntity; import site.coduo.pairroom.repository.PairRoomRepository; import site.coduo.referencelink.domain.Category; import site.coduo.referencelink.domain.OpenGraph; import site.coduo.referencelink.domain.ReferenceLink; -import site.coduo.referencelink.domain.Url; +import site.coduo.referencelink.exception.InvalidUrlFormatException; import site.coduo.referencelink.repository.CategoryEntity; import site.coduo.referencelink.repository.CategoryRepository; import site.coduo.referencelink.repository.ReferenceLinkEntity; @@ -20,6 +23,7 @@ import site.coduo.referencelink.service.dto.ReferenceLinkCreateRequest; import site.coduo.referencelink.service.dto.ReferenceLinkResponse; +@Slf4j @Transactional @RequiredArgsConstructor @Service @@ -33,32 +37,40 @@ public class ReferenceLinkService { public ReferenceLinkResponse createReferenceLink(final String accessCodeText, final ReferenceLinkCreateRequest request) { final AccessCode accessCode = new AccessCode(accessCodeText); - final PairRoom pairRoom = pairRoomRepository.fetchByAccessCode(accessCode); - final ReferenceLink referenceLink = new ReferenceLink(new Url(request.url()), accessCode); + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + final URL url = makeUrl(request.url()); + final ReferenceLink referenceLink = new ReferenceLink(url, accessCode); - final ReferenceLinkEntity referenceLinkEntity = saveReferenceLink(request, pairRoom, referenceLink); - final OpenGraph openGraph = openGraphService.createOpenGraph(referenceLinkEntity); + final ReferenceLinkEntity referenceLinkEntity = saveReferenceLink(request, pairRoomEntity, referenceLink); + final OpenGraph openGraph = openGraphService.createOpenGraph(referenceLinkEntity, url); return new ReferenceLinkResponse(referenceLinkEntity, openGraph); } - public ReferenceLinkEntity saveReferenceLink(final ReferenceLinkCreateRequest request, - final PairRoom pairRoom, - final ReferenceLink referenceLink + private URL makeUrl(final String requestUrl) { + try { + return new URL(requestUrl); + } catch (final MalformedURLException e) { + throw new InvalidUrlFormatException("링크 ν˜•μ‹μ΄ λ§žμ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + } + } + + private ReferenceLinkEntity saveReferenceLink(final ReferenceLinkCreateRequest request, + final PairRoomEntity pairRoomEntity, + final ReferenceLink referenceLink ) { - if (request.categoryName() == null) { - return referenceLinkRepository.save(new ReferenceLinkEntity(referenceLink, pairRoom)); + if (request.categoryId() == null) { + return referenceLinkRepository.save(new ReferenceLinkEntity(referenceLink, pairRoomEntity)); } - final CategoryEntity categoryEntity = categoryRepository.fetchByPairRoomAndCategoryName( - pairRoom, request.categoryName()); - return referenceLinkRepository.save(new ReferenceLinkEntity(referenceLink, categoryEntity, pairRoom)); + final CategoryEntity categoryEntity = categoryRepository.fetchByPairRoomAndCategoryId( + pairRoomEntity, request.categoryId()); + return referenceLinkRepository.save(new ReferenceLinkEntity(referenceLink, categoryEntity, pairRoomEntity)); } @Transactional(readOnly = true) public List readAllReferenceLink(final String accessCodeText) { - final List referenceLinkEntities = referenceLinkRepository.findAll() - .stream() - .filter(link -> link.isSameAccessCode(new AccessCode(accessCodeText))) - .toList(); + final PairRoomEntity pairRoom = pairRoomRepository.fetchByAccessCode(accessCodeText); + + final List referenceLinkEntities = referenceLinkRepository.findByPairRoomEntity(pairRoom); return referenceLinkEntities.stream() .map(this::makeReferenceLinkResponse) @@ -68,14 +80,16 @@ public List readAllReferenceLink(final String accessCodeT @Transactional(readOnly = true) public List findReferenceLinksByCategory( final String accessCodeText, - final String categoryName + final Long categoryId ) { final AccessCode accessCode = new AccessCode(accessCodeText); - final Category category = new Category(categoryName); + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + final CategoryEntity categoryEntity = categoryRepository.fetchByPairRoomAndCategoryId(pairRoomEntity, + categoryId); + final Category category = new Category(categoryEntity.getCategoryName()); - return referenceLinkRepository.findAll() + return referenceLinkRepository.findByPairRoomEntity(pairRoomEntity) .stream() - .filter(link -> link.isSameAccessCode(accessCode)) .filter(link -> link.isSameCategory(category)) .map(this::makeReferenceLinkResponse) .toList(); @@ -84,14 +98,16 @@ public List findReferenceLinksByCategory( @Transactional(readOnly = true) public List findReferenceLinksEntityByCategory( final String accessCodeText, - final String categoryName + final Long categoryId ) { final AccessCode accessCode = new AccessCode(accessCodeText); - final Category category = new Category(categoryName); + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + final CategoryEntity categoryEntity = categoryRepository.fetchByPairRoomAndCategoryId(pairRoomEntity, + categoryId); + final Category category = new Category(categoryEntity.getCategoryName()); - return referenceLinkRepository.findAll() + return referenceLinkRepository.findByPairRoomEntity(pairRoomEntity) .stream() - .filter(link -> link.isSameAccessCode(accessCode)) .filter(link -> link.isSameCategory(category)) .toList(); } @@ -101,8 +117,11 @@ private ReferenceLinkResponse makeReferenceLinkResponse(final ReferenceLinkEntit return new ReferenceLinkResponse(referenceLinkEntity, openGraph); } - public void deleteReferenceLink(final long id) { - openGraphService.deleteByReferenceLinkId(id); - referenceLinkRepository.deleteById(id); + public void deleteReferenceLink(final String accessCodeText, final long id) { + final ReferenceLinkEntity referenceLinkEntity = referenceLinkRepository.fetchById(id); + if (referenceLinkEntity.isSameAccessCode(new AccessCode(accessCodeText))) { + openGraphService.deleteByReferenceLink(referenceLinkEntity); + referenceLinkRepository.delete(referenceLinkEntity); + } } } diff --git a/backend/src/main/java/site/coduo/referencelink/service/dto/CategoryCreateResponse.java b/backend/src/main/java/site/coduo/referencelink/service/dto/CategoryCreateResponse.java index 8bf1b74f..47202521 100644 --- a/backend/src/main/java/site/coduo/referencelink/service/dto/CategoryCreateResponse.java +++ b/backend/src/main/java/site/coduo/referencelink/service/dto/CategoryCreateResponse.java @@ -1,13 +1,18 @@ package site.coduo.referencelink.service.dto; import io.swagger.v3.oas.annotations.media.Schema; +import site.coduo.referencelink.repository.CategoryEntity; @Schema(description = "μΉ΄ν…Œκ³ λ¦¬ 생성 응닡") public record CategoryCreateResponse( @Schema(description = "μΉ΄ν…Œκ³ λ¦¬ ID") - Long id, + String id, @Schema(description = "μΉ΄ν…Œκ³ λ¦¬ κ°’") String value ) { + + public static CategoryCreateResponse from(final CategoryEntity category) { + return new CategoryCreateResponse(String.valueOf(category.getId()), category.getCategoryName()); + } } diff --git a/backend/src/main/java/site/coduo/referencelink/service/dto/CategoryReadResponse.java b/backend/src/main/java/site/coduo/referencelink/service/dto/CategoryReadResponse.java index 0f05bfca..b16851ef 100644 --- a/backend/src/main/java/site/coduo/referencelink/service/dto/CategoryReadResponse.java +++ b/backend/src/main/java/site/coduo/referencelink/service/dto/CategoryReadResponse.java @@ -6,13 +6,13 @@ @Schema(description = "μΉ΄ν…Œκ³ λ¦¬ 쑰회 응닡") public record CategoryReadResponse( @Schema(description = "μΉ΄ν…Œκ³ λ¦¬ ID", example = "0") - Long id, + String id, @Schema(description = "μΉ΄ν…Œκ³ λ¦¬ κ°’", example = "μΉ΄ν…Œκ³ λ¦¬ μ—†μŒ") String value ) { public static CategoryReadResponse from(final CategoryEntity category) { - return new CategoryReadResponse(category.getId(), category.getCategoryName()); + return new CategoryReadResponse(String.valueOf(category.getId()), category.getCategoryName()); } } diff --git a/backend/src/main/java/site/coduo/referencelink/service/dto/CategoryUpdateRequest.java b/backend/src/main/java/site/coduo/referencelink/service/dto/CategoryUpdateRequest.java index 66477311..07ddae90 100644 --- a/backend/src/main/java/site/coduo/referencelink/service/dto/CategoryUpdateRequest.java +++ b/backend/src/main/java/site/coduo/referencelink/service/dto/CategoryUpdateRequest.java @@ -1,14 +1,15 @@ package site.coduo.referencelink.service.dto; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "μΉ΄ν…Œκ³ λ¦¬ μˆ˜μ • μš”μ²­ λ°”λ””") public record CategoryUpdateRequest( - @Schema(description = "이전 μΉ΄ν…Œκ³ λ¦¬ κ°’", example = "μžλ°”") - @NotBlank(message = "빈 μΉ΄ν…Œκ³ λ¦¬λŠ” ν—ˆμš©ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.") - String previousCategoryName, + @Schema(description = "λ³€κ²½ν•˜κ³ μž ν•˜λŠ” μΉ΄ν…Œκ³ λ¦¬ id", example = "μžλ°”") + @NotNull(message = "빈 μΉ΄ν…Œκ³ λ¦¬λŠ” ν—ˆμš©ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.") + Long categoryId, @Schema(description = "μˆ˜μ •ν•  μΉ΄ν…Œκ³ λ¦¬ κ°’", example = "μŠ€ν”„λ§") @NotBlank(message = "빈 μΉ΄ν…Œκ³ λ¦¬λŠ” ν—ˆμš©ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.") diff --git a/backend/src/main/java/site/coduo/referencelink/service/dto/ReferenceLinkCreateRequest.java b/backend/src/main/java/site/coduo/referencelink/service/dto/ReferenceLinkCreateRequest.java index fba5bccb..218c67fd 100644 --- a/backend/src/main/java/site/coduo/referencelink/service/dto/ReferenceLinkCreateRequest.java +++ b/backend/src/main/java/site/coduo/referencelink/service/dto/ReferenceLinkCreateRequest.java @@ -11,8 +11,8 @@ public record ReferenceLinkCreateRequest( @NotBlank(message = "빈 url은 ν—ˆμš©ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.") String url, - @Schema(description = "μΉ΄ν…Œκ³ λ¦¬ κ°’", example = "μžλ°” 슀크립트") + @Schema(description = "μΉ΄ν…Œκ³ λ¦¬ id", example = "μžλ°” 슀크립트") @Nullable - String categoryName + Long categoryId ) { } diff --git a/backend/src/main/java/site/coduo/sync/config/SchedulerConfig.java b/backend/src/main/java/site/coduo/sync/config/SchedulerConfig.java new file mode 100644 index 00000000..caa70ae4 --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/config/SchedulerConfig.java @@ -0,0 +1,17 @@ +package site.coduo.sync.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +@Configuration +public class SchedulerConfig { + + @Bean + public ThreadPoolTaskScheduler threadPoolTaskScheduler() { + final ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); + scheduler.setPoolSize(100); + scheduler.setWaitForTasksToCompleteOnShutdown(true); + return scheduler; + } +} diff --git a/backend/src/main/java/site/coduo/sync/controller/SseController.java b/backend/src/main/java/site/coduo/sync/controller/SseController.java new file mode 100644 index 00000000..74207e6e --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/controller/SseController.java @@ -0,0 +1,38 @@ +package site.coduo.sync.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import site.coduo.sync.controller.docs.SseDocs; +import site.coduo.sync.service.SseService; + +@Slf4j +@RequiredArgsConstructor +@RestController +public class SseController implements SseDocs { + + private final SseService sseService; + + @GetMapping("/{key}/connect") + public ResponseEntity createConnection(@PathVariable("key") final String key) { + final SseEmitter sseEmitter = sseService.connect(key); + + return ResponseEntity.ok() + .header("X-Accel-Buffering", "no") + .body(sseEmitter); + } + + @DeleteMapping("/{key}/connect") + public ResponseEntity deleteConnection(@PathVariable("key") final String key) { + sseService.disconnectAll(key); + + return ResponseEntity.noContent() + .build(); + } +} diff --git a/backend/src/main/java/site/coduo/sync/controller/SseExceptionHandler.java b/backend/src/main/java/site/coduo/sync/controller/SseExceptionHandler.java new file mode 100644 index 00000000..a4c27f9e --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/controller/SseExceptionHandler.java @@ -0,0 +1,76 @@ +package site.coduo.sync.controller; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import lombok.extern.slf4j.Slf4j; +import site.coduo.common.controller.response.ApiErrorResponse; +import site.coduo.sync.controller.error.SseApiError; +import site.coduo.sync.exception.DuplicateTimestampException; +import site.coduo.sync.exception.NotFoundScheduledFutureException; +import site.coduo.sync.exception.NotFoundSseConnectionException; +import site.coduo.sync.exception.NotFoundTimeStampException; +import site.coduo.sync.exception.SseConnectionDuplicationException; +import site.coduo.sync.exception.SseConnectionFailureException; +import site.coduo.sync.exception.SyncException; + +@Slf4j +@RestControllerAdvice +@Order(Ordered.HIGHEST_PRECEDENCE) +public class SseExceptionHandler { + + @ExceptionHandler(DuplicateTimestampException.class) + public ResponseEntity handleDuplicateTimestampException(final DuplicateTimestampException e) { + log.warn(e.getMessage()); + return ResponseEntity.status(SseApiError.TIMER_START_FAILED.getHttpStatus()) + .body(new ApiErrorResponse(SseApiError.TIMER_START_FAILED.getMessage())); + } + + @ExceptionHandler(NotFoundScheduledFutureException.class) + public ResponseEntity handleNotFoundScheduledFutureException( + final NotFoundScheduledFutureException e) { + log.warn(e.getMessage()); + return ResponseEntity.status(SseApiError.TIMER_STOP_FAILED.getHttpStatus()) + .body(new ApiErrorResponse(SseApiError.TIMER_STOP_FAILED.getMessage())); + } + + @ExceptionHandler(NotFoundSseConnectionException.class) + public ResponseEntity handleNotFoundSseConnectionException( + final NotFoundSseConnectionException e) { + log.warn(e.getMessage()); + return ResponseEntity.status(SseApiError.CONNECTION_NOT_FOUND.getHttpStatus()) + .body(new ApiErrorResponse(SseApiError.CONNECTION_NOT_FOUND.getMessage())); + } + + @ExceptionHandler(NotFoundTimeStampException.class) + public ResponseEntity handleNotFoundTimeStampException(final NotFoundTimeStampException e) { + log.warn(e.getMessage()); + return ResponseEntity.status(SseApiError.TIMER_STOP_FAILED.getHttpStatus()) + .body(new ApiErrorResponse(SseApiError.TIMER_STOP_FAILED.getMessage())); + } + + @ExceptionHandler(SseConnectionDuplicationException.class) + public ResponseEntity handleSseConnectionDuplicationException( + final SseConnectionDuplicationException e) { + log.warn(e.getMessage()); + return ResponseEntity.status(SseApiError.CONNECTION_DUPLICATED.getHttpStatus()) + .body(new ApiErrorResponse(SseApiError.CONNECTION_DUPLICATED.getMessage())); + } + + @ExceptionHandler(SseConnectionFailureException.class) + public ResponseEntity handleSseConnectionFailureException(final SseConnectionFailureException e) { + log.warn(e.getMessage()); + return ResponseEntity.status(SseApiError.CONNECTION_FAILED.getHttpStatus()) + .body(new ApiErrorResponse(SseApiError.CONNECTION_FAILED.getMessage())); + } + + @ExceptionHandler(SyncException.class) + public ResponseEntity handleSyncException(final SyncException e) { + log.warn(e.getMessage()); + return ResponseEntity.status(SseApiError.SYNC_FAILED.getHttpStatus()) + .body(new ApiErrorResponse(SseApiError.SYNC_FAILED.getMessage())); + } +} diff --git a/backend/src/main/java/site/coduo/sync/controller/docs/SseDocs.java b/backend/src/main/java/site/coduo/sync/controller/docs/SseDocs.java new file mode 100644 index 00000000..d339859a --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/controller/docs/SseDocs.java @@ -0,0 +1,23 @@ +package site.coduo.sync.controller.docs; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import site.coduo.common.controller.response.ApiErrorResponse; + +public interface SseDocs { + + @Operation(summary = "νŠΉμ • key에 μ†ν•˜λŠ” SSE 연결을 μƒμ„±ν•œλ‹€. 타이머 동기화 μ‹œ keyλŠ” accessCodeκ°€ λœλ‹€.") + @ApiResponse(responseCode = "200", description = "SSE μ—°κ²° 성곡 - event:connect\ndata:OK λ©”μ‹œμ§€λ₯Ό 응닡", content = @Content(mediaType = MediaType.TEXT_EVENT_STREAM_VALUE)) + @ApiResponse(responseCode = "4xx", description = "SSE μ—°κ²° μ‹€νŒ¨", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))) + ResponseEntity createConnection(String key); + + @Operation(summary = "νŠΉμ • key에 μ†ν•˜λŠ” SSE 연결을 λͺ¨λ‘ μ‚­μ œν•œλ‹€.") + @ApiResponse(responseCode = "204", description = "SSE μ‚­μ œ 성곡 - event:close\ndata:OK λ©”μ‹œμ§€λ₯Ό 응닡", content = @Content(mediaType = MediaType.TEXT_EVENT_STREAM_VALUE)) + ResponseEntity deleteConnection(String key); +} diff --git a/backend/src/main/java/site/coduo/sync/controller/error/SseApiError.java b/backend/src/main/java/site/coduo/sync/controller/error/SseApiError.java new file mode 100644 index 00000000..215f5949 --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/controller/error/SseApiError.java @@ -0,0 +1,21 @@ +package site.coduo.sync.controller.error; + +import org.springframework.http.HttpStatus; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum SseApiError { + + SYNC_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "동기화에 μ‹€νŒ¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€."), + TIMER_START_FAILED(HttpStatus.BAD_REQUEST, "타이머 싀행에 μ‹€νŒ¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€."), + TIMER_STOP_FAILED(HttpStatus.BAD_REQUEST, "타이머 쀑지에 μ‹€νŒ¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€."), + CONNECTION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "SSE 연결에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."), + CONNECTION_NOT_FOUND(HttpStatus.NOT_FOUND, "ν•΄λ‹Ή νŽ˜μ–΄λ£Έμ˜ SSE 연결을 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."), + CONNECTION_DUPLICATED(HttpStatus.BAD_REQUEST, "ν•΄λ‹Ή νŽ˜μ–΄λ£Έμ— 이미 μ—°κ²°λœ SSE 연결이 μ‘΄μž¬ν•©λ‹ˆλ‹€."); + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/backend/src/main/java/site/coduo/sync/exception/DuplicateTimestampException.java b/backend/src/main/java/site/coduo/sync/exception/DuplicateTimestampException.java new file mode 100644 index 00000000..7deeaba3 --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/exception/DuplicateTimestampException.java @@ -0,0 +1,8 @@ +package site.coduo.sync.exception; + +public class DuplicateTimestampException extends SyncException { + + public DuplicateTimestampException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/sync/exception/NotFoundScheduledFutureException.java b/backend/src/main/java/site/coduo/sync/exception/NotFoundScheduledFutureException.java new file mode 100644 index 00000000..3496d3c4 --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/exception/NotFoundScheduledFutureException.java @@ -0,0 +1,8 @@ +package site.coduo.sync.exception; + +public class NotFoundScheduledFutureException extends SyncException { + + public NotFoundScheduledFutureException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/sync/exception/NotFoundSseConnectionException.java b/backend/src/main/java/site/coduo/sync/exception/NotFoundSseConnectionException.java new file mode 100644 index 00000000..06440853 --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/exception/NotFoundSseConnectionException.java @@ -0,0 +1,8 @@ +package site.coduo.sync.exception; + +public class NotFoundSseConnectionException extends SyncException { + + public NotFoundSseConnectionException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/sync/exception/NotFoundTimeStampException.java b/backend/src/main/java/site/coduo/sync/exception/NotFoundTimeStampException.java new file mode 100644 index 00000000..eb803a40 --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/exception/NotFoundTimeStampException.java @@ -0,0 +1,8 @@ +package site.coduo.sync.exception; + +public class NotFoundTimeStampException extends SyncException { + + public NotFoundTimeStampException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/sync/exception/SseConnectionDuplicationException.java b/backend/src/main/java/site/coduo/sync/exception/SseConnectionDuplicationException.java new file mode 100644 index 00000000..5c75b59b --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/exception/SseConnectionDuplicationException.java @@ -0,0 +1,8 @@ +package site.coduo.sync.exception; + +public class SseConnectionDuplicationException extends SyncException { + + public SseConnectionDuplicationException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/sync/exception/SseConnectionFailureException.java b/backend/src/main/java/site/coduo/sync/exception/SseConnectionFailureException.java new file mode 100644 index 00000000..148f5d0b --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/exception/SseConnectionFailureException.java @@ -0,0 +1,8 @@ +package site.coduo.sync.exception; + +public class SseConnectionFailureException extends SyncException { + + public SseConnectionFailureException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/sync/exception/SyncException.java b/backend/src/main/java/site/coduo/sync/exception/SyncException.java new file mode 100644 index 00000000..e99eac00 --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/exception/SyncException.java @@ -0,0 +1,8 @@ +package site.coduo.sync.exception; + +public class SyncException extends RuntimeException { + + public SyncException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/sync/service/EventStream.java b/backend/src/main/java/site/coduo/sync/service/EventStream.java new file mode 100644 index 00000000..8b9e321d --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/service/EventStream.java @@ -0,0 +1,12 @@ +package site.coduo.sync.service; + +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +public interface EventStream { + + SseEmitter connect(); + + void flush(String name, String message); + + void close(); +} diff --git a/backend/src/main/java/site/coduo/sync/service/EventStreams.java b/backend/src/main/java/site/coduo/sync/service/EventStreams.java new file mode 100644 index 00000000..1b045210 --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/service/EventStreams.java @@ -0,0 +1,42 @@ +package site.coduo.sync.service; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import lombok.extern.slf4j.Slf4j; +import site.coduo.sync.exception.SseConnectionDuplicationException; + +@Slf4j +public class EventStreams { + + private final List streams = new CopyOnWriteArrayList<>(); + + public SseEmitter publish(final EventStream eventStream) { + final SseEmitter sseEmitter = eventStream.connect(); + sseEmitter.onTimeout(sseEmitter::complete); + sseEmitter.onCompletion(() -> streams.remove(eventStream)); + sseEmitter.onError(error -> streams.remove(eventStream)); + return sseEmitter; + } + + public void add(final EventStream eventStream) { + if (streams.contains(eventStream)) { + throw new SseConnectionDuplicationException("μ€‘λ³΅λœ Sse Connectionμž…λ‹ˆλ‹€."); + } + streams.add(eventStream); + } + + public void broadcast(final String name, final String message) { + streams.forEach(eventStream -> eventStream.flush(name, message)); + } + + public void closeAll() { + streams.forEach(EventStream::close); + } + + public boolean isEmpty() { + return streams.isEmpty(); + } +} diff --git a/backend/src/main/java/site/coduo/sync/service/EventStreamsRegistry.java b/backend/src/main/java/site/coduo/sync/service/EventStreamsRegistry.java new file mode 100644 index 00000000..68f9515a --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/service/EventStreamsRegistry.java @@ -0,0 +1,50 @@ +package site.coduo.sync.service; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import site.coduo.sync.exception.NotFoundSseConnectionException; + +@Component +public class EventStreamsRegistry { + + private final Map registry; + + public EventStreamsRegistry() { + this.registry = new ConcurrentHashMap<>(); + } + + public SseEmitter register(final String key) { + final EventStreams eventStreams = registry.getOrDefault(key, new EventStreams()); + final EventStream eventStream = new SseEventStream(); + eventStreams.add(eventStream); + registry.put(key, eventStreams); + return eventStreams.publish(eventStream); + } + + public void release(final String key) { + if (!registry.containsKey(key)) { + return; + } + final EventStreams eventStreams = registry.get(key); + eventStreams.closeAll(); + registry.remove(key); + } + + public EventStreams findEventStreams(final String key) { + if (registry.containsKey(key)) { + return registry.get(key); + } + throw new NotFoundSseConnectionException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” SSE 컀λ„₯μ…˜μž…λ‹ˆλ‹€."); + } + + public boolean hasNoStreams(final String key) { + if (registry.containsKey(key)) { + return registry.get(key).isEmpty(); + } + throw new NotFoundSseConnectionException("SSE 컀λ„₯μ…˜μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."); + } +} diff --git a/backend/src/main/java/site/coduo/sync/service/SchedulerRegistry.java b/backend/src/main/java/site/coduo/sync/service/SchedulerRegistry.java new file mode 100644 index 00000000..33e52744 --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/service/SchedulerRegistry.java @@ -0,0 +1,44 @@ +package site.coduo.sync.service; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; + +import org.springframework.stereotype.Component; + +import site.coduo.sync.exception.NotFoundScheduledFutureException; + +@Component +public class SchedulerRegistry { + + private final Map> registry; + + public SchedulerRegistry() { + this.registry = new ConcurrentHashMap<>(); + } + + public void register(final String key, final ScheduledFuture future) { + registry.put(key, future); + } + + public void release(final String key) { + if (!registry.containsKey(key)) { + throw new NotFoundScheduledFutureException("킀에 ν•΄λ‹Ήν•˜λŠ” μŠ€μΌ€μ€„λŸ¬ κ²°κ³Όκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + } + registry.get(key) + .cancel(false); + registry.remove(key); + } + + public boolean has(final String key) { + return registry.containsKey(key); + } + + public boolean isActive(final String key) { + if (registry.containsKey(key)) { + final ScheduledFuture scheduledFuture = registry.get(key); + return !scheduledFuture.isDone() && !scheduledFuture.isCancelled(); + } + return false; + } +} diff --git a/backend/src/main/java/site/coduo/sync/service/SchedulerService.java b/backend/src/main/java/site/coduo/sync/service/SchedulerService.java new file mode 100644 index 00000000..8cc4795f --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/service/SchedulerService.java @@ -0,0 +1,83 @@ +package site.coduo.sync.service; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.ScheduledFuture; + +import org.springframework.scheduling.Trigger; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.support.PeriodicTrigger; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import site.coduo.timer.domain.Timer; +import site.coduo.timer.repository.TimerRepository; +import site.coduo.timer.service.TimestampRegistry; + +@Slf4j +@RequiredArgsConstructor +@Component +public class SchedulerService { + + public static final Duration DELAY_SECOND = Duration.of(1, ChronoUnit.SECONDS); + + private final ThreadPoolTaskScheduler taskScheduler; + private final SchedulerRegistry schedulerRegistry; + private final TimestampRegistry timestampRegistry; + private final TimerRepository timerRepository; + private final SseService sseService; + + public void start(final String key) { + if (schedulerRegistry.isActive(key)) { + return; + } + sseService.broadcast(key, "timer", "start"); + if (isInitial(key)) { + final Timer timer = timerRepository.fetchTimerByAccessCode(key) + .toDomain(); + scheduling(key, timer); + timestampRegistry.register(key, timer); + return; + } + final Timer timer = timestampRegistry.get(key); + scheduling(key, timer); + } + + private boolean isInitial(final String key) { + return !schedulerRegistry.has(key) && !timestampRegistry.has(key); + } + + private void scheduling(final String key, final Timer timer) { + final Trigger trigger = new PeriodicTrigger(DELAY_SECOND); + final ScheduledFuture schedule = taskScheduler.schedule(() -> runTimer(key, timer), trigger); + schedulerRegistry.register(key, schedule); + } + + private void runTimer(final String key, final Timer timer) { + if (timer.isTimeUp() && schedulerRegistry.has(key)) { + stop(key, timer); + return; + } + if (sseService.hasNoConnections(key) && schedulerRegistry.has(key)) { + pause(key); + return; + } + timer.decreaseRemainingTime(DELAY_SECOND.toMillis()); + sseService.broadcast(key, "remaining-time", String.valueOf(timer.getRemainingTime())); + } + + public void pause(final String key) { + if (schedulerRegistry.isActive(key)) { + sseService.broadcast(key, "timer", "pause"); + schedulerRegistry.release(key); + } + } + + private void stop(final String key, final Timer timer) { + sseService.broadcast(key, "timer", "stop"); + schedulerRegistry.release(key); + final Timer initalTimer = new Timer(timer.getAccessCode(), timer.getDuration(), timer.getDuration()); + timestampRegistry.register(key, initalTimer); + } +} diff --git a/backend/src/main/java/site/coduo/sync/service/SseEventStream.java b/backend/src/main/java/site/coduo/sync/service/SseEventStream.java new file mode 100644 index 00000000..d026be3e --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/service/SseEventStream.java @@ -0,0 +1,89 @@ +package site.coduo.sync.service; + +import java.io.IOException; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import site.coduo.sync.exception.SseConnectionFailureException; + +@Slf4j +@Getter +public class SseEventStream implements EventStream { + + private static final Duration TIME_OUT = Duration.ofMinutes(20); + private static final String CLOSE_NAME = "close"; + private static final String CONNECT_NAME = "connect"; + private static final String SUCCESS_MESSAGE = "OK"; + + private final AtomicLong id = new AtomicLong(0); + private final SseEmitter sseEmitter; + + public SseEventStream() { + this.sseEmitter = new SseEmitter(TIME_OUT.toMillis()); + } + + public SseEventStream(final Duration timeout) { + this.sseEmitter = new SseEmitter(timeout.toMillis()); + } + + public SseEventStream(final SseEmitter sseEmitter) { + this.sseEmitter = sseEmitter; + } + + @Override + public SseEmitter connect() { + final String eventId = String.valueOf(id.incrementAndGet()); + try { + sseEmitter.send(SseEmitter.event() + .id(eventId) + .name(CONNECT_NAME) + .data(SUCCESS_MESSAGE) + .reconnectTime(1)); + } catch (final IOException e) { + throw new SseConnectionFailureException("SSE 연결이 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); + } + return sseEmitter; + } + + @Override + public void flush(final String name, final String message) { + + final String eventId = String.valueOf(id.incrementAndGet()); + try { + sseEmitter.send(SseEmitter.event() + .id(eventId) + .name(name) + .data(message) + ); + } catch (final IOException ignored) { + } + } + + @Override + public void close() { + flush(CLOSE_NAME, SUCCESS_MESSAGE); + sseEmitter.complete(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final SseEventStream that = (SseEventStream) o; + return Objects.equals(sseEmitter, that.sseEmitter); + } + + @Override + public int hashCode() { + return Objects.hash(sseEmitter); + } +} diff --git a/backend/src/main/java/site/coduo/sync/service/SseService.java b/backend/src/main/java/site/coduo/sync/service/SseService.java new file mode 100644 index 00000000..8c004bdd --- /dev/null +++ b/backend/src/main/java/site/coduo/sync/service/SseService.java @@ -0,0 +1,42 @@ +package site.coduo.sync.service; + +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import site.coduo.timer.service.TimerService; + +@Slf4j +@RequiredArgsConstructor +@Service +public class SseService { + + private final EventStreamsRegistry eventStreamsRegistry; + private final TimerService timerService; + private final SchedulerRegistry schedulerRegistry; + + public SseEmitter connect(final String key) { + final SseEmitter emitter = eventStreamsRegistry.register(key); + final long remainingTime = timerService.readTimerRemainingTime(key); + // todo: SchedulerService λΆ„λ¦¬λœ μƒμˆ˜ν™” μ–΄λ–»κ²Œ 할지 생각 + broadcast(key, "remaining-time", String.valueOf(remainingTime)); + if (schedulerRegistry.isActive(key)) { + broadcast(key, "timer", "running"); + } + return emitter; + } + + public void broadcast(final String key, final String event, final String data) { + final EventStreams emitters = eventStreamsRegistry.findEventStreams(key); + emitters.broadcast(event, data); + } + + public boolean hasNoConnections(final String key) { + return eventStreamsRegistry.hasNoStreams(key); + } + + public void disconnectAll(final String key) { + eventStreamsRegistry.release(key); + } +} diff --git a/backend/src/main/java/site/coduo/timer/controller/TimerController.java b/backend/src/main/java/site/coduo/timer/controller/TimerController.java new file mode 100644 index 00000000..a0772779 --- /dev/null +++ b/backend/src/main/java/site/coduo/timer/controller/TimerController.java @@ -0,0 +1,65 @@ +package site.coduo.timer.controller; + +import jakarta.validation.Valid; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import site.coduo.sync.service.SchedulerService; +import site.coduo.sync.service.SseService; +import site.coduo.timer.controller.docs.TimerDocs; +import site.coduo.timer.service.TimerService; +import site.coduo.timer.service.dto.TimerReadResponse; +import site.coduo.timer.service.dto.TimerUpdateRequest; + +@Slf4j +@RequiredArgsConstructor +@RestController +public class TimerController implements TimerDocs { + + private final TimerService timerService; + private final SchedulerService schedulerService; + private final SseService sseService; + + @PatchMapping("/{accessCode}/timer/start") + public ResponseEntity createTimerStart(@PathVariable("accessCode") final String accessCode) { + schedulerService.start(accessCode); + return ResponseEntity.noContent() + .build(); + } + + @PatchMapping("/{accessCode}/timer/stop") + public ResponseEntity createTimerStop(@PathVariable("accessCode") final String accessCode) { + schedulerService.pause(accessCode); + + return ResponseEntity.noContent() + .build(); + } + + @PatchMapping("/{accessCode}/timer") + public ResponseEntity updateTimer( + @PathVariable("accessCode") final String accessCode, + @Valid @RequestBody final TimerUpdateRequest request + ) { + timerService.updateTimer(accessCode, request); + sseService.broadcast(accessCode, "timer", "update"); + + return ResponseEntity.noContent() + .build(); + } + + @GetMapping("/{accessCode}/timer") + public ResponseEntity getTimer( + @PathVariable("accessCode") final String accessCode + ) { + final TimerReadResponse response = timerService.readTimer(accessCode); + + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/site/coduo/timer/controller/TimerExceptionHandler.java b/backend/src/main/java/site/coduo/timer/controller/TimerExceptionHandler.java new file mode 100644 index 00000000..f5cf5984 --- /dev/null +++ b/backend/src/main/java/site/coduo/timer/controller/TimerExceptionHandler.java @@ -0,0 +1,44 @@ +package site.coduo.timer.controller; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import lombok.extern.slf4j.Slf4j; +import site.coduo.common.controller.response.ApiErrorResponse; +import site.coduo.timer.controller.error.TimerApiError; +import site.coduo.timer.exception.InvalidTimerException; +import site.coduo.timer.exception.TimerException; +import site.coduo.timer.exception.TimerNotFoundException; + +@Slf4j +@RestControllerAdvice +@Order(Ordered.HIGHEST_PRECEDENCE) +public class TimerExceptionHandler { + + @ExceptionHandler(TimerNotFoundException.class) + public ResponseEntity handlePairRoomNotFoundException(final TimerNotFoundException e) { + log.warn(e.getMessage()); + + return ResponseEntity.status(TimerApiError.TIMER_NOT_FOUND.getHttpStatus()) + .body(new ApiErrorResponse(TimerApiError.TIMER_NOT_FOUND.getMessage())); + } + + @ExceptionHandler(InvalidTimerException.class) + public ResponseEntity handleInvalidTimerException(final InvalidTimerException e) { + log.warn(e.getMessage()); + + return ResponseEntity.status(TimerApiError.INVALID_TIMER_REQUEST.getHttpStatus()) + .body(new ApiErrorResponse(TimerApiError.INVALID_TIMER_REQUEST.getMessage())); + } + + @ExceptionHandler(TimerException.class) + public ResponseEntity handleTimerException(final TimerException e) { + log.warn(e.getMessage()); + + return ResponseEntity.status(TimerApiError.INVALID_REQUEST.getHttpStatus()) + .body(new ApiErrorResponse(TimerApiError.INVALID_REQUEST.getMessage())); + } +} diff --git a/backend/src/main/java/site/coduo/timer/controller/docs/TimerDocs.java b/backend/src/main/java/site/coduo/timer/controller/docs/TimerDocs.java new file mode 100644 index 00000000..397370b9 --- /dev/null +++ b/backend/src/main/java/site/coduo/timer/controller/docs/TimerDocs.java @@ -0,0 +1,44 @@ +package site.coduo.timer.controller.docs; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import site.coduo.common.controller.response.ApiErrorResponse; +import site.coduo.timer.service.dto.TimerReadResponse; +import site.coduo.timer.service.dto.TimerUpdateRequest; + +@Tag(name = "타이머 API") +public interface TimerDocs { + + @Operation(summary = "타이머λ₯Ό μ‹œμž‘ν•œλ‹€.") + @ApiResponse(responseCode = "204", description = "타이머 μ‹œμž‘ 성곡") + @ApiResponse(responseCode = "4xx", description = "νŽ˜μ–΄λ£Έ μ‹œμž‘ μ‹€νŒ¨", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))) + ResponseEntity createTimerStart(String accessCode); + + @Operation(summary = "타이머λ₯Ό μ€‘μ§€ν•œλ‹€.") + @ApiResponse(responseCode = "204", description = "타이머 즁자 성곡") + @ApiResponse(responseCode = "4xx", description = "νŽ˜μ–΄λ£Έ 쀑지 μ‹€νŒ¨", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))) + ResponseEntity createTimerStop(String accessCode); + + @Operation(summary = "타이머λ₯Ό μ—…λ°μ΄νŠΈν•œλ‹€.") + @ApiResponse(responseCode = "204", description = "타이머 μ—…λ°μ΄νŠΈ 성곡", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE)) + @ApiResponse(responseCode = "4xx", description = "타이머 μ—…λ°μ΄νŠΈ μ‹€νŒ¨", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))) + ResponseEntity updateTimer( + String accessCode, + @Parameter(description = "타이머 μ—…λ°μ΄νŠΈ μš”μ²­", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE), required = true) + TimerUpdateRequest request + ); + + @Operation(summary = "타이머λ₯Ό μ‘°νšŒν•œλ‹€.") + @ApiResponse(responseCode = "200", description = "νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬ 쑰회 성곡", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = TimerReadResponse.class))) + @ApiResponse(responseCode = "4xx", description = "νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬ 생성 μ‹€νŒ¨", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ApiErrorResponse.class))) + ResponseEntity getTimer( + String accessCode + ); +} diff --git a/backend/src/main/java/site/coduo/timer/controller/error/TimerApiError.java b/backend/src/main/java/site/coduo/timer/controller/error/TimerApiError.java new file mode 100644 index 00000000..d7724bc4 --- /dev/null +++ b/backend/src/main/java/site/coduo/timer/controller/error/TimerApiError.java @@ -0,0 +1,18 @@ +package site.coduo.timer.controller.error; + +import org.springframework.http.HttpStatus; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum TimerApiError { + + INVALID_REQUEST(HttpStatus.BAD_REQUEST, "μœ νš¨ν•˜μ§€ μ•Šμ€ μš”μ²­μž…λ‹ˆλ‹€."), + INVALID_TIMER_REQUEST(HttpStatus.BAD_REQUEST, "μœ νš¨ν•˜μ§€ μ•Šμ€ 타이머 μ‹œκ°„μ΄ μ‘΄μž¬ν•©λ‹ˆλ‹€."), + TIMER_NOT_FOUND(HttpStatus.NOT_FOUND, "타이머λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."); + + private final HttpStatus httpStatus; + private final String message; +} diff --git a/backend/src/main/java/site/coduo/timer/domain/Timer.java b/backend/src/main/java/site/coduo/timer/domain/Timer.java new file mode 100644 index 00000000..d232e239 --- /dev/null +++ b/backend/src/main/java/site/coduo/timer/domain/Timer.java @@ -0,0 +1,66 @@ +package site.coduo.timer.domain; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +import lombok.Getter; +import site.coduo.pairroom.domain.accesscode.AccessCode; +import site.coduo.timer.exception.InvalidTimerException; + +@Getter +public class Timer { + + private final AccessCode accessCode; + private final long duration; + private final AtomicLong remainingTime; + + public Timer(final AccessCode accessCode, final long duration, final long timerRemainingTime) { + validateTime(duration, timerRemainingTime); + this.accessCode = accessCode; + this.duration = duration; + this.remainingTime = new AtomicLong(timerRemainingTime); + } + + private void validateTime(long timerDuration, long timerRemainingTime) { + if (timerDuration < 0 || timerRemainingTime < 0) { + throw new InvalidTimerException("타이머 μ‹œκ°„κ³Ό 남은 μ‹œκ°„μ€ 0 이상이어야 ν•©λ‹ˆλ‹€."); + } + } + + public long getRemainingTime() { + return remainingTime.get(); + } + + public boolean isTimeUp() { + return remainingTime.get() == 0; + } + + public void decreaseRemainingTime(final long decrease) { + if (remainingTime.get() == 0L) { + return; + } + if (remainingTime.get() < decrease) { + remainingTime.set(0); + return; + } + remainingTime.set(remainingTime.get() - decrease); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Timer timer = (Timer) o; + return duration == timer.duration && Objects.equals(accessCode, timer.accessCode) + && Objects.equals(remainingTime, timer.remainingTime); + } + + @Override + public int hashCode() { + return Objects.hash(accessCode, duration, remainingTime); + } +} diff --git a/backend/src/main/java/site/coduo/timer/exception/InvalidTimerException.java b/backend/src/main/java/site/coduo/timer/exception/InvalidTimerException.java new file mode 100644 index 00000000..3b9c0afe --- /dev/null +++ b/backend/src/main/java/site/coduo/timer/exception/InvalidTimerException.java @@ -0,0 +1,8 @@ +package site.coduo.timer.exception; + +public class InvalidTimerException extends TimerException { + + public InvalidTimerException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/timer/exception/NegativeTimeException.java b/backend/src/main/java/site/coduo/timer/exception/NegativeTimeException.java new file mode 100644 index 00000000..c6b76f87 --- /dev/null +++ b/backend/src/main/java/site/coduo/timer/exception/NegativeTimeException.java @@ -0,0 +1,8 @@ +package site.coduo.timer.exception; + +public class NegativeTimeException extends TimerException{ + + public NegativeTimeException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/timer/exception/TimerException.java b/backend/src/main/java/site/coduo/timer/exception/TimerException.java new file mode 100644 index 00000000..8628b3e9 --- /dev/null +++ b/backend/src/main/java/site/coduo/timer/exception/TimerException.java @@ -0,0 +1,8 @@ +package site.coduo.timer.exception; + +public class TimerException extends RuntimeException { + + public TimerException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/timer/exception/TimerNotFoundException.java b/backend/src/main/java/site/coduo/timer/exception/TimerNotFoundException.java new file mode 100644 index 00000000..542c69b1 --- /dev/null +++ b/backend/src/main/java/site/coduo/timer/exception/TimerNotFoundException.java @@ -0,0 +1,8 @@ +package site.coduo.timer.exception; + +public class TimerNotFoundException extends TimerException { + + public TimerNotFoundException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/site/coduo/timer/repository/TimerEntity.java b/backend/src/main/java/site/coduo/timer/repository/TimerEntity.java new file mode 100644 index 00000000..015383ae --- /dev/null +++ b/backend/src/main/java/site/coduo/timer/repository/TimerEntity.java @@ -0,0 +1,74 @@ +package site.coduo.timer.repository; + +import java.util.Objects; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import site.coduo.common.infrastructure.audit.entity.BaseTimeEntity; +import site.coduo.pairroom.domain.accesscode.AccessCode; +import site.coduo.pairroom.repository.PairRoomEntity; +import site.coduo.timer.domain.Timer; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "TIMER") +@Entity +public class TimerEntity extends BaseTimeEntity { + + @Id + @Column(name = "ID", nullable = false) + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne + @JoinColumn(name = "PAIR_ROOM_ID", nullable = false) + private PairRoomEntity pairRoomEntity; + + @Column(name = "DURATION", nullable = false) + private long duration; + + @Column(name = "REMAINING_TIME", nullable = false) + private long remainingTime; + + public TimerEntity(final Timer timer, final PairRoomEntity pairRoomEntity) { + this.pairRoomEntity = pairRoomEntity; + this.duration = timer.getDuration(); + this.remainingTime = timer.getRemainingTime(); + } + + public void updateTimer(final Timer timer) { + this.duration = timer.getDuration(); + this.remainingTime = timer.getRemainingTime(); + } + + public Timer toDomain() { + return new Timer(new AccessCode(pairRoomEntity.getAccessCode()), duration, remainingTime); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final TimerEntity that = (TimerEntity) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/backend/src/main/java/site/coduo/timer/repository/TimerRepository.java b/backend/src/main/java/site/coduo/timer/repository/TimerRepository.java new file mode 100644 index 00000000..12f7cf14 --- /dev/null +++ b/backend/src/main/java/site/coduo/timer/repository/TimerRepository.java @@ -0,0 +1,25 @@ +package site.coduo.timer.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import site.coduo.pairroom.repository.PairRoomEntity; +import site.coduo.timer.exception.TimerNotFoundException; + +public interface TimerRepository extends JpaRepository { + + default TimerEntity fetchTimerByPairRoomEntity(final PairRoomEntity pairRoomEntity) { + return findByPairRoomEntity(pairRoomEntity) + .orElseThrow(() -> new TimerNotFoundException("ν•΄λ‹Ή νŽ˜μ–΄λ£Έμ˜ 타이머가 μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")); + } + + Optional findByPairRoomEntity(PairRoomEntity pairRoomEntity); + + default TimerEntity fetchTimerByAccessCode(final String accessCode) { + return findByPairRoomEntityAccessCode(accessCode) + .orElseThrow(() -> new TimerNotFoundException("ν•΄λ‹Ή 타이머λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.")); + } + + Optional findByPairRoomEntityAccessCode(String accessCode); +} diff --git a/backend/src/main/java/site/coduo/timer/service/TimerService.java b/backend/src/main/java/site/coduo/timer/service/TimerService.java new file mode 100644 index 00000000..dc22962b --- /dev/null +++ b/backend/src/main/java/site/coduo/timer/service/TimerService.java @@ -0,0 +1,53 @@ +package site.coduo.timer.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import site.coduo.pairroom.domain.accesscode.AccessCode; +import site.coduo.pairroom.repository.PairRoomEntity; +import site.coduo.pairroom.repository.PairRoomRepository; +import site.coduo.timer.domain.Timer; +import site.coduo.timer.repository.TimerEntity; +import site.coduo.timer.repository.TimerRepository; +import site.coduo.timer.service.dto.TimerReadResponse; +import site.coduo.timer.service.dto.TimerUpdateRequest; + +@Transactional(readOnly = true) +@RequiredArgsConstructor +@Service +public class TimerService { + + private final TimerRepository timerRepository; + private final TimestampRegistry timestampRegistry; + private final PairRoomRepository pairRoomRepository; + + public TimerReadResponse readTimer(final String accessCode) { + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + final TimerEntity timerEntity = timerRepository.fetchTimerByPairRoomEntity(pairRoomEntity); + return TimerReadResponse.of(timerEntity.getId(), timerEntity.toDomain()); + } + + public long readTimerRemainingTime(final String accessCode) { + if (timestampRegistry.has(accessCode)) { + return timestampRegistry.get(accessCode) + .getRemainingTime(); + } + final Timer timer = timerRepository.fetchTimerByAccessCode(accessCode) + .toDomain(); + return timer.getDuration(); + } + + @Transactional + public void updateTimer(final String accessCode, final TimerUpdateRequest updateRequest) { + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + final TimerEntity timerEntity = timerRepository.fetchTimerByPairRoomEntity(pairRoomEntity); + final Timer newTimer = new Timer( + new AccessCode(pairRoomEntity.getAccessCode()), + updateRequest.duration(), + updateRequest.remainingTime() + ); + timerEntity.updateTimer(newTimer); + timestampRegistry.register(accessCode, newTimer); + } +} diff --git a/backend/src/main/java/site/coduo/timer/service/TimestampRegistry.java b/backend/src/main/java/site/coduo/timer/service/TimestampRegistry.java new file mode 100644 index 00000000..1bd5d567 --- /dev/null +++ b/backend/src/main/java/site/coduo/timer/service/TimestampRegistry.java @@ -0,0 +1,39 @@ +package site.coduo.timer.service; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.stereotype.Component; + +import lombok.NoArgsConstructor; +import site.coduo.sync.exception.NotFoundTimeStampException; +import site.coduo.timer.domain.Timer; + +@Component +@NoArgsConstructor +public class TimestampRegistry { + + private final Map registry = new ConcurrentHashMap<>(); + + public void register(final String key, final Timer timer) { + registry.put(key, timer); + } + + public void release(final String key) { + if (!registry.containsKey(key)) { + throw new NotFoundTimeStampException("킀에 ν•΄λ‹Ήν•˜λŠ” νƒ€μž„ μŠ€νƒ¬ν”„ κ²°κ³Όκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + } + registry.remove(key); + } + + public boolean has(final String key) { + return registry.containsKey(key); + } + + public Timer get(final String key) { + if (!registry.containsKey(key)) { + throw new NotFoundTimeStampException("킀에 ν•΄λ‹Ήν•˜λŠ” νƒ€μž„ μŠ€νƒ¬ν”„ κ²°κ³Όκ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + } + return registry.get(key); + } +} diff --git a/backend/src/main/java/site/coduo/timer/service/dto/TimerCreateRequest.java b/backend/src/main/java/site/coduo/timer/service/dto/TimerCreateRequest.java new file mode 100644 index 00000000..9f736331 --- /dev/null +++ b/backend/src/main/java/site/coduo/timer/service/dto/TimerCreateRequest.java @@ -0,0 +1,17 @@ +package site.coduo.timer.service.dto; + +import jakarta.validation.constraints.NotNull; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "타이머 생성 μš”μ²­ λ°”λ””") +public record TimerCreateRequest( + @Schema(description = "타이머 μ‹œκ°„. μ‹œκ°„μ€ millisecond 기쀀이닀.", example = "100000") + @NotNull + long duration, + + @Schema(description = "타이머가 μ’…λ£Œλ˜κΈ°κΉŒμ§€ 남은 μ‹œκ°„. μ‹œκ°„μ€ millisecond 기쀀이닀.", example = "60000") + @NotNull + long remainingTime +) { +} diff --git a/backend/src/main/java/site/coduo/timer/service/dto/TimerReadResponse.java b/backend/src/main/java/site/coduo/timer/service/dto/TimerReadResponse.java new file mode 100644 index 00000000..6509e56b --- /dev/null +++ b/backend/src/main/java/site/coduo/timer/service/dto/TimerReadResponse.java @@ -0,0 +1,21 @@ +package site.coduo.timer.service.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import site.coduo.timer.domain.Timer; + +@Schema(description = "티이머 쑰회 응닡 λ°”λ””") +public record TimerReadResponse( + @Schema(description = "타이머 μ‹λ³„μž", example = "1") + long id, + + @Schema(description = "타이머 μ‹œκ°„. μ‹œκ°„μ€ millisecond 기쀀이닀.", example = "100000") + long duration, + + @Schema(description = "타이머가 μ’…λ£Œλ˜κΈ°κΉŒμ§€ 남은 μ‹œκ°„. μ‹œκ°„μ€ millisecond 기쀀이닀.", example = "60000") + long remainingTime +) { + + public static TimerReadResponse of(final long id, final Timer timer) { + return new TimerReadResponse(id, timer.getDuration(), timer.getRemainingTime()); + } +} diff --git a/backend/src/main/java/site/coduo/timer/service/dto/TimerUpdateRequest.java b/backend/src/main/java/site/coduo/timer/service/dto/TimerUpdateRequest.java new file mode 100644 index 00000000..7cec4167 --- /dev/null +++ b/backend/src/main/java/site/coduo/timer/service/dto/TimerUpdateRequest.java @@ -0,0 +1,16 @@ +package site.coduo.timer.service.dto; + +import jakarta.validation.constraints.NotNull; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = " 타이머 μ—…λ°μ΄νŠΈ μš”μ²­ λ°”λ””") +public record TimerUpdateRequest( + @Schema(description = "타이머 μ‹œκ°„ (millisecond κΈ°μ€€)", example = "900000") + long duration, + + @Schema(description = "타이머가 μ’…λ£Œλ˜κΈ°κΉŒμ§€ 남은 μ‹œκ°„ (millisecond κΈ°μ€€)", example = "900000") + @NotNull + long remainingTime +) { +} diff --git a/backend/src/main/java/site/coduo/todo/controller/TodoController.java b/backend/src/main/java/site/coduo/todo/controller/TodoController.java index 0b193f5c..7f1dc1c5 100644 --- a/backend/src/main/java/site/coduo/todo/controller/TodoController.java +++ b/backend/src/main/java/site/coduo/todo/controller/TodoController.java @@ -1,16 +1,18 @@ package site.coduo.todo.controller; +import java.net.URI; import java.util.List; import java.util.stream.IntStream; -import org.springframework.web.bind.annotation.CrossOrigin; +import jakarta.validation.Valid; + +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import lombok.RequiredArgsConstructor; @@ -23,15 +25,13 @@ import site.coduo.todo.service.TodoService; @RequiredArgsConstructor -@CrossOrigin(origins = {"http://localhost:3000", "http://3.35.178.58"}) -@RequestMapping("/api") @RestController public class TodoController implements TodoDocs { private final TodoService todoService; @GetMapping("/{accessCode}/todos") - public List getTodos(@PathVariable String accessCode) { + public List getTodos(@PathVariable("accessCode") final String accessCode) { final List allTodos = todoService.getAllOrderBySort(accessCode); return IntStream.range(0, allTodos.size()) .mapToObj(index -> GetTodoResponse.from(allTodos.get(index), index)) @@ -39,27 +39,40 @@ public List getTodos(@PathVariable String accessCode) { } @PostMapping("/{accessCode}/todos") - public void createTodo(@PathVariable String accessCode, @RequestBody CreateTodoRequest request) { + public ResponseEntity createTodo( + @PathVariable("accessCode") final String accessCode, + @RequestBody @Valid final CreateTodoRequest request + ) { todoService.createTodo(accessCode, request.content()); + return ResponseEntity.created(URI.create("/")).build(); } @PatchMapping("/todos/{todoId}/contents") - public void updateContent(@PathVariable Long todoId, @RequestBody UpdateTodoContentRequest request) { + public ResponseEntity updateContent( + @PathVariable("todoId") final long todoId, + @RequestBody @Valid final UpdateTodoContentRequest request) { todoService.updateTodoContent(todoId, request.content()); + return ResponseEntity.noContent().build(); } @PatchMapping("/todos/{todoId}/checked") - public void toggleTodoChecked(@PathVariable Long todoId) { + public ResponseEntity toggleTodoChecked(@PathVariable("todoId") final long todoId) { todoService.toggleTodoChecked(todoId); + return ResponseEntity.noContent().build(); } @PatchMapping("/todos/{todoId}/order") - public void updateTodoOrder(@PathVariable Long todoId, @RequestBody UpdateTodoOrderRequest request) { + public ResponseEntity updateTodoOrder( + @PathVariable("todoId") final long todoId, + @RequestBody UpdateTodoOrderRequest request + ) { todoService.updateTodoSort(todoId, request.order()); + return ResponseEntity.noContent().build(); } @DeleteMapping("/todos/{todoId}") - public void deleteTodo(@PathVariable Long todoId) { + public ResponseEntity deleteTodo(@PathVariable("todoId") final long todoId) { todoService.deleteTodo(todoId); + return ResponseEntity.noContent().build(); } } diff --git a/backend/src/main/java/site/coduo/todo/controller/TodoErrorController.java b/backend/src/main/java/site/coduo/todo/controller/TodoExceptionHandler.java similarity index 96% rename from backend/src/main/java/site/coduo/todo/controller/TodoErrorController.java rename to backend/src/main/java/site/coduo/todo/controller/TodoExceptionHandler.java index ab660c6d..89999859 100644 --- a/backend/src/main/java/site/coduo/todo/controller/TodoErrorController.java +++ b/backend/src/main/java/site/coduo/todo/controller/TodoExceptionHandler.java @@ -14,7 +14,7 @@ @Slf4j @RestControllerAdvice @Order(Ordered.HIGHEST_PRECEDENCE) -public class TodoErrorController { +public class TodoExceptionHandler { @ExceptionHandler(TodoException.class) public ResponseEntity handleTodoException(final TodoException e) { diff --git a/backend/src/main/java/site/coduo/todo/controller/docs/TodoDocs.java b/backend/src/main/java/site/coduo/todo/controller/docs/TodoDocs.java index 738d74b5..46d1aa0e 100644 --- a/backend/src/main/java/site/coduo/todo/controller/docs/TodoDocs.java +++ b/backend/src/main/java/site/coduo/todo/controller/docs/TodoDocs.java @@ -1,7 +1,64 @@ package site.coduo.todo.controller.docs; +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import site.coduo.todo.controller.request.CreateTodoRequest; +import site.coduo.todo.controller.request.UpdateTodoContentRequest; +import site.coduo.todo.controller.request.UpdateTodoOrderRequest; +import site.coduo.todo.controller.response.GetTodoResponse; @Tag(name = "νˆ¬λ‘ API") public interface TodoDocs { + + @Operation(summary = "νŠΉμ • νŽ˜μ–΄λ£Έμ˜ νˆ¬λ‘ λͺ©λ‘μ„ μ‘°νšŒν•œλ‹€.") + @ApiResponse(responseCode = "200", description = "νˆ¬λ‘ λͺ©λ‘ 쑰회 성곡") + List getTodos( + @Parameter(description = "νŽ˜μ–΄λ£Έ μ ‘κ·Ό μ½”λ“œ", required = true) + String accessCode + ); + + @Operation(summary = "νˆ¬λ‘ ν•­λͺ©μ„ μƒμ„±ν•œλ‹€.") + @ApiResponse(responseCode = "201", description = "νˆ¬λ‘ μ €μž₯ 성곡") + ResponseEntity createTodo( + @Parameter(description = "νŽ˜μ–΄λ£Έ μ ‘κ·Ό μ½”λ“œ", required = true) + String accessCode, + @Parameter(description = "νˆ¬λ‘ ν•­λͺ© λ‚΄μš©", required = true) + CreateTodoRequest request + ); + + @Operation(summary = "νˆ¬λ‘ λ‚΄μš©μ„ λ³€κ²½ν•œλ‹€.") + @ApiResponse(responseCode = "204", description = "νˆ¬λ‘ λ‚΄μš© λ³€κ²½ 성곡") + ResponseEntity updateContent( + @Parameter(description = "λ³€κ²½ν•  νˆ¬λ‘ id") final long todoId, + @Parameter(description = "νˆ¬λ‘ λ³€κ²½ν•  λ‚΄μš©", required = true) final UpdateTodoContentRequest request + ); + + @Operation(summary = "νˆ¬λ‘ 체크 μ—¬λΆ€λ₯Ό λ³€κ²½ν•œλ‹€.") + @ApiResponse(responseCode = "204", description = "νˆ¬λ‘ ν† κΈ€ 성곡") + ResponseEntity toggleTodoChecked( + @Parameter(description = "ν† κΈ€ν•  νˆ¬λ‘ id") + @PathVariable("todoId") final long todoId + ); + + @Operation(summary = "νˆ¬λ‘ μ •λ ¬ μˆœμ„œλ₯Ό λ³€κ²½ν•œλ‹€.") + ResponseEntity updateTodoOrder( + @Parameter(description = "λ³€κ²½ν•  νˆ¬λ‘ id") + @PathVariable("todoId") final long todoId, + @Parameter(description = "λ³€κ²½ν•  νˆ¬λ‘ μ •λ ¬ μˆœμ„œ", required = true) + @RequestBody UpdateTodoOrderRequest request + ); + + @Operation(summary = "νˆ¬λ‘λ₯Ό μ‚­μ œν•œλ‹€.") + ResponseEntity deleteTodo( + @Parameter(description = "μ‚­μ œν•  νˆ¬λ‘ id") + @PathVariable("todoId") final long todoId + ); } diff --git a/backend/src/main/java/site/coduo/todo/controller/request/CreateTodoRequest.java b/backend/src/main/java/site/coduo/todo/controller/request/CreateTodoRequest.java index 64f2a1b8..d88bc630 100644 --- a/backend/src/main/java/site/coduo/todo/controller/request/CreateTodoRequest.java +++ b/backend/src/main/java/site/coduo/todo/controller/request/CreateTodoRequest.java @@ -1,4 +1,13 @@ package site.coduo.todo.controller.request; -public record CreateTodoRequest(String content) { +import jakarta.validation.constraints.NotBlank; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "νˆ¬λ‘ ν•­λͺ© 생성 μš”μ²­ λ°”λ””") +public record CreateTodoRequest( + @Schema(description = "νˆ¬λ‘ λ‚΄μš©") + @NotBlank + String content +) { } diff --git a/backend/src/main/java/site/coduo/todo/controller/request/UpdateTodoContentRequest.java b/backend/src/main/java/site/coduo/todo/controller/request/UpdateTodoContentRequest.java index 3cb2492f..aa528fcd 100644 --- a/backend/src/main/java/site/coduo/todo/controller/request/UpdateTodoContentRequest.java +++ b/backend/src/main/java/site/coduo/todo/controller/request/UpdateTodoContentRequest.java @@ -1,4 +1,14 @@ package site.coduo.todo.controller.request; -public record UpdateTodoContentRequest(String content) { +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record UpdateTodoContentRequest( + @Schema(description = "νˆ¬λ‘ λ³€κ²½ν•  λ‚΄μš©") + @NotBlank + @Valid + String content +) { } diff --git a/backend/src/main/java/site/coduo/todo/controller/request/UpdateTodoOrderRequest.java b/backend/src/main/java/site/coduo/todo/controller/request/UpdateTodoOrderRequest.java index bc23bfda..621923d4 100644 --- a/backend/src/main/java/site/coduo/todo/controller/request/UpdateTodoOrderRequest.java +++ b/backend/src/main/java/site/coduo/todo/controller/request/UpdateTodoOrderRequest.java @@ -1,4 +1,9 @@ package site.coduo.todo.controller.request; -public record UpdateTodoOrderRequest(int order) { +import io.swagger.v3.oas.annotations.media.Schema; + +public record UpdateTodoOrderRequest( + @Schema(description = "νˆ¬λ‘ λ³€κ²½ν•  μˆœμ„œ") + int order +) { } diff --git a/backend/src/main/java/site/coduo/todo/domain/Todo.java b/backend/src/main/java/site/coduo/todo/domain/Todo.java index 7498ef15..2eb72d73 100644 --- a/backend/src/main/java/site/coduo/todo/domain/Todo.java +++ b/backend/src/main/java/site/coduo/todo/domain/Todo.java @@ -3,87 +3,43 @@ import java.util.List; import lombok.Getter; -import site.coduo.pairroom.domain.PairRoom; -import site.coduo.todo.exception.InvalidTodoArgumentException; @Getter public class Todo { private final Long id; - private final PairRoom pairRoom; private final TodoContent content; private final TodoSort sort; private final TodoChecked isChecked; - public Todo( - final Long id, - final PairRoom pairRoom, - final String content, - final int sort, - final boolean isChecked - ) { - this( - id, - pairRoom, - new TodoContent(content), - new TodoSort(sort), - new TodoChecked(isChecked) - ); + public Todo(final Long id, final String content, final double sort, final boolean isChecked) { + this.id = id; + this.content = new TodoContent(content); + this.sort = new TodoSort(sort); + this.isChecked = new TodoChecked(isChecked); } - private Todo( - final Long id, - final PairRoom pairRoom, - final TodoContent content, - final TodoSort sort, - final TodoChecked isChecked - ) { - validatePairRoom(pairRoom); - + private Todo(final Long id, final TodoContent content, final TodoSort sort, final TodoChecked isChecked) { this.id = id; - this.pairRoom = pairRoom; this.content = content; this.sort = sort; this.isChecked = isChecked; } - private void validatePairRoom(final PairRoom pairRoom) { - if (pairRoom == null) { - throw new InvalidTodoArgumentException("Pair Room μ •λ³΄λ‘œ null을 μž…λ ₯ν•  수 μ—†μŠ΅λ‹ˆλ‹€."); - } - } - public Todo updateContent(final String content) { - return new Todo( - this.id, - this.getPairRoom(), - new TodoContent(content), - this.getSort(), - this.getIsChecked() - ); + return new Todo(this.id, new TodoContent(content), this.getSort(), this.getIsChecked()); } public Todo toggleTodoChecked() { - return new Todo( - this.id, - this.getPairRoom(), - this.getContent(), - this.getSort(), - this.getIsChecked().toggle() - ); + return new Todo(this.id, this.getContent(), this.getSort(), this.getIsChecked().toggle()); } public Todo updateSort(final List todos, final int destinationSort) { final List todoSorts = todos.stream() .map(Todo::getSort) .toList(); + final TodoSort todoSort = this.getSort().update(todoSorts, destinationSort); - return new Todo( - this.id, - this.getPairRoom(), - this.getContent(), - this.getSort().update(todoSorts, destinationSort), - this.getIsChecked() - ); + return new Todo(this.id, this.getContent(), todoSort, this.getIsChecked()); } } diff --git a/backend/src/main/java/site/coduo/todo/domain/TodoSort.java b/backend/src/main/java/site/coduo/todo/domain/TodoSort.java index d2d6ce44..695c90bc 100644 --- a/backend/src/main/java/site/coduo/todo/domain/TodoSort.java +++ b/backend/src/main/java/site/coduo/todo/domain/TodoSort.java @@ -4,59 +4,62 @@ import java.util.Objects; import lombok.Getter; -import site.coduo.todo.exception.InvalidTodoSortException; import site.coduo.todo.exception.InvalidUpdatedTodoSortException; @Getter public class TodoSort { - private static final int SORT_INTERVAL = 1024; - private static final int FIRST_INDEX = 0; + private static final int SORT_INTERVAL = 3072; + private static final int FIRST_ORDER = 0; - private final int sort; + private final double sort; - public TodoSort(final int sort) { - validateSort(sort); + public TodoSort(final double sort) { this.sort = sort; } - private void validateSort(final int sort) { - if (sort < 0) { - throw new InvalidTodoSortException("todoSortλŠ” μŒμˆ˜κ°€ 될 수 μ—†μŠ΅λ‹ˆλ‹€."); - } - } - public TodoSort countNextSort() { return new TodoSort(sort + SORT_INTERVAL); } public TodoSort update(final List todoSorts, final int destinationOrder) { - validateUpdateSort(todoSorts.size(), destinationOrder); + validateDestinationOrder(todoSorts, destinationOrder); - if (destinationOrder == FIRST_INDEX) { - final int oldFirstItemSortValue = todoSorts.get(FIRST_INDEX).getSort(); + if (destinationOrder == FIRST_ORDER) { + final double oldFirstItemSortValue = todoSorts.get(FIRST_ORDER).getSort(); return new TodoSort(oldFirstItemSortValue - SORT_INTERVAL); } final int lastItemIndex = todoSorts.size() - 1; if (destinationOrder == lastItemIndex) { - final int oldLastItemSortValue = todoSorts.get(lastItemIndex).getSort(); + final double oldLastItemSortValue = todoSorts.get(lastItemIndex).getSort(); return new TodoSort(oldLastItemSortValue + SORT_INTERVAL); } final int currentOrder = todoSorts.indexOf(this); if (destinationOrder < currentOrder) { - final int newSortValue = (todoSorts.get(destinationOrder - 1).sort + todoSorts.get(destinationOrder).sort) / 2; + final double newSortValue = (todoSorts.get(destinationOrder - 1).sort + todoSorts.get(destinationOrder).sort) / 2; return new TodoSort(newSortValue); } - final int newSortValue = (todoSorts.get(destinationOrder).sort + todoSorts.get(destinationOrder + 1).sort) / 2; + final double newSortValue = (todoSorts.get(destinationOrder).sort + todoSorts.get(destinationOrder + 1).sort) / 2; return new TodoSort(newSortValue); } - private void validateUpdateSort(final int allTodoSize, final int destinationSort) { - if (destinationSort < FIRST_INDEX || destinationSort >= allTodoSize) { - throw new InvalidUpdatedTodoSortException("Todo μˆœμ„œλŠ” 전체 Todo λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λŠ” μœ„μΉ˜μΌ 수 μ—†μŠ΅λ‹ˆλ‹€. sort - " + destinationSort); + private void validateDestinationOrder(final List todoSorts, final int destinationSort) { + final TodoSort currentSort = todoSorts.stream() + .filter(todoSort -> todoSort.getSort() == this.sort) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("μž…λ ₯된 νˆ¬λ‘ 정렬에 ν˜„μž¬ 정렬값이 ν¬ν•¨λ˜μ–΄ μžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€.")); + final int currentSortOrder = todoSorts.indexOf(currentSort); + + if (currentSortOrder == destinationSort) { + throw new IllegalArgumentException("ν˜„μž¬ μœ„μΉ˜λ‘œλŠ” 이동할 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + + if (destinationSort < FIRST_ORDER || destinationSort >= todoSorts.size()) { + throw new InvalidUpdatedTodoSortException( + "Todo μˆœμ„œλŠ” 전체 Todo λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λŠ” μœ„μΉ˜μΌ 수 μ—†μŠ΅λ‹ˆλ‹€. sort - " + destinationSort); } } diff --git a/backend/src/main/java/site/coduo/todo/exception/InvalidTodoSortException.java b/backend/src/main/java/site/coduo/todo/exception/InvalidTodoSortException.java deleted file mode 100644 index 26d73d49..00000000 --- a/backend/src/main/java/site/coduo/todo/exception/InvalidTodoSortException.java +++ /dev/null @@ -1,8 +0,0 @@ -package site.coduo.todo.exception; - -public class InvalidTodoSortException extends TodoException { - - public InvalidTodoSortException(final String message) { - super(message); - } -} diff --git a/backend/src/main/java/site/coduo/todo/infrastructure/repository/ProdTodoRepository.java b/backend/src/main/java/site/coduo/todo/infrastructure/repository/ProdTodoRepository.java deleted file mode 100644 index 14504c27..00000000 --- a/backend/src/main/java/site/coduo/todo/infrastructure/repository/ProdTodoRepository.java +++ /dev/null @@ -1,50 +0,0 @@ -package site.coduo.todo.infrastructure.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.stereotype.Repository; - -import lombok.RequiredArgsConstructor; -import site.coduo.pairroom.domain.PairRoom; -import site.coduo.todo.domain.Todo; -import site.coduo.todo.service.port.TodoRepository; - -@RequiredArgsConstructor -@Repository -public class ProdTodoRepository implements TodoRepository { - - private final TodoJpaRepository todoJpaRepository; - - @Override - public List findAllByPairRoomOrderBySortAsc(final PairRoom pairRoom) { - return todoJpaRepository.findAllByPairRoomOrderBySortAsc(pairRoom) - .stream() - .map(TodoEntity::toDomain) - .toList(); - } - - @Override - public Todo save(final Todo todo) { - final TodoEntity entity = new TodoEntity(todo); - return todoJpaRepository.save(entity) - .toDomain(); - } - - @Override - public Optional findTopByPairRoomOrderBySortDesc(final PairRoom pairRoom) { - return todoJpaRepository.findTopByPairRoomOrderBySortDesc(pairRoom) - .map(TodoEntity::toDomain); - } - - @Override - public Optional findById(final Long id) { - return todoJpaRepository.findById(id) - .map(TodoEntity::toDomain); - } - - @Override - public void deleteById(final Long id) { - todoJpaRepository.deleteById(id); - } -} diff --git a/backend/src/main/java/site/coduo/todo/infrastructure/repository/TodoEntity.java b/backend/src/main/java/site/coduo/todo/infrastructure/repository/TodoEntity.java deleted file mode 100644 index 95464a85..00000000 --- a/backend/src/main/java/site/coduo/todo/infrastructure/repository/TodoEntity.java +++ /dev/null @@ -1,74 +0,0 @@ -package site.coduo.todo.infrastructure.repository; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; - -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import site.coduo.common.infrastructure.audit.entity.BaseTimeEntity; -import site.coduo.pairroom.domain.PairRoom; -import site.coduo.todo.domain.Todo; - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "todo") -@Entity -public class TodoEntity extends BaseTimeEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private PairRoom pairRoom; - - @Column(name = "CONTENT", nullable = false, length = 255) - private String content; - - @Column(name = "SORT", nullable = false) - private int sort; - - @Column(name = "IS_CHECKED", nullable = false) - private boolean isChecked; - - public TodoEntity(final Todo todo) { - this( - todo.getId(), - todo.getPairRoom(), - todo.getContent().getContent(), - todo.getSort().getSort(), - todo.getIsChecked().isChecked() - ); - } - - public TodoEntity( - final Long id, - final PairRoom pairRoom, - final String content, - final int sort, - final boolean isChecked - ) { - this.id = id; - this.pairRoom = pairRoom; - this.content = content; - this.sort = sort; - this.isChecked = isChecked; - } - - public Todo toDomain() { - return new Todo( - this.id, - this.pairRoom, - this.content, - this.sort, - this.isChecked - ); - } -} diff --git a/backend/src/main/java/site/coduo/todo/infrastructure/repository/TodoJpaRepository.java b/backend/src/main/java/site/coduo/todo/infrastructure/repository/TodoJpaRepository.java deleted file mode 100644 index 5c95f764..00000000 --- a/backend/src/main/java/site/coduo/todo/infrastructure/repository/TodoJpaRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package site.coduo.todo.infrastructure.repository; - -import java.util.List; -import java.util.Optional; - -import org.springframework.data.jpa.repository.JpaRepository; - -import site.coduo.pairroom.domain.PairRoom; - -public interface TodoJpaRepository extends JpaRepository { - - List findAllByPairRoomOrderBySortAsc(PairRoom pairRoom); - - Optional findTopByPairRoomOrderBySortDesc(PairRoom pairRoom); -} diff --git a/backend/src/main/java/site/coduo/todo/repository/TodoEntity.java b/backend/src/main/java/site/coduo/todo/repository/TodoEntity.java new file mode 100644 index 00000000..2a021d7e --- /dev/null +++ b/backend/src/main/java/site/coduo/todo/repository/TodoEntity.java @@ -0,0 +1,80 @@ +package site.coduo.todo.repository; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import site.coduo.common.infrastructure.audit.entity.BaseTimeEntity; +import site.coduo.pairroom.repository.PairRoomEntity; +import site.coduo.todo.domain.Todo; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "TODO") +@Entity +public class TodoEntity extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "PAIR_ROOM_ID", referencedColumnName = "ID") + private PairRoomEntity pairRoomEntity; + + @Column(name = "CONTENT", nullable = false, length = 255) + private String content; + + @Column(name = "SORT", nullable = false) + private double sort; + + @Column(name = "IS_CHECKED", nullable = false) + private boolean isChecked; + + public TodoEntity(final Todo todo) { + this.id = todo.getId(); + this.content = todo.getContent().getContent(); + this.sort = todo.getSort().getSort(); + this.isChecked = todo.getIsChecked().isChecked(); + } + + public TodoEntity(final Todo todo, final PairRoomEntity pairRoom) { + this.id = todo.getId(); + this.pairRoomEntity = pairRoom; + this.sort = todo.getSort().getSort(); + this.content = todo.getContent().getContent(); + this.isChecked = todo.getIsChecked().isChecked(); + } + + public void updateContent(final String content) { + this.content = content; + } + + public void toggleTodoChecked() { + this.isChecked = !this.isChecked; + } + + public Todo toDomain() { + return new Todo(this.id, this.content, this.sort, this.isChecked); + } + + @Override + public String toString() { + return "TodoEntity{" + + "id=" + id + + ", pairRoomEntity=" + pairRoomEntity + + ", content='" + content + '\'' + + ", sort=" + sort + + ", isChecked=" + isChecked + + '}'; + } +} diff --git a/backend/src/main/java/site/coduo/todo/repository/TodoRepository.java b/backend/src/main/java/site/coduo/todo/repository/TodoRepository.java new file mode 100644 index 00000000..28f3f805 --- /dev/null +++ b/backend/src/main/java/site/coduo/todo/repository/TodoRepository.java @@ -0,0 +1,20 @@ +package site.coduo.todo.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import site.coduo.pairroom.repository.PairRoomEntity; +import site.coduo.todo.exception.TodoNotFoundException; + +public interface TodoRepository extends JpaRepository { + + default TodoEntity fetchById(Long id) { + return findById(id).orElseThrow(() -> new TodoNotFoundException("μ‘΄μž¬ν•˜μ§€ μ•Šμ€ todo idμž…λ‹ˆλ‹€." + id)); + } + + List findAllByPairRoomEntityOrderBySortAsc(PairRoomEntity pairRoomEntity); + + Optional findTopByPairRoomEntityOrderBySortDesc(PairRoomEntity pairRoomEntity); +} diff --git a/backend/src/main/java/site/coduo/todo/service/TodoService.java b/backend/src/main/java/site/coduo/todo/service/TodoService.java index f10fdf59..0ece8be0 100644 --- a/backend/src/main/java/site/coduo/todo/service/TodoService.java +++ b/backend/src/main/java/site/coduo/todo/service/TodoService.java @@ -1,22 +1,23 @@ package site.coduo.todo.service; import java.util.List; -import java.util.Optional; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; -import site.coduo.pairroom.domain.PairRoom; -import site.coduo.pairroom.domain.accesscode.AccessCode; import site.coduo.pairroom.exception.PairRoomNotFoundException; -import site.coduo.pairroom.service.port.PairRoomRepository; +import site.coduo.pairroom.repository.PairRoomEntity; +import site.coduo.pairroom.repository.PairRoomRepository; import site.coduo.todo.domain.Todo; import site.coduo.todo.domain.TodoSort; import site.coduo.todo.exception.TodoNotFoundException; -import site.coduo.todo.service.port.TodoRepository; +import site.coduo.todo.repository.TodoEntity; +import site.coduo.todo.repository.TodoRepository; @RequiredArgsConstructor @Service +@Transactional public class TodoService { private static final int NO_SORT_VALUE = 0; @@ -25,47 +26,59 @@ public class TodoService { private final PairRoomRepository pairRoomRepository; private final TodoRepository todoRepository; + @Transactional(readOnly = true) public List getAllOrderBySort(final String accessCode) { - final PairRoom pairRoom = pairRoomRepository.findByAccessCode(new AccessCode(accessCode)) + final PairRoomEntity pairRoom = pairRoomRepository.findByAccessCode(accessCode) .orElseThrow(() -> new PairRoomNotFoundException("ν•΄λ‹Ή Access Code의 νŽ˜μ–΄λ£Έμ€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. - " + accessCode)); - return todoRepository.findAllByPairRoomOrderBySortAsc(pairRoom); + + return todoRepository.findAllByPairRoomEntityOrderBySortAsc(pairRoom) + .stream() + .map(TodoEntity::toDomain) + .toList(); } public void createTodo(final String accessCode, final String content) { - final PairRoom pairRoom = pairRoomRepository.findByAccessCode(new AccessCode(accessCode)) + final PairRoomEntity pairRoom = pairRoomRepository.findByAccessCode(accessCode) .orElseThrow(() -> new PairRoomNotFoundException("ν•΄λ‹Ή Access Code의 νŽ˜μ–΄λ£Έμ€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. - " + accessCode)); - final TodoSort nextToLastSort = getLastTodoSort(pairRoom).orElseGet(() -> new TodoSort(NO_SORT_VALUE)) - .countNextSort(); - final Todo todo = new Todo(null, pairRoom, content, nextToLastSort.getSort(), INITIAL_TODO_CHECKED); + final TodoSort nextToLastSort = getLastTodoSort(pairRoom); + final Todo todo = new Todo(null, content, nextToLastSort.getSort(), INITIAL_TODO_CHECKED); + final TodoEntity todoEntity = new TodoEntity(todo, pairRoom); - todoRepository.save(todo); + todoRepository.save(todoEntity); } - private Optional getLastTodoSort(final PairRoom pairRoom) { - return todoRepository.findTopByPairRoomOrderBySortDesc(pairRoom) - .map(Todo::getSort); + private TodoSort getLastTodoSort(final PairRoomEntity pairRoom) { + return todoRepository.findTopByPairRoomEntityOrderBySortDesc(pairRoom) + .map(TodoEntity::toDomain) + .map(Todo::getSort) + .orElseGet(() -> new TodoSort(NO_SORT_VALUE)) + .countNextSort(); } public void updateTodoContent(final Long todoId, final String content) { - final Todo todo = todoRepository.findById(todoId) - .orElseThrow(() -> new TodoNotFoundException("μ‘΄μž¬ν•˜μ§€ μ•Šμ€ todo idμž…λ‹ˆλ‹€." + todoId)); - final Todo updatedTodo = todo.updateContent(content); - todoRepository.save(updatedTodo); + final TodoEntity todoEntity = todoRepository.fetchById(todoId); + + todoEntity.updateContent(content); } public void toggleTodoChecked(final Long todoId) { - final Todo todo = todoRepository.findById(todoId) - .orElseThrow(() -> new TodoNotFoundException("μ‘΄μž¬ν•˜μ§€ μ•Šμ€ todo idμž…λ‹ˆλ‹€." + todoId)); - final Todo updatedTodo = todo.toggleTodoChecked(); - todoRepository.save(updatedTodo); + final TodoEntity todoEntity = todoRepository.fetchById(todoId); + + todoEntity.toggleTodoChecked(); } public void updateTodoSort(final Long targetTodoId, final int destinationSort) { - final Todo targetTodo = todoRepository.findById(targetTodoId) + final TodoEntity targetTodo = todoRepository.findById(targetTodoId) .orElseThrow(() -> new TodoNotFoundException("μ‘΄μž¬ν•˜μ§€ μ•Šμ€ todo idμž…λ‹ˆλ‹€." + targetTodoId)); - final List allByPairRoom = todoRepository.findAllByPairRoomOrderBySortAsc(targetTodo.getPairRoom()); - final Todo updated = targetTodo.updateSort(allByPairRoom, destinationSort); - todoRepository.save(updated); + final List allByPairRoom = todoRepository + .findAllByPairRoomEntityOrderBySortAsc(targetTodo.getPairRoomEntity()) + .stream() + .map(TodoEntity::toDomain) + .toList(); + + final Todo updated = targetTodo.toDomain().updateSort(allByPairRoom, destinationSort); + final TodoEntity updatedTodoEntity = new TodoEntity(updated, targetTodo.getPairRoomEntity()); + todoRepository.save(updatedTodoEntity); } public void deleteTodo(final Long todoId) { diff --git a/backend/src/main/java/site/coduo/todo/service/port/TodoRepository.java b/backend/src/main/java/site/coduo/todo/service/port/TodoRepository.java deleted file mode 100644 index 81db7be0..00000000 --- a/backend/src/main/java/site/coduo/todo/service/port/TodoRepository.java +++ /dev/null @@ -1,20 +0,0 @@ -package site.coduo.todo.service.port; - -import java.util.List; -import java.util.Optional; - -import site.coduo.pairroom.domain.PairRoom; -import site.coduo.todo.domain.Todo; - -public interface TodoRepository { - - List findAllByPairRoomOrderBySortAsc(final PairRoom pairRoom); - - Optional findById(Long id); - - Optional findTopByPairRoomOrderBySortDesc(PairRoom pairRoom); - - Todo save(Todo todo); - - void deleteById(Long id); -} diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml new file mode 100644 index 00000000..ac8d9493 --- /dev/null +++ b/backend/src/main/resources/application-prod.yml @@ -0,0 +1,57 @@ +spring: + config: + activate: + on-profile: prod + + datasource: + replica: + master: + driver-class-name: com.mysql.cj.jdbc.Driver + jdbc-url: ${MASTER_DB_URL} + username: ${MASTER_DB_USERNAME} + password: ${MASTER_DB_PASSWORD} + slave: + driver-class-name: com.mysql.cj.jdbc.Driver + jdbc-url: ${SLAVE_DB_URL} + username: ${SLAVE_DB_USERNAME} + password: ${SLAVE_DB_PASSWORD} + + jpa: + database: mysql + hibernate: + ddl-auto: ${DDL_AUTO} + database-platform: org.hibernate.dialect.MySQLDialect + open-in-view: false + +springdoc: + swagger-ui: + persist-authorization: true + default-models-expand-depth: -1 + path: /api/docs + +management: + endpoints: + web: + exposure: + include: prometheus + base-path: /api/actuator + +server: + tomcat: + mbeanregistry: + enabled: true + +oauth: + github: + client-id: ${CLIENT_ID} + client-secret: ${CLIENT_SECRET} + redirect-uri: ${CLIENT_REDIRECT_URI} + +front: + url: coduo.site + +jwt: + sign-key: ${JWT_KEY} + +ec2: + prefix: ${INSTANCE_NAME} diff --git a/backend/src/main/resources/application-test.yml b/backend/src/main/resources/application-test.yml index ef4c2eda..30661da7 100644 --- a/backend/src/main/resources/application-test.yml +++ b/backend/src/main/resources/application-test.yml @@ -1,19 +1,24 @@ spring: - config: - activate: - on-profile: test - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: ${DB_URL} - username: ${DB_USERNAME} - password: ${DB_PASSWORD} + replica: + master: + driver-class-name: com.mysql.cj.jdbc.Driver + jdbc-url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + slave: + driver-class-name: com.mysql.cj.jdbc.Driver + jdbc-url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + jpa: database: mysql hibernate: ddl-auto: ${DDL_AUTO} database-platform: org.hibernate.dialect.MySQLDialect + open-in-view: false springdoc: swagger-ui: @@ -26,11 +31,24 @@ management: web: exposure: include: prometheus + base-path: /api/actuator + +server: + tomcat: + mbeanregistry: + enabled: true + oauth: github: client-id: ${CLIENT_ID} client-secret: ${CLIENT_SECRET} redirect-uri: ${CLIENT_REDIRECT_URI} +front: + url: test.coduo.site + jwt: sign-key: ${JWT_KEY} + +ec2: + prefix: ${INSTANCE_NAME} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 137d6f72..543fa4da 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1 +1,4 @@ -spring.profiles.active: local +spring: + profiles: + active: local + diff --git a/backend/src/test/java/site/coduo/acceptance/AcceptanceFixture.java b/backend/src/test/java/site/coduo/acceptance/AcceptanceFixture.java index aedae1a8..50206535 100644 --- a/backend/src/test/java/site/coduo/acceptance/AcceptanceFixture.java +++ b/backend/src/test/java/site/coduo/acceptance/AcceptanceFixture.java @@ -12,10 +12,11 @@ import site.coduo.config.TestConfig; import site.coduo.member.domain.repository.MemberRepository; import site.coduo.pairroom.repository.PairRoomRepository; -import site.coduo.pairroomhistory.repository.PairRoomHistoryRepository; +import site.coduo.pairroom.repository.PairRoomMemberRepository; import site.coduo.referencelink.repository.CategoryRepository; import site.coduo.referencelink.repository.OpenGraphRepository; import site.coduo.referencelink.repository.ReferenceLinkRepository; +import site.coduo.timer.repository.TimerRepository; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @Import(TestConfig.class) @@ -37,7 +38,10 @@ abstract class AcceptanceFixture { private CategoryRepository categoryRepository; @Autowired - private PairRoomHistoryRepository pairRoomHistoryRepository; + private TimerRepository timerRepository; + + @Autowired + private PairRoomMemberRepository pairRoomMemberRepository; @LocalServerPort private int port; @@ -49,7 +53,8 @@ void setUp() { @AfterEach void tearDown() { - pairRoomHistoryRepository.deleteAll(); + pairRoomMemberRepository.deleteAll(); + timerRepository.deleteAll(); memberRepository.deleteAll(); openGraphRepository.deleteAll(); referenceLinkRepository.deleteAll(); diff --git a/backend/src/test/java/site/coduo/acceptance/AuthAcceptanceTest.java b/backend/src/test/java/site/coduo/acceptance/AuthAcceptanceTest.java index 8666081f..3494df21 100644 --- a/backend/src/test/java/site/coduo/acceptance/AuthAcceptanceTest.java +++ b/backend/src/test/java/site/coduo/acceptance/AuthAcceptanceTest.java @@ -2,6 +2,8 @@ import static org.hamcrest.Matchers.is; +import static site.coduo.common.config.web.filter.AccessTokenCookieFilter.TEMPORARY_ACCESS_TOKEN_COOKIE_NAME; + import java.util.Map; import org.apache.http.HttpStatus; @@ -15,16 +17,20 @@ import site.coduo.fake.FakeGithubOAuthClient; import site.coduo.member.domain.Member; import site.coduo.member.domain.repository.MemberRepository; +import site.coduo.member.infrastructure.security.JwtProvider; class AuthAcceptanceTest extends AcceptanceFixture { @Autowired private MemberRepository memberRepository; + @Autowired + private JwtProvider jwtProvider; + @Test @DisplayName("둜그인 검증 & 둜그인 토큰을 λ°œκΈ‰ν•œλ‹€.") void verify_login_and_publish_login_token() { - final String sessionId = GithubAcceptanceTest.createAccessTokenThenReturnSessionId(); + final String cookie = GithubAcceptanceTest.createAccessTokenCookie(); final Member member = createMember(); memberRepository.save(member); @@ -32,7 +38,7 @@ void verify_login_and_publish_login_token() { // when RestAssured .given() - .cookie("JSESSIONID", sessionId) + .cookie(TEMPORARY_ACCESS_TOKEN_COOKIE_NAME, cookie) .when() .get("/api/sign-in/callback") @@ -46,12 +52,12 @@ void verify_login_and_publish_login_token() { @Test @DisplayName("둜그인 검증 & 둜그인 토큰을 λ°œκΈ‰ν•œλ‹€. - 둜그인 μ‹€νŒ¨ μΌ€μ΄μŠ€") void verify_login_and_publish_login_token_dose_not_exists_case() { - final String sessionId = GithubAcceptanceTest.createAccessTokenThenReturnSessionId(); + final String tempCookie = GithubAcceptanceTest.createAccessTokenCookie(); // when RestAssured .given() - .cookie("JSESSIONID", sessionId) + .cookie(TEMPORARY_ACCESS_TOKEN_COOKIE_NAME, tempCookie) .when() .get("/api/sign-in/callback") @@ -66,7 +72,7 @@ void verify_login_and_publish_login_token_dose_not_exists_case() { @DisplayName("λ‘œκ·Έμ•„μ›ƒ μš”μ²­μ„ ν•˜λ©΄ JWT μΏ ν‚€κ°€ μ‚­μ œλœλ‹€.") void remove_jwt_cookie_when_accept_logout_request() { // given - final String loginToken = login(); + final String loginToken = jwtProvider.sign(FakeGithubOAuthClient.OAUTH_CLIENT_ID); // when & then RestAssured @@ -84,12 +90,12 @@ void remove_jwt_cookie_when_accept_logout_request() { @DisplayName("νšŒμ›μ˜ 둜그인 μƒνƒœλ₯Ό ν™•μΈν•œλ‹€.") void check_member_login_state() { // given - final String loginToken = login(); + final String signInToken = jwtProvider.sign(FakeGithubOAuthClient.OAUTH_CLIENT_ID); // when & then RestAssured .given() - .cookie("coduo_whoami", loginToken) + .cookie("coduo_whoami", signInToken) .when() .get("/api/sign-in/check") @@ -99,38 +105,11 @@ void check_member_login_state() { .body("signedIn", is(true)); } - String login() { - final String sessionId = GithubAcceptanceTest.createAccessTokenThenReturnSessionId(); - final Member member = createMember(); - - memberRepository.save(member); - - return RestAssured - .given() - .cookie("JSESSIONID", sessionId) - - .when() - .get("/api/sign-in/callback") - - .thenReturn() - .cookie("coudo_whoami"); - } - - private Member createMember() { - return Member.builder() - .username("test user") - .userId(FakeGithubApiClient.USER_ID) - .loginId(FakeGithubApiClient.LOGIN_ID) - .accessToken(FakeGithubOAuthClient.ACCESS_TOKEN.getCredential()) - .profileImage(FakeGithubApiClient.PROFILE_IMAGE) - .build(); - } - @Test @DisplayName("인가 정보λ₯Ό 톡해 νšŒμ›κ°€μž…μ„ ν•œλ‹€.") void sign_up_via_authorization_info() { // given - final String sessionId = GithubAcceptanceTest.createAccessTokenThenReturnSessionId(); + final String tempCookie = GithubAcceptanceTest.createAccessTokenCookie(); final Map body = Map.of("username", "λ‹‰λ„€μž„"); // when & then @@ -138,7 +117,7 @@ void sign_up_via_authorization_info() { .given().log().all() .contentType(ContentType.JSON) .body(body) - .cookie("JSESSIONID", sessionId) + .cookie(TEMPORARY_ACCESS_TOKEN_COOKIE_NAME, tempCookie) .when() .post("/api/sign-up") @@ -146,4 +125,29 @@ void sign_up_via_authorization_info() { .then().log().all() .statusCode(HttpStatus.SC_MOVED_TEMPORARILY); } + + @Test + @DisplayName("μ•‘μ„ΈμŠ€ 토큰이 없을 λ•Œ μ˜ˆμ™Έλ₯Ό λ˜μ§„λ‹€.") + void no_access_token() { + RestAssured + .given().log().all() + .contentType(ContentType.JSON) + + .when() + .post("/api/sign-up") + + .then().log().all() + .statusCode(HttpStatus.SC_UNAUTHORIZED); + } + + private Member createMember() { + return Member.builder() + .username("test user") + .userId(FakeGithubApiClient.USER_ID) + .loginId(FakeGithubApiClient.LOGIN_ID) + .accessToken(FakeGithubOAuthClient.ACCESS_TOKEN.getCredential()) + .profileImage(FakeGithubApiClient.PROFILE_IMAGE) + .build(); + } + } diff --git a/backend/src/test/java/site/coduo/acceptance/CategoryAcceptanceTest.java b/backend/src/test/java/site/coduo/acceptance/CategoryAcceptanceTest.java index d821d35c..2a14ffbb 100644 --- a/backend/src/test/java/site/coduo/acceptance/CategoryAcceptanceTest.java +++ b/backend/src/test/java/site/coduo/acceptance/CategoryAcceptanceTest.java @@ -1,15 +1,20 @@ package site.coduo.acceptance; +import static org.assertj.core.api.Assertions.assertThat; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import org.springframework.transaction.annotation.Transactional; import io.restassured.RestAssured; -import site.coduo.pairroom.dto.PairRoomCreateRequest; -import site.coduo.pairroom.dto.PairRoomCreateResponse; +import site.coduo.pairroom.domain.PairRoomStatus; +import site.coduo.pairroom.service.dto.PairRoomCreateRequest; +import site.coduo.pairroom.service.dto.PairRoomCreateResponse; import site.coduo.referencelink.service.dto.CategoryCreateRequest; import site.coduo.referencelink.service.dto.CategoryCreateResponse; +import site.coduo.referencelink.service.dto.CategoryUpdateRequest; +import site.coduo.referencelink.service.dto.CategoryUpdateResponse; @Transactional class CategoryAcceptanceTest extends AcceptanceFixture { @@ -34,7 +39,8 @@ static CategoryCreateResponse createCategory(final String accessCode, final Cate void show_category() { //given final PairRoomCreateResponse pairRoomUrl = PairRoomAcceptanceTest.createPairRoom( - new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ")); + new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ", 10000L, 10000L, "https://missionUrl.xxx", + PairRoomStatus.IN_PROGRESS.name())); createCategory(pairRoomUrl.accessCode(), new CategoryCreateRequest("μƒˆλ‘œμš΄ μΉ΄ν…Œκ³ λ¦¬")); @@ -53,4 +59,67 @@ void show_category() { .all() .statusCode(200); } + + @Test + @DisplayName("μΉ΄ν…Œκ³ λ¦¬λ₯Ό μ—…λ°μ΄νŠΈ ν•œλ‹€.") + void update_category() { + //given + final PairRoomCreateResponse pairRoomUrl = PairRoomAcceptanceTest.createPairRoom( + new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ", 10000L, 10000L, "https://missionUrl.xxx", + PairRoomStatus.IN_PROGRESS.name())); + + final CategoryCreateResponse previousCategory = createCategory(pairRoomUrl.accessCode(), + new CategoryCreateRequest("이전 μΉ΄ν…Œκ³ λ¦¬")); + + final String updateName = "λ³€κ²½λœ μΉ΄ν…Œκ³ λ¦¬"; + final CategoryUpdateRequest request = new CategoryUpdateRequest(Long.parseLong(previousCategory.id()), + updateName); + + //when & then + final CategoryUpdateResponse categoryUpdateResponse = RestAssured + .given() + .log() + .all() + .contentType("application/json") + + .when() + .body(request) + .patch("/api/{accessCode}/category", pairRoomUrl.accessCode()) + + .then() + .log() + .all() + .statusCode(200) + .extract() + .as(CategoryUpdateResponse.class); + + assertThat(categoryUpdateResponse.updatedCategoryName()).isEqualTo(updateName); + } + + @Test + @DisplayName("μΉ΄ν…Œκ³ λ¦¬ μ‚­μ œμ— μ„±κ³΅ν•œλ‹€.") + void delete_category() { + //given + final PairRoomCreateResponse pairRoomUrl = PairRoomAcceptanceTest.createPairRoom( + new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ", 10000L, 10000L, "https://missionUrl.xxx", + PairRoomStatus.IN_PROGRESS.name())); + + final CategoryCreateResponse category = createCategory(pairRoomUrl.accessCode(), + new CategoryCreateRequest("μžλ°”")); + + //when & then + RestAssured + .given() + .log() + .all() + .contentType("application/json") + + .when() + .delete("/api/{accessCode}/category/{categoryId}", pairRoomUrl.accessCode(), category.id()) + + .then() + .log() + .all() + .statusCode(204); + } } diff --git a/backend/src/test/java/site/coduo/acceptance/GithubAcceptanceTest.java b/backend/src/test/java/site/coduo/acceptance/GithubAcceptanceTest.java index 10565f5f..350afd1e 100644 --- a/backend/src/test/java/site/coduo/acceptance/GithubAcceptanceTest.java +++ b/backend/src/test/java/site/coduo/acceptance/GithubAcceptanceTest.java @@ -3,6 +3,8 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; +import static site.coduo.common.config.web.filter.AccessTokenCookieFilter.TEMPORARY_ACCESS_TOKEN_COOKIE_NAME; + import java.util.Map; import org.apache.http.HttpStatus; @@ -13,21 +15,18 @@ import io.restassured.RestAssured; import site.coduo.fake.FakeGithubApiClient; import site.coduo.fake.FakeGithubOAuthClient; -import site.coduo.fake.FixedNanceProvider; +import site.coduo.fake.FixedNonceProvider; import site.coduo.member.domain.Member; class GithubAcceptanceTest extends AcceptanceFixture { - static String createAccessTokenThenReturnSessionId() { - final String session = callAuthorizeThenReturnSessionId(); - + static String createAccessTokenCookie() { final Map query = Map.of("code", "authorization code", - "state", FixedNanceProvider.FIXED_VALUE); + "state", FixedNonceProvider.FIXED_VALUE); - RestAssured + return RestAssured .given() .queryParams(query) - .sessionId("JSESSIONID", session) .redirects() .follow(false) .log().all() @@ -35,23 +34,8 @@ static String createAccessTokenThenReturnSessionId() { .when() .get("/api/github/callback") - .then() - .statusCode(HttpStatus.SC_MOVED_TEMPORARILY); - - return session; - } - - static String callAuthorizeThenReturnSessionId() { - return RestAssured - .given() - .redirects() - .follow(false) - - .when() - .get("/api/sign-in/oauth/github") - .thenReturn() - .getSessionId(); + .getCookie(TEMPORARY_ACCESS_TOKEN_COOKIE_NAME); } @Test @@ -69,7 +53,7 @@ void request_to_github_authorization_end_point() { .assertThat() .statusCode(HttpStatus.SC_OK) .body("endpoint", - is("https://www.github.com/login/oauth/authorize?client_id=test&state=random%20number&redirect_uri=http://test.test")); + is("https://www.github.com/login/oauth/authorize?client_id=test&state=randomNumber&redirect_uri=http://test.test")); } @Test @@ -92,16 +76,13 @@ void call_github_authorize_endpoint() { @DisplayName("callback μ—”λ“œν¬μΈνŠΈ 호좜") void call_callback_end_point() { // given - final String session = callAuthorizeThenReturnSessionId(); - final Map query = Map.of("code", "authorization code", - "state", FixedNanceProvider.FIXED_VALUE); + "state", FixedNonceProvider.FIXED_VALUE); // when & then RestAssured .given() .queryParams(query) - .sessionId("JSESSIONID", session) .redirects() .follow(false) .log().all() @@ -110,7 +91,7 @@ void call_callback_end_point() { .get("/api/github/callback") .then() - .statusCode(HttpStatus.SC_MOVED_TEMPORARILY); + .statusCode(HttpStatus.SC_TEMPORARY_REDIRECT); } @Test @@ -124,9 +105,8 @@ void try_login_when_call_callback_end_point() { .accessToken(FakeGithubOAuthClient.ACCESS_TOKEN.getCredential()) .profileImage(FakeGithubApiClient.PROFILE_IMAGE) .build(); - final String session = callAuthorizeThenReturnSessionId(); final Map query = Map.of("code", "authorization code", - "state", FixedNanceProvider.FIXED_VALUE); + "state", FixedNonceProvider.FIXED_VALUE); memberRepository.save(member); @@ -134,7 +114,6 @@ void try_login_when_call_callback_end_point() { RestAssured .given() .queryParams(query) - .sessionId("JSESSIONID", session) .redirects() .follow(false) .log().all() @@ -143,6 +122,6 @@ void try_login_when_call_callback_end_point() { .get("/api/github/callback") .then().log().all() - .statusCode(302); + .statusCode(HttpStatus.SC_TEMPORARY_REDIRECT); } } diff --git a/backend/src/test/java/site/coduo/acceptance/MemberAcceptanceTest.java b/backend/src/test/java/site/coduo/acceptance/MemberAcceptanceTest.java new file mode 100644 index 00000000..f8cffac0 --- /dev/null +++ b/backend/src/test/java/site/coduo/acceptance/MemberAcceptanceTest.java @@ -0,0 +1,116 @@ +package site.coduo.acceptance; + +import static org.hamcrest.Matchers.is; + +import static site.coduo.common.config.web.filter.SignInCookieFilter.SIGN_IN_COOKIE_NAME; + +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import io.restassured.RestAssured; +import site.coduo.member.domain.Member; +import site.coduo.member.domain.repository.MemberRepository; +import site.coduo.member.infrastructure.security.JwtProvider; + +class MemberAcceptanceTest extends AcceptanceFixture { + + @Autowired + private JwtProvider jwtProvider; + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("νšŒμ›μ˜ 정보λ₯Ό μ‘°νšŒν•œλ‹€.") + void search_member_info() { + // given + final Member member = Member.builder() + .userId("123") + .accessToken("access") + .loginId("login") + .username("username") + .profileImage("some image") + .build(); + + final String loginToken = jwtProvider.sign(member.getUserId()); + memberRepository.save(member); + + // when & then + RestAssured + .given() + .cookie(SIGN_IN_COOKIE_NAME, loginToken) + + .when() + .get("/api/member") + + .then() + .statusCode(HttpStatus.SC_OK) + .body("username", is(member.getUsername())); + } + + @Test + @DisplayName("νšŒμ›μ„ μ‚­μ œν•œλ‹€.") + void delete_member() { + //given + final Member member = Member.builder() + .userId("123") + .accessToken("access") + .loginId("login") + .username("username") + .profileImage("some image") + .build(); + + final String loginToken = jwtProvider.sign(member.getUserId()); + memberRepository.save(member); + + //when && then + RestAssured + .given() + .cookie(SIGN_IN_COOKIE_NAME, loginToken) + + .when() + .delete("/api/member") + + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + } + + @Test + @DisplayName("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” νšŒμ›μ„ μ‚­μ œν•œλ‹€.") + void delete_not_member() { + //given + final Member member = Member.builder() + .userId("123") + .accessToken("access") + .loginId("login") + .username("username") + .profileImage("some image") + .build(); + + final String loginToken = jwtProvider.sign(member.getUserId()); + memberRepository.save(member); + + //when && then + RestAssured + .given() + .cookie(SIGN_IN_COOKIE_NAME, loginToken) + + .when() + .delete("/api/member") + + .then() + .statusCode(HttpStatus.SC_NO_CONTENT); + + RestAssured + .given() + .cookie(SIGN_IN_COOKIE_NAME, loginToken) + + .when() + .delete("/api/member") + + .then() + .statusCode(HttpStatus.SC_NOT_FOUND); + } +} diff --git a/backend/src/test/java/site/coduo/acceptance/PairRoomAcceptanceTest.java b/backend/src/test/java/site/coduo/acceptance/PairRoomAcceptanceTest.java index a80a325f..f799d14b 100644 --- a/backend/src/test/java/site/coduo/acceptance/PairRoomAcceptanceTest.java +++ b/backend/src/test/java/site/coduo/acceptance/PairRoomAcceptanceTest.java @@ -1,25 +1,28 @@ package site.coduo.acceptance; +import static org.assertj.core.api.Assertions.assertThat; + import java.util.Map; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; -import org.springframework.transaction.annotation.Transactional; import io.restassured.RestAssured; -import site.coduo.pairroom.dto.PairRoomCreateRequest; -import site.coduo.pairroom.dto.PairRoomCreateResponse; +import io.restassured.http.ContentType; +import site.coduo.pairroom.domain.PairRoomStatus; +import site.coduo.pairroom.service.dto.PairRoomCreateRequest; +import site.coduo.pairroom.service.dto.PairRoomCreateResponse; +import site.coduo.pairroom.service.dto.PairRoomExistResponse; -@Transactional class PairRoomAcceptanceTest extends AcceptanceFixture { - static PairRoomCreateResponse createPairRoom(final PairRoomCreateRequest pairRoom) { + static PairRoomCreateResponse createPairRoom(final PairRoomCreateRequest request) { return RestAssured .given() .contentType(MediaType.APPLICATION_JSON_VALUE) .accept(MediaType.APPLICATION_JSON_VALUE) - .body(pairRoom) + .body(request) .when() .post("/api/pair-room") @@ -29,110 +32,148 @@ static PairRoomCreateResponse createPairRoom(final PairRoomCreateRequest pairRoo .as(PairRoomCreateResponse.class); } - static void createTimerDuration(final String accessCode, final long timerDuration) { - final Map request = Map.of("timerDuration", timerDuration); + @Test + @DisplayName("νŽ˜μ–΄λ£Έ μš”μ²­ μ‹œ 정보λ₯Ό λ°˜ν™˜ν•œλ‹€.") + void show_pair_room() { + //given + final PairRoomCreateResponse pairRoomUrl = + createPairRoom( + new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ", 10000L, 10000L, "https://missionUrl.xxx", "IN_PROGRESS")); + //when & then RestAssured .given() .log() .all() .contentType("application/json") - .body(request) .when() - .patch("/api/pair-room/{accessCode}/timer", accessCode) + .get("/api/pair-room/" + pairRoomUrl.accessCode()) .then() .log() .all() - .statusCode(201); + .statusCode(200); } @Test - @DisplayName("νŽ˜μ–΄λ£Έ μš”μ²­ μ‹œ 정보λ₯Ό λ°˜ν™˜ν•œλ‹€.") - void show_pair_room() { + @DisplayName("νŽ˜μ–΄λ£Έμ˜ μƒνƒœλ₯Ό λ³€κ²½ν•œλ‹€.") + void update_pair_room_status() { //given - final PairRoomCreateResponse pairRoomUrl = createPairRoom(new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ")); - createTimerDuration(pairRoomUrl.accessCode(), 600000); + final PairRoomCreateResponse accessCode = + createPairRoom( + new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ", 1000L, 100L, "https://missionUrl.xxx", "IN_PROGRESS")); + final Map status = Map.of("status", PairRoomStatus.IN_PROGRESS.name()); - //when & then + // when & then RestAssured .given() .log() .all() - .contentType("application/json") + .contentType(ContentType.JSON) + .body(status) .when() - .get("/api/pair-room/" + pairRoomUrl.accessCode()) + .patch("/api/pair-room/{accessCode}/status", accessCode.accessCode()) .then() .log() .all() - .statusCode(200); + .statusCode(204); } @Test - @DisplayName("타이머 μ‹œκ°„μ„ μ €μž₯ν•œλ‹€.") - void save_timer_duration() { + @DisplayName("νŽ˜μ–΄λ£Έμ˜ λ“œλΌμ΄λ²„μ™€ λ‚΄λΉ„κ²Œμ΄ν„°λ₯Ό λ³€κ²½ν•œλ‹€.") + void update_driver_navigator() { // given - final PairRoomCreateResponse pairRoomUrl = createPairRoom(new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ")); - final Map request = Map.of("timerDuration", 600000); + final PairRoomCreateResponse accessCode = + createPairRoom( + new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ", 1000L, 100L, "https://missionUrl.xxx", "IN_PROGRESS")); // when & then RestAssured .given() .log() .all() - .contentType("application/json") - .body(request) .when() - .patch("/api/pair-room/{accessCode}/timer", pairRoomUrl.accessCode()) + .patch("/api/pair-room/{access-code}/pair-swap", accessCode.accessCode()) .then() - .log() - .all() - .statusCode(201); + .statusCode(204); } @Test - @DisplayName("νŽ˜μ–΄λ£Έμ„ μ‚­μ œν•œλ‹€.") - void delete_pair_room() { + @DisplayName("νŽ˜μ–΄λ£Έμ΄ μ‘΄μž¬ν•˜λ©΄ trueλ₯Ό λ°˜ν™˜ν•œλ‹€.") + void exist_pair_room_true() { //given - final PairRoomCreateResponse pairRoomUrl = createPairRoom(new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ")); + final PairRoomCreateResponse accessCode = + createPairRoom( + new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ", 1000L, 100L, "https://missionUrl.xxx", "IN_PROGRESS")); - //when & then - RestAssured + // when & then + final PairRoomExistResponse response = RestAssured .given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE) .log() .all() - .contentType("application/json") .when() - .delete("/api/pair-room/" + pairRoomUrl.accessCode()) + .queryParam("access_code", accessCode.accessCode()) + .get("/api/pair-room/exists") .then() + .statusCode(200) + .extract() + .as(PairRoomExistResponse.class); + + assertThat(response.exists()).isTrue(); + } + + @Test + @DisplayName("νŽ˜μ–΄λ£Έμ΄ μ‘΄μž¬ν•˜λ©΄ falseλ₯Ό λ°˜ν™˜ν•œλ‹€.") + void exist_pair_room_false() { + //given + + // when & then + final PairRoomExistResponse response = RestAssured + .given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE) .log() .all() - .statusCode(204); + + .when() + .queryParam("access_code", "babyroom") + .get("/api/pair-room/exists") + + .then() + .statusCode(200) + .extract() + .as(PairRoomExistResponse.class); + + assertThat(response.exists()).isFalse(); } @Test - @DisplayName("μ‘΄μž¬ν•˜μ§€ μ•Šμ€ accessCode둜 νŽ˜μ–΄λ£Έ μ‚­μ œμ‹œ μ‹€νŒ¨ν•œλ‹€.") - void fail_delete_pair_room() { - //when & then + @DisplayName("νŽ˜μ–΄λ£Έμ„ μ‚­μ œν•œλ‹€.") + void delete_pair_room() { + // given + final PairRoomCreateResponse accessCode = + createPairRoom( + new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ", 1000L, 100L, "https://missionUrl.xxx", "IN_PROGRESS")); + + // when & then RestAssured .given() .log() .all() - .contentType("application/json") .when() - .delete("/api/pair-room/" + "zzzzzz") + .delete("/api/pair-room/{access-code}", accessCode.accessCode()) .then() - .log() - .all() - .statusCode(404); + .statusCode(204); } } diff --git a/backend/src/test/java/site/coduo/acceptance/PairRoomHistoryAcceptanceTest.java b/backend/src/test/java/site/coduo/acceptance/PairRoomHistoryAcceptanceTest.java deleted file mode 100644 index d04da826..00000000 --- a/backend/src/test/java/site/coduo/acceptance/PairRoomHistoryAcceptanceTest.java +++ /dev/null @@ -1,115 +0,0 @@ -package site.coduo.acceptance; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.http.MediaType; - -import io.restassured.RestAssured; -import site.coduo.pairroom.dto.PairRoomCreateRequest; -import site.coduo.pairroom.dto.PairRoomCreateResponse; -import site.coduo.pairroomhistory.dto.PairRoomHistoryCreateRequest; -import site.coduo.pairroomhistory.dto.PairRoomHistoryUpdateRequest; - -class PairRoomHistoryAcceptanceTest extends AcceptanceFixture { - - static PairRoomCreateResponse createPairRoom(final PairRoomCreateRequest pairRoom) { - return RestAssured - .given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .accept(MediaType.APPLICATION_JSON_VALUE) - .body(pairRoom) - - .when() - .post("/api/pair-room") - - .then() - .extract() - .as(PairRoomCreateResponse.class); - } - - static void savePairRoomHistory(final String accessCode, final PairRoomHistoryCreateRequest request) { - RestAssured - .given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(request) - - .when() - .post("/api/{accessCode}/history", accessCode) - - .then() - .statusCode(201); - } - - @Test - @DisplayName("νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬λ₯Ό μ €μž₯ν•œλ‹€.") - void create_pair_room_history() { - // given - final PairRoomCreateResponse pairRoomCreateResponse = createPairRoom(new PairRoomCreateRequest("ν•΄μ‹œ", "νŒŒλž€")); - final PairRoomHistoryCreateRequest request = new PairRoomHistoryCreateRequest( - "ν•΄μ‹œ", - "νŒŒλž€", - 123, - 600000 - ); - - // when & then - RestAssured - .given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(request) - - .when() - .post("/api/{accessCode}/history", pairRoomCreateResponse.accessCode()) - - .then() - .statusCode(201); - } - - @Test - @DisplayName("νŽ˜μ–΄λ£Έμ˜ κ°€μž₯ 졜근 νžˆμŠ€ν† λ¦¬λ₯Ό λ°˜ν™˜ν•œλ‹€.") - void get_latest_pair_room_history() { - // given - final PairRoomCreateResponse pairRoomCreateResponse = createPairRoom(new PairRoomCreateRequest("켈리", "νŒŒλž€")); - savePairRoomHistory( - pairRoomCreateResponse.accessCode(), - new PairRoomHistoryCreateRequest("켈리", "νŒŒλž€", 0, 600000) - ); - - // when & then - RestAssured - .given() - - .when() - .get("/api/{accessCode}/history/latest", pairRoomCreateResponse.accessCode()) - - .then() - .statusCode(200); - } - - @Test - @DisplayName("νŽ˜μ–΄λ£Έμ˜ 타이머 남은 μ‹œκ°„μ„ μ—…λ°μ΄νŠΈ ν•œλ‹€.") - void update_timer_remaining_time() { - // given - final PairRoomCreateResponse pairRoomCreateResponse = createPairRoom(new PairRoomCreateRequest("μž‰ν¬", "파슬리")); - savePairRoomHistory( - pairRoomCreateResponse.accessCode(), - new PairRoomHistoryCreateRequest("μž‰ν¬", "파슬리", 0, 600000) - ); - savePairRoomHistory( - pairRoomCreateResponse.accessCode(), - new PairRoomHistoryCreateRequest("파슬리", "μž‰ν¬", 1, 300000) - ); - - // when & then - RestAssured - .given() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(new PairRoomHistoryUpdateRequest(100000)) - - .when() - .patch("/api/{accessCode}/history/latest/timer-remaining-time", pairRoomCreateResponse.accessCode()) - - .then() - .statusCode(200); - } -} diff --git a/backend/src/test/java/site/coduo/acceptance/ReferenceAcceptanceTest.java b/backend/src/test/java/site/coduo/acceptance/ReferenceAcceptanceTest.java index e7dc95cf..2b4c2db6 100644 --- a/backend/src/test/java/site/coduo/acceptance/ReferenceAcceptanceTest.java +++ b/backend/src/test/java/site/coduo/acceptance/ReferenceAcceptanceTest.java @@ -10,23 +10,24 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.transaction.annotation.Transactional; import io.restassured.RestAssured; import io.restassured.http.ContentType; -import site.coduo.pairroom.dto.PairRoomCreateRequest; -import site.coduo.pairroom.dto.PairRoomCreateResponse; +import site.coduo.pairroom.service.dto.PairRoomCreateRequest; +import site.coduo.pairroom.service.dto.PairRoomCreateResponse; import site.coduo.referencelink.service.dto.CategoryCreateRequest; import site.coduo.referencelink.service.dto.CategoryCreateResponse; +import site.coduo.referencelink.service.dto.ReferenceLinkResponse; -@Transactional class ReferenceAcceptanceTest extends AcceptanceFixture { @Test @DisplayName("레퍼런슀 링크 생성 μš”μ²­") void reference_link_create_request() { // given - final PairRoomCreateResponse pairRoom = createPairRoom(new PairRoomCreateRequest("레λͺ¨λ„€", "ν”„λžŒ")); + final PairRoomCreateResponse pairRoom = + createPairRoom(new PairRoomCreateRequest("레λͺ¨λ„€", "ν”„λžŒ", 10000L, 10000L, + "https://missionUrl.xxx", "IN_PROGRESS")); final CategoryCreateResponse category = CategoryAcceptanceTest.createCategory( pairRoom.accessCode(), new CategoryCreateRequest("νƒ€μž…μŠ€ν¬λ¦½νŠΈ")); @@ -52,7 +53,9 @@ void reference_link_create_request() { @DisplayName("λͺ¨λ“  레퍼런슀 링크λ₯Ό μ‘°νšŒν•˜λŠ” μš”μ²­") void read_all_reference_link_request() { // given - final PairRoomCreateResponse pairRoom = createPairRoom(new PairRoomCreateRequest("레λͺ¨λ„€", "ν”„λžŒ")); + final PairRoomCreateResponse pairRoom = + createPairRoom(new PairRoomCreateRequest("레λͺ¨λ„€", "ν”„λžŒ", 10000L, 10000L, + "https://missionUrl.xxx", "IN_PROGRESS")); createReferenceLink("http://www.some1.url", pairRoom.accessCode(), "μΉ΄ν…Œκ³ λ¦¬1"); createReferenceLink("http://www.some2.url", pairRoom.accessCode(), "μΉ΄ν…Œκ³ λ¦¬2"); @@ -74,7 +77,9 @@ void read_all_reference_link_request() { @DisplayName("μ˜€ν”ˆκ·Έλž˜ν”„ 정보가 μ—†λŠ” 레퍼런슀 링크λ₯Ό μ‘°νšŒν•˜λ©΄ λ„λ©”μΈλ§Œ λ„£μ–΄ λ°˜ν™˜ν•œλ‹€.") void read_reference_link_without_open_graph() { // given - final PairRoomCreateResponse pairRoom = createPairRoom(new PairRoomCreateRequest("μž‰ν¬", "ν•΄μ‹œ")); + final PairRoomCreateResponse pairRoom = + createPairRoom( + new PairRoomCreateRequest("μž‰ν¬", "ν•΄μ‹œ", 1000L, 100L, "https://missionUrl.xxx", "IN_PROGRESS")); final String expectedUrl = "http://www.deleasfsdte.com"; createReferenceLink(expectedUrl, pairRoom.accessCode(), "μΉ΄ν…Œκ³ λ¦¬"); @@ -90,33 +95,40 @@ void read_reference_link_without_open_graph() { .assertThat() .statusCode(HttpStatus.OK.value()) .body("[0].url", is(expectedUrl)) - .body("[0].headTitle", is("deleasfsdte.com")) + .body("[0].headTitle", is("www.deleasfsdte.com")) .body("[0].openGraphTitle", is("")) .body("[0].description", is("")) .body("[0].image", is("")); } - void createReferenceLink(final String url, String accessCodeText, String categoryName) { + ReferenceLinkResponse createReferenceLink(final String url, String accessCodeText, String categoryName) { final CategoryCreateResponse response = CategoryAcceptanceTest.createCategory( accessCodeText, new CategoryCreateRequest(categoryName)); final Map request = Map.of("url", url, "categoryId", response.id()); - RestAssured + return RestAssured .given() .contentType(ContentType.JSON) .body(request) .when() - .post("/api/" + accessCodeText + "/reference-link"); + .post("/api/" + accessCodeText + "/reference-link") + + .then() + .extract() + .as(ReferenceLinkResponse.class); } @Test @DisplayName("레퍼런슀 링크λ₯Ό μ‚­μ œν•˜λŠ” μš”μ²­") void delete_reference_link_request() { // given - final PairRoomCreateResponse pairRoom = createPairRoom(new PairRoomCreateRequest("레λͺ¨λ„€", "ν”„λžŒ")); + final PairRoomCreateResponse pairRoom = + createPairRoom( + new PairRoomCreateRequest("레λͺ¨λ„€", "ν”„λžŒ", 1000L, 1000L, "https://missionUrl.xxx", "IN_PROGRESS")); - createReferenceLink("http://www.delete.com", pairRoom.accessCode(), "μΉ΄ν…Œκ³ λ¦¬ 이름"); + final ReferenceLinkResponse response = createReferenceLink("http://www.delete.com", pairRoom.accessCode(), + "μΉ΄ν…Œκ³ λ¦¬ 이름"); // when & then RestAssured @@ -125,7 +137,7 @@ void delete_reference_link_request() { .when() .log().all() - .delete("/api/" + pairRoom.accessCode() + "/reference-link/1") + .delete("/api/" + pairRoom.accessCode() + "/reference-link/" + response.id()) .then() .assertThat() diff --git a/backend/src/test/java/site/coduo/acceptance/SseAcceptanceTest.java b/backend/src/test/java/site/coduo/acceptance/SseAcceptanceTest.java new file mode 100644 index 00000000..09e2a881 --- /dev/null +++ b/backend/src/test/java/site/coduo/acceptance/SseAcceptanceTest.java @@ -0,0 +1,75 @@ +package site.coduo.acceptance; + +import static site.coduo.acceptance.PairRoomAcceptanceTest.createPairRoom; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.restassured.RestAssured; +import site.coduo.pairroom.domain.PairRoomStatus; +import site.coduo.pairroom.service.dto.PairRoomCreateRequest; + +class SseAcceptanceTest extends AcceptanceFixture { + + static void createConnect(final String accessCode) { + RestAssured + .given() + + .when() + .log().all() + .get("/api/{key}/connect", accessCode) + + .then() + .log().all() + .statusCode(200); + } + + @Test + @DisplayName("νŽ˜μ–΄λ£Έμ— μ ‘μ†ν•˜μ—¬ SSE connection을 μƒμ„±ν•œλ‹€.") + void create_sse_connection() { + // given + final PairRoomCreateRequest request = new PairRoomCreateRequest("ν”„λžŒ", "레λͺ¨λ„€", 10000L, + 10000L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name()); + final String accessCode = createPairRoom(request).accessCode(); + + // when & then + RestAssured + .given() + + .when() + .log().all() + .get("/api/{key}/connect", accessCode) + + .then() + .log().all() + .statusCode(200); + } + + @Test + @DisplayName("νŽ˜μ–΄λ£Έμ˜ λͺ¨λ“  SSE connection을 μ’…λ£Œν•œλ‹€.") + void delete_sse_connection() { + // given + final PairRoomCreateRequest request = new PairRoomCreateRequest( + "ν•΄μ‹œ", + "μž‰ν¬", + 1000L, + 1000L, + "https://missionUrl.xxx", + PairRoomStatus.IN_PROGRESS.name() + ); + final String accessCode = createPairRoom(request).accessCode(); + createConnect(accessCode); + + // when & then + RestAssured + .given() + + .when() + .log().all() + .delete("/api/{key}/connect", accessCode) + + .then() + .log().all() + .statusCode(204); + } +} diff --git a/backend/src/test/java/site/coduo/acceptance/TimerAcceptanceTest.java b/backend/src/test/java/site/coduo/acceptance/TimerAcceptanceTest.java new file mode 100644 index 00000000..b0d3330c --- /dev/null +++ b/backend/src/test/java/site/coduo/acceptance/TimerAcceptanceTest.java @@ -0,0 +1,136 @@ +package site.coduo.acceptance; + +import static site.coduo.acceptance.SseAcceptanceTest.createConnect; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; + +import io.restassured.RestAssured; +import site.coduo.pairroom.domain.PairRoomStatus; +import site.coduo.pairroom.service.dto.PairRoomCreateRequest; +import site.coduo.pairroom.service.dto.PairRoomCreateResponse; +import site.coduo.timer.service.dto.TimerUpdateRequest; + +class TimerAcceptanceTest extends AcceptanceFixture { + + static String createPairRoom(final PairRoomCreateRequest pairRoom) { + final PairRoomCreateResponse response = RestAssured + .given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE) + .body(pairRoom) + + .when() + .post("/api/pair-room") + + .then() + .extract() + .as(PairRoomCreateResponse.class); + + return response.accessCode(); + } + + private static void timerStart(final String accessCode) { + createConnect(accessCode); + RestAssured + .given() + + .when() + .patch("/api/{accessCode}/timer/start", accessCode) + + .then() + .statusCode(204); + } + + @Test + @DisplayName("타이머λ₯Ό μ‘°νšŒν•œλ‹€.") + void get_timer() { + // given + final String accessCode = createPairRoom(new PairRoomCreateRequest( + "켈리", + "νŒŒλž€", + 10000L, + 10000L, + "https://missionUrl.xxx", + PairRoomStatus.IN_PROGRESS.name()) + ); + + // when & then + RestAssured + .given() + + .when() + .get("/api/{accessCode}/timer", accessCode) + + .then() + .statusCode(200); + } + + @Test + @DisplayName("νŽ˜μ–΄λ£Έμ˜ 타이머λ₯Ό μ—…λ°μ΄νŠΈ ν•œλ‹€.") + void update_timer_duration() { + // given + final String accessCode = createPairRoom(new PairRoomCreateRequest( + "ν•΄μ‹œ", + "파슬리", + 10000L, + 10000L, + "https://missionUrl.xxx", + PairRoomStatus.IN_PROGRESS.name()) + ); + final TimerUpdateRequest request = new TimerUpdateRequest(20000L, 3000L); + createConnect(accessCode); + // when & then + RestAssured + .given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + + .when() + .patch("/api/{accessCode}/timer", accessCode) + + .then() + .statusCode(204); + } + + @Test + @DisplayName("타이머λ₯Ό μ‹œμž‘ν•œλ‹€.") + void start_timer() { + // given + final String accessCode = createPairRoom( + new PairRoomCreateRequest("fram", "lemone", 10000L, 10000L, "https://missionUrl.xxx", + PairRoomStatus.IN_PROGRESS.name())); + createConnect(accessCode); + + // when & then + RestAssured + .given() + + .when() + .patch("/api/{accessCode}/timer/start", accessCode) + + .then() + .statusCode(204); + } + + @Test + @DisplayName("타이머λ₯Ό μ’…λ£Œν•œλ‹€.") + void stop_timer() { + // given + final String accessCode = createPairRoom( + new PairRoomCreateRequest("fram", "lemone", 10000L, 10000L, "https://missionUrl.xxx", + PairRoomStatus.IN_PROGRESS.name())); + timerStart(accessCode); + + // when & then + RestAssured + .given() + + .when() + .patch("/api/{accessCode}/timer/stop", accessCode) + + .then() + .statusCode(204); + } +} diff --git a/backend/src/test/java/site/coduo/common/infrastructure/security/JwtProviderTest.java b/backend/src/test/java/site/coduo/common/infrastructure/security/JwtProviderTest.java deleted file mode 100644 index de6538bf..00000000 --- a/backend/src/test/java/site/coduo/common/infrastructure/security/JwtProviderTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package site.coduo.common.infrastructure.security; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.test.util.ReflectionTestUtils; - -import site.coduo.member.infrastructure.security.JwtProvider; - -class JwtProviderTest { - - private final JwtProvider jwtProvider = new JwtProvider(); - - @BeforeEach - void setUp() { - ReflectionTestUtils.setField(jwtProvider, "key", "test-key+test-key+test-key+test-key+test-key+test"); - } - - @Test - @DisplayName("sub으둜 κ΅¬μ„±λœ jwt 토큰을 μƒμ„±ν•œλ‹€.") - void produce_jwt_with_sub() { - // given - final String sub = "subject"; - - // when - final String jwtToken = jwtProvider.sign(sub); - - // then - assertThat(jwtProvider.isValid(jwtToken)).isTrue(); - } - - @Test - @DisplayName("jwt ν† ν°μ—μ„œ sub을 μΆ”μΆœν•œλ‹€.") - void extract_sub_in_jwt() { - // given - final String sub = "subject"; - final String token = jwtProvider.sign(sub); - - // when - final String extract = jwtProvider.extractSubject(token); - - // then - assertThat(extract).isEqualTo(sub); - } -} diff --git a/backend/src/test/java/site/coduo/config/TestConfig.java b/backend/src/test/java/site/coduo/config/TestConfig.java index fb622a3a..6e33d651 100644 --- a/backend/src/test/java/site/coduo/config/TestConfig.java +++ b/backend/src/test/java/site/coduo/config/TestConfig.java @@ -3,23 +3,24 @@ import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import site.coduo.fake.FakeEventStreamRegistry; import site.coduo.fake.FakeGithubApiClient; import site.coduo.fake.FakeGithubOAuthClient; -import site.coduo.fake.FakeJwtProvider; -import site.coduo.fake.FixedNanceProvider; +import site.coduo.fake.FixedNonceProvider; import site.coduo.member.client.GithubApiClient; import site.coduo.member.client.GithubOAuthClient; -import site.coduo.member.infrastructure.security.JwtProvider; -import site.coduo.member.infrastructure.security.NanceProvider; +import site.coduo.member.infrastructure.security.NonceProvider; +import site.coduo.sync.service.EventStreamsRegistry; @TestConfiguration public class TestConfig { @Bean @Primary - public NanceProvider fakeNanceFactory() { - return new FixedNanceProvider(); + public NonceProvider fakeNonceFactory() { + return new FixedNonceProvider(); } @Bean @@ -36,7 +37,13 @@ public GithubApiClient fakeGithubApiClient() { @Bean @Primary - public JwtProvider fakeJwtProvider() { - return new FakeJwtProvider(); + public ThreadPoolTaskScheduler testThreadPoolTaskScheduler() { + return new ThreadPoolTaskScheduler(); + } + + @Bean + @Primary + public EventStreamsRegistry testEventStreamRegistry() { + return new FakeEventStreamRegistry(); } } diff --git a/backend/src/test/java/site/coduo/fake/FakeClient.java b/backend/src/test/java/site/coduo/fake/FakeClient.java new file mode 100644 index 00000000..99e4087f --- /dev/null +++ b/backend/src/test/java/site/coduo/fake/FakeClient.java @@ -0,0 +1,456 @@ +package site.coduo.fake; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.URI; +import java.nio.charset.Charset; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.StreamingHttpOutputMessage.Body; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestInitializer; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.observation.ClientRequestObservationConvention; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.mock.http.client.MockClientHttpRequest; +import org.springframework.mock.http.client.MockClientHttpResponse; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClient.ResponseSpec.ErrorHandler; +import org.springframework.web.util.UriBuilder; +import org.springframework.web.util.UriBuilderFactory; + +import io.micrometer.observation.ObservationRegistry; +import site.coduo.member.client.dto.GithubUserResponse; + +public class FakeClient implements RestClient { + + @Override + public RequestHeadersUriSpec get() { + return new FakeRequestHeaderUriSpec(); + } + + @Override + public RequestHeadersUriSpec head() { + return new FakeRequestHeaderUriSpec(); + } + + @Override + public RequestBodyUriSpec post() { + return new FakeRequestBodyBodyUriSpec(); + } + + @Override + public RequestBodyUriSpec put() { + return new FakeRequestBodyBodyUriSpec(); + } + + @Override + public RequestBodyUriSpec patch() { + return new FakeRequestBodyBodyUriSpec(); + } + + @Override + public RequestHeadersUriSpec delete() { + return new FakeRequestHeaderUriSpec(); + } + + @Override + public RequestHeadersUriSpec options() { + return new FakeRequestHeaderUriSpec(); + } + + @Override + public RequestBodyUriSpec method(final HttpMethod method) { + return new FakeRequestBodyBodyUriSpec(); + } + + @Override + public Builder mutate() { + return new FakeBuilder(); + } + + + class FakeBuilder implements Builder { + + @Override + public Builder baseUrl(final String baseUrl) { + return this; + } + + @Override + public Builder defaultUriVariables(final Map defaultUriVariables) { + return this; + } + + @Override + public Builder uriBuilderFactory(final UriBuilderFactory uriBuilderFactory) { + return this; + } + + @Override + public Builder defaultHeader(final String header, final String... values) { + return this; + } + + @Override + public Builder defaultHeaders(final Consumer headersConsumer) { + return this; + } + + @Override + public Builder defaultRequest(final Consumer> defaultRequest) { + return this; + } + + @Override + public Builder defaultStatusHandler(final Predicate statusPredicate, + final ErrorHandler errorHandler) { + return this; + } + + @Override + public Builder defaultStatusHandler(final ResponseErrorHandler errorHandler) { + return this; + } + + @Override + public Builder requestInterceptor(final ClientHttpRequestInterceptor interceptor) { + return this; + } + + @Override + public Builder requestInterceptors(final Consumer> interceptorsConsumer) { + return this; + } + + @Override + public Builder requestInitializer(final ClientHttpRequestInitializer initializer) { + return this; + } + + @Override + public Builder requestInitializers(final Consumer> initializersConsumer) { + return this; + } + + @Override + public Builder requestFactory(final ClientHttpRequestFactory requestFactory) { + return this; + } + + @Override + public Builder messageConverters(final Consumer>> configurer) { + return this; + } + + @Override + public Builder observationRegistry(final ObservationRegistry observationRegistry) { + return this; + } + + @Override + public Builder observationConvention(final ClientRequestObservationConvention observationConvention) { + return this; + } + + @Override + public Builder apply(final Consumer builderConsumer) { + return this; + } + + @Override + public Builder clone() { + return this; + } + + @Override + public RestClient build() { + return new FakeClient(); + } + } + + class FakeResponseSpec implements ResponseSpec { + + private final HttpRequest request; + private final ClientHttpResponse response; + private final HttpStatus httpStatus; + + public FakeResponseSpec(final HttpStatus httpStatus, final HttpMethod method, final URI url) { + this.httpStatus = httpStatus; + this.request = new MockClientHttpRequest(method, url); + this.response = new MockClientHttpResponse(new byte[]{}, httpStatus.value()); + } + + @Override + public ResponseSpec onStatus(final Predicate statusPredicate, final ErrorHandler errorHandler) { + final boolean error = statusPredicate.test(httpStatus); + if (error) { + try { + errorHandler.handle(request, response); + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + return this; + } + + @Override + public ResponseSpec onStatus(final ResponseErrorHandler errorHandler) { + try { + errorHandler.handleError(response); + } catch (IOException e) { + throw new RuntimeException(e); + } + return this; + } + + @Override + public T body(final Class bodyType) { + if (bodyType.isInstance(new GithubUserResponse("", "", ""))) { + try { + return bodyType.getDeclaredConstructor(String.class, String.class, String.class) + .newInstance("userId", "login", "avatar_url"); + } catch (final NoSuchMethodException | InstantiationException | IllegalAccessException | + InvocationTargetException e) { + throw new RuntimeException(e); + } + } + return null; + } + + @Override + public T body(final ParameterizedTypeReference bodyType) { + return null; + } + + @Override + public ResponseEntity toEntity(final Class bodyType) { + return null; + } + + @Override + public ResponseEntity toEntity(final ParameterizedTypeReference bodyType) { + return null; + } + + @Override + public ResponseEntity toBodilessEntity() { + return null; + } + } + + class FakeRequestHeaderUriSpec implements RequestHeadersUriSpec { + + private final HttpHeaders headers = new HttpHeaders(); + private String endPoint; + + @Override + public RequestHeadersSpec accept(final MediaType... acceptableMediaTypes) { + Stream.of(acceptableMediaTypes) + .forEach(value -> headers.add(HttpHeaders.ACCEPT, value.toString())); + + return this; + } + + @Override + public RequestHeadersSpec acceptCharset(final Charset... acceptableCharsets) { + Stream.of(acceptableCharsets) + .forEach(value -> headers.add(HttpHeaders.ACCEPT_CHARSET, value.displayName())); + + return this; + } + + @Override + public RequestHeadersSpec ifModifiedSince(final ZonedDateTime ifModifiedSince) { + headers.add(HttpHeaders.IF_MODIFIED_SINCE, ifModifiedSince.toString()); + return this; + } + + @Override + public RequestHeadersSpec ifNoneMatch(final String... ifNoneMatches) { + Stream.of(ifNoneMatches) + .forEach(value -> headers.add(HttpHeaders.IF_NONE_MATCH, value)); + return this; + } + + @Override + public RequestHeadersSpec header(final String headerName, final String... headerValues) { + Stream.of(headerValues) + .forEach(value -> headers.add(headerName, value)); + return this; + } + + @Override + public RequestHeadersSpec headers(final Consumer consumer) { + consumer.accept(headers); + return this; + } + + @Override + public RequestHeadersSpec httpRequest(final Consumer consumer) { + return this; + } + + @Override + public ResponseSpec retrieve() { + if (endPoint.equals("/user")) { + final String token = headers.get(HttpHeaders.AUTHORIZATION).get(0).substring(7); + if (token.isBlank()) { + return new FakeResponseSpec(HttpStatus.INTERNAL_SERVER_ERROR, HttpMethod.GET, URI.create("/user")); + } + return new FakeResponseSpec(HttpStatus.OK, HttpMethod.GET, URI.create("/user")); + } + return null; + } + + @Override + public Object exchange(final ExchangeFunction exchangeFunction, final boolean close) { + return null; + } + + @Override + public RequestHeadersSpec uri(final URI uri) { + endPoint = uri.toString(); + return this; + } + + @Override + public RequestHeadersSpec uri(final String uri, final Object... uriVariables) { + endPoint = uri; + return this; + } + + @Override + public RequestHeadersSpec uri(final String uri, final Map uriVariables) { + endPoint = uri; + return this; + } + + @Override + public RequestHeadersSpec uri(final String uri, final Function function) { + endPoint = uri; + return this; + } + + @Override + public RequestHeadersSpec uri(final Function function) { + return this; + } + } + + class FakeRequestBodyBodyUriSpec implements RequestBodyUriSpec { + + @Override + public RequestBodySpec contentLength(final long contentLength) { + return this; + } + + @Override + public RequestBodySpec contentType(final MediaType contentType) { + return this; + } + + @Override + public RequestBodySpec body(final Object body) { + return this; + } + + @Override + public RequestBodySpec body(final T body, final ParameterizedTypeReference bodyType) { + return this; + } + + @Override + public RequestBodySpec body(final Body body) { + return this; + } + + @Override + public RequestBodySpec accept(final MediaType... acceptableMediaTypes) { + return this; + } + + @Override + public RequestBodySpec acceptCharset(final Charset... acceptableCharsets) { + return this; + } + + @Override + public RequestBodySpec ifModifiedSince(final ZonedDateTime ifModifiedSince) { + return this; + } + + @Override + public RequestBodySpec ifNoneMatch(final String... ifNoneMatches) { + return this; + } + + @Override + public RequestBodySpec header(final String headerName, final String... headerValues) { + return this; + } + + @Override + public RequestBodySpec headers(final Consumer headersConsumer) { + return this; + } + + @Override + public RequestBodySpec httpRequest(final Consumer requestConsumer) { + return this; + } + + @Override + public ResponseSpec retrieve() { + return null; + } + + @Override + public T exchange(final ExchangeFunction exchangeFunction, final boolean close) { + return null; + } + + @Override + public RequestBodySpec uri(final URI uri) { + return this; + } + + @Override + public RequestBodySpec uri(final String uri, final Object... uriVariables) { + return this; + } + + @Override + public RequestBodySpec uri(final String uri, final Map uriVariables) { + return this; + } + + @Override + public RequestBodySpec uri(final String uri, final Function uriFunction) { + return this; + } + + @Override + public RequestBodySpec uri(final Function uriFunction) { + return this; + } + } + +} diff --git a/backend/src/test/java/site/coduo/fake/FakeEvenStream.java b/backend/src/test/java/site/coduo/fake/FakeEvenStream.java new file mode 100644 index 00000000..8b3e519d --- /dev/null +++ b/backend/src/test/java/site/coduo/fake/FakeEvenStream.java @@ -0,0 +1,35 @@ +package site.coduo.fake; + +import java.time.Duration; + +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import site.coduo.sync.service.EventStream; + +public class FakeEvenStream implements EventStream { + + private static final long CONNECTION_TIME_OUT_MILLISECONDS = Duration.ofMillis(500).toMillis(); + + private final SseEmitter sseEmitter; + + public FakeEvenStream() { + sseEmitter = new SseEmitter(CONNECTION_TIME_OUT_MILLISECONDS); + } + + @Override + public SseEmitter connect() { + final SseEmitter sseEmitter = new SseEmitter(CONNECTION_TIME_OUT_MILLISECONDS); + sseEmitter.onTimeout(sseEmitter::complete); + return sseEmitter; + } + + @Override + public void flush(final String name, final String message) { + sseEmitter.complete(); + } + + @Override + public void close() { + sseEmitter.complete(); + } +} diff --git a/backend/src/test/java/site/coduo/fake/FakeEventStreamRegistry.java b/backend/src/test/java/site/coduo/fake/FakeEventStreamRegistry.java new file mode 100644 index 00000000..80cb91a3 --- /dev/null +++ b/backend/src/test/java/site/coduo/fake/FakeEventStreamRegistry.java @@ -0,0 +1,36 @@ +package site.coduo.fake; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import site.coduo.sync.exception.NotFoundSseConnectionException; +import site.coduo.sync.service.EventStreams; +import site.coduo.sync.service.EventStreamsRegistry; + +public class FakeEventStreamRegistry extends EventStreamsRegistry { + + private final Map registry; + + public FakeEventStreamRegistry() { + this.registry = new HashMap<>(); + } + + @Override + public SseEmitter register(final String key) { + final EventStreams eventStreams = registry.getOrDefault(key, new EventStreams()); + registry.put(key, eventStreams); + final FakeEvenStream fakeEvenStream = new FakeEvenStream(); + eventStreams.add(fakeEvenStream); + return eventStreams.publish(fakeEvenStream); + } + + @Override + public EventStreams findEventStreams(final String key) { + if (!registry.containsKey(key)) { + throw new NotFoundSseConnectionException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” SSE 컀λ„₯μ…˜μž…λ‹ˆλ‹€."); + } + return registry.get(key); + } +} diff --git a/backend/src/test/java/site/coduo/fake/FakeJwtProvider.java b/backend/src/test/java/site/coduo/fake/FakeJwtProvider.java deleted file mode 100644 index 91599c4a..00000000 --- a/backend/src/test/java/site/coduo/fake/FakeJwtProvider.java +++ /dev/null @@ -1,10 +0,0 @@ -package site.coduo.fake; - -import site.coduo.member.infrastructure.security.JwtProvider; - -public class FakeJwtProvider extends JwtProvider { - @Override - public boolean isValid(final String token) { - return !token.isBlank(); - } -} diff --git a/backend/src/test/java/site/coduo/fake/FakeScheduledFuture.java b/backend/src/test/java/site/coduo/fake/FakeScheduledFuture.java new file mode 100644 index 00000000..8a91a04a --- /dev/null +++ b/backend/src/test/java/site/coduo/fake/FakeScheduledFuture.java @@ -0,0 +1,53 @@ +package site.coduo.fake; + +import java.util.concurrent.Delayed; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class FakeScheduledFuture implements ScheduledFuture { + + private boolean isCancelled = false; + + @Override + public long getDelay(final TimeUnit unit) { + return 0; + } + + @Override + public int compareTo(final Delayed o) { + return 0; + } + + // λ°˜ν™˜κ°’μ΄ false = cancel μ‹€νŒ¨ + @Override + public boolean cancel(final boolean mayInterruptIfRunning) { + if (isCancelled) { + return false; + } + isCancelled = true; + return true; + } + + @Override + public boolean isCancelled() { + return isCancelled; + } + + @Override + public boolean isDone() { + return false; + } + + @Override + public Object get() throws InterruptedException, ExecutionException { + return null; + } + + @Override + public Object get(final long timeout, final TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return null; + } +} diff --git a/backend/src/test/java/site/coduo/fake/FixedNanceProvider.java b/backend/src/test/java/site/coduo/fake/FixedNanceProvider.java deleted file mode 100644 index 001b5030..00000000 --- a/backend/src/test/java/site/coduo/fake/FixedNanceProvider.java +++ /dev/null @@ -1,12 +0,0 @@ -package site.coduo.fake; - -import site.coduo.member.infrastructure.security.NanceProvider; - -public class FixedNanceProvider implements NanceProvider { - public static final String FIXED_VALUE = "random number"; - - @Override - public String generate() { - return FIXED_VALUE; - } -} diff --git a/backend/src/test/java/site/coduo/fake/FixedNonceProvider.java b/backend/src/test/java/site/coduo/fake/FixedNonceProvider.java new file mode 100644 index 00000000..f9bbbb9b --- /dev/null +++ b/backend/src/test/java/site/coduo/fake/FixedNonceProvider.java @@ -0,0 +1,13 @@ +package site.coduo.fake; + +import site.coduo.member.infrastructure.security.NonceProvider; + +public class FixedNonceProvider implements NonceProvider { + + public static final String FIXED_VALUE = "randomNumber"; + + @Override + public String generate() { + return FIXED_VALUE; + } +} diff --git a/backend/src/test/java/site/coduo/fake/SequentialAccessCodeStrategy.java b/backend/src/test/java/site/coduo/fake/SequentialAccessCodeStrategy.java deleted file mode 100644 index 1c385ec5..00000000 --- a/backend/src/test/java/site/coduo/fake/SequentialAccessCodeStrategy.java +++ /dev/null @@ -1,18 +0,0 @@ -package site.coduo.fake; - -import java.util.List; - -import site.coduo.pairroom.domain.accesscode.AccessCodeStrategy; - -public class SequentialAccessCodeStrategy implements AccessCodeStrategy { - - private static final List SEQUENCE = List.of("1", "2", "3", "4", "5"); - - private static int INDEX = 0; - - @Override - public String generateAccessCode() { - return ("FAKE_" + SEQUENCE.get(INDEX++)) - .substring(0, ACCESS_CODE_LENGTH); - } -} diff --git a/backend/src/test/java/site/coduo/fixture/AccessCodeFixture.java b/backend/src/test/java/site/coduo/fixture/AccessCodeFixture.java new file mode 100644 index 00000000..fe0caa1b --- /dev/null +++ b/backend/src/test/java/site/coduo/fixture/AccessCodeFixture.java @@ -0,0 +1,11 @@ +package site.coduo.fixture; + +import site.coduo.pairroom.domain.accesscode.AccessCode; + +public class AccessCodeFixture { + + public static final AccessCode ACCESS_CODE = new AccessCode("c0d1ng"); + public static final AccessCode NUMBER_ACCESS_CODE = new AccessCode("123456"); + public static final AccessCode ALPHABET_ACCESS_CODE = new AccessCode("abcdef"); + +} diff --git a/backend/src/test/java/site/coduo/fixture/PairRoomFixture.java b/backend/src/test/java/site/coduo/fixture/PairRoomFixture.java new file mode 100644 index 00000000..c214b14f --- /dev/null +++ b/backend/src/test/java/site/coduo/fixture/PairRoomFixture.java @@ -0,0 +1,42 @@ +package site.coduo.fixture; + +import static site.coduo.fixture.AccessCodeFixture.ACCESS_CODE; +import static site.coduo.fixture.AccessCodeFixture.ALPHABET_ACCESS_CODE; +import static site.coduo.fixture.AccessCodeFixture.NUMBER_ACCESS_CODE; + +import site.coduo.pairroom.domain.MissionUrl; +import site.coduo.pairroom.domain.Pair; +import site.coduo.pairroom.domain.PairName; +import site.coduo.pairroom.domain.PairRoom; +import site.coduo.pairroom.domain.PairRoomStatus; + +public class PairRoomFixture { + + public static final PairRoom INK_REDDDY_ROOM = new PairRoom( + PairRoomStatus.IN_PROGRESS, + new Pair( + new PairName("μž‰ν¬"), + new PairName("λ ˆλ””") + ), + new MissionUrl("https://github.com/coduo-missions/coduo-javascript-rps"), + ACCESS_CODE); + + public static final PairRoom FRAM_LEMONE_ROOM = new PairRoom( + PairRoomStatus.IN_PROGRESS, + new Pair( + new PairName("ν”„λžŒ"), + new PairName("레λͺ¨λ„€") + ), + new MissionUrl("https://github.com/coduo-missions/coduo-javascript-rps"), + ALPHABET_ACCESS_CODE); + + public static final PairRoom KELY_LEMONE_ROOM = new PairRoom( + PairRoomStatus.IN_PROGRESS, + new Pair( + new PairName("켈리"), + new PairName("레λͺ¨λ„€") + ), + new MissionUrl("https://github.com/coduo-missions/coduo-javascript-rps"), + NUMBER_ACCESS_CODE + ); +} diff --git a/backend/src/test/java/site/coduo/member/client/GithubApiClientTest.java b/backend/src/test/java/site/coduo/member/client/GithubApiClientTest.java new file mode 100644 index 00000000..5ad40b70 --- /dev/null +++ b/backend/src/test/java/site/coduo/member/client/GithubApiClientTest.java @@ -0,0 +1,45 @@ +package site.coduo.member.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import site.coduo.fake.FakeClient; +import site.coduo.member.client.dto.GithubUserRequest; +import site.coduo.member.client.dto.GithubUserResponse; +import site.coduo.member.exception.ExternalApiCallException; +import site.coduo.member.infrastructure.http.Bearer; + +class GithubApiClientTest { + + + @Test + @DisplayName("Githubλ‘œλΆ€ν„° νšŒμ›μ •λ³΄λ₯Ό λΆˆλŸ¬μ˜¨λ‹€.") + void get_member_from_github() { + // given + final FakeClient client = new FakeClient(); + final GithubApiClient githubApiClient = new GithubApiClient(client); + final GithubUserRequest request = new GithubUserRequest(new Bearer("ok")); + + // when + final GithubUserResponse response = githubApiClient.getUser(request); + + // then + assertThat(response).isEqualTo(new GithubUserResponse("userId", "login", "avatar_url")); + } + + @Test + @DisplayName("토큰값이 잘λͺ»λœ 경우 μ˜ˆμ™Έ λ°œμƒν•œλ‹€.") + void throw_exception_when_token_is_invalid() { + // given + final FakeClient client = new FakeClient(); + final GithubApiClient githubApiClient = new GithubApiClient(client); + final GithubUserRequest request = new GithubUserRequest(new Bearer("")); + + // when & then + assertThatThrownBy(() -> githubApiClient.getUser(request)) + .isInstanceOf(ExternalApiCallException.class); + } +} diff --git a/backend/src/test/java/site/coduo/member/service/AuthServiceTest.java b/backend/src/test/java/site/coduo/member/service/AuthServiceTest.java index 1c62fa49..0081d33e 100644 --- a/backend/src/test/java/site/coduo/member/service/AuthServiceTest.java +++ b/backend/src/test/java/site/coduo/member/service/AuthServiceTest.java @@ -35,13 +35,14 @@ void tearDown() { } @Test - @DisplayName("μ—‘μ„ΈμŠ€ ν† ν°μœΌλ‘œ νšŒμ›μ„ μ‘°νšŒν•œλ‹€.") + @DisplayName("JWT둜 감싸진 μ—‘μ„ΈμŠ€ ν† ν°μœΌλ‘œ νšŒμ›μ„ μ‘°νšŒν•œλ‹€.") void search_member_by_access_token() { // given final Member member = createMember("username", FakeGithubApiClient.ACCESS_TOKEN, FakeGithubApiClient.USER_ID); + final String sign = jwtProvider.sign(member.getAccessToken()); // when - final SignInServiceResponse signInToken = authService.createSignInToken(member.getAccessToken()); + final SignInServiceResponse signInToken = authService.createSignInToken(sign); // then assertThat(signInToken.signedIn()).isTrue(); @@ -52,9 +53,10 @@ void search_member_by_access_token() { void throw_exception_when_search_by_does_not_exists_access_token() { // given final String token = "does not exist token"; + final String sign = jwtProvider.sign(token); // when - final SignInServiceResponse signInToken = authService.createSignInToken(token); + final SignInServiceResponse signInToken = authService.createSignInToken(sign); // then assertThat(signInToken.token()).isEmpty(); @@ -77,9 +79,10 @@ private Member createMember(final String username, final String accessToken, fin void renewal_member_access_token_when_create_sign_in_token() { // given final Member member = createMember("username", "origin", FakeGithubApiClient.USER_ID); + final String sign = jwtProvider.sign("change"); // when - authService.createSignInToken("change"); + authService.createSignInToken(sign); // then assertThat(memberRepository.findById(member.getId()).orElseThrow()) @@ -104,7 +107,7 @@ void return_true_when_token_is_valid() { @DisplayName("ν•΄λ‹Ή 토큰이 μœ νš¨ν•œμ§€ ν™•μΈν•œλ‹€. - 거짓") void return_false_when_token_is_invalid() { // given - final String invalidToken = ""; + final String invalidToken = "hello, world"; // when final boolean signedIn = authService.isSignedIn(invalidToken); diff --git a/backend/src/test/java/site/coduo/member/service/GithubOAuthServiceTest.java b/backend/src/test/java/site/coduo/member/service/GithubOAuthServiceTest.java index 89c14b40..48be00a7 100644 --- a/backend/src/test/java/site/coduo/member/service/GithubOAuthServiceTest.java +++ b/backend/src/test/java/site/coduo/member/service/GithubOAuthServiceTest.java @@ -1,6 +1,7 @@ package site.coduo.member.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -10,9 +11,9 @@ import site.coduo.config.TestConfig; import site.coduo.fake.FakeGithubOAuthClient; -import site.coduo.fake.FixedNanceProvider; -import site.coduo.member.client.dto.TokenResponse; -import site.coduo.member.controller.dto.oauth.GithubAuthQuery; +import site.coduo.fake.FixedNonceProvider; +import site.coduo.member.infrastructure.security.JwtProvider; +import site.coduo.member.service.dto.oauth.GithubAuthQuery; @SpringBootTest @Import(TestConfig.class) @@ -21,6 +22,9 @@ class GithubOAuthServiceTest { @Autowired private GithubOAuthService githubOAuthService; + @Autowired + private JwtProvider jwtProvider; + @Test @DisplayName("인가 μš”μ²­μ„ μœ„ν•œ 정보λ₯Ό μƒμ„±ν•œλ‹€.") void create_info_for_authorization_request_to_third_party() { @@ -28,7 +32,7 @@ void create_info_for_authorization_request_to_third_party() { final GithubAuthQuery expect = new GithubAuthQuery( FakeGithubOAuthClient.OAUTH_CLIENT_ID, FakeGithubOAuthClient.OAUTH_REDIRECT_URI, - FixedNanceProvider.FIXED_VALUE + FixedNonceProvider.FIXED_VALUE ); // when @@ -45,9 +49,10 @@ void get_access_token() { final String code = "code"; // when - final TokenResponse tokenResponse = githubOAuthService.invokeOAuthCallback(code); + final String tempToken = githubOAuthService.invokeOAuthCallback(code); // then - assertThat(tokenResponse.accessToken()).isEqualTo(FakeGithubOAuthClient.ACCESS_TOKEN.getCredential()); + assertThatCode(() -> jwtProvider.extractSubject(tempToken)) + .doesNotThrowAnyException(); } } diff --git a/backend/src/test/java/site/coduo/member/service/MemberServiceTest.java b/backend/src/test/java/site/coduo/member/service/MemberServiceTest.java index 670ae987..ef8eead9 100644 --- a/backend/src/test/java/site/coduo/member/service/MemberServiceTest.java +++ b/backend/src/test/java/site/coduo/member/service/MemberServiceTest.java @@ -2,6 +2,8 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.List; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -10,14 +12,20 @@ import org.springframework.context.annotation.Import; import site.coduo.config.TestConfig; +import site.coduo.member.domain.Member; import site.coduo.member.domain.repository.MemberRepository; +import site.coduo.member.infrastructure.security.JwtProvider; +import site.coduo.member.service.dto.member.MemberReadResponse; @SpringBootTest @Import(TestConfig.class) class MemberServiceTest { @Autowired - private MemberService MemberService; + private MemberService memberService; + + @Autowired + private JwtProvider jwtProvider; @Autowired private MemberRepository memberRepository; @@ -32,12 +40,79 @@ void tearDown() { void save_member() { // given final String credential = "access-token"; + final String token = jwtProvider.sign(credential); final String username = "username"; // when - MemberService.createMember(username, credential); + memberService.createMember(username, token); // then assertThat(memberRepository.findAll()).hasSize(1); } + + @Test + @DisplayName("둜그인 토큰을 λ°”νƒ•μœΌλ‘œ νšŒμ›μ΄λ¦„μ„ μ‘°νšŒν•œλ‹€.") + void search_username_by_login_token() { + // given + final Member member = Member.builder() + .userId("userid") + .accessToken("access") + .loginId("login") + .username("username") + .profileImage("some image") + .build(); + final String sign = jwtProvider.sign(member.getUserId()); + memberRepository.save(member); + + // when + final MemberReadResponse response = memberService.findMemberNameByCredential(sign); + + // then + assertThat(response.username()).isEqualTo(member.getUsername()); + } + + @Test + @DisplayName("둜그인 토큰을 λ°”νƒ•μœΌλ‘œ νšŒμ› μ—”ν‹°ν‹°λ₯Ό μ‘°νšŒν•œλ‹€.") + void search_member_by_login_token() { + // given + final Member member = Member.builder() + .userId("userid") + .accessToken("access") + .loginId("login") + .username("username") + .profileImage("some image") + .build(); + final String sign = jwtProvider.sign(member.getUserId()); + memberRepository.save(member); + + // when + final Member findMember = memberService.findMemberByCredential(sign); + + // then + assertThat(findMember.getUsername()).isEqualTo(member.getUsername()); + } + + @Test + @DisplayName("νšŒμ›μ„ μ‚­μ œν•œλ‹€.") + void delete_member() { + // given + final Member member = Member.builder() + .userId("userid") + .accessToken("access") + .loginId("login") + .username("username") + .profileImage("some image") + .build(); + final String token = jwtProvider.sign(member.getUserId()); + + memberRepository.save(member); + final List beforeDelete = memberRepository.findAll(); + + // when + memberService.deleteMember(token); + + //then + final List afterDelete = memberRepository.findAll(); + assertThat(afterDelete).hasSize(beforeDelete.size() - 1); + } } diff --git a/backend/src/test/java/site/coduo/pairroom/domain/AccessCodeFactoryTest.java b/backend/src/test/java/site/coduo/pairroom/domain/AccessCodeFactoryTest.java deleted file mode 100644 index 6e389347..00000000 --- a/backend/src/test/java/site/coduo/pairroom/domain/AccessCodeFactoryTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package site.coduo.pairroom.domain; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.List; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import site.coduo.fake.SequentialAccessCodeStrategy; -import site.coduo.pairroom.domain.accesscode.AccessCode; -import site.coduo.pairroom.domain.accesscode.AccessCodeFactory; - -class AccessCodeFactoryTest { - - @Test - @DisplayName("쀑볡이 μ—†λŠ” μ—‘μ„ΈμŠ€ μ½”λ“œλ₯Ό μƒμ„±ν•œλ‹€.") - void generate_unique_access_code() { - // given - final AccessCodeFactory sut = new AccessCodeFactory(new SequentialAccessCodeStrategy()); - final List accessCodes = List.of(new AccessCode("FAKE_1"), - new AccessCode("FAKE_2"), - new AccessCode("FAKE_3"), - new AccessCode("FAKE_4")); - - // when - final AccessCode accessCode = sut.generateWithoutDuplication(accessCodes); - - // then - assertThat(accessCode).isEqualTo(new AccessCode("FAKE_5")); - } -} diff --git a/backend/src/test/java/site/coduo/pairroom/domain/PairRoomEntityStatusTest.java b/backend/src/test/java/site/coduo/pairroom/domain/PairRoomEntityStatusTest.java new file mode 100644 index 00000000..7c0370f5 --- /dev/null +++ b/backend/src/test/java/site/coduo/pairroom/domain/PairRoomEntityStatusTest.java @@ -0,0 +1,36 @@ +package site.coduo.pairroom.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import site.coduo.pairroom.exception.InvalidPairRoomStatusException; + +class PairRoomEntityStatusTest { + + @Test + @DisplayName("νŽ˜μ–΄λ£Έ μƒνƒœ μ΄λ¦„μœΌλ‘œ νŽ˜μ–΄λ£Έ μƒνƒœ enum을 μ°Ύμ•„ λ°˜ν™˜ν•œλ‹€.") + void find_pair_room_status_by_name() { + // given + final String pairRoomStatusName = "IN_PROGRESS"; + + // when + final PairRoomStatus pairRoomStatus = PairRoomStatus.findByName(pairRoomStatusName); + + // then + assertThat(pairRoomStatus).isEqualTo(PairRoomStatus.IN_PROGRESS); + } + + @Test + @DisplayName("νŽ˜μ–΄λ£Έ μƒνƒœ 이름이 enum에 μ‘΄μž¬ν•˜μ§€ μ•Šμ„ 경우 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.") + void throw_exception_when_pair_room_status_name_not_exist() { + // given + final String pairRoomStatusName = "NOT_EXIST"; + + // when & then + assertThatThrownBy(() -> PairRoomStatus.findByName(pairRoomStatusName)) + .isInstanceOf(InvalidPairRoomStatusException.class); + } +} diff --git a/backend/src/test/java/site/coduo/pairroom/domain/PairRoomEntityTest.java b/backend/src/test/java/site/coduo/pairroom/domain/PairRoomEntityTest.java new file mode 100644 index 00000000..69818feb --- /dev/null +++ b/backend/src/test/java/site/coduo/pairroom/domain/PairRoomEntityTest.java @@ -0,0 +1,86 @@ +package site.coduo.pairroom.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import static site.coduo.fixture.AccessCodeFixture.ACCESS_CODE; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import site.coduo.pairroom.domain.accesscode.AccessCode; +import site.coduo.pairroom.repository.PairRoomEntity; + +class PairRoomEntityTest { + + @Test + @DisplayName("νŽ˜μ–΄λ£Έμ„ μƒμ„±ν•œλ‹€.") + void create_pair_room() { + // given + final String firstName = "first"; + final String secondName = "second"; + final Pair pair = new Pair(new PairName(firstName), new PairName(secondName)); + final PairRoomStatus pairRoomStatus = PairRoomStatus.IN_PROGRESS; + final MissionUrl missionUrl = new MissionUrl("https://missionUrl.xxx"); + + // when & then + assertThatCode(() -> new PairRoom(pairRoomStatus, pair, missionUrl, ACCESS_CODE)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("λ“œλ¦¬μ–΄λΉ„μ™€ λ‚΄λΉ„κ²Œμ΄ν„°λ₯Ό λ³€κ²½ν•œν•œλ‹€.") + void change_nav_and_driver() { + // given + final PairRoomEntity sut = PairRoomEntity.from( + new PairRoom(PairRoomStatus.IN_PROGRESS, + new Pair(new PairName("navi"), new PairName("dri")), + new MissionUrl("https://missionUrl.xxx"), + new AccessCode("access")) + ); + + // when + sut.swapNavigatorWithDriver(); + + // then + assertThat(sut) + .extracting("navigator", "driver") + .contains("dri", "navi"); + } + + @Test + @DisplayName("νŽ˜μ–΄λ£Έ μƒνƒœκ°€ DELETEλ©΄ trueλ₯Ό λ°˜ν™˜ν•œλ‹€.") + void pairRoomEntityStatusIsDelete() { + // Given + final PairRoomEntity sut = PairRoomEntity.from( + new PairRoom(PairRoomStatus.DELETED, + new Pair(new PairName("navi"), new PairName("dri")), + new MissionUrl("https://missionUrl.xxx"), + new AccessCode("access")) + ); + + // When + final boolean isDelete = sut.isDelete(); + + // Then + assertThat(isDelete).isTrue(); + } + + @Test + @DisplayName("νŽ˜μ–΄λ£Έ μƒνƒœκ°€ DELETEκ°€ μ•„λ‹ˆλ©΄ falseλ₯Ό λ°˜ν™˜ν•œλ‹€.") + void pairRoomEntityStatusIsNotDelete() { + // Given + final PairRoomEntity sut = PairRoomEntity.from( + new PairRoom(PairRoomStatus.IN_PROGRESS, + new Pair(new PairName("navi"), new PairName("dri")), + new MissionUrl("https://missionUrl.xxx"), + new AccessCode("access")) + ); + + // When + final boolean isDelete = sut.isDelete(); + + // Then + assertThat(isDelete).isFalse(); + } +} diff --git a/backend/src/test/java/site/coduo/pairroom/domain/PairRoomTest.java b/backend/src/test/java/site/coduo/pairroom/domain/PairRoomTest.java deleted file mode 100644 index df533d24..00000000 --- a/backend/src/test/java/site/coduo/pairroom/domain/PairRoomTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package site.coduo.pairroom.domain; - -import static org.assertj.core.api.Assertions.assertThatCode; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import site.coduo.pairroom.domain.accesscode.AccessCode; - -class PairRoomTest { - - @Test - @DisplayName("νŽ˜μ–΄λ£Έμ„ μƒμ„±ν•œλ‹€.") - void create_pair_room() { - // given - final String firstName = "first"; - final String secondName = "second"; - final Pair pair = new Pair(new PairName(firstName), new PairName(secondName)); - - // when & then - assertThatCode(() -> new PairRoom(pair, new AccessCode("code"))) - .doesNotThrowAnyException(); - } -} diff --git a/backend/src/test/java/site/coduo/pairroom/domain/PairTest.java b/backend/src/test/java/site/coduo/pairroom/domain/PairTest.java index b979bb60..6470a73b 100644 --- a/backend/src/test/java/site/coduo/pairroom/domain/PairTest.java +++ b/backend/src/test/java/site/coduo/pairroom/domain/PairTest.java @@ -13,10 +13,10 @@ class PairTest { @DisplayName("각 νŽ˜μ–΄μ˜ 이름이 μ€‘λ³΅λ˜λ©΄ μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.") void throw_exception_when_pair_name_is_duplicated() { // given - final PairName firstPair = new PairName("λ ˆλ””"); - final PairName secondPair = new PairName("λ ˆλ””"); + final PairName navigator = new PairName("λ ˆλ””"); + final PairName driver = new PairName("λ ˆλ””"); - assertThatThrownBy(() -> new Pair(firstPair, secondPair)) - .isInstanceOf(DuplicatePairNameException.class); + assertThatThrownBy(() -> new Pair(navigator, driver)) + .isInstanceOf(DuplicatePairNameException.class); } } diff --git a/backend/src/test/java/site/coduo/pairroom/mock/FakePairRoomRepository.java b/backend/src/test/java/site/coduo/pairroom/mock/FakePairRoomRepository.java deleted file mode 100644 index f2a7f87f..00000000 --- a/backend/src/test/java/site/coduo/pairroom/mock/FakePairRoomRepository.java +++ /dev/null @@ -1,41 +0,0 @@ -package site.coduo.pairroom.mock; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -import site.coduo.pairroom.domain.PairRoom; -import site.coduo.pairroom.domain.accesscode.AccessCode; -import site.coduo.pairroom.service.port.PairRoomRepository; - -public class FakePairRoomRepository implements PairRoomRepository { - - private Long autoGeneratedId = 0L; - public final List data = new ArrayList<>(); - - @Override - public PairRoom save(final PairRoom pairRoom) { - if (pairRoom.getId() == null || pairRoom.getId() == 0) { - final PairRoom createdPairRoom = new PairRoom(++autoGeneratedId, pairRoom.getPair(), pairRoom.getAccessCode()); - data.add(createdPairRoom); - return createdPairRoom; - } else { - data.removeIf(item -> Objects.equals(item.getId(), pairRoom.getId())); - data.add(pairRoom); - return pairRoom; - } - } - - @Override - public Optional findById(final Long id) { - return data.stream() - .filter(pairRoom -> pairRoom.getId().equals(id)).findAny(); - } - - @Override - public Optional findByAccessCode(final AccessCode accessCode) { - return data.stream() - .filter(pairRoom -> pairRoom.getAccessCode().equals(accessCode)).findAny(); - } -} diff --git a/backend/src/test/java/site/coduo/pairroom/repository/PairRoomEntityRepositoryTest.java b/backend/src/test/java/site/coduo/pairroom/repository/PairRoomEntityRepositoryTest.java new file mode 100644 index 00000000..49d6ee15 --- /dev/null +++ b/backend/src/test/java/site/coduo/pairroom/repository/PairRoomEntityRepositoryTest.java @@ -0,0 +1,79 @@ +package site.coduo.pairroom.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import site.coduo.pairroom.domain.MissionUrl; +import site.coduo.pairroom.domain.Pair; +import site.coduo.pairroom.domain.PairName; +import site.coduo.pairroom.domain.PairRoom; +import site.coduo.pairroom.domain.PairRoomStatus; +import site.coduo.pairroom.domain.accesscode.AccessCode; + +@SpringBootTest +@Transactional +class PairRoomEntityRepositoryTest { + + @Autowired + private PairRoomRepository pairRoomRepository; + + @Test + @DisplayName("μ—‘μ„ΈμŠ€ μ½”λ“œλ₯Ό λ°”νƒ•μœΌλ‘œ μ˜μ†μ„±μ„ μ‘°νšŒν•œλ‹€.- μ˜μ†μ„± 쑴재 O") + void search_persistence_by_access_code_exists_case() { + // given + final Pair pair = new Pair(new PairName("hello"), new PairName("world")); + final MissionUrl missionUrl = new MissionUrl("https://missionUrl.xxx"); + final PairRoom pairRoom = new PairRoom(PairRoomStatus.IN_PROGRESS, pair, missionUrl, new AccessCode("code")); + final PairRoomEntity entity = PairRoomEntity.from(pairRoom); + pairRoomRepository.save(entity); + + // when + final Optional persistence = pairRoomRepository.findByAccessCode( + pairRoom.getAccessCodeText()); + + // then + assertThat(persistence).hasValue(entity); + } + + @Test + @DisplayName("μ—‘μ„ΈμŠ€ μ½”λ“œλ₯Ό λ°”νƒ•μœΌλ‘œ μ˜μ†μ„±μ„ μ‘°νšŒν•œλ‹€.- μ˜μ†μ„± 쑴재 X") + void search_persistence_by_access_code_not_exists_case() { + // given + final Pair pair = new Pair(new PairName("hello"), new PairName("world")); + final MissionUrl missionUrl = new MissionUrl("https://missionUrl.xxx"); + final PairRoom pairRoom = new PairRoom(PairRoomStatus.IN_PROGRESS, pair, missionUrl, new AccessCode("code")); + + // when + final Optional persistence = pairRoomRepository.findByAccessCode( + pairRoom.getAccessCodeText()); + + // then + assertThat(persistence).isEmpty(); + } + + @Test + @DisplayName("μ—‘μ„ΈμŠ€ μ½”λ“œ 도메인을 λ°”νƒ•μœΌλ‘œ μ˜μ†μ„±μ„ μ‘°νšŒν•œλ‹€.- μ˜μ†μ„± 쑴재 O") + void search_persistence_by_access_code_domain_exists_case() { + // given + final Pair pair = new Pair(new PairName("hello"), new PairName("world")); + final AccessCode code = new AccessCode("code"); + final MissionUrl missionUrl = new MissionUrl("https://missionUrl.xxx"); + final PairRoom pairRoom = new PairRoom(PairRoomStatus.IN_PROGRESS, pair, missionUrl, code); + pairRoomRepository.save(PairRoomEntity.from(pairRoom)); + + // when + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(code); + + // then + assertThat(pairRoomEntity) + .extracting("accessCode") + .isEqualTo(code.getValue()); + } +} diff --git a/backend/src/test/java/site/coduo/pairroom/repository/PairRoomRepositoryTest.java b/backend/src/test/java/site/coduo/pairroom/repository/PairRoomRepositoryTest.java deleted file mode 100644 index 77f8ffed..00000000 --- a/backend/src/test/java/site/coduo/pairroom/repository/PairRoomRepositoryTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package site.coduo.pairroom.repository; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.Optional; - -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -import site.coduo.pairroom.domain.Pair; -import site.coduo.pairroom.domain.PairName; -import site.coduo.pairroom.domain.PairRoom; -import site.coduo.pairroom.domain.accesscode.AccessCode; - -@SpringBootTest -@Transactional -class PairRoomRepositoryTest { - - @Autowired - private PairRoomRepository pairRoomRepository; - - @Test - @DisplayName("μ—‘μ„ΈμŠ€ μ½”λ“œλ₯Ό λ°”νƒ•μœΌλ‘œ μ˜μ†μ„±μ„ μ‘°νšŒν•œλ‹€.- μ˜μ†μ„± 쑴재 O") - void search_persistence_by_access_code_exists_case() { - // given - final Pair pair = new Pair(new PairName("hello"), new PairName("world")); - final PairRoom pairRoom = new PairRoom(pair, new AccessCode("code")); - pairRoomRepository.save(pairRoom); - - // when - final Optional persistence = pairRoomRepository.findByAccessCode(pairRoom.getAccessCode()); - - // then - assertThat(persistence).hasValue(pairRoom); - } - - @Test - @DisplayName("μ—‘μ„ΈμŠ€ μ½”λ“œλ₯Ό λ°”νƒ•μœΌλ‘œ μ˜μ†μ„±μ„ μ‘°νšŒν•œλ‹€.- μ˜μ†μ„± 쑴재 X") - void search_persistence_by_access_code_not_exists_case() { - // given - final Pair pair = new Pair(new PairName("hello"), new PairName("world")); - final PairRoom pairRoom = new PairRoom(pair, new AccessCode("code")); - - // when - final Optional persistence = pairRoomRepository.findByAccessCode(pairRoom.getAccessCode()); - - // then - assertThat(persistence).isEmpty(); - } -} diff --git a/backend/src/test/java/site/coduo/pairroom/service/PairRoomServiceTest.java b/backend/src/test/java/site/coduo/pairroom/service/PairRoomServiceTest.java index 3f85604c..62a5939f 100644 --- a/backend/src/test/java/site/coduo/pairroom/service/PairRoomServiceTest.java +++ b/backend/src/test/java/site/coduo/pairroom/service/PairRoomServiceTest.java @@ -2,80 +2,281 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; -import java.util.stream.Stream; +import java.util.List; +import java.util.Random; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.transaction.annotation.Transactional; -import site.coduo.pairroom.dto.PairRoomCreateRequest; -import site.coduo.pairroom.dto.TimerDurationCreateRequest; +import site.coduo.member.domain.Member; +import site.coduo.member.domain.repository.MemberRepository; +import site.coduo.member.infrastructure.security.JwtProvider; +import site.coduo.pairroom.domain.MissionUrl; +import site.coduo.pairroom.domain.Pair; +import site.coduo.pairroom.domain.PairName; +import site.coduo.pairroom.domain.PairRoom; +import site.coduo.pairroom.domain.PairRoomStatus; +import site.coduo.pairroom.domain.accesscode.AccessCode; +import site.coduo.pairroom.exception.DeletePairRoomException; import site.coduo.pairroom.exception.PairRoomNotFoundException; +import site.coduo.pairroom.repository.PairRoomEntity; +import site.coduo.pairroom.repository.PairRoomRepository; +import site.coduo.pairroom.service.dto.PairRoomCreateRequest; +import site.coduo.pairroom.service.dto.PairRoomMemberResponse; +import site.coduo.pairroom.service.dto.PairRoomReadResponse; +import site.coduo.timer.domain.Timer; +import site.coduo.timer.repository.TimerEntity; +import site.coduo.timer.repository.TimerRepository; +@Transactional @SpringBootTest class PairRoomServiceTest { @Autowired private PairRoomService pairRoomService; + @Autowired + private JwtProvider jwtProvider; + @Autowired + private MemberRepository memberRepository; + @Autowired + private TimerRepository timerRepository; + @Autowired + private PairRoomRepository pairRoomRepository; + + @Test + @DisplayName("νŽ˜μ–΄λ£Έμ„ μƒμ„±ν•œλ‹€.") + void create_pair_room() { + // given + final PairRoomCreateRequest request = + new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ", 1000L, 100L, "https://missionUrl.xxx", + PairRoomStatus.IN_PROGRESS.name()); + + // when + final String accessCode = pairRoomService.savePairRoom(request, null); + + // then + assertThatCode(() -> pairRoomService.findPairRoomAndTimer(accessCode)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("νŽ˜μ–΄λ£Έμ„ μƒμ„±ν• λ•Œ 타이머도 ν•¨κ»˜ μƒμ„±λœλ‹€.") + void create_timer_when_create_pair_room() { + // given + final PairRoomCreateRequest request = + new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ", 1000L, 100L, "https://missionUrl.xxx", + PairRoomStatus.IN_PROGRESS.name()); + + // when + pairRoomService.savePairRoom(request, null); + + // then + assertThat(timerRepository.findAll()).hasSize(1); + } + @Test - @Transactional @DisplayName("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” νŽ˜μ–΄λ£Έ μ ‘κ·Ό μ½”λ“œλ₯Ό 찾으면 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.") void throw_exception_when_find_not_exist_access_code() { // given final String notSavedAccessCode = "123456"; // when & then - assertThatThrownBy(() -> pairRoomService.findByAccessCode(notSavedAccessCode)) + assertThatThrownBy(() -> pairRoomService.findPairRoomAndTimer(notSavedAccessCode)) .isExactlyInstanceOf(PairRoomNotFoundException.class); } - @DisplayName("타이머 μ‹œκ°„ μ €μž₯ ν›„, λ³€κ²½λœ 타이머 μ‹œκ°„μ„ μ €μž₯ν•˜κ³ , νŽ˜μ–΄λ£Έ 정보λ₯Ό μ‘°νšŒν•œλ‹€.") - @TestFactory - Stream pairRoom_create_timerDuration_save_pairRoom_get() { - final PairRoomCreateRequest pairRoomCreateRequest = new PairRoomCreateRequest("μž‰ν¬", "ν”„λžŒ"); - final String accessCode = pairRoomService.savePairNameAndAccessCode(pairRoomCreateRequest); - - return Stream.of( - dynamicTest("타이머 μ‹œκ°„μ„ μ €μž₯ν•œλ‹€", () -> { - // given - final long expected = 600000; - - // when - pairRoomService.saveTimerDuration(accessCode, new TimerDurationCreateRequest(expected)); - - // then - assertThat(pairRoomService.findByAccessCode(accessCode).getTimerDuration()) - .isEqualTo(expected); - }), - dynamicTest("μƒˆλ‘œμš΄ 타이머 μ‹œκ°„μ„ μ €μž₯ν•œλ‹€", () -> { - // given - final long expected = 420000; - - // when - pairRoomService.saveTimerDuration(accessCode, new TimerDurationCreateRequest(expected)); - - // then - assertThat(pairRoomService.findByAccessCode(accessCode).getTimerDuration()) - .isEqualTo(expected); - }), - dynamicTest("νŽ˜μ–΄λ£Έμ„ λ°˜ν™˜ν•œ ν›„, μƒˆλ‘œμš΄ 타이머 μ‹œκ°„μœΌλ‘œ λ³€κ²½λ˜μ–΄ μžˆλŠ” 것을 ν™•μΈν•œλ‹€", () -> { - // given - final long expected = 420000; - - // when - pairRoomService.findByAccessCode(accessCode); - - // then - assertThat(pairRoomService.findByAccessCode(accessCode).getTimerDuration()) - .isEqualTo(expected); - }) + @Test + @DisplayName("μ‚­μ œλœ νŽ˜μ–΄λ£Έμ˜ μ ‘κ·Ό μ½”λ“œλ₯Ό 찾으면 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.") + void throw_exception_when_find_delete_pair_room_access_code() { + // given + final PairRoomCreateRequest request = + new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ", 1000L, 100L, + "https://missionUrl.xxx", PairRoomStatus.DELETED.name()); + final String accessCode = pairRoomService.savePairRoom(request, null); + + // when & then + assertThatThrownBy(() -> pairRoomService.findPairRoomAndTimer(accessCode)) + .isExactlyInstanceOf(DeletePairRoomException.class); + } + + @Test + @DisplayName("νŽ˜μ–΄λ£Έ μƒνƒœλ₯Ό λ³€κ²½ν•œλ‹€.") + void update_pair_room_status() { + // given + final PairRoomCreateRequest request = + new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ", 1000L, 100L, "https://missionUrl.xxx", + PairRoomStatus.IN_PROGRESS.name()); + final String accessCode = pairRoomService.savePairRoom(request, null); + + // when + pairRoomService.updatePairRoomStatus(accessCode, PairRoomStatus.COMPLETED.name()); + + // then + assertThat(PairRoomStatus.findByName(pairRoomService.findPairRoomAndTimer(accessCode).status())) + .isEqualTo(PairRoomStatus.COMPLETED); + } + + @Test + @DisplayName("μ‚­μ œλœ νŽ˜μ–΄λ£Έ μƒνƒœλ₯Ό λ³€κ²½ν•˜λ €κ³  ν•˜λ©΄ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚¨λ‹€.") + void update_delete_pair_room_status() { + // given + final PairRoomCreateRequest request = + new PairRoomCreateRequest("λ ˆλ””", "ν”„λžŒ", 1000L, 100L, "https://missionUrl.xxx", + PairRoomStatus.DELETED.name()); + final String accessCode = pairRoomService.savePairRoom(request, null); + + // when & then + assertThatThrownBy(() -> pairRoomService.updatePairRoomStatus(accessCode, PairRoomStatus.COMPLETED.name())) + .isExactlyInstanceOf(DeletePairRoomException.class); + } + + @Test + @DisplayName("νŽ˜μ–΄ 역할을 λ³€κ²½ν•œλ‹€.") + void change_pair_room() { + // given + final PairRoomEntity entity = PairRoomEntity.from( + new PairRoom(PairRoomStatus.IN_PROGRESS, + new Pair(new PairName("fram"), new PairName("lemonL")), + new MissionUrl("https://missionUrl.xxx"), + new AccessCode("1234")) + ); + pairRoomRepository.save(entity); + + // when + pairRoomService.updateNavigatorWithDriver(entity.getAccessCode()); + + // then + assertThat(entity) + .extracting("navigator", "driver") + .contains("lemonL", "fram"); + } + + @Test + @DisplayName("μ‚­μ œλœ νŽ˜μ–΄λ£Έμ˜ νŽ˜μ–΄ 역할을 λ³€κ²½ν•˜λ €ν•˜λ©΄ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚¨λ‹€.") + void change_delete_pair_room_role() { + // given + final PairRoomEntity entity = PairRoomEntity.from( + new PairRoom(PairRoomStatus.DELETED, + new Pair(new PairName("fram"), new PairName("lemonL")), + new MissionUrl("https://missionUrl.xxx"), + new AccessCode("1234")) + ); + pairRoomRepository.save(entity); + + // when & then + assertThatThrownBy(() -> pairRoomService.updateNavigatorWithDriver(entity.getAccessCode())) + .isExactlyInstanceOf(DeletePairRoomException.class); + } + + + @DisplayName("μ‚­μ œλ˜μ§€ μ•Šμ€, λ©€λ²„μ˜ λ°© λͺ©λ‘μ„ κ°€μ Έμ˜¨λ‹€.") + @Test + void find_rooms_by_member() { + //given + final Member memberA = createMember("reddevilmidzy"); + final Member memberB = createMember("test"); + + final PairRoomCreateRequest pairRoomCreateRequest = new PairRoomCreateRequest("λ ˆλ””", "μž‰ν¬", 1, 1, + "https://missionUrl.xxx", + "IN_PROGRESS"); + + final String accessCodeA_1 = pairRoomService.savePairRoom(pairRoomCreateRequest, memberA.getAccessToken()); + final String accessCodeA_2 = pairRoomService.savePairRoom(pairRoomCreateRequest, memberA.getAccessToken()); + final String accessCodeB_1 = pairRoomService.savePairRoom(pairRoomCreateRequest, memberB.getAccessToken()); + pairRoomService.savePairRoom(pairRoomCreateRequest, null); + + final PairRoomCreateRequest deletePairRoomCreateRequest = new PairRoomCreateRequest("λ ˆλ””", "μž‰ν¬", 1, 1, + "https://missionUrl.xxx", PairRoomStatus.DELETED.name()); + pairRoomService.savePairRoom(deletePairRoomCreateRequest, memberA.getAccessToken()); + pairRoomService.savePairRoom(deletePairRoomCreateRequest, memberA.getAccessToken()); + pairRoomService.savePairRoom(deletePairRoomCreateRequest, memberA.getAccessToken()); + + final List memberAExpected = List.of(accessCodeA_1, accessCodeA_2); + final List memberBExpected = List.of(accessCodeB_1); + + //when + final List findAccessCodesForMemberA = pairRoomService.findPairRooms(memberA.getAccessToken()) + .stream() + .map(PairRoomMemberResponse::accessCode) + .toList(); + final List findAccessCodesForMemberB = pairRoomService.findPairRooms(memberB.getAccessToken()) + .stream() + .map(PairRoomMemberResponse::accessCode) + .toList(); + + //then + assertThat(findAccessCodesForMemberA).hasSize(2) + .containsAll(memberAExpected); + assertThat(findAccessCodesForMemberB).hasSize(1) + .containsAll(memberBExpected); + } + + private Member createMember(final String userId) { + final String token = jwtProvider.sign(userId); + final Member member = Member.builder() + .accessToken(token) + .loginId("login id") + .profileImage("profile image") + .username("hello" + new Random().nextInt()) + .userId(userId) + .build(); + return memberRepository.save(member); + } + + @Test + @DisplayName("νŽ˜μ–΄λ£Έμ„ λ°˜ν™˜ν•  λ•Œ 타이머 정보도 ν•¨κ»˜ λ°˜ν™˜ν•œλ‹€.") + void get_pair_room_and_timer() { + // given + final PairRoomEntity pairRoomEntity = PairRoomEntity.from( + new PairRoom(PairRoomStatus.IN_PROGRESS, + new Pair(new PairName("λ ˆλ””"), new PairName("파슬리")), + new MissionUrl("https://missionUrl.xxx"), + new AccessCode("123456")) + ); + final Timer timer = new Timer( + new AccessCode(pairRoomEntity.getAccessCode()), + 10000, + 10000 + ); + pairRoomRepository.save(pairRoomEntity); + timerRepository.save(new TimerEntity(timer, pairRoomEntity)); + + // when + final PairRoomReadResponse actual = pairRoomService.findPairRoomAndTimer( + pairRoomEntity.getAccessCode()); + + // then + assertThat(actual) + .extracting("navigator", "driver", "status", "duration", "remainingTime") + .contains(pairRoomEntity.getNavigator(), pairRoomEntity.getDriver(), + pairRoomEntity.getStatus().toString(), timer.getDuration(), timer.getRemainingTime()); + } + + @Test + @DisplayName("νŽ˜μ–΄λ£Έμ΄ μ‘΄μž¬ν•˜λŠ”μ§€ ν™•μΈν•œλ‹€.") + void exists_pair_room() { + //given + final AccessCode accessCode = new AccessCode("123456"); + final PairRoomEntity pairRoomEntity = PairRoomEntity.from( + new PairRoom(PairRoomStatus.IN_PROGRESS, + new Pair(new PairName("λ ˆλ””"), new PairName("레λͺ¨λ„€")), + new MissionUrl("https://missionUrl.xxx"), + accessCode + )); + pairRoomRepository.save(pairRoomEntity); + + //when & then + assertAll( + () -> assertThat(pairRoomService.existsByAccessCode("not-exist")).isFalse(), + () -> assertThat(pairRoomService.existsByAccessCode(accessCode.getValue())).isTrue() + ); } } diff --git a/backend/src/test/java/site/coduo/pairroomhistory/repository/PairRoomHistoryRepositoryTest.java b/backend/src/test/java/site/coduo/pairroomhistory/repository/PairRoomHistoryRepositoryTest.java deleted file mode 100644 index 8ecf97eb..00000000 --- a/backend/src/test/java/site/coduo/pairroomhistory/repository/PairRoomHistoryRepositoryTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package site.coduo.pairroomhistory.repository; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -import site.coduo.pairroom.domain.Pair; -import site.coduo.pairroom.domain.PairName; -import site.coduo.pairroom.domain.PairRoom; -import site.coduo.pairroom.domain.accesscode.AccessCode; -import site.coduo.pairroom.repository.PairRoomRepository; -import site.coduo.pairroomhistory.domain.PairRoomHistory; - -@Transactional -@SpringBootTest -class PairRoomHistoryRepositoryTest { - - @Autowired - private PairRoomHistoryRepository pairRoomHistoryRepository; - - @Autowired - private PairRoomRepository pairRoomRepository; - - @AfterEach - void tearDown() { - pairRoomHistoryRepository.deleteAll(); - pairRoomRepository.deleteAll(); - } - - @Test - @DisplayName("νŠΉμ • νŽ˜μ–΄λ£Έμ˜ κ°€μž₯ 졜근 νžˆμŠ€ν† λ¦¬λ₯Ό μ‘°νšŒν•œλ‹€.") - void inquiry_recent_history() { - // given - final PairRoom pairRoom = new PairRoom(new Pair(new PairName("λ ˆλ¨Έλ„€"), new PairName("ν”„λžŒ")), - new AccessCode("hello1")); - pairRoomRepository.save(pairRoom); - - final PairRoomHistory history1 = PairRoomHistory.builder() - .pairRoom(pairRoom) - .driver("λ ˆλ¨Έλ„€") - .navigator("ν”„λžŒ") - .timerRound(1) - .timerRemainingTime(1000) - .build(); - - final PairRoomHistory history2 = PairRoomHistory.builder() - .pairRoom(pairRoom) - .driver("ν”„λžŒ") - .navigator("λ ˆλ¨Έλ„€") - .timerRound(2) - .timerRemainingTime(1000) - .build(); - - pairRoomHistoryRepository.save(new PairRoomHistoryEntity(history1)); - pairRoomHistoryRepository.save(new PairRoomHistoryEntity(history2)); - - // when - final PairRoomHistoryEntity actual = pairRoomHistoryRepository - .findTopByPairRoomIdOrderByCreatedAtDesc(pairRoom.getId()) - .orElseThrow(); - - // then - assertThat(actual) - .extracting("driver", "navigator", "timerRound", "timerRemainingTime") - .contains(history2.getDriver(), history2.getNavigator(), history2.getTimerRound(), - history2.getTimerRemainingTime()); - } - - @Test - @DisplayName("타이머 남은 μ‹œκ°„μ„ μ—…λ°μ΄νŠΈν•œλ‹€.") - void update_timer_remaining_time() { - // given - final PairRoom pairRoom = new PairRoom(new Pair(new PairName("솔라"), new PairName("λ„€μ˜€")), - new AccessCode("hello2")); - pairRoomRepository.save(pairRoom); - - final PairRoomHistory history = PairRoomHistory.builder() - .pairRoom(pairRoom) - .driver("솔라") - .navigator("λ„€μ˜€") - .timerRound(1) - .timerRemainingTime(1000) - .build(); - PairRoomHistoryEntity pairRoomHistoryEntity = pairRoomHistoryRepository.save(new PairRoomHistoryEntity(history)); - - final long expectedTimerRemainingTime = 30; - - // when - pairRoomHistoryRepository.updateByIdTimerRemainingTime(pairRoomHistoryEntity.getId(), expectedTimerRemainingTime); - - // then - final PairRoomHistoryEntity actual = pairRoomHistoryRepository - .findById(pairRoomHistoryEntity.getId()) - .orElseThrow(); - - assertThat(actual.getTimerRemainingTime()).isEqualTo(expectedTimerRemainingTime); - } -} diff --git a/backend/src/test/java/site/coduo/pairroomhistory/service/PairRoomHistoryServiceTest.java b/backend/src/test/java/site/coduo/pairroomhistory/service/PairRoomHistoryServiceTest.java deleted file mode 100644 index 283b6148..00000000 --- a/backend/src/test/java/site/coduo/pairroomhistory/service/PairRoomHistoryServiceTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package site.coduo.pairroomhistory.service; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.junit.jupiter.api.Assertions.assertAll; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -import site.coduo.pairroom.dto.PairRoomCreateRequest; -import site.coduo.pairroom.service.PairRoomService; -import site.coduo.pairroomhistory.dto.PairRoomHistoryCreateRequest; -import site.coduo.pairroomhistory.dto.PairRoomHistoryReadResponse; -import site.coduo.utils.CascadeCleaner; - -@Transactional -@SpringBootTest -class PairRoomHistoryServiceTest extends CascadeCleaner { - - @Autowired - private PairRoomService pairRoomService; - - @Autowired - private PairRoomHistoryService pairRoomHistoryService; - - @AfterEach - void tearDown() { - deleteAllPairRoomCascade(); - } - - @Test - @DisplayName("νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬λ₯Ό μ €μž₯ν•œλ‹€.") - void create_pair_room_history() { - // given - final String accessCode = pairRoomService.savePairNameAndAccessCode(new PairRoomCreateRequest("켈리", "레λͺ¨λ„€")); - final PairRoomHistoryCreateRequest request = new PairRoomHistoryCreateRequest( - "켈리", - "레λͺ¨λ„€", - 0, - 600000 - ); - - // when & then - assertThatCode(() -> pairRoomHistoryService.createPairRoomHistory(accessCode, request)) - .doesNotThrowAnyException(); - } - - @DisplayName("νŽ˜μ–΄λ£Έ νžˆμŠ€ν† λ¦¬ 쀑 κ°€μž₯ 졜근 νžˆμŠ€ν† λ¦¬λ₯Ό λ°˜ν™˜ν•œλ‹€.") - @Test - void get_latest_pair_room_history() { - // given - final String accessCode = pairRoomService - .savePairNameAndAccessCode(new PairRoomCreateRequest("μž‰ν¬", "λ ˆλ””")); - final PairRoomHistoryCreateRequest request = new PairRoomHistoryCreateRequest( - "μž‰ν¬", - "λ ˆλ””", - 0, - 600000 - ); - final PairRoomHistoryCreateRequest secondRequest = new PairRoomHistoryCreateRequest( - "λ ˆλ””", - "μž‰ν¬", - 1, - 300000 - ); - pairRoomHistoryService.createPairRoomHistory(accessCode, request); - pairRoomHistoryService.createPairRoomHistory(accessCode, secondRequest); - - // when - final PairRoomHistoryReadResponse actual = pairRoomHistoryService.readLatestPairRoomHistory(accessCode); - - // then - assertAll( - () -> assertThat(actual.driver()).isEqualTo(secondRequest.driver()), - () -> assertThat(actual.navigator()).isEqualTo(secondRequest.navigator()), - () -> assertThat(actual.timerRound()).isEqualTo(secondRequest.timerRound()), - () -> assertThat(actual.timerRemainingTime()).isEqualTo(secondRequest.timerRemainingTime()) - ); - } -} diff --git a/backend/src/test/java/site/coduo/referencelink/domain/CategoryTest.java b/backend/src/test/java/site/coduo/referencelink/domain/CategoryTest.java new file mode 100644 index 00000000..80084658 --- /dev/null +++ b/backend/src/test/java/site/coduo/referencelink/domain/CategoryTest.java @@ -0,0 +1,39 @@ +package site.coduo.referencelink.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import site.coduo.referencelink.exception.InvalidCategoryException; + +class CategoryTest { + + @Test + @DisplayName("10자 이상인 경우 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.") + void validate_category() { + //given + final String categoryName = "0123456789μ‹­"; + + //when & then + assertThatThrownBy(() -> new Category(categoryName)) + .isInstanceOf(InvalidCategoryException.class); + } + + @ParameterizedTest + @DisplayName("곡백을 μ œκ±°ν•œ ν›„ 이름을 μ €μž₯ν•œλ‹€.") + @ValueSource(strings = {" μŠ€ν”„λ§", "μŠ€ν”„λ§ ", " μŠ€ν”„λ§ ", " μŠ€ν”„λ§ "}) + void trim_category_name(final String categoryName) { + // given + final Category springCategory = new Category("μŠ€ν”„λ§"); + + // when + final Category category = new Category(categoryName); + + // then + assertThat(category).isEqualTo(springCategory); + } +} diff --git a/backend/src/test/java/site/coduo/referencelink/domain/ReferenceLinkTest.java b/backend/src/test/java/site/coduo/referencelink/domain/ReferenceLinkTest.java index 14b115bf..0d25faab 100644 --- a/backend/src/test/java/site/coduo/referencelink/domain/ReferenceLinkTest.java +++ b/backend/src/test/java/site/coduo/referencelink/domain/ReferenceLinkTest.java @@ -2,6 +2,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import java.net.URL; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -16,7 +18,7 @@ void create_reference_link() { final String url = "https://www.google.com"; // when & then - assertThatCode(() -> new ReferenceLink(new Url(url), new AccessCode("value"))) + assertThatCode(() -> new ReferenceLink(new URL(url), new AccessCode("value"))) .doesNotThrowAnyException(); } } diff --git a/backend/src/test/java/site/coduo/referencelink/domain/UrlTest.java b/backend/src/test/java/site/coduo/referencelink/domain/UrlTest.java deleted file mode 100644 index 751bdbc6..00000000 --- a/backend/src/test/java/site/coduo/referencelink/domain/UrlTest.java +++ /dev/null @@ -1,80 +0,0 @@ -package site.coduo.referencelink.domain; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; - -import org.assertj.core.api.AssertionsForClassTypes; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import site.coduo.referencelink.exception.InvalidUrlFormatException; - -class UrlTest { - - @Test - @DisplayName("Url을 μƒμ„±ν•œλ‹€.") - void generate_url() { - // given - final String urlText = "http://www.some.url"; - - // when & then - assertThatCode(() -> new Url(urlText)) - .doesNotThrowAnyException(); - } - - @Test - @DisplayName("http:// λ˜λŠ” https:// 둜 μ‹œμž‘ν•˜μ§€ μ•ŠμœΌλ©΄ μ˜ˆμ™Έλ₯Ό λ°œμƒν•œλ‹€.") - void validate_url_protocols() { - // given - final String url = "httpx://www.name.com"; - - // when & then - assertThatThrownBy(() -> new Url(url)) - .isInstanceOf(InvalidUrlFormatException.class); - } - - @Test - @DisplayName("ν”„λ‘œν† μ½œ 뒀에 .이 ν•˜λ‚˜ μ΄μƒμ—†μœΌλ©΄ μ˜ˆμ™Έλ₯Ό λ°œμƒν•œλ‹€.") - void validate_url_dot() { - // given - final String urlText = "https://wwwwww"; - - // when & then - assertThatThrownBy(() -> new Url(urlText)) - .isInstanceOf(InvalidUrlFormatException.class); - } - - @Test - @DisplayName("레퍼런슀 링크가 λΉˆκ°’μ΄λ©΄ μ˜ˆμ™Έλ₯Ό λ°œμƒν•œλ‹€") - void throw_exception_when_create_reference_link_with_blank_url() { - // given - final String url = ""; - - // when & then - AssertionsForClassTypes.assertThatThrownBy(() -> new Url(url)) - .isInstanceOf(InvalidUrlFormatException.class); - } - - - @Test - @DisplayName("레퍼런슀 링크가 널값이면 μ˜ˆμ™Έλ₯Ό λ°œμƒν•œλ‹€") - void throw_exception_when_create_reference_link_with_null_url() { - // given - final String url = null; - - // when & then - AssertionsForClassTypes.assertThatThrownBy(() -> new Url(url)) - .isInstanceOf(InvalidUrlFormatException.class); - } - - @Test - @DisplayName("Documentλ₯Ό κ°€μ Έμ˜¨λ‹€.") - void get_document() { - // given - final Url url = new Url("http://www.google.com"); - - // when & then - assertThat(url.getDocument()).isNotNull(); - } -} diff --git a/backend/src/test/java/site/coduo/referencelink/fake/FakeServer.java b/backend/src/test/java/site/coduo/referencelink/fake/FakeServer.java new file mode 100644 index 00000000..81718b7d --- /dev/null +++ b/backend/src/test/java/site/coduo/referencelink/fake/FakeServer.java @@ -0,0 +1,55 @@ +package site.coduo.referencelink.fake; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import site.coduo.referencelink.exception.ReferenceLinkException; + +public class FakeServer { + + public static final String testUrl; + + static { + final String html = "" + + "ν—€λ“œ 타이틀" + + "" + + "" + + "" + + ""; + testUrl = "http://localhost:" + createAndStartFakeServer(html) + "/test"; + } + + public static int createAndStartFakeServer(final String html) { + HttpServer server; + try { + server = HttpServer.create(new InetSocketAddress(0), 0); + } catch (final IOException e) { + throw new ReferenceLinkException("ν…ŒμŠ€νŠΈμš© μ„œλ²„ 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); + } + final int assignedPort = server.getAddress().getPort(); + + server.createContext("/test", createHandler(html)); + server.setExecutor(null); + server.start(); + return assignedPort; + } + + private static HttpHandler createHandler(final String html) { + return new HttpHandler() { + @Override + public void handle(final HttpExchange exchange) throws IOException { + exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8"); + exchange.sendResponseHeaders(200, html.getBytes(StandardCharsets.UTF_8).length); + final OutputStream os = exchange.getResponseBody(); + os.write(html.getBytes(StandardCharsets.UTF_8)); + os.close(); + } + }; + } +} diff --git a/backend/src/test/java/site/coduo/referencelink/fake/FakeServerTest.java b/backend/src/test/java/site/coduo/referencelink/fake/FakeServerTest.java new file mode 100644 index 00000000..ca52af9c --- /dev/null +++ b/backend/src/test/java/site/coduo/referencelink/fake/FakeServerTest.java @@ -0,0 +1,21 @@ +package site.coduo.referencelink.fake; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class FakeServerTest { + + @DisplayName("페이크 μ„œλ²„ 연결에 μ„±κ³΅ν•œλ‹€.") + @Test + void connect_success() throws IOException { + final URL url = new URL(FakeServer.testUrl); + final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + assertThat(connection.getResponseCode()).isEqualTo(200); + } +} diff --git a/backend/src/test/java/site/coduo/referencelink/service/CategoryServiceTest.java b/backend/src/test/java/site/coduo/referencelink/service/CategoryServiceTest.java new file mode 100644 index 00000000..770e604e --- /dev/null +++ b/backend/src/test/java/site/coduo/referencelink/service/CategoryServiceTest.java @@ -0,0 +1,179 @@ +package site.coduo.referencelink.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import static site.coduo.fixture.AccessCodeFixture.ACCESS_CODE; +import static site.coduo.fixture.PairRoomFixture.FRAM_LEMONE_ROOM; +import static site.coduo.fixture.PairRoomFixture.INK_REDDDY_ROOM; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import site.coduo.pairroom.repository.PairRoomEntity; +import site.coduo.pairroom.repository.PairRoomRepository; +import site.coduo.referencelink.domain.Category; +import site.coduo.referencelink.domain.ReferenceLink; +import site.coduo.referencelink.exception.InvalidCategoryException; +import site.coduo.referencelink.fake.FakeServer; +import site.coduo.referencelink.repository.CategoryEntity; +import site.coduo.referencelink.repository.CategoryRepository; +import site.coduo.referencelink.repository.ReferenceLinkEntity; +import site.coduo.referencelink.repository.ReferenceLinkRepository; +import site.coduo.referencelink.service.dto.CategoryCreateRequest; +import site.coduo.referencelink.service.dto.CategoryCreateResponse; +import site.coduo.referencelink.service.dto.CategoryReadResponse; +import site.coduo.referencelink.service.dto.CategoryUpdateRequest; +import site.coduo.referencelink.service.dto.CategoryUpdateResponse; +import site.coduo.utils.CascadeCleaner; + +@SpringBootTest +class CategoryServiceTest extends CascadeCleaner { + + @Autowired + private CategoryService categoryService; + + @Autowired + private CategoryRepository categoryRepository; + + @Autowired + private ReferenceLinkRepository referenceLinkRepository; + + @Autowired + private PairRoomRepository pairRoomRepository; + + @AfterEach + void tearDown() { + deleteAllPairRoomCascade(); + } + + @Test + @DisplayName("νŽ˜μ–΄λ£Έ 생성 ν›„ μΉ΄ν…Œκ³ λ¦¬λ₯Ό μ €μž₯ν•œλ‹€.") + void save_category() { + //given + final PairRoomEntity entity = pairRoomRepository.save(PairRoomEntity.from(INK_REDDDY_ROOM)); + pairRoomRepository.save(entity); + + //when + final CategoryCreateResponse createdCategory = categoryService.createCategory(ACCESS_CODE.getValue(), + new CategoryCreateRequest("μžλ°”")); + + //then + final List categories = categoryService.findAllByPairRoomAccessCode( + ACCESS_CODE.getValue()); + assertThat(categories.stream().anyMatch( + category -> category.id().equals(createdCategory.id()) && + category.value().equals(createdCategory.value()))) + .isTrue(); + } + + @Test + @DisplayName("μΉ΄ν…Œκ³ λ¦¬λ₯Ό μˆ˜μ •ν•œλ‹€.") + void update_category() { + //given + final PairRoomEntity entity = pairRoomRepository.save(PairRoomEntity.from(INK_REDDDY_ROOM)); + pairRoomRepository.save(entity); + + final CategoryCreateRequest request = new CategoryCreateRequest("μžλ°”"); + final CategoryCreateResponse createdCategory = categoryService.createCategory(ACCESS_CODE.getValue(), request); + + //when + final CategoryUpdateResponse updatedCategory = categoryService.updateCategoryName(ACCESS_CODE.getValue(), + new CategoryUpdateRequest(Long.parseLong(createdCategory.id()), "파이썬")); + + //then + final List categories = categoryService.findAllByPairRoomAccessCode( + ACCESS_CODE.getValue()); + assertThat(categories.stream().anyMatch( + category -> category.id().equals(createdCategory.id()) && + category.value().equals(updatedCategory.updatedCategoryName()))) + .isTrue(); + } + + @Test + @DisplayName("νŽ˜μ–΄λ£Έμ— μ€‘λ³΅λœ μΉ΄ν…Œκ³ λ¦¬κ°€ μžˆλŠ” 경우 μ €μž₯에 μ‹€νŒ¨ν•œλ‹€.") + void fail_save_category() { + //given + final PairRoomEntity entity = pairRoomRepository.save(PairRoomEntity.from(INK_REDDDY_ROOM)); + pairRoomRepository.save(entity); + + final CategoryCreateRequest categoryCreateRequest = new CategoryCreateRequest("μžλ°”"); + final String accessCode = ACCESS_CODE.getValue(); + categoryService.createCategory(accessCode, categoryCreateRequest); + + //when & then + assertThatThrownBy(() -> categoryService.createCategory(accessCode, categoryCreateRequest)) + .isInstanceOf(InvalidCategoryException.class); + } + + @Test + @DisplayName("νŽ˜μ–΄λ£Έμ΄ λ‹€λ₯Έ 경우 μ€‘λ³΅λœ μΉ΄ν…Œκ³ λ¦¬ μ €μž₯이 κ°€λŠ₯ν•˜λ‹€.") + void success_save_category() { + //given + final PairRoomEntity pairRoom1 = pairRoomRepository.save(PairRoomEntity.from(INK_REDDDY_ROOM)); + final PairRoomEntity pairRoom2 = pairRoomRepository.save(PairRoomEntity.from(FRAM_LEMONE_ROOM)); + + pairRoomRepository.save(pairRoom1); + pairRoomRepository.save(pairRoom2); + + final CategoryCreateRequest categoryCreateRequest = new CategoryCreateRequest("μžλ°”"); + categoryService.createCategory(pairRoom1.getAccessCode(), categoryCreateRequest); + + //when & then + assertThatCode(() -> categoryService.createCategory(pairRoom2.getAccessCode(), + categoryCreateRequest)).doesNotThrowAnyException(); + } + + @Test + @DisplayName("μΉ΄ν…Œκ³ λ¦¬λ₯Ό μ‚­μ œν•œλ‹€.") + void delete_category() { + //given + final PairRoomEntity entity = pairRoomRepository.save(PairRoomEntity.from(INK_REDDDY_ROOM)); + pairRoomRepository.save(entity); + + //when + final CategoryCreateResponse category = categoryService.createCategory(ACCESS_CODE.getValue(), + new CategoryCreateRequest("μžλ°”")); + final List beforeDelete = categoryService.findAllByPairRoomAccessCode( + ACCESS_CODE.getValue()); + + categoryService.deleteCategory(ACCESS_CODE.getValue(), Long.parseLong(category.id())); + + final List afterDelete = categoryService.findAllByPairRoomAccessCode( + ACCESS_CODE.getValue()); + + //then + assertThat(beforeDelete).hasSize(1); + assertThat(afterDelete).isEmpty(); + } + + @Test + @DisplayName("μΉ΄ν…Œκ³ λ¦¬κ°€ μ‚­μ œλ˜λ©΄ ν•΄λ‹Ή μΉ΄ν…Œκ³ λ¦¬λ‘œ λΆ„λ₯˜λ˜μ–΄ 있던 링크의 μΉ΄ν…Œκ³ λ¦¬λŠ” null이 λœλ‹€.") + void remove_category_and_update_reference_category_value() throws MalformedURLException { + final Category category = new Category("μžλ°”"); + final PairRoomEntity entity = pairRoomRepository.save(PairRoomEntity.from(INK_REDDDY_ROOM)); + + final CategoryEntity savedCategory = categoryRepository.save(new CategoryEntity(entity, category)); + final ReferenceLink referenceLink = new ReferenceLink(new URL(FakeServer.testUrl), ACCESS_CODE); + + final ReferenceLinkEntity beforeDeleteCategory = referenceLinkRepository.save( + new ReferenceLinkEntity(referenceLink, savedCategory, entity)); + + //when + categoryService.deleteCategory(ACCESS_CODE.getValue(), savedCategory.getId()); + + //then + final ReferenceLinkEntity afterDeleteCategory = referenceLinkRepository.findById(beforeDeleteCategory.getId()) + .orElseThrow(); + assertThat(beforeDeleteCategory.getCategoryEntity()).isEqualTo(savedCategory); + assertThat(afterDeleteCategory.getCategoryEntity()).isNull(); + } +} diff --git a/backend/src/test/java/site/coduo/referencelink/service/OpenGraphServiceTest.java b/backend/src/test/java/site/coduo/referencelink/service/OpenGraphServiceTest.java index ba68595d..e649176c 100644 --- a/backend/src/test/java/site/coduo/referencelink/service/OpenGraphServiceTest.java +++ b/backend/src/test/java/site/coduo/referencelink/service/OpenGraphServiceTest.java @@ -3,21 +3,26 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; +import static site.coduo.fixture.PairRoomFixture.INK_REDDDY_ROOM; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; + import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import site.coduo.pairroom.domain.Pair; -import site.coduo.pairroom.domain.PairName; -import site.coduo.pairroom.domain.PairRoom; import site.coduo.pairroom.domain.accesscode.AccessCode; +import site.coduo.pairroom.repository.PairRoomEntity; import site.coduo.pairroom.repository.PairRoomRepository; import site.coduo.referencelink.domain.Category; import site.coduo.referencelink.domain.OpenGraph; import site.coduo.referencelink.domain.ReferenceLink; -import site.coduo.referencelink.domain.Url; +import site.coduo.referencelink.fake.FakeServer; import site.coduo.referencelink.repository.CategoryEntity; import site.coduo.referencelink.repository.CategoryRepository; import site.coduo.referencelink.repository.OpenGraphRepository; @@ -45,6 +50,16 @@ class OpenGraphServiceTest extends CascadeCleaner { @Autowired private CategoryRepository categoryRepository; + private PairRoomEntity pairRoomEntity; + private CategoryEntity category; + + @BeforeEach + void setUp() { + pairRoomEntity = pairRoomRepository.save(PairRoomEntity.from(INK_REDDDY_ROOM)); + category = categoryRepository.save( + new CategoryEntity(pairRoomEntity, new Category("μŠ€ν”„λ§"))); + } + @AfterEach void tearDown() { deleteAllPairRoomCascade(); @@ -52,30 +67,22 @@ void tearDown() { @Test @DisplayName("μ˜€ν”ˆκ·Έλž˜ν”„λ₯Ό 생성 ν›„ μ €μž₯ν•œλ‹€.") - void create_open_graph() { + void create_open_graph_exactly() throws IOException { //given - final PairRoom pairRoom = pairRoomRepository.save( - new PairRoom( - new Pair( - new PairName("μž‰ν¬"), - new PairName("레λͺ¨λ„€") - ) - , new AccessCode("123456")) - ); - final CategoryEntity category = categoryRepository.save(new CategoryEntity(pairRoom, new Category("μŠ€ν”„λ§"))); - final ReferenceLinkEntity referenceLink = new ReferenceLinkEntity( - new ReferenceLink(new Url("https://www.naver.com"), new AccessCode("123456")), - category, - pairRoom - ); + final URL url = new URL(FakeServer.testUrl); + final ReferenceLinkEntity referenceLink = generateReferenceLinkEntity(url); final ReferenceLinkEntity referenceLinkEntity = referenceLinkRepository.save(referenceLink); // when - openGraphService.createOpenGraph(referenceLinkEntity); + final OpenGraph openGraph = openGraphService.createOpenGraph(referenceLinkEntity, url); - // then - assertThat(openGraphRepository.findAll()) - .hasSize(1); + //then + assertAll( + () -> assertThat(openGraphRepository.findAll()).hasSize(1), + () -> assertThat(openGraph) + .extracting("headTitle", "openGraphTitle", "description", "image") + .contains("ν—€λ“œ 타이틀", "μ˜€ν”ˆκ·Έλž˜ν”„ 타이틀", "μ˜€ν”ˆκ·Έλž˜ν”„ μ„€λͺ…", "μ˜€ν”ˆκ·Έλž˜ν”„ 이미지") + ); } @DisplayName("μΌμΉ˜ν•˜λŠ” μ˜€ν”ˆκ·Έλž˜ν”„κ°€ μ—†μœΌλ©΄ κΈ°λ³Έ 값을 넣은 μ˜€ν”ˆκ·Έλž˜ν”„λ₯Ό λ°˜ν™˜ν•œλ‹€.") @@ -85,40 +92,89 @@ void return_null_when_cannot_find_open_graph() { final OpenGraph openGraph = openGraphService.findOpenGraph(1L); // then - assertAll( - () -> assertThat(openGraph.getHeadTitle()).isEqualTo(DEFAULT_OPEN_GRAPH_VALUE), - () -> assertThat(openGraph.getOpenGraphTitle()).isEqualTo(DEFAULT_OPEN_GRAPH_VALUE), - () -> assertThat(openGraph.getDescription()).isEqualTo(DEFAULT_OPEN_GRAPH_VALUE), - () -> assertThat(openGraph.getImage()).isEqualTo(DEFAULT_OPEN_GRAPH_VALUE) - ); + assertThat(openGraph) + .extracting("headTitle", "openGraphTitle", "description", "image") + .contains(DEFAULT_OPEN_GRAPH_VALUE, DEFAULT_OPEN_GRAPH_VALUE, DEFAULT_OPEN_GRAPH_VALUE, + DEFAULT_OPEN_GRAPH_VALUE); } - @DisplayName("레퍼런슀링크 id둜 μ˜€ν”ˆκ·Έλž˜ν”„λ₯Ό μ‚­μ œν•œλ‹€.") + @DisplayName("레퍼런슀링크둜 μ˜€ν”ˆκ·Έλž˜ν”„λ₯Ό μ‚­μ œν•œλ‹€.") @Test - void delete_open_graph_by_reference_link_id() { + void delete_open_graph_by_reference_link_id() throws MalformedURLException { // given - final PairRoom pairRoom = pairRoomRepository.save( - new PairRoom( - new Pair( - new PairName("μž‰ν¬"), - new PairName("레λͺ¨λ„€") - ) - , new AccessCode("123456")) - ); + final URL url = new URL(FakeServer.testUrl); + final ReferenceLinkEntity referenceLink = generateReferenceLinkEntity(url); + final ReferenceLinkEntity referenceLinkEntity = referenceLinkRepository.save(referenceLink); + openGraphService.createOpenGraph(referenceLinkEntity, url); - final CategoryEntity category = categoryRepository.save(new CategoryEntity(pairRoom, new Category("μŠ€ν”„λ§"))); - final ReferenceLinkEntity referenceLink = new ReferenceLinkEntity( - new ReferenceLink(new Url("https://www.naver.com"), new AccessCode("123456")), - category, - pairRoom + // when + openGraphService.deleteByReferenceLink(referenceLinkEntity); + + // then + assertThat(openGraphRepository.findAll()).isEmpty(); + } + + @DisplayName("링크의 λ„νλ¨ΌνŠΈλ₯Ό κ°€μ Έμ˜€μ§€ λͺ»ν–ˆμ„λ•Œ ν—€λ“œνƒ€μ΄ν‹€μ— 도메인을 λ„£μ–΄ 생성 ν›„ μ €μž₯ν•œλ‹€.") + @Test + void create_openGraph_when_cannot_get_document() throws IOException { + //given + final int assignedPort = FakeServer.createAndStartFakeServer(null); + final URL url = new URL("http://localhost:" + assignedPort + "/test"); + final ReferenceLinkEntity referenceLink = generateReferenceLinkEntity(url); + final ReferenceLinkEntity referenceLinkEntity = referenceLinkRepository.save(referenceLink); + + // when + final OpenGraph openGraph = openGraphService.createOpenGraph(referenceLinkEntity, url); + + // then + assertAll( + () -> assertThat(openGraphRepository.findAll()).hasSize(1), + () -> assertThat(openGraph) + .extracting("headTitle", "openGraphTitle", "description", "image") + .contains("localhost", DEFAULT_OPEN_GRAPH_VALUE, DEFAULT_OPEN_GRAPH_VALUE, + DEFAULT_OPEN_GRAPH_VALUE) ); + } + + @DisplayName("링크의 μ˜€ν”ˆκ·Έλž˜ν”„ 타이틀, ν—€λ“œνƒ€μ΄ν‹€μ΄ μ—†μœΌλ©΄ ν—€λ“œνƒ€μ΄ν‹€μ— 도메인을 λ„£μ–΄ 생성 ν›„ μ €μž₯ν•œλ‹€.") + @Test + void create_openGraph_when_titles_are_empty() throws IOException { + //given + final String html = "" + + "" + + "" + + ""; + final int assignedPort = FakeServer.createAndStartFakeServer(html); + + final URL url = new URL("http://localhost:" + assignedPort + "/test"); + final ReferenceLinkEntity referenceLink = generateReferenceLinkEntity(url); final ReferenceLinkEntity referenceLinkEntity = referenceLinkRepository.save(referenceLink); - openGraphService.createOpenGraph(referenceLinkEntity); // when - openGraphService.deleteByReferenceLinkId(referenceLinkEntity.getId()); + final OpenGraph openGraph = openGraphService.createOpenGraph(referenceLinkEntity, url); // then - assertThat(openGraphRepository.findAll()).isEmpty(); + assertAll( + () -> assertThat(openGraphRepository.findAll()).hasSize(1), + () -> assertThat(openGraph.getHeadTitle()).isEqualTo("localhost"), + () -> assertThat(openGraph.getOpenGraphTitle()).isEqualTo(DEFAULT_OPEN_GRAPH_VALUE), + () -> assertThat(openGraph.getDescription()).isEqualTo("μ˜€ν”ˆκ·Έλž˜ν”„ μ„€λͺ…"), + () -> assertThat(openGraph.getImage()).isEqualTo("μ˜€ν”ˆκ·Έλž˜ν”„ 이미지") + ); + assertAll( + () -> assertThat(openGraphRepository.findAll()).hasSize(1), + () -> assertThat(openGraph) + .extracting("headTitle", "openGraphTitle", "description", "image") + .contains("localhost", DEFAULT_OPEN_GRAPH_VALUE, "μ˜€ν”ˆκ·Έλž˜ν”„ μ„€λͺ…", + "μ˜€ν”ˆκ·Έλž˜ν”„ 이미지") + ); + } + + private ReferenceLinkEntity generateReferenceLinkEntity(final URL url) { + return new ReferenceLinkEntity( + new ReferenceLink(url, new AccessCode(pairRoomEntity.getAccessCode())), + category, + pairRoomEntity + ); } } diff --git a/backend/src/test/java/site/coduo/referencelink/service/ReferenceLinkServiceTest.java b/backend/src/test/java/site/coduo/referencelink/service/ReferenceLinkServiceTest.java index 72a09fdf..66bb0f48 100644 --- a/backend/src/test/java/site/coduo/referencelink/service/ReferenceLinkServiceTest.java +++ b/backend/src/test/java/site/coduo/referencelink/service/ReferenceLinkServiceTest.java @@ -1,24 +1,29 @@ package site.coduo.referencelink.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; +import static site.coduo.fixture.PairRoomFixture.INK_REDDDY_ROOM; + +import java.net.MalformedURLException; +import java.net.URL; import java.util.List; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import site.coduo.pairroom.domain.Pair; -import site.coduo.pairroom.domain.PairName; -import site.coduo.pairroom.domain.PairRoom; import site.coduo.pairroom.domain.accesscode.AccessCode; +import site.coduo.pairroom.repository.PairRoomEntity; import site.coduo.pairroom.repository.PairRoomRepository; import site.coduo.referencelink.domain.Category; import site.coduo.referencelink.domain.ReferenceLink; -import site.coduo.referencelink.domain.Url; +import site.coduo.referencelink.exception.InvalidUrlFormatException; +import site.coduo.referencelink.fake.FakeServer; import site.coduo.referencelink.repository.CategoryEntity; import site.coduo.referencelink.repository.CategoryRepository; import site.coduo.referencelink.repository.OpenGraphRepository; @@ -46,6 +51,17 @@ class ReferenceLinkServiceTest extends CascadeCleaner { @Autowired private CategoryRepository categoryRepository; + private PairRoomEntity pairRoomEntity; + private CategoryEntity reactCategory; + private CategoryEntity springCategory; + + @BeforeEach + void setUp() { + pairRoomEntity = pairRoomRepository.save(PairRoomEntity.from(INK_REDDDY_ROOM)); + reactCategory = categoryRepository.save(new CategoryEntity(pairRoomEntity, new Category("λ¦¬μ•‘νŠΈ"))); + springCategory = categoryRepository.save(new CategoryEntity(pairRoomEntity, new Category("μŠ€ν”„λ§"))); + } + @AfterEach void tearDown() { deleteAllPairRoomCascade(); @@ -55,12 +71,11 @@ void tearDown() { @DisplayName("레퍼런슀 링크와 μ˜€ν”ˆκ·Έλž˜ν”„λ₯Ό ν•¨κ»˜ μ €μž₯ν•œλ‹€.") void save_reference_link_and_open_graph() { // given - final Pair pair = new Pair(new PairName("first"), new PairName("second")); - final PairRoom pairRoom = pairRoomRepository.save(new PairRoom(pair, new AccessCode("code`"))); - final ReferenceLinkCreateRequest request = new ReferenceLinkCreateRequest("https://www.naver.com", null); + final ReferenceLinkCreateRequest request = new ReferenceLinkCreateRequest( + FakeServer.testUrl, springCategory.getId()); // when - referenceLinkService.createReferenceLink(pairRoom.getAccessCodeText(), request); + referenceLinkService.createReferenceLink(pairRoomEntity.getAccessCode(), request); // then assertAll( @@ -68,50 +83,51 @@ void save_reference_link_and_open_graph() { () -> assertThat(openGraphRepository.findAll()).hasSize(1), () -> { final ReferenceLinkResponse referenceLinkResponses = - referenceLinkService.readAllReferenceLink(pairRoom.getAccessCodeText()).get(0); - assertThat(referenceLinkResponses.url()).isEqualTo(request.url()); - assertThat(referenceLinkResponses.headTitle()).isEqualTo("NAVER"); - assertThat(referenceLinkResponses.openGraphTitle()).isEqualTo("넀이버"); + referenceLinkService.readAllReferenceLink(pairRoomEntity.getAccessCode()).get(0); + assertThat(referenceLinkResponses) + .extracting("url", "headTitle", "openGraphTitle", "description", "image", "categoryName") + .contains(request.url(), "ν—€λ“œ 타이틀", "μ˜€ν”ˆκ·Έλž˜ν”„ 타이틀", "μ˜€ν”ˆκ·Έλž˜ν”„ μ„€λͺ…", "μ˜€ν”ˆκ·Έλž˜ν”„ 이미지", "μŠ€ν”„λ§"); } ); } + @Test + @DisplayName("잘λͺ»λœ url둜 μ €μž₯을 μ‹œλ„ν•˜λ©΄ μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.") + void throw_exception_when_invalid_url_format() { + // given + final ReferenceLinkCreateRequest request = new ReferenceLinkCreateRequest("failUrl", null); + + // when & then + assertThatThrownBy( + () -> referenceLinkService.createReferenceLink(pairRoomEntity.getAccessCode(), request)) + .isInstanceOf(InvalidUrlFormatException.class); + } + @Test @DisplayName("λͺ¨λ“  레퍼런슀 링크λ₯Ό μ‘°νšŒν•œλ‹€.") - void search_all_reference_link() { + void search_all_reference_link() throws MalformedURLException { // given - final Pair pair = new Pair(new PairName("first"), new PairName("second")); - final PairRoom pairRoom = pairRoomRepository.save(new PairRoom(pair, new AccessCode("code"))); - final CategoryEntity category = categoryRepository.save(new CategoryEntity(pairRoom, new Category("μžλ°”"))); - final AccessCode accessCode = pairRoom.getAccessCode(); - referenceLinkRepository.save( - new ReferenceLinkEntity(new ReferenceLink(new Url("http://url1.com"), accessCode), category, pairRoom)); - referenceLinkRepository.save( - new ReferenceLinkEntity(new ReferenceLink(new Url("http://url2.com"), accessCode), category, pairRoom)); - referenceLinkRepository.save( - new ReferenceLinkEntity(new ReferenceLink(new Url("http://url3.com"), accessCode), category, pairRoom)); + final AccessCode accessCode = new AccessCode(pairRoomEntity.getAccessCode()); + referenceLinkRepository.save(generateReferenceLink(springCategory)); + referenceLinkRepository.save(generateReferenceLink(springCategory)); + referenceLinkRepository.save(generateReferenceLink(reactCategory)); // when final List responses = referenceLinkService.readAllReferenceLink( accessCode.getValue()); - // then assertThat(responses).hasSize(3); } @Test @DisplayName("레퍼런슀 링크와 μ˜€ν”ˆκ·Έλž˜ν”„λ₯Ό μ‚­μ œν•œλ‹€.") - void delete_reference_link_and_open_graph() { + void delete_reference_link_and_open_graph() throws MalformedURLException { // given - final Pair pair = new Pair(new PairName("first"), new PairName("second")); - AccessCode code = new AccessCode("hello"); - final PairRoom pairRoom = pairRoomRepository.save(new PairRoom(pair, code)); - final CategoryEntity category = categoryRepository.save(new CategoryEntity(pairRoom, new Category("λ¦¬μ•‘νŠΈ"))); - final ReferenceLinkEntity link = referenceLinkRepository.save( - new ReferenceLinkEntity(new ReferenceLink(new Url("http://url1.com"), code), category, pairRoom)); + final ReferenceLinkEntity referenceLink = generateReferenceLink(reactCategory); + final ReferenceLinkEntity saved = referenceLinkRepository.save(referenceLink); // when - referenceLinkService.deleteReferenceLink(link.getId()); + referenceLinkService.deleteReferenceLink(pairRoomEntity.getAccessCode(), saved.getId()); // then assertAll( @@ -119,4 +135,48 @@ void delete_reference_link_and_open_graph() { () -> assertThat(openGraphRepository.findAll()).isEmpty() ); } + + @DisplayName("μ•‘μ„ΈμŠ€μ½”λ“œκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμœΌλ©΄ μ‚­μ œλ₯Ό μ‹œλ„ν•΄λ„ μ‚­μ œλ˜μ§€ μ•ŠλŠ”λ‹€.") + @Test + void cannot_delete_reference_link_and_open_graph_when_invalid_access_code() throws MalformedURLException { + // given + final ReferenceLinkCreateRequest request = + new ReferenceLinkCreateRequest(FakeServer.testUrl, springCategory.getId()); + final ReferenceLinkResponse referenceLink = referenceLinkService.createReferenceLink( + pairRoomEntity.getAccessCode(), request); + + // when + referenceLinkService.deleteReferenceLink("abcdef", referenceLink.id()); + + assertAll( + () -> assertThat(referenceLinkRepository.findAll()).hasSize(1), + () -> assertThat(openGraphRepository.findAll()).hasSize(1) + ); + } + + @Test + @DisplayName("μΉ΄ν…Œκ³ λ¦¬κ°€ μΌμΉ˜ν•˜λŠ” λͺ¨λ“  레퍼런슀 링크λ₯Ό μ‘°νšŒν•œλ‹€.") + void find_reference_links_by_category() throws MalformedURLException { + // given + final ReferenceLinkEntity reactReferenceLink = generateReferenceLink(reactCategory); + final ReferenceLinkEntity reactReferenceLink2 = generateReferenceLink(reactCategory); + final ReferenceLinkEntity springReferenceLink = generateReferenceLink(springCategory); + + referenceLinkRepository.save(reactReferenceLink); + referenceLinkRepository.save(reactReferenceLink2); + referenceLinkRepository.save(springReferenceLink); + + // when + final List referenceLinksByCategory = referenceLinkService.findReferenceLinksByCategory( + pairRoomEntity.getAccessCode(), reactCategory.getId()); + + // then + assertThat(referenceLinksByCategory).hasSize(2); + } + + private ReferenceLinkEntity generateReferenceLink(final CategoryEntity category) throws MalformedURLException { + return new ReferenceLinkEntity(new ReferenceLink(new URL(FakeServer.testUrl), + new AccessCode(pairRoomEntity.getAccessCode())), + category, pairRoomEntity); + } } diff --git a/backend/src/test/java/site/coduo/sync/service/EventStreamsRegistryTest.java b/backend/src/test/java/site/coduo/sync/service/EventStreamsRegistryTest.java new file mode 100644 index 00000000..ee2401a8 --- /dev/null +++ b/backend/src/test/java/site/coduo/sync/service/EventStreamsRegistryTest.java @@ -0,0 +1,95 @@ +package site.coduo.sync.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import site.coduo.sync.exception.NotFoundSseConnectionException; + +class EventStreamsRegistryTest { + + @Test + @DisplayName("Emitterλ₯Ό λ ˆμ§€μŠ€νŠΈλ¦¬μ—μ„œ 생성후 λ°˜ν™˜ν•œλ‹€.") + void add_emitter_to_registry() { + // given + final EventStreamsRegistry sut = new EventStreamsRegistry(); + + // when + final SseEmitter emitter = sut.register("hello"); + + // then + assertThat(emitter).isNotNull(); + } + + @Test + @DisplayName("λ“±λ‘λœ 이벀트 μŠ€νŠΈλ¦Όμ„ κ°€μ Έμ˜¨λ‹€.") + void return_stored_event_stream() { + // given + final EventStreamsRegistry eventStreamsRegistry = new EventStreamsRegistry(); + final String key = "test"; + eventStreamsRegistry.register(key); + + // when & then + assertThatCode(() -> eventStreamsRegistry.findEventStreams(key)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("λ“±λ‘λ˜μ§€ μ•Šμ€ 이벀트 μŠ€νŠΈλ¦Όλ“€μ„ μ‘°νšŒν•  경우 μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚¨λ‹€.") + void throw_exception_when_search_un_registered_event_stream() { + // given + final EventStreamsRegistry eventStreamsRegistry = new EventStreamsRegistry(); + final String key = "test"; + + // when & then + assertThatThrownBy(() -> eventStreamsRegistry.findEventStreams(key)) + .isInstanceOf(NotFoundSseConnectionException.class); + } + + @Test + @DisplayName("킀에 ν•΄λ‹Ήν•˜λŠ” 컀λ„₯μ…˜μ΄ 유무λ₯Ό ν™•μΈν•œλ‹€. - 거짓") + void check_has_any_connection_with_specific_key_false_case() { + // given + final EventStreamsRegistry eventStreamsRegistry = new EventStreamsRegistry(); + final String key = "test"; + eventStreamsRegistry.register(key); + + // when + final boolean hasEmptyConnection = eventStreamsRegistry.hasNoStreams(key); + + // then + assertThat(hasEmptyConnection).isFalse(); + } + + @Test + @DisplayName("킀에 ν•΄λ‹Ήν•˜λŠ” 컀λ„₯μ…˜μ΄ 유무λ₯Ό ν™•μΈν• λ•Œ ν•΄λ‹Ή ν‚€κ°€ μ—†μœΌλ©΄ μ˜ˆμ™Έλ₯Ό λ˜μ§„λ‹€.") + void throw_exception_when_check_contain_connection_with_unsaved_key() { + // given + final EventStreamsRegistry eventStreamsRegistry = new EventStreamsRegistry(); + final String key = "tes"; + + // when & then + assertThatThrownBy(() -> eventStreamsRegistry.hasNoStreams(key)) + .isInstanceOf(NotFoundSseConnectionException.class); + } + + @Test + @DisplayName("킀에 ν•΄λ‹Ήν•˜λŠ” λͺ¨λ“  μŠ€νŠΈλ¦Όμ„ μ’…λ£Œ ν›„ μ‚­μ œν•œλ‹€.") + void close_all_of_key() { + // given + final String key = "hellow"; + final EventStreamsRegistry registry = new EventStreamsRegistry(); + registry.register(key); + + // when + registry.release(key); + + // then + assertThatThrownBy(() -> registry.findEventStreams(key)) + .isExactlyInstanceOf(NotFoundSseConnectionException.class); + } +} diff --git a/backend/src/test/java/site/coduo/sync/service/EventStreamsTest.java b/backend/src/test/java/site/coduo/sync/service/EventStreamsTest.java new file mode 100644 index 00000000..d713e10b --- /dev/null +++ b/backend/src/test/java/site/coduo/sync/service/EventStreamsTest.java @@ -0,0 +1,130 @@ +package site.coduo.sync.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.time.Duration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter.SseEventBuilder; + +import site.coduo.sync.exception.SseConnectionDuplicationException; + +class EventStreamsTest { + + @Test + @DisplayName("μƒˆλ‘œμš΄ Event Stream을 λ°œν–‰ν•œλ‹€.") + void publish_new_event_stream_connection() { + // given + final EventStreams eventStreams = new EventStreams(); + final SseEventStream eventStream = new SseEventStream(Duration.ZERO); + + // when + final SseEmitter publish = eventStreams.publish(eventStream); + + // then + assertThat(eventStream).isEqualTo(new SseEventStream(publish)); + } + + @Test + @DisplayName("컀λ„₯μ…˜μ„ μΆ”κ°€ν•œλ‹€.") + void add_sse_connection() { + // given + final EventStreams eventStreams = new EventStreams(); + final EventStream eventStream = new SseEventStream(Duration.ZERO); + + // when & then + assertThatCode(() -> eventStreams.add(eventStream)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("같은 컀λ„₯μ…˜μ„ λ‘λ²ˆμ΄μƒ μΆ”κ°€ν•  μ‹œ μ˜ˆμ™Έλ₯Ό λ˜μ§„λ‹€.") + void throw_exception_when_add_same_sse_connection_greater_equal_then_2() { + // given + final EventStreams eventStreams = new EventStreams(); + final EventStream eventStream = new SseEventStream(Duration.ZERO); + eventStreams.add(eventStream); + + // when & then + assertThatThrownBy(() -> eventStreams.add(eventStream)) + .isInstanceOf(SseConnectionDuplicationException.class); + } + + @Test + @DisplayName("가지고 μžˆλŠ” λͺ¨λ“  이벀트 슀트림으둜 메세지λ₯Ό λΈŒλ‘œλ“œμΊμŠ€νŒ…ν•œλ‹€.") + void broadcast_entire_event_stream_that_it_has() throws IOException { + // given + final SseEmitter emitter1 = mock(SseEmitter.class); + final SseEmitter emitter2 = mock(SseEmitter.class); + final SseEmitter emitter3 = mock(SseEmitter.class); + final EventStreams eventStreams = new EventStreams(); + final EventStream eventStream1 = new SseEventStream(emitter1); + final EventStream eventStream2 = new SseEventStream(emitter2); + final EventStream eventStream3 = new SseEventStream(emitter3); + eventStreams.add(eventStream1); + eventStreams.add(eventStream2); + eventStreams.add(eventStream3); + + // when + eventStreams.broadcast("hello", "test"); + + // then + verify(emitter1, times(1)) + .send(any(SseEventBuilder.class)); + verify(emitter2, times(1)) + .send(any(SseEventBuilder.class)); + verify(emitter3, times(1)) + .send(any(SseEventBuilder.class)); + } + + @Test + @DisplayName("λ“±λ‘λœ 에미터가 λΉ„μ—ˆμ§€ ν™•μΈν•œλ‹€. - μ°Έ") + void check_emitters_empty_true_case() { + // given + final EventStreams eventStreams = new EventStreams(); + + // when + final boolean empty = eventStreams.isEmpty(); + + // then + assertThat(empty).isTrue(); + } + + @Test + @DisplayName("λ“±λ‘λœ 에미터가 λΉ„μ—ˆλŠ”μ§€ ν™•μΈν•œλ‹€. - 거짓") + void check_emitters_empty_false_case() { + // given + final EventStreams eventStreams = new EventStreams(); + eventStreams.add(new SseEventStream()); + + // when + final boolean empty = eventStreams.isEmpty(); + + // then + assertThat(empty).isFalse(); + } + + @Test + @DisplayName("λ“±λ‘λœ 에미터가 λͺ¨λ‘ μ’…λ£Œλ˜μ—ˆλŠ”μ§€ ν™•μΈν•œλ‹€.") + void check_emitters_all_close() { + // given + final EventStreams eventStreams = new EventStreams(); + final SseEventStream target = new SseEventStream(); + eventStreams.add(target); + + // when + eventStreams.closeAll(); + + // then + assertThatThrownBy(() -> target.flush("μ’…λ£Œ ν›„ 메세지 보내면", "μ—λŸ¬ λ°œμƒ")); + } +} diff --git a/backend/src/test/java/site/coduo/sync/service/SchedulerRegistryTest.java b/backend/src/test/java/site/coduo/sync/service/SchedulerRegistryTest.java new file mode 100644 index 00000000..ecf633f0 --- /dev/null +++ b/backend/src/test/java/site/coduo/sync/service/SchedulerRegistryTest.java @@ -0,0 +1,89 @@ +package site.coduo.sync.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.util.concurrent.ScheduledFuture; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import site.coduo.fake.FakeScheduledFuture; + +class SchedulerRegistryTest { + + @Test + @DisplayName("key의 μŠ€μΌ€μ€„λ§ μƒνƒœλ₯Ό λ“±λ‘ν•œλ‹€.") + void register_scheduler() { + // given + final SchedulerRegistry schedulerRegistry = new SchedulerRegistry(); + final String key = "access"; + final ScheduledFuture future = new FakeScheduledFuture(); + + // when & then + assertThatCode(() -> schedulerRegistry.register(key, future)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("key의 μŠ€μΌ€μ€„λ§ μƒνƒœλ₯Ό μ’…λ£Œλ‘œ λ³€κ²½ν•œ λ’€ registryμ—μ„œ μ‚­μ œν•œλ‹€.") + void release_scheduler() { + // given + final SchedulerRegistry schedulerRegistry = new SchedulerRegistry(); + final String key = "access"; + final ScheduledFuture future = new FakeScheduledFuture(); + schedulerRegistry.register(key, future); + + // when + schedulerRegistry.release(key); + + // then + assertThat(future.isCancelled()).isTrue(); + } + + @Test + @DisplayName("key의 μŠ€μΌ€μ€„λ§ κ²°κ³Όκ°€ μ‘΄μž¬ν•˜λŠ”μ§€ μ‘°νšŒν•œλ‹€.") + void has_scheduler() { + // given + final SchedulerRegistry schedulerRegistry = new SchedulerRegistry(); + final String key = "access"; + final ScheduledFuture future = new FakeScheduledFuture(); + schedulerRegistry.register(key, future); + + // when & then + assertThat(schedulerRegistry.has(key)).isTrue(); + } + + @Test + @DisplayName("key의 μŠ€μΌ€μ€„λ§μ΄ μ’…λ£Œλ˜μ—ˆμ„ 경우 falseλ₯Ό λ°˜ν™˜ν•œλ‹€.") + void is_scheduler_done() { + // given + final SchedulerRegistry schedulerRegistry = new SchedulerRegistry(); + final String key = "access"; + final ScheduledFuture future = new FakeScheduledFuture(); + schedulerRegistry.register(key, future); + schedulerRegistry.release(key); + + // when + final boolean actual = schedulerRegistry.isActive(key); + + // then + assertThat(actual).isFalse(); + } + + @Test + @DisplayName("key의 μŠ€μΌ€μ€„λ§μ΄ 싀행쀑일 경우 trueλ₯Ό λ°˜ν™˜ν•œλ‹€.") + void is_scheduler_active() { + // given + final SchedulerRegistry schedulerRegistry = new SchedulerRegistry(); + final String key = "access"; + final ScheduledFuture future = new FakeScheduledFuture(); + schedulerRegistry.register(key, future); + + // when + final boolean actual = schedulerRegistry.isActive(key); + + // then + assertThat(actual).isTrue(); + } +} diff --git a/backend/src/test/java/site/coduo/sync/service/SchedulerServiceTest.java b/backend/src/test/java/site/coduo/sync/service/SchedulerServiceTest.java new file mode 100644 index 00000000..0dbdfb7d --- /dev/null +++ b/backend/src/test/java/site/coduo/sync/service/SchedulerServiceTest.java @@ -0,0 +1,123 @@ +package site.coduo.sync.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.concurrent.TimeUnit; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; + +import site.coduo.pairroom.domain.accesscode.AccessCode; +import site.coduo.pairroom.repository.PairRoomEntity; +import site.coduo.timer.domain.Timer; +import site.coduo.timer.repository.TimerEntity; +import site.coduo.timer.repository.TimerRepository; +import site.coduo.timer.service.TimestampRegistry; + +@Disabled +@SpringBootTest +class SchedulerServiceTest { + + @Autowired + private ThreadPoolTaskScheduler taskScheduler; + + @Autowired + private SchedulerRegistry schedulerRegistry; + + @Autowired + private TimestampRegistry timestampRegistry; + + private TimerRepository timerRepository; + private SseService sseService; + private SchedulerService schedulerService; + + @BeforeEach + void setUp() { + timerRepository = mock(TimerRepository.class); + sseService = mock(SseService.class); + + schedulerService = new SchedulerService( + taskScheduler, + schedulerRegistry, + timestampRegistry, + timerRepository, + sseService + ); + } + + @Test + @DisplayName("타이머λ₯Ό 2초 μ‹€ν–‰ν•œλ‹€.") + void start_timer_3_seconds() { + // given + final String key = "access"; + final PairRoomEntity pairRoomEntity = PairRoomEntity.builder().accessCode(key).build(); + final Timer timer = new Timer(new AccessCode(key), 5000L, 5000L); + when(timerRepository.fetchTimerByAccessCode(key)) + .thenReturn(new TimerEntity(timer, pairRoomEntity)); + + // when + schedulerService.start(key); + + // then + Awaitility + .await() + .atMost(2, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) + .untilAsserted(() -> { + final long expected = timer.getRemainingTime() - 2 * 1000; + assertThat(timestampRegistry.get(key).getRemainingTime()).isEqualTo(expected); + }); + } + + @Test + @DisplayName("타이머 μ‹€ν–‰μ‹œ 남은 μ‹œκ°„μ΄ 0이 되면 μŠ€μΌ€μ€„λ§μ΄ μ’…λ£Œλ˜κ³  νƒ€μž„μŠ€νƒ¬ν”„κ°€ μ‚­μ œλœλ‹€.") + void start_timer_unit_zero() { + // given + final String key = "access"; + final PairRoomEntity pairRoomEntity = PairRoomEntity.builder().accessCode(key).build(); + final Timer timer = new Timer(new AccessCode(key), 3000L, 3000L); + when(timerRepository.fetchTimerByAccessCode(key)) + .thenReturn(new TimerEntity(timer, pairRoomEntity)); + + // when + schedulerService.start(key); + + // then + Awaitility + .await() + .pollInterval(1, TimeUnit.SECONDS) + .untilAsserted(() -> + assertThat(timestampRegistry.has(key)).isFalse() + ); + } + + @Test + @DisplayName("싀행쀑인 타이머λ₯Ό μ€‘μ§€ν•œλ‹€.") + void stop_timer() { + // given + final String key = "access"; + final PairRoomEntity pairRoomEntity = PairRoomEntity.builder().accessCode(key).build(); + final Timer timer = new Timer(new AccessCode(key), 5000L, 5000L); + when(timerRepository.fetchTimerByAccessCode(key)) + .thenReturn(new TimerEntity(timer, pairRoomEntity)); + schedulerService.start(key); + + // when & then + Awaitility + .await() + .pollDelay(1, TimeUnit.SECONDS) + .pollInterval(1, TimeUnit.SECONDS) + .untilAsserted(() -> { + schedulerService.pause(key); + assertThat(schedulerRegistry.has(key)).isFalse(); + }); + } +} diff --git a/backend/src/test/java/site/coduo/sync/service/SseEventStreamTest.java b/backend/src/test/java/site/coduo/sync/service/SseEventStreamTest.java new file mode 100644 index 00000000..f3d5b9c8 --- /dev/null +++ b/backend/src/test/java/site/coduo/sync/service/SseEventStreamTest.java @@ -0,0 +1,46 @@ +package site.coduo.sync.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.io.IOException; +import java.time.Duration; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter.SseEventBuilder; + +class SseEventStreamTest { + + @Test + @DisplayName("SSE event 처리 객체λ₯Ό λ§Œλ“ λ‹€.") + void create_sse_object() { + // given + final EventStream eventStream = new SseEventStream(Duration.ZERO); + + // when + final SseEmitter connect = eventStream.connect(); + + // then + assertThat(connect.getTimeout()).isEqualTo(Duration.ZERO.toMillis()); + } + + @Test + @DisplayName("SSE eventλ₯Ό λ°œμƒμ‹œν‚¨λ‹€.") + void generate_sse_event() throws IOException { + // given + final SseEmitter sseEmitter = mock(SseEmitter.class); + + final EventStream eventStream = new SseEventStream(sseEmitter); + + // when + eventStream.flush("test", "hello"); + + // then + verify(sseEmitter, atLeastOnce()).send(any(SseEventBuilder.class)); + } +} diff --git a/backend/src/test/java/site/coduo/timer/domain/TimerTest.java b/backend/src/test/java/site/coduo/timer/domain/TimerTest.java new file mode 100644 index 00000000..acd5e763 --- /dev/null +++ b/backend/src/test/java/site/coduo/timer/domain/TimerTest.java @@ -0,0 +1,82 @@ +package site.coduo.timer.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import site.coduo.pairroom.domain.MissionUrl; +import site.coduo.pairroom.domain.Pair; +import site.coduo.pairroom.domain.PairName; +import site.coduo.pairroom.domain.PairRoom; +import site.coduo.pairroom.domain.PairRoomStatus; +import site.coduo.pairroom.domain.accesscode.AccessCode; +import site.coduo.timer.exception.InvalidTimerException; + +class TimerTest { + + @Test + @DisplayName("타이머λ₯Ό μƒμ„±ν•œλ‹€.") + void create_timer() { + // given + final PairRoom pairRoom = createPairRoom("ν”„λžŒ", "레λͺ¨λ„€"); + final long timerDuration = 10; + final long timerRemainingTime = 10; + + // when & then + assertThatCode(() -> new Timer(pairRoom.getAccessCode(), timerDuration, timerRemainingTime)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("타이머 μ‹œκ°„μ΄ 음수일 경우 μ˜ˆμ™Έκ°€ λ°œμƒν•œλ‹€.") + void throw_exception_when_time_is_negative() { + // given + final PairRoom pairRoom = createPairRoom("레λͺ¨λ„€", "ν”„λžŒ"); + final long timerDuration = -1; + final long timerRemainingTime = 0; + + // when, then + assertThatThrownBy(() -> new Timer(pairRoom.getAccessCode(), timerDuration, timerRemainingTime)) + .isInstanceOf(InvalidTimerException.class); + } + + @Test + @DisplayName("타어미 μ‹œκ°„μ„ νŠΉμ • μ‹œκ°„ λ‹¨μœ„λ‘œ κ°μ†Œ μ‹œν‚¨λ‹€.") + void decrease_timer_remaining_time_by_specific_value() { + // given + final PairRoom pairRoom = createPairRoom("fram", "lemone"); + final Timer timer = new Timer(pairRoom.getAccessCode(), 10000L, 10000L); + + // when + timer.decreaseRemainingTime(1000L); + + // then + assertThat(timer.getRemainingTime()).isEqualTo(9000L); + } + + @Test + @DisplayName("타이머 μ‹œκ°„λ³΄λ‹€ κ°μ†Œμ‹œν‚€λ €λŠ” μ‹œκ°„μ΄ 더 크면 0으둜 μ„€μ •ν•œλ‹€.") + void when_timer_remaining_time_negative_value_then_set_zero() { + // given + final PairRoom pairRoom = createPairRoom("fram", "lemone"); + final Timer timer = new Timer(pairRoom.getAccessCode(), 10000L, 10000L); + + // when + timer.decreaseRemainingTime(10001L); + + // then + assertThat(timer.getRemainingTime()).isZero(); + } + + private PairRoom createPairRoom(final String navigator, final String driver) { + return new PairRoom( + PairRoomStatus.IN_PROGRESS, + new Pair(new PairName(navigator), new PairName(driver)), + new MissionUrl("https://missionUrl.xxx"), + new AccessCode("123456") + ); + } +} diff --git a/backend/src/test/java/site/coduo/timer/repository/TimerRepositoryTest.java b/backend/src/test/java/site/coduo/timer/repository/TimerRepositoryTest.java new file mode 100644 index 00000000..f527d8e0 --- /dev/null +++ b/backend/src/test/java/site/coduo/timer/repository/TimerRepositoryTest.java @@ -0,0 +1,63 @@ +package site.coduo.timer.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import site.coduo.pairroom.domain.MissionUrl; +import site.coduo.pairroom.domain.Pair; +import site.coduo.pairroom.domain.PairName; +import site.coduo.pairroom.domain.PairRoom; +import site.coduo.pairroom.domain.PairRoomStatus; +import site.coduo.pairroom.domain.accesscode.AccessCode; +import site.coduo.pairroom.repository.PairRoomEntity; +import site.coduo.pairroom.repository.PairRoomRepository; +import site.coduo.timer.domain.Timer; + +@Transactional +@SpringBootTest +class TimerRepositoryTest { + + @Autowired + private TimerRepository timerRepository; + + @Autowired + private PairRoomRepository pairRoomRepository; + + @AfterEach + void tearDown() { + timerRepository.deleteAll(); + pairRoomRepository.deleteAll(); + } + + @Test + @DisplayName("νŠΉμ • νŽ˜μ–΄λ£Έμ˜ 타이머λ₯Ό μ‘°νšŒν•œλ‹€.") + void inquiry_timer() { + // given + final PairRoom pairRoom = new PairRoom( + PairRoomStatus.IN_PROGRESS, + new Pair(new PairName("λ ˆλ¨Έλ„€"), new PairName("ν”„λžŒ")), + new MissionUrl("https://missionUrl.xxx"), + new AccessCode("hello1") + ); + final PairRoomEntity entity = site.coduo.pairroom.repository.PairRoomEntity.from( + pairRoom); + pairRoomRepository.save(entity); + final Timer timer = new Timer(pairRoom.getAccessCode(), 1111, 234); + timerRepository.save(new TimerEntity(timer, entity)); + + // when + final TimerEntity actual = timerRepository + .fetchTimerByPairRoomEntity(entity); + + // then + assertThat(actual) + .extracting("duration", "remainingTime") + .contains(timer.getDuration(), timer.getRemainingTime()); + } +} diff --git a/backend/src/test/java/site/coduo/timer/service/TimerServiceTest.java b/backend/src/test/java/site/coduo/timer/service/TimerServiceTest.java new file mode 100644 index 00000000..cdad8f90 --- /dev/null +++ b/backend/src/test/java/site/coduo/timer/service/TimerServiceTest.java @@ -0,0 +1,122 @@ +package site.coduo.timer.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import site.coduo.pairroom.domain.PairRoomStatus; +import site.coduo.pairroom.domain.accesscode.AccessCode; +import site.coduo.pairroom.service.PairRoomService; +import site.coduo.pairroom.service.dto.PairRoomCreateRequest; +import site.coduo.timer.domain.Timer; +import site.coduo.timer.service.dto.TimerReadResponse; +import site.coduo.timer.service.dto.TimerUpdateRequest; +import site.coduo.utils.CascadeCleaner; + +@Transactional +@SpringBootTest +class TimerServiceTest extends CascadeCleaner { + + @Autowired + private PairRoomService pairRoomService; + + @Autowired + private TimestampRegistry timestampRegistry; + + @Autowired + private TimerService timerService; + + @AfterEach + void tearDown() { + deleteAllPairRoomCascade(); + } + + @Test + @DisplayName("타이머λ₯Ό μ €μž₯ν•œλ‹€.") + void create_timer() { + // given + final PairRoomCreateRequest request = new PairRoomCreateRequest("켈리", "레λͺ¨λ„€", 10000L, + 1000L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name()); + + // when & then + assertThatCode(() -> pairRoomService.savePairRoom(request, null)) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("타이머λ₯Ό λ°˜ν™˜ν•œλ‹€.") + void get_latest_timer() { + // given + final PairRoomCreateRequest request = new PairRoomCreateRequest("μž‰ν¬", "λ ˆλ””", 1000L, + 1000L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name()); + final String accessCode = pairRoomService.savePairRoom(request, null); + + // when + final TimerReadResponse actual = timerService.readTimer(accessCode); + + // then + assertAll( + () -> assertThat(actual.duration()).isEqualTo(request.timerDuration()), + () -> assertThat(actual.remainingTime()).isEqualTo(request.timerRemainingTime()) + ); + } + + @Test + @DisplayName("타이머λ₯Ό μ—…λ°μ΄νŠΈ ν•œλ‹€.") + void update_timer() { + // given + final PairRoomCreateRequest request = new PairRoomCreateRequest("μž‰ν¬", "λ ˆλ””", 10000000L, + 100L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name()); + final String accessCode = pairRoomService.savePairRoom(request, null); + + final TimerUpdateRequest timerRequest = new TimerUpdateRequest(10000, 5000); + + // when + timerService.updateTimer(accessCode, timerRequest); + + // then + final TimerReadResponse actual = timerService.readTimer(accessCode); + assertThat(actual) + .extracting("duration", "remainingTime") + .contains(timerRequest.duration(), timerRequest.remainingTime()); + } + + @Test + @DisplayName("타이머 남은 μ‹œκ°„μ„ λ°˜ν™˜ν•œλ‹€. - 타이머 νƒ€μž„ μŠ€νƒ¬ν”„κ°€ μ‘΄μž¬ν•  경우") + void get_remaining_time_when_exist_timestamp() { + // given + final PairRoomCreateRequest pairRoomCreateRequest = new PairRoomCreateRequest("켈리", "레λͺ¨λ„€", + 3000L, 3000L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name()); + final String accessCode = pairRoomService.savePairRoom(pairRoomCreateRequest, null); + final Timer timeStamp = new Timer(new AccessCode(accessCode), 10000L, 10000L); + timestampRegistry.register(accessCode, timeStamp); + + // when + final long remainingTime = timerService.readTimerRemainingTime(accessCode); + + // then + assertThat(remainingTime).isEqualTo(timeStamp.getRemainingTime()); + } + + @Test + @DisplayName("타이머 남은 μ‹œκ°„μ„ λ°˜ν™˜ν•œλ‹€. - 타이머가 ν•œλ²ˆλ„ λ™μž‘ν•˜μ§€ μ•Šμ•˜μ„ 경우") + void get_remaining_time_when_not_exist_timestamp() { + // given + final PairRoomCreateRequest pairRoomCreateRequest = new PairRoomCreateRequest("켈리", "레λͺ¨λ„€", + 3000L, 3000L, "https://missionUrl.xxx", PairRoomStatus.IN_PROGRESS.name()); + final String accessCode = pairRoomService.savePairRoom(pairRoomCreateRequest, null); + + // when + final long remainingTime = timerService.readTimerRemainingTime(accessCode); + + // then + assertThat(remainingTime).isEqualTo(pairRoomCreateRequest.timerRemainingTime()); + } +} diff --git a/backend/src/test/java/site/coduo/todo/domain/TodoSortTest.java b/backend/src/test/java/site/coduo/todo/domain/TodoSortTest.java index b774b126..661e2611 100644 --- a/backend/src/test/java/site/coduo/todo/domain/TodoSortTest.java +++ b/backend/src/test/java/site/coduo/todo/domain/TodoSortTest.java @@ -11,7 +11,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import site.coduo.todo.exception.InvalidTodoSortException; import site.coduo.todo.exception.InvalidUpdatedTodoSortException; @DisplayName("TodoSort 도메인 ν…ŒμŠ€νŠΈ") @@ -38,7 +37,7 @@ void createTodoSort() { void countNextSort() { // Given final TodoSort todoSort = new TodoSort(1024); - final int expect = 2048; + final int expect = 4096; // When final TodoSort nextSort = todoSort.countNextSort(); @@ -50,18 +49,6 @@ void countNextSort() { }); } - @DisplayName("μŒμˆ˜κ°’μ„ μž…λ ₯ν•˜λ©΄ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚¨λ‹€.") - @Test - void createTodoSortWithNegative() { - // Given - final int input = -1; - - // When & Then - assertThatThrownBy(() -> new TodoSort(input)) - .isInstanceOf(InvalidTodoSortException.class) - .hasMessage("todoSortλŠ” μŒμˆ˜κ°€ 될 수 μ—†μŠ΅λ‹ˆλ‹€."); - } - @DisplayName("첫 번째 μœ„μΉ˜λ‘œ μ΄λ™ν• κ²½μš° κΈ°μ‘΄ 첫 번째 μ•„μ΄ν…œμ˜ μ •λ ¬κ°’μ—μ„œ μ•„μ΄ν…œ 간격 λ§ŒνΌμ„ λΊ€ κ°’μœΌλ‘œ 정렬값을 λ³€κ²½ν•œλ‹€.") @Test void updateSortToFirst() { @@ -76,7 +63,7 @@ void updateSortToFirst() { ); final int destinationSort = 0; - final int expect = 0; + final int expect = -2048; // When final TodoSort updatedSort = targetSort.update(todoSorts, destinationSort); @@ -102,7 +89,7 @@ void updateSortToLast() { ); final int destinationSort = 3; - final int expect = 5120; + final int expect = 7168; // When final TodoSort updatedSort = targetSort.update(todoSorts, destinationSort); @@ -141,6 +128,45 @@ void updateSortBetweenItems() { }); } + @DisplayName("ν˜„μž¬ 정렬값이 ν¬ν•¨λ˜μ–΄ μžˆμ§€ μ•Šμ€ 리슀트λ₯Ό μž…λ ₯ν•˜λ©΄ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚¨λ‹€.") + @Test + void updateWithoutCurrentSort() { + // Given + final TodoSort targetSort = new TodoSort(2048); + final int destinationSort = 3; + final List todoSorts = List.of( + new TodoSort(1024), + new TodoSort(3072), + new TodoSort(4000), + new TodoSort(4096) + ); + + // When & Then + assertThatThrownBy(() -> targetSort.update(todoSorts, destinationSort)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("μž…λ ₯된 νˆ¬λ‘ 정렬에 ν˜„μž¬ 정렬값이 ν¬ν•¨λ˜μ–΄ μžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + } + + @DisplayName("ν˜„μž¬ μœ„μΉ˜λ‘œ μ •λ ¬ μˆœμ„œλ₯Ό λ³€κ²½ν•˜λ €κ³  ν•˜λ©΄ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚¨λ‹€.") + @Test + void updateCurrentSort() { + // Given + final TodoSort targetSort = new TodoSort(2048); + final int destinationSort = 1; + final List todoSorts = List.of( + new TodoSort(1024), + new TodoSort(2048), + new TodoSort(3072), + new TodoSort(4000), + new TodoSort(4096) + ); + + // When & Then + assertThatThrownBy(() -> targetSort.update(todoSorts, destinationSort)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("ν˜„μž¬ μœ„μΉ˜λ‘œλŠ” 이동할 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + @DisplayName("전체 νˆ¬λ‘ μ•„μ΄ν…œ λ²”μœ„λ₯Ό λ²—μ–΄λ‚˜λŠ” μœ„μΉ˜λ‘œ μ΄λ™ν•˜λ €ν•˜λ©΄ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚¨λ‹€.") @ValueSource(ints = {-1, 6}) @ParameterizedTest diff --git a/backend/src/test/java/site/coduo/todo/domain/TodoTest.java b/backend/src/test/java/site/coduo/todo/domain/TodoTest.java index 8b9c5a05..cc8f2e6e 100644 --- a/backend/src/test/java/site/coduo/todo/domain/TodoTest.java +++ b/backend/src/test/java/site/coduo/todo/domain/TodoTest.java @@ -1,7 +1,6 @@ package site.coduo.todo.domain; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; import java.util.List; @@ -9,71 +8,40 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import site.coduo.pairroom.domain.Pair; -import site.coduo.pairroom.domain.PairName; -import site.coduo.pairroom.domain.PairRoom; -import site.coduo.pairroom.domain.accesscode.AccessCode; -import site.coduo.todo.exception.InvalidTodoArgumentException; - @DisplayName("Todo 도메인 ν…ŒμŠ€νŠΈ") class TodoTest { - @DisplayName("μœ νš¨ν•œ pairRoom, content, sort 정보λ₯Ό μž…λ ₯ν•˜λ©΄ 객체λ₯Ό μƒμ„±ν•œλ‹€.") + @DisplayName("μœ νš¨ν•œ content, sort 정보λ₯Ό μž…λ ₯ν•˜λ©΄ 객체λ₯Ό μƒμ„±ν•œλ‹€.") @Test void createTodo() { // Given final Long id = 1L; - final PairRoom pairRoom = new PairRoom( - new Pair(new PairName("A"), new PairName("B")), - new AccessCode("ACCESS-CODE") - ); final String content = "content!"; final int sort = 2048; final boolean isChecked = false; // When - final Todo todo = new Todo(id, pairRoom, content, sort, isChecked); + final Todo todo = new Todo(id, content, sort, isChecked); // Then assertSoftly(softAssertions -> { softAssertions.assertThat(todo).isNotNull(); softAssertions.assertThat(todo.getId()).isEqualTo(id); - softAssertions.assertThat(todo.getPairRoom()).isEqualTo(pairRoom); softAssertions.assertThat(todo.getContent().getContent()).isEqualTo(content); softAssertions.assertThat(todo.getSort().getSort()).isEqualTo(sort); softAssertions.assertThat(todo.getIsChecked().isChecked()).isEqualTo(isChecked); }); } - @DisplayName("pairRomm μ •λ³΄λ‘œ null을 μž…λ ₯ν•˜λ©΄ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚¨λ‹€.") - @Test - void createTodoWithNullPairRoom() { - // Given - final Long id = 1L; - final PairRoom pairRoom = null; - final String content = "content!"; - final int sort = 2048; - final boolean isChecked = false; - - // When & Then - assertThatThrownBy(() -> new Todo(id, pairRoom, content, sort, isChecked)) - .isInstanceOf(InvalidTodoArgumentException.class) - .hasMessage("Pair Room μ •λ³΄λ‘œ null을 μž…λ ₯ν•  수 μ—†μŠ΅λ‹ˆλ‹€."); - } - @DisplayName("μƒˆλ‘œμš΄ contentκ°€ μž…λ ₯되면 ν•΄λ‹Ή contentλ₯Ό 가진 Todo 객체λ₯Ό λ°˜ν™˜ν•œλ‹€.") @Test void updateContent() { // Given final Long id = 1L; - final PairRoom pairRoom = new PairRoom( - new Pair(new PairName("A"), new PairName("B")), - new AccessCode("ACCESS-CODE") - ); final String content = "content!"; final int sort = 2048; final boolean isChecked = false; - final Todo todo = new Todo(id, pairRoom, content, sort, isChecked); + final Todo todo = new Todo(id, content, sort, isChecked); final String newContent = "이거슨 μƒˆλ‘œμš΄ λ‚΄μš©!"; @@ -92,14 +60,10 @@ void updateContent() { void toggleTodoChecked() { // Given final Long id = 1L; - final PairRoom pairRoom = new PairRoom( - new Pair(new PairName("A"), new PairName("B")), - new AccessCode("ACCESS-CODE") - ); final String content = "content!"; final int sort = 2048; final boolean isChecked = false; - final Todo todo = new Todo(id, pairRoom, content, sort, isChecked); + final Todo todo = new Todo(id, content, sort, isChecked); // When final Todo updated = todo.toggleTodoChecked(); @@ -115,24 +79,19 @@ void toggleTodoChecked() { @Test void updateSort() { // Given - final PairRoom pairRoom = new PairRoom( - new Pair(new PairName("A"), new PairName("B")), - new AccessCode("ACCESS-CODE") - ); final Todo todo = new Todo( 1L, - pairRoom, "content!", 2048, false ); final List todos = List.of( - new Todo(1L, pairRoom, "content!", 1024, false), - new Todo(1L, pairRoom, "content!", 2048, false), - new Todo(1L, pairRoom, "content!", 3072, false), - new Todo(1L, pairRoom, "content!", 4000, false), - new Todo(1L, pairRoom, "content!", 4096, false) + new Todo(1L, "content!", 1024, false), + new Todo(1L, "content!", 2048, false), + new Todo(1L, "content!", 3072, false), + new Todo(1L, "content!", 4000, false), + new Todo(1L, "content!", 4096, false) ); final int destinationSort = 3; diff --git a/backend/src/test/java/site/coduo/todo/mock/FakeTodoRepository.java b/backend/src/test/java/site/coduo/todo/mock/FakeTodoRepository.java deleted file mode 100644 index 299126cd..00000000 --- a/backend/src/test/java/site/coduo/todo/mock/FakeTodoRepository.java +++ /dev/null @@ -1,67 +0,0 @@ -package site.coduo.todo.mock; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -import site.coduo.pairroom.domain.PairRoom; -import site.coduo.todo.domain.Todo; -import site.coduo.todo.service.port.TodoRepository; - -public class FakeTodoRepository implements TodoRepository { - - private Long autoGeneratedId = 0L; - public final List data = new ArrayList<>(); - - @Override - public Todo save(final Todo todo) { - if (todo.getId() == null || todo.getId() == 0) { - final Todo createdTodo = new Todo( - ++autoGeneratedId, - todo.getPairRoom(), - todo.getContent().getContent(), - todo.getSort().getSort(), - todo.getIsChecked().isChecked() - ); - data.add(createdTodo); - return createdTodo; - } else { - data.removeIf(item -> Objects.equals(item.getId(), todo.getId())); - data.add(todo); - return todo; - } - } - - @Override - public Optional findTopByPairRoomOrderBySortDesc(final PairRoom pairRoom) { - return data.stream() - .max(Comparator.comparingInt(todo -> todo.getSort().getSort())); - } - - @Override - public Optional findById(final Long id) { - return data.stream() - .filter(item -> Objects.equals(item.getId(), id)).findFirst(); - } - - @Override - public void deleteById(final Long id) { - data.removeIf(item -> Objects.equals(item.getId(), id)); - } - - @Override - public List findAllByPairRoomOrderBySortAsc(final PairRoom pairRoom) { - return data.stream() - .filter(todo -> Objects.equals(todo.getPairRoom().getId(), pairRoom.getId())) - .sorted(Comparator.comparingInt(todo -> todo.getSort().getSort())) - .toList(); - } - - public void saveAll(final List todos) { - for (Todo todo : todos) { - save(todo); - } - } -} diff --git a/backend/src/test/java/site/coduo/todo/service/TodoServiceTest.java b/backend/src/test/java/site/coduo/todo/service/TodoServiceTest.java index bed49aef..a09b6a47 100644 --- a/backend/src/test/java/site/coduo/todo/service/TodoServiceTest.java +++ b/backend/src/test/java/site/coduo/todo/service/TodoServiceTest.java @@ -5,68 +5,82 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import java.util.List; -import java.util.Optional; import java.util.stream.Stream; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; -import site.coduo.pairroom.domain.Pair; -import site.coduo.pairroom.domain.PairName; -import site.coduo.pairroom.domain.PairRoom; -import site.coduo.pairroom.domain.accesscode.AccessCode; import site.coduo.pairroom.exception.PairRoomNotFoundException; -import site.coduo.pairroom.mock.FakePairRoomRepository; +import site.coduo.pairroom.repository.PairRoomEntity; +import site.coduo.pairroom.repository.PairRoomRepository; +import site.coduo.pairroom.service.PairRoomService; +import site.coduo.pairroom.service.dto.PairRoomCreateRequest; import site.coduo.todo.domain.Todo; +import site.coduo.todo.domain.TodoContent; import site.coduo.todo.exception.TodoNotFoundException; -import site.coduo.todo.mock.FakeTodoRepository; +import site.coduo.todo.repository.TodoEntity; +import site.coduo.todo.repository.TodoRepository; +@SpringBootTest +@Transactional @DisplayName("TodoService ν…ŒμŠ€νŠΈ") class TodoServiceTest { - private FakePairRoomRepository pairRoomRepository; - private FakeTodoRepository todoRepository; + @Autowired + private PairRoomRepository pairRoomRepository; + + @Autowired + private PairRoomService pairRoomService; + + @Autowired + private TodoRepository todoRepository; + + @Autowired private TodoService todoService; - @BeforeEach - void setUp() { - final FakePairRoomRepository fakePairRoomRepository = new FakePairRoomRepository(); - final FakeTodoRepository fakeTodoRepository = new FakeTodoRepository(); - final TodoService todoService = new TodoService(fakePairRoomRepository, fakeTodoRepository); + private static Stream destinationSortAndExpectOrder() { + return Stream.of( + Arguments.of(0, List.of("content4!", "content1!", "content2!", "content3!", "content5!", "content6!", + "content7!")), + Arguments.of(6, List.of("content1!", "content2!", "content3!", "content5!", "content6!", "content7!", + "content4!")), + Arguments.of(1, List.of("content1!", "content4!", "content2!", "content3!", "content5!", "content6!", + "content7!")), + Arguments.of(5, List.of("content1!", "content2!", "content3!", "content5!", "content6!", "content4!", + "content7!")) + ); + } - this.pairRoomRepository = fakePairRoomRepository; - this.todoRepository = fakeTodoRepository; - this.todoService = todoService; + @AfterEach + void clean() { + todoRepository.deleteAll(); + pairRoomRepository.deleteAll(); } @DisplayName("νŽ˜μ–΄λ£Έ 아이디, νˆ¬λ‘ λ‚΄μš©μ„ μž…λ ₯λ°›μœΌλ©΄ Todo 객체λ₯Ό 생성해 μ €μž₯ν•œλ‹€.") @Test void createTodo() { // Given - final String pairRoomAccessCode = "ACCESS-CODE"; - final PairRoom pairRoom = new PairRoom( - new Pair(new PairName("A"), new PairName("B")), - new AccessCode(pairRoomAccessCode) - ); - pairRoomRepository.save(pairRoom); - - final Long pairRoomId = 1L; - final String content = "켈리 μΉ˜ν‚¨ 사주기이이이이이"; + final String accessCode = pairRoomService.savePairRoom( + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); + final String content = "content!"; // When - todoService.createTodo(pairRoomAccessCode, content); + todoService.createTodo(accessCode, content); // Then - final Optional findSavedTodo = todoRepository.findById(1L); - assertThat(findSavedTodo).isPresent(); + final List allSaved = todoRepository.findAll(); + assertThat(allSaved).isNotEmpty(); - final Todo savedTodo = findSavedTodo.get(); + final Todo savedTodo = allSaved.get(0).toDomain(); assertSoftly(softAssertions -> { - softAssertions.assertThat(savedTodo.getPairRoom().getId()).isEqualTo(pairRoomId); softAssertions.assertThat(savedTodo.getContent().getContent()).isEqualTo(content); softAssertions.assertThat(savedTodo.getIsChecked().isChecked()).isFalse(); }); @@ -76,62 +90,58 @@ void createTodo() { @Test void createPairRoomWithNotFoundPairRoomId() { // Given - final PairRoom pairRoom = new PairRoom( - new Pair(new PairName("A"), new PairName("B")), - new AccessCode("ACCESS-CODE") - ); - pairRoomRepository.save(pairRoom); - - final String pariRoomAccessCode = "code"; - final String content = "켈리 μΉ˜ν‚¨ 사주기이이이이이"; + pairRoomService.savePairRoom( + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); + final String content = "content!"; // When & Then - assertThatThrownBy(() -> todoService.createTodo(pariRoomAccessCode, content)) + assertThatThrownBy(() -> todoService.createTodo("NOCODE", content)) .isInstanceOf(PairRoomNotFoundException.class) - .hasMessage("ν•΄λ‹Ή Access Code의 νŽ˜μ–΄λ£Έμ€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. - " + pariRoomAccessCode); + .hasMessage("ν•΄λ‹Ή Access Code의 νŽ˜μ–΄λ£Έμ€ μ‘΄μž¬ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. - " + "NOCODE"); } @DisplayName("νˆ¬λ‘ id와 μƒˆλ‘œμš΄ νˆ¬λ‘ λ‚΄μš©μ΄ μž…λ ₯되면 ν•΄λ‹Ή νˆ¬λ‘λ₯Ό μ €μž₯μ†Œμ—μ„œ 가져와 λ‚΄μš©μ„ λ³€κ²½ν•œλ’€ μ €μž₯ν•œλ‹€.") @Test void updateTodoContent() { // Given - final PairRoom pairRoom = new PairRoom( - new Pair(new PairName("A"), new PairName("B")), - new AccessCode("ACCESS-CODE") - ); + final String accessCode = pairRoomService.savePairRoom( + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + final String content = "content!"; final int sort = 2048; final boolean isChecked = false; - final Todo todo = new Todo(1L, pairRoom, content, sort, isChecked); - todoRepository.save(todo); + final Todo todo = new Todo(null, content, sort, isChecked); + final TodoEntity todoEntity = new TodoEntity(todo, pairRoomEntity); + final TodoEntity savedTodo = todoRepository.save(todoEntity); - final Long todoId = 1L; final String newContent = "이거슨 μƒˆλ‘œμš΄ λ‚΄μš©!"; // When - todoService.updateTodoContent(todoId, newContent); + todoService.updateTodoContent(savedTodo.getId(), newContent); // Then - final Optional findUpdatedTodo = todoRepository.findById(1L); - assertThat(findUpdatedTodo).isPresent(); - - final Todo updatedTodo = findUpdatedTodo.get(); - assertThat(updatedTodo.getContent().getContent()).isEqualTo(newContent); + final Todo findUpdatedTodo = todoRepository.findById(savedTodo.getId()) + .map(TodoEntity::toDomain) + .orElse(null); + assertThat(findUpdatedTodo).isNotNull(); + assertThat(findUpdatedTodo.getContent().getContent()).isEqualTo(newContent); } @DisplayName("μ‘΄μž¬ν•˜μ§€ μ•Šμ€ νˆ¬λ‘ 아이디와 ν•¨κ»˜ λ‚΄μš© λ³€κ²½ μš”μ²­μ„ ν•˜λ©΄ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚¨λ‹€.") @Test void updateTodoContentWithNotFoundTodoId() { // Given - final PairRoom pairRoom = new PairRoom( - new Pair(new PairName("A"), new PairName("B")), - new AccessCode("ACCESS-CODE") - ); + final String accessCode = pairRoomService.savePairRoom( + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + final String content = "content!"; final int sort = 2048; final boolean isChecked = false; - final Todo todo = new Todo(1L, pairRoom, content, sort, isChecked); - todoRepository.save(todo); + final Todo todo = new Todo(1L, content, sort, isChecked); + final TodoEntity todoEntity = new TodoEntity(todo, pairRoomEntity); + todoRepository.save(todoEntity); final Long todoId = 12323L; final String newContent = "이거슨 μƒˆλ‘œμš΄ λ‚΄μš©!"; @@ -146,42 +156,42 @@ void updateTodoContentWithNotFoundTodoId() { @Test void toggleTodoChecked() { // Given - final PairRoom pairRoom = new PairRoom( - new Pair(new PairName("A"), new PairName("B")), - new AccessCode("ACCESS-CODE") - ); + final String accessCode = pairRoomService.savePairRoom( + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + final String content = "content!"; final int sort = 2048; final boolean isChecked = false; - final Todo todo = new Todo(1L, pairRoom, content, sort, isChecked); - todoRepository.save(todo); - - final Long todoId = 1L; + final Todo todo = new Todo(null, content, sort, isChecked); + final TodoEntity todoEntity = new TodoEntity(todo, pairRoomEntity); + final TodoEntity savedTodoEntity = todoRepository.save(todoEntity); // When - todoService.toggleTodoChecked(todoId); + todoService.toggleTodoChecked(savedTodoEntity.getId()); // Then - final Optional findUpdatedTodo = todoRepository.findById(1L); - assertThat(findUpdatedTodo).isPresent(); - - final Todo updatedTodo = findUpdatedTodo.get(); - assertThat(updatedTodo.getIsChecked().isChecked()).isTrue(); + final Todo findUpdatedTodo = todoRepository.findById(savedTodoEntity.getId()) + .map(TodoEntity::toDomain) + .orElse(null); + assertThat(findUpdatedTodo).isNotNull(); + assertThat(findUpdatedTodo.getIsChecked().isChecked()).isTrue(); } @DisplayName("μ‘΄μž¬ν•˜μ§€ μ•Šμ€ νˆ¬λ‘ id와 ν•¨κ»˜ ν† κΈ€ μš”μ²­μ΄ λ“€μ–΄μ˜€λ©΄ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚¨λ‹€.") @Test void toggleTodoCheckedWithNotFoundTodoId() { // Given - final PairRoom pairRoom = new PairRoom( - new Pair(new PairName("A"), new PairName("B")), - new AccessCode("ACCESS-CODE") - ); + final String accessCode = pairRoomService.savePairRoom( + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + final String content = "content!"; final int sort = 2048; final boolean isChecked = false; - final Todo todo = new Todo(1L, pairRoom, content, sort, isChecked); - todoRepository.save(todo); + final Todo todo = new Todo(1L, content, sort, isChecked); + final TodoEntity todoEntity = new TodoEntity(todo, pairRoomEntity); + todoRepository.save(todoEntity); final Long todoId = 12323L; @@ -195,15 +205,16 @@ void toggleTodoCheckedWithNotFoundTodoId() { @Test void deleteTodo() { // Given - final PairRoom pairRoom = new PairRoom( - new Pair(new PairName("A"), new PairName("B")), - new AccessCode("ACCESS-CODE") - ); + final String accessCode = pairRoomService.savePairRoom( + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + final String content = "content!"; final int sort = 2048; final boolean isChecked = false; - final Todo todo = new Todo(1L, pairRoom, content, sort, isChecked); - todoRepository.save(todo); + final Todo todo = new Todo(1L, content, sort, isChecked); + final TodoEntity todoEntity = new TodoEntity(todo, pairRoomEntity); + todoRepository.save(todoEntity); final Long todoId = 1L; @@ -211,42 +222,46 @@ void deleteTodo() { todoService.deleteTodo(todoId); // Then - final Optional findSavedTodo = todoRepository.findById(todoId); - assertThat(findSavedTodo).isEmpty(); + final Todo findSavedTodo = todoRepository.findById(todoId) + .map(TodoEntity::toDomain) + .orElse(null); + assertThat(findSavedTodo).isNull(); } @DisplayName("μ €μž₯된 λͺ¨λ“  νˆ¬λ‘λ₯Ό sort값을 κΈ°μ€€μœΌλ‘œ μ˜€λ¦„μ°¨μˆœ μ •λ ¬ ν›„ λ°˜ν™˜ν•œλ‹€.") @Test void getAll() { // Given - final String pairRoomAccessCode = "ACCESS-CODE"; - final PairRoom pairRoom = new PairRoom( - 1L, - new Pair(new PairName("A"), new PairName("B")), - new AccessCode(pairRoomAccessCode) - ); - pairRoomRepository.save(pairRoom); + final String accessCode = pairRoomService.savePairRoom( + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + final List todos = List.of( - new Todo(1L, pairRoom, "νˆ¬λ‘1!!", 5555, false), - new Todo(2L, pairRoom, "νˆ¬λ‘2!!", 1024, false), - new Todo(3L, pairRoom, "νˆ¬λ‘3!!", 3434, true), - new Todo(4L, pairRoom, "νˆ¬λ‘4!!", 2048, false) + new Todo(null, "νˆ¬λ‘1!!", 5555, false), + new Todo(null, "νˆ¬λ‘2!!", 1024, false), + new Todo(null, "νˆ¬λ‘3!!", 3434, true), + new Todo(null, "νˆ¬λ‘4!!", 2048, false) ); - todoRepository.saveAll(todos); - - final Long pairRoomId = 1L; + final List todoEntities = todos.stream() + .map(todo -> new TodoEntity(todo, pairRoomEntity)) + .toList(); + todoRepository.saveAll(todoEntities); final int expectSize = 4; - final List expectOrder = List.of(2L, 4L, 3L, 1L); + final List expectOrder = List.of("νˆ¬λ‘2!!", "νˆ¬λ‘4!!", "νˆ¬λ‘3!!", "νˆ¬λ‘1!!"); // When - final List all = todoService.getAllOrderBySort(pairRoomAccessCode); + final List all = todoService.getAllOrderBySort(accessCode); // Then - final List ids = all.stream().map(Todo::getId).toList(); + final List contents = all.stream() + .map(Todo::getContent) + .map(TodoContent::getContent) + .toList(); + assertSoftly(softAssertions -> { softAssertions.assertThat(all).hasSize(expectSize); - softAssertions.assertThat(ids).isEqualTo(expectOrder); + softAssertions.assertThat(contents).isEqualTo(expectOrder); }); } @@ -254,19 +269,20 @@ void getAll() { @Test void getAllOrderBySortWithNotExistPairRoomId() { // Given - final PairRoom pairRoom = new PairRoom( - 1L, - new Pair(new PairName("A"), new PairName("B")), - new AccessCode("ACCESS CODE") - ); - pairRoomRepository.save(pairRoom); + final String accessCode = pairRoomService.savePairRoom( + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + final List todos = List.of( - new Todo(1L, pairRoom, "νˆ¬λ‘1!!", 5555, false), - new Todo(2L, pairRoom, "νˆ¬λ‘2!!", 1024, false), - new Todo(3L, pairRoom, "νˆ¬λ‘3!!", 3434, true), - new Todo(4L, pairRoom, "νˆ¬λ‘4!!", 2048, false) + new Todo(null, "νˆ¬λ‘1!!", 5555, false), + new Todo(null, "νˆ¬λ‘2!!", 1024, false), + new Todo(null, "νˆ¬λ‘3!!", 3434, true), + new Todo(null, "νˆ¬λ‘4!!", 2048, false) ); - todoRepository.saveAll(todos); + final List todoEntities = todos.stream() + .map(todo -> new TodoEntity(todo, pairRoomEntity)) + .toList(); + todoRepository.saveAll(todoEntities); final String pairRoomAccessCode = "CODE"; @@ -279,65 +295,62 @@ void getAllOrderBySortWithNotExistPairRoomId() { @DisplayName("λŒ€μƒ νˆ¬λ‘ 아이디와 λ³€κ²½ν•  μˆœμ„œλ₯Ό μž…λ ₯λ°›μœΌλ©΄ μœ„μΉ˜λ₯Ό λ³€κ²½μ‹œν‚¨λ‹€.") @MethodSource("destinationSortAndExpectOrder") @ParameterizedTest - void updateTodoSort(final int destinationSort, final List expect) { + void updateTodoSort(final int destinationSort, final List expect) { // Given - final PairRoom pairRoom = new PairRoom( - 1L, - new Pair(new PairName("A"), new PairName("B")), - new AccessCode("ACCESS-CODE") - ); - pairRoomRepository.save(pairRoom); + final String accessCode = pairRoomService.savePairRoom( + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + final List todos = List.of( - new Todo(1L, pairRoom, "content!", 1024, false), - new Todo(2L, pairRoom, "content!", 2048, false), - new Todo(3L, pairRoom, "content!", 3072, false), - new Todo(4L, pairRoom, "content!", 4000, false), - new Todo(5L, pairRoom, "content!", 4096, false), - new Todo(6L, pairRoom, "content!", 5500, false), - new Todo(7L, pairRoom, "content!", 6000, false) + new Todo(null, "content1!", 1024, false), + new Todo(null, "content2!", 2048, false), + new Todo(null, "content3!", 3072, false), + new Todo(null, "content4!", 4000, false), + new Todo(null, "content5!", 4096, false), + new Todo(null, "content6!", 5500, false), + new Todo(null, "content7!", 6000, false) ); - todoRepository.saveAll(todos); + final List todoEntities = todos.stream() + .map(todo -> new TodoEntity(todo, pairRoomEntity)) + .toList(); + final List savedTodos = todoRepository.saveAll(todoEntities); - final long targetTodoId = 4L; + final Long targetTodoId = savedTodos.get(3).getId(); // When todoService.updateTodoSort(targetTodoId, destinationSort); // Then - final List orders = todoRepository.findAllByPairRoomOrderBySortAsc(pairRoom) - .stream().map(Todo::getId).toList(); + final List orders = todoRepository.findAllByPairRoomEntityOrderBySortAsc(pairRoomEntity) + .stream() + .map(TodoEntity::toDomain) + .map(Todo::getContent) + .map(TodoContent::getContent) + .toList(); assertThat(orders).isEqualTo(expect); } - private static Stream destinationSortAndExpectOrder() { - return Stream.of( - Arguments.of(0, List.of(4L, 1L, 2L, 3L, 5L, 6L, 7L)), - Arguments.of(6, List.of(1L, 2L, 3L, 5L, 6L, 7L, 4L)), - Arguments.of(1, List.of(1L, 4L, 2L, 3L, 5L, 6L, 7L)), - Arguments.of(5, List.of(1L, 2L, 3L, 5L, 6L, 4L, 7L)) - ); - } - @DisplayName("μ‘΄μž¬ν•˜μ§€ μ•Šμ€ νŽ˜μ–΄λ£Έ 아이디와 ν•¨κ»˜ νˆ¬λ‘ μˆœμ„œ λ³€κ²½ μš”μ²­μ„ ν•˜λ©΄ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚¨λ‹€.") @Test void updateTodoSortWithNotExistPairRoomId() { // Given - final PairRoom pairRoom = new PairRoom( - 1L, - new Pair(new PairName("A"), new PairName("B")), - new AccessCode("ACCESS-CODE") - ); - pairRoomRepository.save(pairRoom); + final String accessCode = pairRoomService.savePairRoom( + new PairRoomCreateRequest("A", "B", 60_000, 60_000, "https://missionUrl.xxx", "IN_PROGRESS"), null); + final PairRoomEntity pairRoomEntity = pairRoomRepository.fetchByAccessCode(accessCode); + final List todos = List.of( - new Todo(1L, pairRoom, "content!", 1024, false), - new Todo(2L, pairRoom, "content!", 2048, false), - new Todo(3L, pairRoom, "content!", 3072, false), - new Todo(4L, pairRoom, "content!", 4000, false), - new Todo(5L, pairRoom, "content!", 4096, false) + new Todo(null, "content!", 1024, false), + new Todo(null, "content!", 2048, false), + new Todo(null, "content!", 3072, false), + new Todo(null, "content!", 4000, false), + new Todo(null, "content!", 4096, false) ); - todoRepository.saveAll(todos); + final List todoEntities = todos.stream() + .map(todo -> new TodoEntity(todo, pairRoomEntity)) + .toList(); + todoRepository.saveAll(todoEntities); - final long targetTodoId = 7L; + final long targetTodoId = 70000L; final int destinationSort = 3; // When & Then diff --git a/backend/src/test/java/site/coduo/utils/CascadeCleaner.java b/backend/src/test/java/site/coduo/utils/CascadeCleaner.java index bee44a5d..47c4ce43 100644 --- a/backend/src/test/java/site/coduo/utils/CascadeCleaner.java +++ b/backend/src/test/java/site/coduo/utils/CascadeCleaner.java @@ -1,11 +1,11 @@ package site.coduo.utils; -import org.junit.jupiter.api.AfterEach; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import site.coduo.pairroom.repository.PairRoomRepository; -import site.coduo.pairroomhistory.repository.PairRoomHistoryRepository; +import site.coduo.pairroom.repository.PairRoomMemberRepository; +import site.coduo.timer.repository.TimerRepository; import site.coduo.referencelink.repository.CategoryRepository; import site.coduo.referencelink.repository.OpenGraphRepository; import site.coduo.referencelink.repository.ReferenceLinkRepository; @@ -14,7 +14,7 @@ public class CascadeCleaner { @Autowired - private PairRoomHistoryRepository pairRoomHistoryRepository; + private TimerRepository timerRepository; @Autowired private OpenGraphRepository openGraphRepository; @@ -28,8 +28,12 @@ public class CascadeCleaner { @Autowired private PairRoomRepository pairRoomRepository; + @Autowired + private PairRoomMemberRepository pairRoomMemberRepository; + public void deleteAllPairRoomCascade() { - pairRoomHistoryRepository.deleteAll(); + pairRoomMemberRepository.deleteAll(); + timerRepository.deleteAll(); openGraphRepository.deleteAll(); referenceLinkRepository.deleteAll(); categoryRepository.deleteAll(); diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 00000000..0c106164 --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,146 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:jsx-a11y/recommended', + 'plugin:import/errors', + 'plugin:import/warnings', + 'plugin:import/typescript', + 'plugin:storybook/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs', 'jest.config.cjs', '**/*.config.js'], + parser: '@typescript-eslint/parser', + settings: { + react: { + version: 'detect', + }, + 'import/resolver': { + node: {}, + typescript: { + directory: './src', + }, + }, + 'import/parsers': { '@typescript-eslint/parser': ['.ts', '.tsx'] }, + }, + rules: { + // ν•¨μˆ˜ μ„ μ–Έ + 'prefer-arrow-callback': 'error', + 'func-style': ['error', 'expression'], + + // νƒ€μž… + '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], + + // 넀이밍 + 'id-length': ['error', { min: 2 }], + 'consistent-this': ['error', 'self'], + + // μƒμˆ˜ + 'no-var': 'error', + 'prefer-const': 'error', + 'no-duplicate-imports': 'error', + + // μ»΄ν¬λ„ŒνŠΈ 넀이밍 + 'react/jsx-pascal-case': 'error', + 'react/react-in-jsx-scope': 'off', + + // 기타 + 'import/no-named-as-default': 0, + 'import/no-unresolved': 'off', + + // importλ¬Έ μˆœμ„œ + 'import/order': [ + 'error', + { + groups: [ + 'builtin', // Node.js λ‚΄μž₯ λͺ¨λ“ˆ + 'external', // μ™ΈλΆ€ 라이브러리 + 'internal', // λ‚΄λΆ€ λͺ¨λ“ˆ + ['parent', 'sibling', 'index'], // μƒλŒ€ 경둜 λͺ¨λ“ˆ + ], + pathGroups: [ + { + pattern: 'react*', + group: 'builtin', + position: 'before', + }, + { + pattern: '@/assets', + group: 'internal', + position: 'after', + }, + { + pattern: '@/pages/**', + group: 'internal', + position: 'after', + }, + { + pattern: '@/validations/**', + group: 'internal', + position: 'after', + }, + { + pattern: '@/components/**', + group: 'internal', + position: 'after', + }, + + { + pattern: '@/common/**', + group: 'internal', + position: 'after', + }, + { + pattern: '@/stores/**', + group: 'internal', + position: 'after', + }, + { + pattern: '@/apis/**', + group: 'internal', + position: 'after', + }, + { + pattern: '@/hooks/**', + group: 'internal', + position: 'after', + }, + { + pattern: '@/queries/**', + group: 'internal', + position: 'after', + }, + { + pattern: '@/utils/**', + group: 'internal', + position: 'after', + }, + { + pattern: '@/types/**', + group: 'internal', + position: 'after', + }, + { + pattern: '@/constants/**', + group: 'internal', + position: 'after', + }, + { + pattern: '@/styles/**', + group: 'internal', + position: 'after', + }, + ], + pathGroupsExcludedImportTypes: ['builtin'], + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + 'newlines-between': 'always', + }, + ], + }, +}; diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..48ceaa25 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,7 @@ +node_modules +.env.* +*storybook.log + +nginx.conf +.dockerignore + diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 00000000..071a7b7a --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,11 @@ +{ + "endOfLine": "auto", + "singleQuote": true, + "semi": true, + "useTabs": false, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 120, + "bracketSpacing": true, + "arrowParens": "always" +} diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts new file mode 100644 index 00000000..06d0ca84 --- /dev/null +++ b/frontend/.storybook/main.ts @@ -0,0 +1,35 @@ +import type { StorybookConfig } from '@storybook/react-webpack5'; +import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'; + +const config: StorybookConfig = { + stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)', '../src/**/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: [ + '@storybook/addon-webpack5-compiler-swc', + '@storybook/addon-onboarding', + '@storybook/addon-links', + '@storybook/addon-essentials', + '@chromatic-com/storybook', + '@storybook/addon-interactions', + '@storybook/addon-viewport', + ], + framework: { + name: '@storybook/react-webpack5', + options: {}, + }, + swc: () => ({ + jsc: { + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }), + webpackFinal: async (config) => { + if (config.resolve) { + config.resolve.plugins = [...(config.resolve.plugins || []), new TsconfigPathsPlugin()]; + } + return config; + }, +}; +export default config; diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx new file mode 100644 index 00000000..d682afb7 --- /dev/null +++ b/frontend/.storybook/preview.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import type { Preview } from '@storybook/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ThemeProvider } from 'styled-components'; +import { BrowserRouter } from 'react-router-dom'; +import { theme } from '../src/styles/theme'; +import GlobalStyle from '../src/styles/Global.style'; +import '../src/styles/font.css'; + +const queryClient = new QueryClient(); + +const preview: Preview = { + parameters: { + viewport: { + viewport: { defaultViewport: 'iphonese2' }, + }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, + decorators: [ + (Story) => ( + + + + + + + + + ), + ], +}; + +export default preview; diff --git a/frontend/.stylelintrc b/frontend/.stylelintrc new file mode 100644 index 00000000..c6c1810e --- /dev/null +++ b/frontend/.stylelintrc @@ -0,0 +1,107 @@ +{ + "extends": ["stylelint-config-standard"], + "plugins": ["stylelint-order"], + "customSyntax": "postcss-styled-syntax", + "rules": { + "declaration-empty-line-before": [ + "always", + { + "ignore": ["first-nested", "after-comment", "after-declaration", "inside-single-line-block"] + } + ], + "order/order": ["custom-properties", "declarations"], + "order/properties-order": [ + { + "groupName": "Display", + "emptyLineBefore": "always", + "noEmptyLineBetween": true, + "properties": [ + "display", + "flex", + "flex-grow", + "flex-wrap", + "flex-direction", + "justify-content", + "align-items", + "grid-template-columns", + "gap", + "visibility", + "overflow", + "overflow-x", + "overflow-y" + ] + }, + { + "groupName": "Position", + "emptyLineBefore": "always", + "noEmptyLineBetween": true, + "properties": ["position", "top", "right", "bottom", "left", "z-index"] + }, + { + "groupName": "Box", + "emptyLineBefore": "always", + "noEmptyLineBetween": true, + "properties": [ + "width", + "min-width", + "max-width", + "height", + "min-height", + "max-height", + "margin", + "margin-top", + "margin-right", + "margin-bottom", + "margin-left", + "padding", + "padding-top", + "padding-right", + "padding-bottom", + "padding-left", + "border", + "border-radius" + ] + }, + { + "groupName": "Background & Font", + "emptyLineBefore": "always", + "noEmptyLineBetween": true, + "properties": [ + "background", + "background-color", + "background-image", + "background-repeat", + "background-position", + "opacity", + "box-shadow", + "color", + "font-style", + "font-size", + "font-weight", + "line-height", + "letter-spacing", + "text-align", + "text-indent", + "text-overflow", + "text-decoration", + "vertical-align", + "white-space", + "word-break" + ] + }, + { + "groupName": "Animation", + "emptyLineBefore": "always", + "noEmptyLineBetween": true, + "properties": ["animation", "transform", "transition"] + }, + { + "groupName": "Etc", + "emptyLineBefore": "always", + "noEmptyLineBetween": true, + "properties": ["cursor"] + } + ], + "media-query-no-invalid": null + } +} diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 00000000..9b6f3cc7 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,14 @@ +FROM node:22-alpine as build-stage +WORKDIR /app +COPY package.json yarn.lock ./ +RUN yarn install +COPY . . +RUN yarn build + +# production stage +FROM node:22-alpine as production-stage +WORKDIR /app +RUN yarn global add serve +COPY --from=build-stage /app/dist ./dist +EXPOSE 3000 +CMD ["serve", "-s", "dist"] diff --git a/frontend/jest.config.cjs b/frontend/jest.config.cjs new file mode 100644 index 00000000..a645ed17 --- /dev/null +++ b/frontend/jest.config.cjs @@ -0,0 +1,11 @@ +module.exports = { + testEnvironment: 'jsdom', + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + transform: { + '^.+\\.tsx?$': ['ts-jest', { useESM: true }], + }, + testMatch: ['**/?(*.)+(spec|test).[tj]s?(x)'], +}; diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..4cecf260 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,80 @@ +{ + "type": "module", + "name": "coduo-frontend", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "scripts": { + "start": "webpack serve --config=webpack.development.config.js", + "build": "webpack --config=webpack.production.config.js", + "prod": "webpack serve --port 3001 --config=webpack.production.config.js", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", + "test": "jest", + "lint": "eslint .", + "lint:css": "stylelint './src/**/*.styles.{ts,tsx}'", + "lint:css:fix": "stylelint './src/**/*.styles.{ts,tsx}' --fix" + }, + "dependencies": { + "@octokit/rest": "^21.0.1", + "@sentry/react": "^8.22.0", + "@sentry/webpack-plugin": "^2.21.1", + "@tanstack/react-query": "^5.51.11", + "dotenv-webpack": "^8.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-ga4": "^2.1.0", + "react-icons": "^5.3.0", + "react-router-dom": "^6.25.1", + "styled-components": "^6.1.12", + "zustand": "^4.5.4" + }, + "devDependencies": { + "@chromatic-com/storybook": "^1.6.1", + "@jest/globals": "^29.7.0", + "@storybook/addon-essentials": "^8.2.4", + "@storybook/addon-interactions": "^8.2.4", + "@storybook/addon-links": "^8.2.4", + "@storybook/addon-onboarding": "^8.2.4", + "@storybook/addon-viewport": "^8.2.4", + "@storybook/addon-webpack5-compiler-swc": "^1.0.4", + "@storybook/blocks": "^8.2.4", + "@storybook/react": "^8.2.4", + "@storybook/react-webpack5": "^8.2.4", + "@storybook/test": "^8.2.4", + "@testing-library/react": "^16.0.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.16.0", + "@typescript-eslint/parser": "^7.16.1", + "chromatic": "^11.5.6", + "copy-webpack-plugin": "^12.0.2", + "css-loader": "^7.1.2", + "eslint": "8.x", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jsx-a11y": "^6.9.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react": "^7.34.3", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-storybook": "^0.8.0", + "html-webpack-plugin": "^5.6.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "postcss-styled-syntax": "^0.6.4", + "storybook": "^8.2.4", + "style-loader": "^4.0.0", + "stylelint": "^16.8.1", + "stylelint-config-standard": "^36.0.1", + "stylelint-order": "^6.0.4", + "ts-jest": "^29.2.3", + "ts-loader": "^9.5.1", + "tsconfig-paths-webpack-plugin": "^4.1.0", + "typescript": "^5.5.3", + "webpack": "^5.92.1", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^5.0.4", + "webpack-merge": "^6.0.1" + } +} diff --git a/frontend/public/coduo_metadata.jpg b/frontend/public/coduo_metadata.jpg new file mode 100644 index 00000000..99737134 Binary files /dev/null and b/frontend/public/coduo_metadata.jpg differ diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 00000000..76af60f4 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 00000000..997a7154 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + μ½”λ”©ν•΄λ“€μ˜€ + + +
+ + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 00000000..6b2bed03 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,123 @@ +import { useEffect, Suspense, lazy } from 'react'; +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ThemeProvider } from 'styled-components'; + +const PairRoom = lazy(() => import('@/pages/PairRoom/PairRoom')); + +import Callback from '@/pages/Callback/Callback'; +import CoduoDocs from '@/pages/CoduoDocs/CoduoDocs'; +import Error from '@/pages/Error/Error'; +import Landing from '@/pages/Landing/Landing'; +import Layout from '@/pages/Layout'; +import Loading from '@/pages/Loading/Loading'; +import Main from '@/pages/Main/Main'; +import MyPage from '@/pages/MyPage/MyPage'; +import PairRoomOnboarding from '@/pages/PairRoomOnboarding/PairRoomOnboarding'; +import SignUp from '@/pages/SignUp/SignUp'; + +import HowToPair from '@/components/Landing/HowToPair/HowToPair'; + +import useUserStore from '@/stores/userStore'; + +import { getMember } from '@/apis/member'; +import { getIsUserLoggedIn } from '@/apis/oauth'; + +import GlobalStyles from './styles/Global.style'; +import { theme } from './styles/theme'; + +const queryClient = new QueryClient(); + +const App = () => { + const { setUser } = useUserStore(); + + const updateUser = async () => { + const { signedIn } = await getIsUserLoggedIn(); + + if (!signedIn) { + setUser('', 'SIGNED_OUT'); + return; + } + + const { username } = await getMember(); + + setUser(username, 'SIGNED_IN'); + }; + + useEffect(() => { + if (window.location.pathname !== '/callback') updateUser(); + }, []); + + const router = createBrowserRouter([ + { + path: '/', + element: , + errorElement: , + children: [ + { + path: '', + element: , + }, + { + path: 'main', + element:
, + }, + { + path: 'how-to-pair', + element: , + }, + { + path: 'onboarding', + element: ( + }> + {' '} + + ), + }, + { + path: 'room/:accessCode', + element: ( + }> + + + ), + }, + { + path: 'sign-up', + element: , + }, + { + path: 'coduo-docs', + element: , + }, + { + path: 'callback', + element: , + }, + { + path: 'my-page', + element: , + }, + { + path: 'error', + element: , + }, + { + path: '*', + element: , + }, + ], + }, + ]); + + return ( + + + + + + + ); +}; +export default App; diff --git a/frontend/src/apis/category.ts b/frontend/src/apis/category.ts new file mode 100644 index 00000000..6a9e38c5 --- /dev/null +++ b/frontend/src/apis/category.ts @@ -0,0 +1,57 @@ +import fetcher from '@/apis/fetcher'; + +import { ERROR_MESSAGES } from '@/constants/message'; + +const API_URL = process.env.REACT_APP_API_URL; + +interface GetCategoriesResponse { + value: string; + id: string; +} + +export const getCategories = async (accessCode: string): Promise => { + const response = await fetcher.get({ + url: `${API_URL}/${accessCode}/category`, + errorMessage: ERROR_MESSAGES.GET_CATEGORIES, + }); + + return await response.json(); +}; + +interface AddCategoryRequest { + accessCode: string; + category: string; +} +export const addCategory = async ({ category, accessCode }: AddCategoryRequest) => { + const response = await fetcher.post({ + url: `${API_URL}/${accessCode}/category`, + body: JSON.stringify({ value: category }), + errorMessage: ERROR_MESSAGES.ADD_CATEGORY, + }); + + return await response.json(); +}; + +interface DeleteCategoryRequest { + accessCode: string; + categoryId: string; +} + +export const deleteCategory = async ({ categoryId, accessCode }: DeleteCategoryRequest) => { + await fetcher.delete({ + url: `${API_URL}/${accessCode}/category/${categoryId}`, + }); +}; + +interface UpdateCategoryRequest { + accessCode: string; + categoryId: string; + updatedCategoryName: string; +} + +export const updateCategory = async ({ categoryId, updatedCategoryName, accessCode }: UpdateCategoryRequest) => { + await fetcher.patch({ + url: `${API_URL}/${accessCode}/category`, + body: JSON.stringify({ categoryId, updatedCategoryName }), + }); +}; diff --git a/frontend/src/apis/fetcher.ts b/frontend/src/apis/fetcher.ts new file mode 100644 index 00000000..c3897e92 --- /dev/null +++ b/frontend/src/apis/fetcher.ts @@ -0,0 +1,63 @@ +import * as Sentry from '@sentry/react'; + +interface RequestProps { + url: string; + method: 'GET' | 'POST' | 'DELETE' | 'PATCH' | 'PUT'; + errorMessage?: string; + //TODO: errorMessage 제거 + body?: string; + headers?: Record; +} + +type FetchProps = Omit; + +const fetcher = { + async request({ url, method, body, headers, errorMessage }: RequestProps): Promise { + try { + const response = await fetch(url, { + method, + headers: headers && headers, + body: body && body, + credentials: 'include', + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || errorMessage); + } + + return response; + } catch (error) { + if (!(error instanceof Error)) (error as { message: string }).message; + + Sentry.captureException(error); + throw error; + } + }, + + get(props: FetchProps) { + return this.request({ ...props, method: 'GET' }); + }, + post(props: FetchProps) { + return this.request({ + ...props, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + }, + delete(props: FetchProps) { + return this.request({ ...props, method: 'DELETE', headers: { 'Content-Type': 'application/json' } }); + }, + patch(props: FetchProps) { + return this.request({ + ...props, + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + }); + }, + put(props: FetchProps) { + return this.request({ ...props, method: 'PUT' }); + }, +}; + +export default fetcher; diff --git a/frontend/src/apis/github.ts b/frontend/src/apis/github.ts new file mode 100644 index 00000000..53a6cf04 --- /dev/null +++ b/frontend/src/apis/github.ts @@ -0,0 +1,83 @@ +import { Octokit } from '@octokit/rest'; +import * as Sentry from '@sentry/react'; + +const octokit = new Octokit({ + auth: process.env.REACT_APP_GITHUB_AUTH, +}); + +const CODUO_ORGANIZATION = 'coduo-missions'; + +export const getSHAforMain = async (repositoryName: string) => { + try { + const response = await octokit.request(`GET /repos/${CODUO_ORGANIZATION}/${repositoryName}/git/refs/heads/main`, { + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }); + return response.data.object.sha; + } catch (error) { + if (error instanceof Error) throw new Error(error.message); + Sentry.captureException(error); + } +}; + +interface CreateBranchProps { + repositoryName: string; + branchName: string; + sha: string; +} + +export const createBranch = async ({ repositoryName, branchName, sha }: CreateBranchProps) => { + try { + const result = await octokit.request(`POST /repos/${CODUO_ORGANIZATION}/${repositoryName}/git/refs`, { + ref: `refs/heads/${branchName}`, + sha: sha, + }); + return result; + } catch (error) { + if (error instanceof Error) throw new Error(error.message); + Sentry.captureException(error); + } +}; + +interface Repository { + archive_url: string; + id: string; + name: string; + description: string; +} + +export const getRepositories = async (): Promise => { + try { + const response = await octokit.request(`GET /orgs/${CODUO_ORGANIZATION}/repos`, { + org: 'ORG', + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }); + + return response.data; + } catch (error) { + if (error instanceof Error) throw new Error(error.message); + Sentry.captureException(error); + } +}; + +interface Branch { + name: string; +} + +export const getBranches = async (repositoryName: string): Promise => { + try { + const response = await octokit.request(`GET /repos/${CODUO_ORGANIZATION}/${repositoryName}/branches`, { + headers: { + 'X-GitHub-Api-Version': '2022-11-28', + }, + }); + + return response.data; + } catch (error) { + if (error instanceof Error) throw new Error(error.message); + Sentry.captureException(error); + } +}; diff --git a/frontend/src/apis/member.ts b/frontend/src/apis/member.ts new file mode 100644 index 00000000..475b84dc --- /dev/null +++ b/frontend/src/apis/member.ts @@ -0,0 +1,39 @@ +import fetcher from '@/apis/fetcher'; +import type { PairRoomStatus } from '@/apis/pairRoom'; + +import { ERROR_MESSAGES } from '@/constants/message'; + +const API_URL = process.env.REACT_APP_API_URL; + +export const getSignOut = async (): Promise => { + await fetcher.get({ + url: `${API_URL}/sign-out`, + errorMessage: ERROR_MESSAGES.SIGN_OUT, + }); +}; + +export const getMember = async (): Promise<{ username: string }> => { + const response = await fetcher.get({ + url: `${API_URL}/member`, + errorMessage: ERROR_MESSAGES.GET_MEMBER, + }); + + return response.json(); +}; + +interface GetMyPairRoomsResponse { + id: number; + status: PairRoomStatus; + navigator: string; + driver: string; + accessCode: string; +} + +export const getMyPairRooms = async (): Promise => { + const response = await fetcher.get({ + url: `${API_URL}/my-pair-rooms`, + errorMessage: ERROR_MESSAGES.GET_MEMBER, + }); + + return response.json(); +}; diff --git a/frontend/src/apis/oauth.ts b/frontend/src/apis/oauth.ts new file mode 100644 index 00000000..a5d417e4 --- /dev/null +++ b/frontend/src/apis/oauth.ts @@ -0,0 +1,54 @@ +import fetcher from '@/apis/fetcher'; + +import { ERROR_MESSAGES } from '@/constants/message'; + +const API_URL = process.env.REACT_APP_API_URL; + +export interface SignInGithubResponse { + endpoint: string; +} + +export const getSignInGithub = async (): Promise => { + const response = await fetcher.get({ + url: `${API_URL}/sign-in/oauth/github`, + errorMessage: ERROR_MESSAGES.SIGN_IN, + }); + + return await response.json(); +}; + +export interface IsUserLoggedInResponse { + signedIn: boolean; +} + +export const getIsUserLoggedIn = async (): Promise => { + const response = await fetcher.get({ + url: `${API_URL}/sign-in/check`, + errorMessage: ERROR_MESSAGES.CHECK_USER_LOGIN, + }); + + return await response.json(); +}; + +export const addSignUp = async (username: string): Promise => { + const response = await fetcher.post({ + url: `${API_URL}/sign-up`, + body: JSON.stringify({ username }), + errorMessage: ERROR_MESSAGES.SIGN_UP, + }); + + return await response.json(); +}; + +export interface SignInCallbackResponse { + signedUp: boolean; +} + +export const getSignInCallback = async (): Promise => { + const response = await fetcher.get({ + url: `${API_URL}/sign-in/callback`, + errorMessage: ERROR_MESSAGES.SIGN_IN, + }); + + return await response.json(); +}; diff --git a/frontend/src/apis/pairRoom.ts b/frontend/src/apis/pairRoom.ts new file mode 100644 index 00000000..2a917c66 --- /dev/null +++ b/frontend/src/apis/pairRoom.ts @@ -0,0 +1,62 @@ +import fetcher from '@/apis/fetcher'; + +import { ERROR_MESSAGES } from '@/constants/message'; + +const API_URL = process.env.REACT_APP_API_URL; + +export type PairRoomStatus = 'IN_PROGRESS' | 'COMPLETED'; + +export interface GetPairRoomResponse { + id: number; + navigator: string; + driver: string; + status: PairRoomStatus; +} + +export const getPairRoom = async (accessCode: string): Promise => { + const response = await fetcher.get({ + url: `${API_URL}/pair-room/${accessCode}`, + errorMessage: ERROR_MESSAGES.GET_PAIR_ROOM, + }); + + return await response.json(); +}; + +export const getPairRoomExists = async (accessCode: string): Promise<{ exists: boolean }> => { + const response = await fetcher.get({ + url: `${API_URL}/pair-room/exists?access_code=${accessCode}`, + errorMessage: ERROR_MESSAGES.GET_PAIR_ROOM, + }); + + return await response.json(); +}; + +interface AddPairRoomRequest { + driver: string; + navigator: string; + timerDuration: number; + timerRemainingTime: number; +} + +export const addPairRoom = async ({ driver, navigator, timerDuration, timerRemainingTime }: AddPairRoomRequest) => { + const response = await fetcher.post({ + url: `${API_URL}/pair-room`, + body: JSON.stringify({ driver, navigator, timerDuration, timerRemainingTime, status: 'IN_PROGRESS' }), + errorMessage: '', + }); + + const { accessCode } = await response.json(); + + return accessCode; +}; + +interface UpdatePairRoleRequest { + accessCode: string; +} + +export const updatePairRole = async ({ accessCode }: UpdatePairRoleRequest) => { + await fetcher.patch({ + url: `${API_URL}/pair-room/${accessCode}/pair-swap`, + errorMessage: '', + }); +}; diff --git a/frontend/src/apis/referenceLink.ts b/frontend/src/apis/referenceLink.ts new file mode 100644 index 00000000..1477bc73 --- /dev/null +++ b/frontend/src/apis/referenceLink.ts @@ -0,0 +1,57 @@ +import fetcher from '@/apis/fetcher'; + +import { ERROR_MESSAGES } from '@/constants/message'; + +const API_URL = process.env.REACT_APP_API_URL; + +export interface Link { + id: number; + url: string; + headTitle: string; + openGraphTitle: string; + description: string; + image: string; + categoryName: string; +} + +interface GetReferenceLinksRequest { + accessCode: string; + categoryId: string; +} + +export const getReferenceLinks = async ({ accessCode, categoryId }: GetReferenceLinksRequest): Promise => { + const categoryParamsUrl = categoryId === '0' ? `` : `?categoryId=${categoryId}`; + + const response = await fetcher.get({ + url: `${API_URL}/${accessCode}/reference-link${categoryParamsUrl}`, + errorMessage: ERROR_MESSAGES.GET_REFERENCE_LINKS, + }); + + return await response.json(); +}; + +interface AddReferenceLinkRequest { + url: string; + accessCode: string; + categoryId: string | null; +} + +export const addReferenceLink = async ({ url, accessCode, categoryId }: AddReferenceLinkRequest) => { + await fetcher.post({ + url: `${API_URL}/${accessCode}/reference-link`, + body: JSON.stringify({ url, categoryId }), + errorMessage: ERROR_MESSAGES.ADD_REFERENCE_LINKS, + }); +}; + +interface DeleteReferenceLinkRequest { + id: number; + accessCode: string; +} + +export const deleteReferenceLink = async ({ id, accessCode }: DeleteReferenceLinkRequest) => { + await fetcher.delete({ + url: `${API_URL}/${accessCode}/reference-link/${id}`, + errorMessage: ERROR_MESSAGES.DELETE_REFERENCE_LINKS, + }); +}; diff --git a/frontend/src/apis/timer.ts b/frontend/src/apis/timer.ts new file mode 100644 index 00000000..358fe295 --- /dev/null +++ b/frontend/src/apis/timer.ts @@ -0,0 +1,49 @@ +import fetcher from '@/apis/fetcher'; + +const API_URL = process.env.REACT_APP_API_URL; + +export const getSSEConnection = (accessCode: string) => { + return new EventSource(`${API_URL}/${accessCode}/connect`); +}; + +export interface GetTimerResponse { + id: number; + duration: number; + remainingTime: number; +} + +export const getTimer = async (accessCode: string): Promise => { + const response = await fetcher.get({ + url: `${API_URL}/${accessCode}/timer`, + errorMessage: '', + }); + + return response.json(); +}; + +interface UpdateDurationRequest { + duration: string; + accessCode: string; +} + +export const updateDuration = async ({ duration, accessCode }: UpdateDurationRequest) => { + await fetcher.patch({ + url: `${API_URL}/${accessCode}/timer`, + body: JSON.stringify({ duration: Number(duration) * 60 * 1000, remainingTime: Number(duration) * 60 * 1000 }), + errorMessage: '', + }); +}; + +export const startTimer = async (accessCode: string) => { + await fetcher.patch({ + url: `${API_URL}/${accessCode}/timer/start`, + errorMessage: '', + }); +}; + +export const stopTimer = async (accessCode: string) => { + await fetcher.patch({ + url: `${API_URL}/${accessCode}/timer/stop`, + errorMessage: '', + }); +}; diff --git a/frontend/src/apis/todo.ts b/frontend/src/apis/todo.ts new file mode 100644 index 00000000..fe021ae4 --- /dev/null +++ b/frontend/src/apis/todo.ts @@ -0,0 +1,82 @@ +import fetcher from '@/apis/fetcher'; + +import { ERROR_MESSAGES } from '@/constants/message'; + +const API_URL = process.env.REACT_APP_API_URL; + +export interface Todo { + id: number; + content: string; + isChecked: boolean; + order: number; +} + +export const getTodos = async (accessCode: string): Promise => { + const response = await fetcher.get({ + url: `${API_URL}/${accessCode}/todos`, + errorMessage: ERROR_MESSAGES.GET_TODOS, + }); + + return await response.json(); +}; + +interface AddTodosRequest { + content: string; + accessCode: string; +} + +export const addTodos = async ({ content, accessCode }: AddTodosRequest) => { + await fetcher.post({ + url: `${API_URL}/${accessCode}/todos`, + body: JSON.stringify({ content }), + errorMessage: ERROR_MESSAGES.ADD_TODO, + }); +}; + +interface UpdateContentsRequest { + todoId: number; + contents: string; +} + +export const updateContents = async ({ todoId, contents }: UpdateContentsRequest) => { + await fetcher.patch({ + url: `${API_URL}/todos/${todoId}/contents`, + body: JSON.stringify({ contents }), + errorMessage: ERROR_MESSAGES.UPDATE_TODO, + }); +}; + +interface UpdateOrderRequest { + todoId: number; + order: number; +} + +export const updateOrder = async ({ todoId, order }: UpdateOrderRequest) => { + await fetcher.patch({ + url: `${API_URL}/todos/${todoId}/order`, + body: JSON.stringify({ order }), + errorMessage: ERROR_MESSAGES.UPDATE_TODO, + }); +}; + +interface UpdateCheckedRequest { + todoId: number; +} + +export const updateChecked = async ({ todoId }: UpdateCheckedRequest) => { + await fetcher.patch({ + url: `${API_URL}/todos/${todoId}/checked`, + errorMessage: ERROR_MESSAGES.UPDATE_TODO, + }); +}; + +interface DeleteTodoRequest { + todoId: number; +} + +export const deleteTodo = async ({ todoId }: DeleteTodoRequest) => { + await fetcher.delete({ + url: `${API_URL}/todos/${todoId}`, + errorMessage: ERROR_MESSAGES.DELETE_TODO, + }); +}; diff --git a/frontend/src/assets/audio/alarm_sound.mp3 b/frontend/src/assets/audio/alarm_sound.mp3 new file mode 100644 index 00000000..74d7b773 Binary files /dev/null and b/frontend/src/assets/audio/alarm_sound.mp3 differ diff --git a/frontend/src/assets/images/characters/driver.png b/frontend/src/assets/images/characters/driver.png new file mode 100644 index 00000000..4e19932e Binary files /dev/null and b/frontend/src/assets/images/characters/driver.png differ diff --git a/frontend/src/assets/images/characters/navigator.png b/frontend/src/assets/images/characters/navigator.png new file mode 100644 index 00000000..6320a56f Binary files /dev/null and b/frontend/src/assets/images/characters/navigator.png differ diff --git a/frontend/src/assets/images/check_box_checked.svg b/frontend/src/assets/images/check_box_checked.svg new file mode 100644 index 00000000..c98d8bc8 --- /dev/null +++ b/frontend/src/assets/images/check_box_checked.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/images/check_box_unchecked.svg b/frontend/src/assets/images/check_box_unchecked.svg new file mode 100644 index 00000000..4efd76f9 --- /dev/null +++ b/frontend/src/assets/images/check_box_unchecked.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/assets/images/docs/check-branch-created.png b/frontend/src/assets/images/docs/check-branch-created.png new file mode 100644 index 00000000..e4435e72 Binary files /dev/null and b/frontend/src/assets/images/docs/check-branch-created.png differ diff --git a/frontend/src/assets/images/docs/check-branch-created.webp b/frontend/src/assets/images/docs/check-branch-created.webp new file mode 100644 index 00000000..548c8d25 Binary files /dev/null and b/frontend/src/assets/images/docs/check-branch-created.webp differ diff --git a/frontend/src/assets/images/docs/clone.png b/frontend/src/assets/images/docs/clone.png new file mode 100644 index 00000000..c7108457 Binary files /dev/null and b/frontend/src/assets/images/docs/clone.png differ diff --git a/frontend/src/assets/images/docs/clone.webp b/frontend/src/assets/images/docs/clone.webp new file mode 100644 index 00000000..654c2d0a Binary files /dev/null and b/frontend/src/assets/images/docs/clone.webp differ diff --git a/frontend/src/assets/images/docs/complete-onboarding.png b/frontend/src/assets/images/docs/complete-onboarding.png new file mode 100644 index 00000000..e4a8fa5b Binary files /dev/null and b/frontend/src/assets/images/docs/complete-onboarding.png differ diff --git a/frontend/src/assets/images/docs/complete-onboarding.webp b/frontend/src/assets/images/docs/complete-onboarding.webp new file mode 100644 index 00000000..d80eae9f Binary files /dev/null and b/frontend/src/assets/images/docs/complete-onboarding.webp differ diff --git a/frontend/src/assets/images/docs/create-branch.png b/frontend/src/assets/images/docs/create-branch.png new file mode 100644 index 00000000..27454f4f Binary files /dev/null and b/frontend/src/assets/images/docs/create-branch.png differ diff --git a/frontend/src/assets/images/docs/create-branch.webp b/frontend/src/assets/images/docs/create-branch.webp new file mode 100644 index 00000000..3d661762 Binary files /dev/null and b/frontend/src/assets/images/docs/create-branch.webp differ diff --git a/frontend/src/assets/images/docs/create-fork.png b/frontend/src/assets/images/docs/create-fork.png new file mode 100644 index 00000000..324b5ae6 Binary files /dev/null and b/frontend/src/assets/images/docs/create-fork.png differ diff --git a/frontend/src/assets/images/docs/create-fork.webp b/frontend/src/assets/images/docs/create-fork.webp new file mode 100644 index 00000000..8c3db033 Binary files /dev/null and b/frontend/src/assets/images/docs/create-fork.webp differ diff --git a/frontend/src/assets/images/docs/create-room.png b/frontend/src/assets/images/docs/create-room.png new file mode 100644 index 00000000..8358dc02 Binary files /dev/null and b/frontend/src/assets/images/docs/create-room.png differ diff --git a/frontend/src/assets/images/docs/create-room.webp b/frontend/src/assets/images/docs/create-room.webp new file mode 100644 index 00000000..22cc7d96 Binary files /dev/null and b/frontend/src/assets/images/docs/create-room.webp differ diff --git a/frontend/src/assets/images/docs/fork-repository.png b/frontend/src/assets/images/docs/fork-repository.png new file mode 100644 index 00000000..9a8d1422 Binary files /dev/null and b/frontend/src/assets/images/docs/fork-repository.png differ diff --git a/frontend/src/assets/images/docs/fork-repository.webp b/frontend/src/assets/images/docs/fork-repository.webp new file mode 100644 index 00000000..f2e371f4 Binary files /dev/null and b/frontend/src/assets/images/docs/fork-repository.webp differ diff --git a/frontend/src/assets/images/docs/input-name.png b/frontend/src/assets/images/docs/input-name.png new file mode 100644 index 00000000..234369f9 Binary files /dev/null and b/frontend/src/assets/images/docs/input-name.png differ diff --git a/frontend/src/assets/images/docs/input-name.webp b/frontend/src/assets/images/docs/input-name.webp new file mode 100644 index 00000000..0b491126 Binary files /dev/null and b/frontend/src/assets/images/docs/input-name.webp differ diff --git a/frontend/src/assets/images/docs/input-pair-name.png b/frontend/src/assets/images/docs/input-pair-name.png new file mode 100644 index 00000000..9b04526c Binary files /dev/null and b/frontend/src/assets/images/docs/input-pair-name.png differ diff --git a/frontend/src/assets/images/docs/input-pair-name.webp b/frontend/src/assets/images/docs/input-pair-name.webp new file mode 100644 index 00000000..46ca0f0e Binary files /dev/null and b/frontend/src/assets/images/docs/input-pair-name.webp differ diff --git a/frontend/src/assets/images/docs/select-driver.png b/frontend/src/assets/images/docs/select-driver.png new file mode 100644 index 00000000..d4974bea Binary files /dev/null and b/frontend/src/assets/images/docs/select-driver.png differ diff --git a/frontend/src/assets/images/docs/select-driver.webp b/frontend/src/assets/images/docs/select-driver.webp new file mode 100644 index 00000000..0cbb6ecb Binary files /dev/null and b/frontend/src/assets/images/docs/select-driver.webp differ diff --git a/frontend/src/assets/images/docs/select-mission.png b/frontend/src/assets/images/docs/select-mission.png new file mode 100644 index 00000000..a46df445 Binary files /dev/null and b/frontend/src/assets/images/docs/select-mission.png differ diff --git a/frontend/src/assets/images/docs/select-mission.webp b/frontend/src/assets/images/docs/select-mission.webp new file mode 100644 index 00000000..52754e77 Binary files /dev/null and b/frontend/src/assets/images/docs/select-mission.webp differ diff --git a/frontend/src/assets/images/docs/set-role.png b/frontend/src/assets/images/docs/set-role.png new file mode 100644 index 00000000..8a53ae4b Binary files /dev/null and b/frontend/src/assets/images/docs/set-role.png differ diff --git a/frontend/src/assets/images/docs/set-role.webp b/frontend/src/assets/images/docs/set-role.webp new file mode 100644 index 00000000..c50750b8 Binary files /dev/null and b/frontend/src/assets/images/docs/set-role.webp differ diff --git a/frontend/src/assets/images/docs/set-timer.png b/frontend/src/assets/images/docs/set-timer.png new file mode 100644 index 00000000..28aac747 Binary files /dev/null and b/frontend/src/assets/images/docs/set-timer.png differ diff --git a/frontend/src/assets/images/docs/set-timer.webp b/frontend/src/assets/images/docs/set-timer.webp new file mode 100644 index 00000000..20915ab1 Binary files /dev/null and b/frontend/src/assets/images/docs/set-timer.webp differ diff --git a/frontend/src/assets/images/docs/start-free.png b/frontend/src/assets/images/docs/start-free.png new file mode 100644 index 00000000..55d1f8be Binary files /dev/null and b/frontend/src/assets/images/docs/start-free.png differ diff --git a/frontend/src/assets/images/docs/start-free.webp b/frontend/src/assets/images/docs/start-free.webp new file mode 100644 index 00000000..3931919a Binary files /dev/null and b/frontend/src/assets/images/docs/start-free.webp differ diff --git a/frontend/src/assets/images/docs/start-with-mission.png b/frontend/src/assets/images/docs/start-with-mission.png new file mode 100644 index 00000000..3cff8f87 Binary files /dev/null and b/frontend/src/assets/images/docs/start-with-mission.png differ diff --git a/frontend/src/assets/images/docs/start-with-mission.webp b/frontend/src/assets/images/docs/start-with-mission.webp new file mode 100644 index 00000000..2a63b352 Binary files /dev/null and b/frontend/src/assets/images/docs/start-with-mission.webp differ diff --git a/frontend/src/assets/images/github-mark-white.png b/frontend/src/assets/images/github-mark-white.png new file mode 100644 index 00000000..50b81752 Binary files /dev/null and b/frontend/src/assets/images/github-mark-white.png differ diff --git a/frontend/src/assets/images/github-mark.png b/frontend/src/assets/images/github-mark.png new file mode 100644 index 00000000..6cb3b705 Binary files /dev/null and b/frontend/src/assets/images/github-mark.png differ diff --git a/frontend/src/assets/images/logo_icon.svg b/frontend/src/assets/images/logo_icon.svg new file mode 100644 index 00000000..102f4595 --- /dev/null +++ b/frontend/src/assets/images/logo_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/images/logo_icon_with_title.svg b/frontend/src/assets/images/logo_icon_with_title.svg new file mode 100644 index 00000000..5c8c7e38 --- /dev/null +++ b/frontend/src/assets/images/logo_icon_with_title.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/assets/images/logo_title.svg b/frontend/src/assets/images/logo_title.svg new file mode 100644 index 00000000..77a02428 --- /dev/null +++ b/frontend/src/assets/images/logo_title.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/images/wave.svg b/frontend/src/assets/images/wave.svg new file mode 100644 index 00000000..91b05aa7 --- /dev/null +++ b/frontend/src/assets/images/wave.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/assets/index.ts b/frontend/src/assets/index.ts new file mode 100644 index 00000000..ab07cd10 --- /dev/null +++ b/frontend/src/assets/index.ts @@ -0,0 +1,25 @@ +import AlarmSound from '@/assets/audio/alarm_sound.mp3'; +import Driver from '@/assets/images/characters/driver.png'; +import Navigator from '@/assets/images/characters/navigator.png'; +import CheckBoxChecked from '@/assets/images/check_box_checked.svg'; +import CheckBoxUnchecked from '@/assets/images/check_box_unchecked.svg'; +import GithubLogoWhite from '@/assets/images/github-mark-white.png'; +import GithubLogo from '@/assets/images/github-mark.png'; +import LogoIcon from '@/assets/images/logo_icon.svg'; +import LogoIconWithTitle from '@/assets/images/logo_icon_with_title.svg'; +import LogoTitle from '@/assets/images/logo_title.svg'; +import Wave from '@/assets/images/wave.svg'; + +export { + GithubLogo, + GithubLogoWhite, + CheckBoxUnchecked, + CheckBoxChecked, + LogoIcon, + LogoIconWithTitle, + LogoTitle, + Wave, + AlarmSound, + Driver, + Navigator, +}; diff --git a/frontend/src/components/CoduoDocs/DocsImage/DocsImage.stories.tsx b/frontend/src/components/CoduoDocs/DocsImage/DocsImage.stories.tsx new file mode 100644 index 00000000..0cbce9e3 --- /dev/null +++ b/frontend/src/components/CoduoDocs/DocsImage/DocsImage.stories.tsx @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import DocsImage from '@/components/CoduoDocs/DocsImage/DocsImage'; + +const meta = { + title: 'component/CoduoDocs/DocsImage', + component: DocsImage, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + information: '1. λ°© λ§Œλ“€κΈ° λ²„νŠΌμ„ λˆ„λ₯΄λ©΄ νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ„ 진행할 방이 μƒμ„±λ©λ‹ˆλ‹€.', + src: 'https://i.pravatar.cc', + }, +}; diff --git a/frontend/src/components/CoduoDocs/DocsImage/DocsImage.styles.ts b/frontend/src/components/CoduoDocs/DocsImage/DocsImage.styles.ts new file mode 100644 index 00000000..d9106e53 --- /dev/null +++ b/frontend/src/components/CoduoDocs/DocsImage/DocsImage.styles.ts @@ -0,0 +1,23 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + display: flex; + flex-direction: column; + gap: 2rem; +`; +export const Contents = styled.p` + color: ${({ theme }) => theme.color.black[90]}; + font-size: ${({ theme }) => theme.fontSize.lg}; +`; + +export const Image = styled.img` + width: 90rem; + height: 50rem; + object-fit: cover; + object-position: center; + + @media (width <= 1000px) { + width: 60rem; + height: 30rem; + } +`; diff --git a/frontend/src/components/CoduoDocs/DocsImage/DocsImage.tsx b/frontend/src/components/CoduoDocs/DocsImage/DocsImage.tsx new file mode 100644 index 00000000..0dc79dcb --- /dev/null +++ b/frontend/src/components/CoduoDocs/DocsImage/DocsImage.tsx @@ -0,0 +1,24 @@ +import * as S from './DocsImage.styles'; + +interface DocsImageProps { + information?: string; + src: string; + webpSrc: string; + alt: string; + id?: string; +} + +const DocsImage = ({ information, src, alt, webpSrc, id, children }: React.PropsWithChildren) => { + return ( + + {information && {information}} + {children} + + + {alt} + + + ); +}; + +export default DocsImage; diff --git a/frontend/src/components/CoduoDocs/FloatingSidebar/ContentBox.tsx b/frontend/src/components/CoduoDocs/FloatingSidebar/ContentBox.tsx new file mode 100644 index 00000000..9c5c2a7a --- /dev/null +++ b/frontend/src/components/CoduoDocs/FloatingSidebar/ContentBox.tsx @@ -0,0 +1,36 @@ +import { useNavigate } from 'react-router-dom'; + +import { Content } from '@/pages/CoduoDocs/CoduoDocs.type'; + +import * as S from './FloatingSidebar.styles'; + +interface ContentBoxProps { + title: string; + contents: Content[]; + activeSection: string; +} + +const ContentBox = ({ title, contents, activeSection }: ContentBoxProps) => { + const navigate = useNavigate(); + return ( + + {title} + + {contents.map((content) => { + return ( + navigate(`#${content.id}`)} + key={content.id} + to={`#${content.id}`} + $isActive={content.id === activeSection} + > + {content.subtitle} + + ); + })} + + + ); +}; + +export default ContentBox; diff --git a/frontend/src/components/CoduoDocs/FloatingSidebar/FloatingSidebar.stories.tsx b/frontend/src/components/CoduoDocs/FloatingSidebar/FloatingSidebar.stories.tsx new file mode 100644 index 00000000..1c244f4a --- /dev/null +++ b/frontend/src/components/CoduoDocs/FloatingSidebar/FloatingSidebar.stories.tsx @@ -0,0 +1,23 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import ContentBox from '@/components/CoduoDocs/FloatingSidebar/ContentBox'; +import FloatingSidebar from '@/components/CoduoDocs/FloatingSidebar/FloatingSidebar'; + +import { START_CONTENT } from '@/constants/coduoDocs'; + +const meta = { + title: 'component/CoduoDocs/FloatingSidebar', + component: FloatingSidebar, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => ( + + + + ), +}; diff --git a/frontend/src/components/CoduoDocs/FloatingSidebar/FloatingSidebar.styles.ts b/frontend/src/components/CoduoDocs/FloatingSidebar/FloatingSidebar.styles.ts new file mode 100644 index 00000000..41e09c0c --- /dev/null +++ b/frontend/src/components/CoduoDocs/FloatingSidebar/FloatingSidebar.styles.ts @@ -0,0 +1,90 @@ +import { Link } from 'react-router-dom'; + +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 2.3rem; + + position: fixed; + top: 15rem; + left: 4%; + + background-color: ${({ theme }) => theme.color.black[10]}; + + @media (width <= 1400px) { + gap: 1.8rem; + + top: 12rem; + left: 4%; + + padding: 2rem; + } + + @media (width <= 1000px) { + display: none; + } +`; + +export const Title = styled.p` + color: ${({ theme }) => theme.color.primary[700]}; + font-size: ${({ theme }) => theme.fontSize.base}; + font-weight: ${({ theme }) => theme.fontWeight.semibold}; + + @media (width <= 1400px) { + font-size: ${({ theme }) => theme.fontSize.base}; + } +`; + +export const ContentList = styled.ul` + display: flex; + flex-direction: column; + gap: 1rem; + + position: relative; + + padding-left: 15px; + + @media (width <= 1400px) { + gap: 0.8rem; + } +`; + +export const ContentItem = styled(Link)<{ $isActive: boolean }>` + position: relative; + + color: ${({ $isActive, theme }) => ($isActive ? theme.color.black[90] : theme.color.black[60])}; + font-size: ${({ theme }) => theme.fontSize.lg}; + text-decoration: none; + + transition: all 0.1s; + + &::before { + position: absolute; + top: 0; + left: -2rem; + + width: 3px; + height: 145%; + + background-color: ${({ $isActive, theme }) => ($isActive ? theme.color.secondary[500] : theme.color.black[30])}; + + transition: all 0.2s; + content: ''; + } + + @media (width <= 1400px) { + font-size: ${({ theme }) => theme.fontSize.md}; + } +`; + +export const Container = styled.div` + display: flex; + flex-direction: column; + gap: 1.1rem; + + @media (width <= 1400px) { + gap: 0.9rem; + } +`; diff --git a/frontend/src/components/CoduoDocs/FloatingSidebar/FloatingSidebar.tsx b/frontend/src/components/CoduoDocs/FloatingSidebar/FloatingSidebar.tsx new file mode 100644 index 00000000..659643c9 --- /dev/null +++ b/frontend/src/components/CoduoDocs/FloatingSidebar/FloatingSidebar.tsx @@ -0,0 +1,7 @@ +import * as S from './FloatingSidebar.styles'; + +const FloatingSidebar = ({ children }: React.PropsWithChildren) => { + return {children}; +}; + +export default FloatingSidebar; diff --git a/frontend/src/components/CoduoDocs/Quote/Quote.styles.ts b/frontend/src/components/CoduoDocs/Quote/Quote.styles.ts new file mode 100644 index 00000000..a1c7809d --- /dev/null +++ b/frontend/src/components/CoduoDocs/Quote/Quote.styles.ts @@ -0,0 +1,34 @@ +import styled from 'styled-components'; + +export const Container = styled.div` + display: flex; + align-items: center; + gap: 0.8rem; + + text-decoration: none; +`; + +export const QuoteBar = styled.span` + color: ${({ theme }) => theme.color.black[50]}; + font-size: ${({ theme }) => theme.fontSize.h5}; + font-weight: ${({ theme }) => theme.fontWeight.extraBold}; +`; + +export const Content = styled.p` + color: ${({ theme }) => theme.color.black[90]}; + font-size: ${({ theme }) => theme.fontSize.base}; + font-weight: ${({ theme }) => theme.fontWeight.extraLight}; +`; + +export const TextLink = styled.a` + color: ${({ theme }) => theme.color.primary[700]}; + font-size: ${({ theme }) => theme.fontSize.base}; + font-weight: ${({ theme }) => theme.fontWeight.semibold}; + text-decoration: underline; + + transition: color 0.2s ease; + + &:hover { + color: ${({ theme }) => theme.color.primary[800]}; + } +`; diff --git a/frontend/src/components/CoduoDocs/Quote/Quote.tsx b/frontend/src/components/CoduoDocs/Quote/Quote.tsx new file mode 100644 index 00000000..e6ab9ee6 --- /dev/null +++ b/frontend/src/components/CoduoDocs/Quote/Quote.tsx @@ -0,0 +1,24 @@ +import * as S from './Quote.styles'; + +interface QuoteProps { + text: string; + href?: string; + linkText?: string; + isNewBrowserOpen?: boolean; +} + +const Quote = ({ text, href, linkText, isNewBrowserOpen = false }: QuoteProps) => { + return ( + + | + {text} + {href && linkText && ( + + {linkText} + + )} + + ); +}; + +export default Quote; diff --git a/frontend/src/components/CoduoDocs/SourceCode/SourceCode.styles.ts b/frontend/src/components/CoduoDocs/SourceCode/SourceCode.styles.ts new file mode 100644 index 00000000..0c02e5b3 --- /dev/null +++ b/frontend/src/components/CoduoDocs/SourceCode/SourceCode.styles.ts @@ -0,0 +1,46 @@ +import { AiFillCopy } from 'react-icons/ai'; +import styled from 'styled-components'; + +export const Container = styled.div` + display: flex; + align-items: center; + gap: 0.8rem; + + position: relative; + + padding: 3rem 4rem; + border-radius: 0.5rem; + + background-color: ${({ theme }) => theme.color.black[30]}; +`; + +export const Content = styled.p` + color: ${({ theme }) => theme.color.black[90]}; + font-size: ${({ theme }) => theme.fontSize.base}; + line-height: 1.8; + white-space: pre-wrap; + word-break: break-word; +`; + +export const CopyIcon = styled(AiFillCopy)` + position: absolute; + top: 1rem; + right: 1rem; + + color: ${({ theme }) => theme.color.black[50]}; + font-size: 2rem; + + cursor: pointer; + + &:hover { + color: ${({ theme }) => theme.color.black[60]}; + + transform: scale(1.03); + } + + &:active { + color: ${({ theme }) => theme.color.black[70]}; + + transform: scale(1.06); + } +`; diff --git a/frontend/src/components/CoduoDocs/SourceCode/SourceCode.tsx b/frontend/src/components/CoduoDocs/SourceCode/SourceCode.tsx new file mode 100644 index 00000000..d2a70351 --- /dev/null +++ b/frontend/src/components/CoduoDocs/SourceCode/SourceCode.tsx @@ -0,0 +1,19 @@ +import useCopyClipBoard from '@/hooks/common/useCopyClipboard'; + +import * as S from './SourceCode.styles'; + +interface SourceCodeProps { + code: string; +} + +const SourceCode = ({ code }: SourceCodeProps) => { + const [, onCopy] = useCopyClipBoard(); + return ( + + {code} + onCopy(code)} /> + + ); +}; + +export default SourceCode; diff --git a/frontend/src/components/Landing/HowToPair/HowToPair.styles.ts b/frontend/src/components/Landing/HowToPair/HowToPair.styles.ts new file mode 100644 index 00000000..f6c62678 --- /dev/null +++ b/frontend/src/components/Landing/HowToPair/HowToPair.styles.ts @@ -0,0 +1,127 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 15rem; + overflow-x: hidden; + + position: relative; + + padding: 10rem 4rem; + + background: linear-gradient( + 75deg, + ${({ theme }) => theme.color.secondary[100]}, + ${({ theme }) => theme.color.primary[200]} + ); + background-color: ${({ theme }) => theme.color.black[10]}; + color: ${({ theme }) => theme.color.black[80]}; + line-height: 1.2; +`; + +export const Section = styled.section<{ $textAlign?: 'left' | 'center' | 'right' }>` + display: flex; + flex-direction: row; + + width: 100%; + border-radius: 1rem; + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + flex-direction: column; + justify-content: center; + align-items: center; + + text-align: center; + word-break: keep-all; + } + + text-align: ${({ $textAlign = 'left' }) => $textAlign}; +`; + +export const TextBoxContainer = styled.div` + display: flex; + flex-direction: row; + gap: 5rem; + + & > * { + flex-basis: 0; + + flex-grow: 1; + } + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + flex-direction: column; + } +`; + +export const TextBox = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + + width: 36rem; + padding: 2rem 4rem; + border-radius: 3rem; + + background-color: #ffeeb4; +`; + +export const SectionText = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + + width: 100%; +`; + +export const SectionTitle = styled.h2` + margin: 2rem 0; + + color: ${({ theme }) => theme.color.primary[800]}; + font-size: ${({ theme }) => theme.fontSize.h3}; + font-weight: ${({ theme }) => theme.fontWeight.bold}; +`; + +export const Paragraph = styled.p` + margin: 1rem 0; + + color: ${({ theme }) => theme.color.black[80]}; + font-size: ${({ theme }) => theme.fontSize.h6}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; + line-height: 2; +`; + +export const Strong = styled.strong` + color: ${({ theme }) => theme.color.primary[700]}; + font-weight: ${({ theme }) => theme.fontWeight.bold}; +`; + +export const Highlighted = styled.span` + color: ${({ theme }) => theme.color.primary[700]}; + font-size: ${({ theme }) => theme.fontSize.h6}; + font-weight: ${({ theme }) => theme.fontWeight.bold}; +`; + +export const Conclusion = styled.p` + padding-left: 1rem; + + color: ${({ theme }) => theme.color.primary[800]}; + font-style: italic; + font-size: ${({ theme }) => theme.fontSize.lg}; + font-weight: ${({ theme }) => theme.fontWeight.semibold}; + line-height: 1.5; + text-align: center; +`; + +export const Character = styled.img<{ $right?: string; $bottom?: string; $left?: string }>` + position: relative; + right: ${({ $right }) => $right}; + bottom: ${({ $bottom }) => $bottom}; + left: ${({ $left }) => $left}; + + width: 36rem; + height: 36rem; +`; diff --git a/frontend/src/components/Landing/HowToPair/HowToPair.tsx b/frontend/src/components/Landing/HowToPair/HowToPair.tsx new file mode 100644 index 00000000..42a0f261 --- /dev/null +++ b/frontend/src/components/Landing/HowToPair/HowToPair.tsx @@ -0,0 +1,119 @@ +import { Driver, Navigator } from '@/assets'; + +import { ScrollAnimationContainer } from '@/components/common/Animation/ScrollAnimationContainer'; + +import * as S from './HowToPair.styles'; + +const HowToPair = () => { + return ( + + + + + νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ΄λž€? + + νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°(Pair Programming)은 두 λͺ…μ˜ ν”„λ‘œκ·Έλž˜λ¨Έκ°€ ν•œ μ»΄ν“¨ν„°μ—μ„œ ν•¨κ»˜ + μž‘μ—…ν•˜λ©° μ†Œν”„νŠΈμ›¨μ–΄ μ½”λ“œλ₯Ό μž‘μ„±ν•˜λŠ” ν˜‘μ—… λ°©μ‹μž…λ‹ˆλ‹€. + + + νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ—μ„œλŠ” 두 μ‚¬λžŒμ΄ 각각 'λ“œλΌμ΄λ²„'(Driver) 와 + 'λ‚΄λΉ„κ²Œμ΄ν„°'(Navigator) 역할을 λ²ˆκ°ˆμ•„ κ°€λ©° μˆ˜ν–‰ν•©λ‹ˆλ‹€: + + + + + + + + + + λ“œλΌμ΄λ²„ +
μ‹€μ œλ‘œ μ½”λ“œλ₯Ό μž‘μ„±ν•˜λŠ” μ‚¬λžŒμœΌλ‘œ,
+ λ‚΄λΉ„κ²Œμ΄ν„°μ˜ 섀계에 따라 +
μ½”λ“œλ₯Ό νƒ€μ΄ν•‘ν•©λ‹ˆλ‹€. +
+
+ + + λ‚΄λΉ„κ²Œμ΄ν„° +
μž‘μ„±λœ μ½”λ“œλ₯Ό μ‹€μ‹œκ°„μœΌλ‘œ κ²€ν† ν•˜κ³ 
+ κ°œμ„ ν•  뢀뢄을 μ œμ•ˆν•˜λ©°,
+ μ½”λ“œμ˜ μ „λ°˜μ μΈ ꡬ쑰λ₯Ό μ„€κ³„ν•©λ‹ˆλ‹€. +
+
+
+
+ + + + + + μ™œ νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ„ ν•΄μ•Ό ν• κΉŒμš”? + + μ„œλ‘œμ˜ λŒ€ν™”λ₯Ό 톡해 μžμ—°μŠ€λŸ½κ²Œ μ½”λ“œ 리뷰가 이루어져 + 였λ₯˜λ₯Ό 쑰기에 λ°œκ²¬ν•˜κ³  μˆ˜μ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€. +
+ λ˜ν•œ μ„œλ‘œ λ‹€λ₯Έ μ‹œκ°μ—μ„œ 문제λ₯Ό 바라보며 더 창의적이고 효율적인 해결책을 + 찾을 수 μžˆμŠ΅λ‹ˆλ‹€. +
+
+
+
+ + + + + νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ˜ 방법 + + μ„œλ‘œ μΌμ •ν•œ μ‹œκ°„ κ°„κ²©μœΌλ‘œ 역할을 κ΅ν™˜ν•˜λ©° 지속적인 λŒ€ν™”λ₯Ό 톡해 μ½”λ“œμ˜ μ§ˆμ„ + ν–₯μƒμ‹œν‚΅λ‹ˆλ‹€.
λ˜ν•œ 주기적으둜 μž‘μ—… 과정을 λ˜λŒμ•„λ³΄κ³  κ°œμ„ μ μ„ λ…Όμ˜ν•˜μ—¬ λ‹€μŒ μ„Έμ…˜μ—μ„œ{' '} + 더 λ‚˜μ€ ν˜‘μ—…μ„ ν•  수 μžˆλ„λ‘ ν•©λ‹ˆλ‹€. +
+
+ +
+
+ + + + νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ€ λ‹¨μˆœνžˆ μ½”λ“œλ₯Ό ν•¨κ»˜ μž‘μ„±ν•˜λŠ” 것을 λ„˜μ–΄, +
ν˜‘μ—…μ„ 톡해 더 λ‚˜μ€ μ½”λ“œλ₯Ό λ§Œλ“€μ–΄ λ‚˜κ°€λŠ” κ³Όμ •μž…λ‹ˆλ‹€. +
+ 이λ₯Ό 톡해 κ°œλ°œμžλ“€μ€ μ„œλ‘œ 배우고 μ„±μž₯ν•˜λ©°, 더 λ‚˜μ€ κ²°κ³Όλ₯Ό λ„μΆœν•  수 μžˆμŠ΅λ‹ˆλ‹€. +
+
+
+ ); +}; + +export default HowToPair; diff --git a/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateComplete/PairRoomCreateComplete.stories.tsx b/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateComplete/PairRoomCreateComplete.stories.tsx new file mode 100644 index 00000000..6a8ae51b --- /dev/null +++ b/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateComplete/PairRoomCreateComplete.stories.tsx @@ -0,0 +1,25 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Modal } from '@/components/common/Modal'; + +import PairRoomCreateComplete from './PairRoomCreateComplete'; + +const meta = { + title: 'component/PairRoomCreateModal/PairRoomCreateComplete', + component: PairRoomCreateComplete, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => { + return ( + console.log()} size="60rem" height="45rem"> + console.log()} /> + console.log()} accessCode="12345" /> + + ); + }, +}; diff --git a/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateComplete/PairRoomCreateComplete.tsx b/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateComplete/PairRoomCreateComplete.tsx new file mode 100644 index 00000000..5202550d --- /dev/null +++ b/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateComplete/PairRoomCreateComplete.tsx @@ -0,0 +1,49 @@ +import { Link } from 'react-router-dom'; + +import { FaRegPaste, FaCheck } from 'react-icons/fa6'; + +import Button from '@/components/common/Button/Button'; +import { Modal } from '@/components/common/Modal'; + +import useCopyClipBoard from '@/hooks/common/useCopyClipboard'; + +import { BUTTON_TEXT } from '@/constants/button'; + +import * as S from '../PairRoomCreateModal.styles'; + +interface PairRoomCreateCompleteProps { + accessCode: string; + closeModal: () => void; +} + +const PairRoomCreateComplete = ({ accessCode, closeModal }: PairRoomCreateCompleteProps) => { + const [isCopy, onCopy] = useCopyClipBoard(); + + const handleCopyClipBoard = (text: string) => { + onCopy(text); + }; + + return ( + <> + + + + handleCopyClipBoard(accessCode)}> + {accessCode} + {isCopy ? : } + + + + + + + + + + + ); +}; + +export default PairRoomCreateComplete; diff --git a/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateModal.stories.tsx b/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateModal.stories.tsx new file mode 100644 index 00000000..5e4123e2 --- /dev/null +++ b/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateModal.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import PairRoomCreateModal from './PairRoomCreateModal'; + +const meta = { + title: 'component/PairRoomCreateModal/PairRoomCreateModal', + component: PairRoomCreateModal, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => { + return console.log()} />; + }, + args: { + isOpen: true, + }, +}; diff --git a/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateModal.styles.ts b/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateModal.styles.ts new file mode 100644 index 00000000..6c9503cd --- /dev/null +++ b/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateModal.styles.ts @@ -0,0 +1,67 @@ +import styled, { css } from 'styled-components'; + +export const InputLayout = styled.div` + display: flex; + flex-direction: column; + gap: 2rem; +`; + +export const Content = styled.div` + display: flex; + justify-content: center; + align-items: center; + gap: 1.2rem; + + padding: 1.2rem 3.2rem; + border-radius: 5rem; + + transition: background-color 0.2s ease-in-out; + + cursor: pointer; + + &:hover { + background-color: ${({ theme }) => theme.color.black[30]}; + } + + &:active { + background-color: ${({ theme }) => theme.color.black[40]}; + } +`; +export const PairRoomCode = styled.p` + font-size: ${({ theme }) => theme.fontSize.h1}; + font-weight: ${({ theme }) => theme.fontWeight.semibold}; +`; + +export const IconBox = styled.div` + padding: 0.5rem; + padding-bottom: 0; + border-radius: 0.5rem; + + color: ${({ theme }) => theme.color.primary[500]}; +`; + +export const ModalBodyWrapper = styled.div` + display: flex; + justify-content: center; + align-items: center; + + width: 100%; + height: 100%; +`; + +export const Layout = styled.div` + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + + width: 100%; + margin-top: 2.5rem; +`; + +export const buttonStyles = css` + width: 100%; + height: 6rem; + + font-size: ${({ theme }) => theme.fontSize.lg}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; +`; diff --git a/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateModal.tsx b/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateModal.tsx new file mode 100644 index 00000000..614808a1 --- /dev/null +++ b/frontend/src/components/Main/PairRoomCreateModal/PairRoomCreateModal.tsx @@ -0,0 +1,51 @@ +import { Link } from 'react-router-dom'; + +import Button from '@/components/common/Button/Button'; +import { Modal } from '@/components/common/Modal'; + +import * as S from './PairRoomCreateModal.styles'; + +interface PairRoomCreateModalProps { + isOpen: boolean; + closeModal: () => void; +} + +const PairRoomCreateModal = ({ isOpen, closeModal }: PairRoomCreateModalProps) => { + // const handleSuccess = () => setStatus('COMPLETE'); + + // const { addPairRoom, accessCode, isPending } = useAddPairRoom(handleSuccess); + + // const createPairRoom = (firstPair: string, secondPair: string) => addPairRoom({ firstPair, secondPair }); + + // if (isPending) + // return ( + // + // + // + // + // + // + // + // ); + + return ( + + + + + + + + + + + + + ); +}; + +export default PairRoomCreateModal; diff --git a/frontend/src/components/Main/PairRoomEntryModal/PairRoomEntryModal.tsx b/frontend/src/components/Main/PairRoomEntryModal/PairRoomEntryModal.tsx new file mode 100644 index 00000000..6da86409 --- /dev/null +++ b/frontend/src/components/Main/PairRoomEntryModal/PairRoomEntryModal.tsx @@ -0,0 +1,62 @@ +import { useNavigate } from 'react-router-dom'; + +import Button from '@/components/common/Button/Button'; +import Input from '@/components/common/Input/Input'; +import { Modal } from '@/components/common/Modal'; + +import useToastStore from '@/stores/toastStore'; + +import { getPairRoomExists } from '@/apis/pairRoom'; + +import useInput from '@/hooks/common/useInput'; + +import { BUTTON_TEXT } from '@/constants/button'; + +interface PairRoomEntryModal { + isOpen: boolean; + closeModal: () => void; +} + +const PairRoomEntryModal = ({ isOpen, closeModal }: PairRoomEntryModal) => { + const navigate = useNavigate(); + + const { addToast } = useToastStore(); + const { value, status, message, handleChange } = useInput(); + + const enterPairRoom = async () => { + const { exists } = await getPairRoomExists(value); + + if (!exists) { + addToast({ status: 'ERROR', message: 'ν•΄λ‹Ή μ½”λ“œμ™€ μΌμΉ˜ν•˜λŠ” 방이 μ—†μŠ΅λ‹ˆλ‹€.' }); + return; + } + + navigate(`/room/${value}`); + }; + + return ( + + + + + + + + + + + + ); +}; + +export default PairRoomEntryModal; diff --git a/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.styles.ts b/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.styles.ts new file mode 100644 index 00000000..d00b9d6a --- /dev/null +++ b/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.styles.ts @@ -0,0 +1,109 @@ +import styled, { keyframes, css } from 'styled-components'; + +import type { PairRoomStatus } from '@/apis/pairRoom'; + +const flow = keyframes` + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +`; + +export const Layout = styled.button<{ $status: PairRoomStatus }>` + display: flex; + justify-content: space-between; + align-items: center; + overflow: hidden; + + position: relative; + + width: 100%; + padding: 3rem; + border-radius: 1rem; + + font-size: ${({ theme }) => theme.fontSize.base}; + + &::before { + content: ''; + + position: absolute; + top: 0; + left: 0; + z-index: -1; + + width: 100%; + height: 100%; + border-radius: 1rem; + + background: ${({ $status, theme }) => + $status === 'IN_PROGRESS' + ? `linear-gradient( + 120deg, + ${theme.color.secondary[100]} 0 75%, + ${theme.color.secondary[600]} 75% 100% + )` + : `linear-gradient( + 120deg, + ${theme.color.black[30]} 0 75%, + ${theme.color.black[60]} 75% 100% + )`}; + opacity: 0.7; + + transition: opacity 0.2s ease-in-out; + } + + &:hover::before { + opacity: 1; + } +`; + +export const RoleTextContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; +`; + +export const RoleText = styled.p<{ $status: PairRoomStatus }>` + display: flex; + align-items: center; + gap: 1rem; + + font-size: ${({ theme }) => theme.fontSize.md}; + + span { + color: ${({ $status, theme }) => ($status === 'IN_PROGRESS' ? theme.color.secondary[600] : theme.color.black[70])}; + font-size: ${({ theme }) => theme.fontSize.lg}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; + } +`; + +const inProgressText = css` + background: linear-gradient( + 90deg, + ${({ theme }) => theme.color.black[60]}, + ${({ theme }) => theme.color.black[70]}, + ${({ theme }) => theme.color.black[60]} + ); + + animation: ${flow} 4s linear infinite; + background-size: 200% 100%; + background-clip: text; +`; + +export const StatusText = styled.p<{ $status: PairRoomStatus }>` + color: ${({ $status, theme }) => ($status === 'IN_PROGRESS' ? 'transparent' : theme.color.black[70])}; + font-size: ${({ theme }) => theme.fontSize.base}; + letter-spacing: 0.15rem; + + ${({ $status }) => $status === 'IN_PROGRESS' && inProgressText} +`; + +export const ConnectText = styled.div` + display: flex; + align-items: center; + gap: 0.4rem; + + color: ${({ theme }) => theme.color.black[10]}; +`; diff --git a/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.tsx b/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.tsx new file mode 100644 index 00000000..2f1a7c2e --- /dev/null +++ b/frontend/src/components/MyPage/PairRoomButton/PairRoomButton.tsx @@ -0,0 +1,40 @@ +import { Link } from 'react-router-dom'; + +import { IoIosArrowForward } from 'react-icons/io'; + +import type { PairRoomStatus } from '@/apis/pairRoom'; + +import * as S from './PairRoomButton.styles'; + +interface PairRoomButtonProps { + driver: string; + navigator: string; + status: PairRoomStatus; + accessCode: string; +} + +const PairRoomButton = ({ driver, navigator, status, accessCode }: PairRoomButtonProps) => { + return ( + + + + + λ“œλΌμ΄λ²„ + {driver} + + + λ‚΄λΉ„κ²Œμ΄ν„° + {navigator} + + + {status === 'IN_PROGRESS' ? '진행 쀑' : '진행 μ™„λ£Œ'} + + 접속 + + + + + ); +}; + +export default PairRoomButton; diff --git a/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.stories.tsx b/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.stories.tsx new file mode 100644 index 00000000..efd17047 --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import DeleteButton from './DeleteButton'; + +const meta = { + title: 'component/PairRoom/PairListCard/DeleteButton', + component: DeleteButton, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + isOpen: true, + onRoomDelete: () => alert('Room deleted'), + }, +}; + +export const Closed: Story = { + args: { + isOpen: false, + onRoomDelete: () => alert('Room deleted'), + }, +}; diff --git a/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.styles.ts b/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.styles.ts new file mode 100644 index 00000000..379a87b5 --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.styles.ts @@ -0,0 +1,22 @@ +import styled from 'styled-components'; + +export const Layout = styled.button` + display: flex; + justify-content: center; + align-items: center; + gap: 0.8rem; + + position: absolute; + bottom: 0; + + width: 100%; + height: 6rem; + margin-top: auto; + border-radius: 0 0 2rem 2rem; + + background-color: ${({ theme }) => theme.color.danger[200]}; + color: ${({ theme }) => theme.color.danger[600]}; + font-size: ${({ theme }) => theme.fontSize.base}; + + cursor: pointer; +`; diff --git a/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.tsx b/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.tsx new file mode 100644 index 00000000..162847db --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/DeleteButton/DeleteButton.tsx @@ -0,0 +1,17 @@ +import { FaTrashAlt } from 'react-icons/fa'; + +import * as S from './DeleteButton.styles'; + +interface DeleteButtonProps { + isOpen: boolean; + onRoomDelete: () => void; +} + +const DeleteButton = ({ isOpen, onRoomDelete }: DeleteButtonProps) => ( + + + {isOpen && λ°© μ‚­μ œν•˜κΈ°} + +); + +export default DeleteButton; diff --git a/frontend/src/components/PairRoom/PairListCard/Header/Header.stories.tsx b/frontend/src/components/PairRoom/PairListCard/Header/Header.stories.tsx new file mode 100644 index 00000000..4be5932d --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/Header/Header.stories.tsx @@ -0,0 +1,24 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Header from './Header'; + +const meta = { + title: 'component/PairRoom/PairListCard/Header', + component: Header, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + isOpen: true, + }, +}; + +export const Closed: Story = { + args: { + isOpen: false, + }, +}; diff --git a/frontend/src/components/PairRoom/PairListCard/Header/Header.styles.ts b/frontend/src/components/PairRoom/PairListCard/Header/Header.styles.ts new file mode 100644 index 00000000..ad50ee05 --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/Header/Header.styles.ts @@ -0,0 +1,43 @@ +import { IoIosArrowBack } from 'react-icons/io'; +import styled, { css } from 'styled-components'; + +import { PairRoomCard } from '@/components/PairRoom/PairRoomCard'; + +export const Layout = styled(PairRoomCard.Header)` + p { + height: 4.8rem; + + white-space: nowrap; + } +`; + +export const expandButton = css` + position: absolute; + right: 1rem; + + width: 4rem; + height: 4rem; + border: none; + + background-color: ${({ theme }) => theme.color.black[10]}; + color: ${({ theme }) => theme.color.black[90]}; + + &:hover { + border: none; + + background-color: ${({ theme }) => theme.color.black[30]}; + } + + &:active { + border: none; + + background-color: ${({ theme }) => theme.color.black[50]}; + } +`; + +export const ArrowIcon = styled(IoIosArrowBack)<{ $isOpen: boolean }>` + color: ${({ theme }) => theme.color.black[90]}; + + transform: rotate(${({ $isOpen }) => ($isOpen ? 0 : 180)}deg); + transition: transform 0.2s ease-in-out; +`; diff --git a/frontend/src/components/PairRoom/PairListCard/Header/Header.tsx b/frontend/src/components/PairRoom/PairListCard/Header/Header.tsx new file mode 100644 index 00000000..58da2f4a --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/Header/Header.tsx @@ -0,0 +1,22 @@ +import { IoPeople } from 'react-icons/io5'; + +import Button from '@/components/common/Button/Button'; + +import { theme } from '@/styles/theme'; + +import * as S from './Header.styles'; + +interface HeaderProps { + isOpen: boolean; + toggleOpen: () => void; +} + +const Header = ({ isOpen, toggleOpen }: HeaderProps) => ( + : <>} title={isOpen ? 'νŽ˜μ–΄' : ''}> + + +); + +export default Header; diff --git a/frontend/src/components/PairRoom/PairListCard/PairListCard.stories.tsx b/frontend/src/components/PairRoom/PairListCard/PairListCard.stories.tsx new file mode 100644 index 00000000..e45dc2c4 --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/PairListCard.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import PairListCard from './PairListCard'; + +const meta = { + title: 'component/PairRoom/PairListCard', + component: PairListCard, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + roomCode: 'IUUIASDFJK', + driver: '퍼렁', + navigator: '포둱', + onRoomDelete: () => alert('방이 μ‚­μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.'), + }, +}; diff --git a/frontend/src/components/PairRoom/PairListCard/PairListCard.styles.ts b/frontend/src/components/PairRoom/PairListCard/PairListCard.styles.ts new file mode 100644 index 00000000..823d1e99 --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/PairListCard.styles.ts @@ -0,0 +1,20 @@ +import styled from 'styled-components'; + +export const Layout = styled.div<{ $isOpen: boolean }>` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + position: relative; + + min-width: ${(props) => (props.$isOpen ? '24rem' : '6rem')}; + + white-space: nowrap; + + transition: min-width 0.3s; +`; + +export const Sidebar = styled.div` + overflow: hidden; +`; diff --git a/frontend/src/components/PairRoom/PairListCard/PairListCard.tsx b/frontend/src/components/PairRoom/PairListCard/PairListCard.tsx new file mode 100644 index 00000000..68f02478 --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/PairListCard.tsx @@ -0,0 +1,37 @@ +import { useState } from 'react'; + +// import DeleteButton from '@/components/PairRoom/PairListCard/DeleteButton/DeleteButton'; +import Header from '@/components/PairRoom/PairListCard/Header/Header'; +import PairListSection from '@/components/PairRoom/PairListCard/PairListSection/PairListSection'; +import RoomCodeSection from '@/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection'; +import { PairRoomCard } from '@/components/PairRoom/PairRoomCard'; + +import * as S from './PairListCard.styles'; + +interface PairListCardProps { + driver: string; + navigator: string; + roomCode: string; + onRoomDelete?: () => void; +} + +const PairListCard = ({ driver, navigator, roomCode }: PairListCardProps) => { + const [isOpen, setIsOpen] = useState(true); + + const toggleOpen = () => setIsOpen(!isOpen); + + return ( + + +
+ + + + {/* */} + + + + ); +}; + +export default PairListCard; diff --git a/frontend/src/components/PairRoom/PairListCard/PairListSection/PairListSection.stories.tsx b/frontend/src/components/PairRoom/PairListCard/PairListSection/PairListSection.stories.tsx new file mode 100644 index 00000000..6564bff2 --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/PairListSection/PairListSection.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import PairListSection from './PairListSection'; + +const meta = { + title: 'component/PairRoom/PairListCard/PairListSection', + component: PairListSection, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + isOpen: true, + driver: '퍼렁', + navigator: '포둱', + }, +}; + +export const Closed: Story = { + args: { + isOpen: false, + driver: '퍼렁', + navigator: '포둱', + }, +}; diff --git a/frontend/src/components/PairRoom/PairListCard/PairListSection/PairListSection.styles.ts b/frontend/src/components/PairRoom/PairListCard/PairListSection/PairListSection.styles.ts new file mode 100644 index 00000000..c7dd8841 --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/PairListSection/PairListSection.styles.ts @@ -0,0 +1,35 @@ +import styled from 'styled-components'; + +type Role = 'DRIVER' | 'NAVIGATOR'; + +export const PairItem = styled.div` + display: flex; + justify-content: start; + align-items: center; + gap: 1rem; + + height: 6rem; + padding: 0 1.6rem; + + border-bottom: 1px solid ${({ theme }) => theme.color.black[40]}; +`; + +export const PairRole = styled.span<{ $role: Role }>` + width: 7rem; + padding: 0.4rem 0.8rem; + border-radius: 1.2rem; + + background-color: ${({ theme, $role }) => + $role === 'DRIVER' ? theme.color.primary[500] : theme.color.secondary[500]}; + color: white; + font-size: ${({ theme }) => theme.fontSize.sm}; + text-align: center; +`; + +export const PairName = styled.span` + overflow: hidden; + + font-size: ${({ theme }) => theme.fontSize.base}; + text-overflow: ellipsis; + white-space: nowrap; +`; diff --git a/frontend/src/components/PairRoom/PairListCard/PairListSection/PairListSection.tsx b/frontend/src/components/PairRoom/PairListCard/PairListSection/PairListSection.tsx new file mode 100644 index 00000000..699a9ca2 --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/PairListSection/PairListSection.tsx @@ -0,0 +1,22 @@ +import * as S from './PairListSection.styles'; + +interface PairListSectionProps { + isOpen: boolean; + driver: string; + navigator: string; +} + +const PairListSection = ({ isOpen, driver, navigator }: PairListSectionProps) => ( +
+ + {isOpen && λ“œλΌμ΄λ²„} + {driver} + + + {isOpen && λ‚΄λΉ„κ²Œμ΄ν„°} + {navigator} + +
+); + +export default PairListSection; diff --git a/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.stories.tsx b/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.stories.tsx new file mode 100644 index 00000000..119e4795 --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import RoomCodeSection from './RoomCodeSection'; + +const meta = { + title: 'component/PairRoom/PairListCard/RoomCodeSection', + component: RoomCodeSection, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + isOpen: true, + roomCode: 'IUUIASDFJK', + }, +}; + +export const Closed: Story = { + args: { + isOpen: false, + roomCode: 'IUUIASDFJK', + }, +}; diff --git a/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.styles.ts b/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.styles.ts new file mode 100644 index 00000000..80885e59 --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.styles.ts @@ -0,0 +1,36 @@ +import styled from 'styled-components'; + +export const Layout = styled.div<{ $isOpen: boolean }>` + display: flex; + justify-content: ${({ $isOpen }) => ($isOpen ? 'space-between' : 'center')}; + align-items: center; + + width: 100%; + height: 6rem; + padding: 2rem; + + background-color: ${({ theme }) => theme.color.black[30]}; + + transition: background-color 0.3s ease-out; + + cursor: pointer; +`; + +export const RoomCodeWrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + gap: 1.2rem; +`; + +export const RoomCodeTitle = styled.span` + height: 2rem; + + color: ${({ theme }) => theme.color.black[70]}; + font-size: ${({ theme }) => theme.fontSize.base}; + font-weight: ${({ theme }) => theme.fontWeight.bold}; +`; + +export const RoomCode = styled.span` + font-size: ${({ theme }) => theme.fontSize.md}; +`; diff --git a/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.tsx b/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.tsx new file mode 100644 index 00000000..c202771f --- /dev/null +++ b/frontend/src/components/PairRoom/PairListCard/RoomCodeSection/RoomCodeSection.tsx @@ -0,0 +1,32 @@ +import { FaRegPaste } from 'react-icons/fa6'; + +import useCopyClipBoard from '@/hooks/common/useCopyClipboard'; + +import * as S from './RoomCodeSection.styles'; + +interface RoomCodeSectionProps { + isOpen: boolean; + roomCode: string; +} + +const RoomCodeSection = ({ isOpen, roomCode }: RoomCodeSectionProps) => { + const [, onCopy] = useCopyClipBoard(); + + const handleCopyClipBoard = (text: string) => { + onCopy(text); + }; + + return ( + handleCopyClipBoard(roomCode)}> + {isOpen && ( + + λ°© μ½”λ“œ + {roomCode} + + )} + + + ); +}; + +export default RoomCodeSection; diff --git a/frontend/src/components/PairRoom/PairRoleCard/PairRoleCard.stories.tsx b/frontend/src/components/PairRoom/PairRoleCard/PairRoleCard.stories.tsx new file mode 100644 index 00000000..d842c686 --- /dev/null +++ b/frontend/src/components/PairRoom/PairRoleCard/PairRoleCard.stories.tsx @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import PairRoleCard from '@/components/PairRoom/PairRoleCard/PairRoleCard'; + +const meta = { + title: 'component/PairRoom/PairRoleCard', + component: PairRoleCard, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + driver: '퍼렁', + navigator: '포둱', + }, +}; diff --git a/frontend/src/components/PairRoom/PairRoleCard/PairRoleCard.styles.ts b/frontend/src/components/PairRoom/PairRoleCard/PairRoleCard.styles.ts new file mode 100644 index 00000000..7a9728aa --- /dev/null +++ b/frontend/src/components/PairRoom/PairRoleCard/PairRoleCard.styles.ts @@ -0,0 +1,96 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + + min-width: 60rem; + min-height: 14rem; +`; + +export const RoleBoxContainer = styled.div` + display: flex; + justify-content: space-evenly; + align-items: center; + gap: 2rem; + + height: 100%; + padding: 2rem; +`; + +const RoleBox = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + + width: 18rem; + height: 9.4rem; + padding: 1.6rem 2.4rem; + border-radius: 2rem; +`; + +export const DriverBox = styled(RoleBox)` + background: linear-gradient( + 180deg, + ${({ theme }) => theme.color.secondary[100]}, + ${({ theme }) => theme.color.secondary[300]} + ); +`; + +export const NavigatorBox = styled(RoleBox)` + background: linear-gradient(180deg, ${({ theme }) => theme.color.black[30]}, ${({ theme }) => theme.color.black[50]}); +`; + +export const RoleIcon = styled.p` + font-size: ${({ theme }) => theme.fontSize.h2}; +`; + +export const RoleTextContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 0.6rem; + + p { + font-size: ${({ theme }) => theme.fontSize.h4}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; + } +`; + +const RoleLabel = styled.div` + width: 8rem; + padding: 0.4rem 0; + border-radius: 10rem; + + color: ${({ theme }) => theme.color.black[10]}; + font-size: ${({ theme }) => theme.fontSize.sm}; + text-align: center; +`; + +export const DriverLabel = styled(RoleLabel)` + background-color: ${({ theme }) => theme.color.secondary[600]}; +`; + +export const NavigatorLabel = styled(RoleLabel)` + background-color: ${({ theme }) => theme.color.primary[700]}; +`; + +export const DriverText = styled.p` + overflow: hidden; + + max-width: 10rem; + + color: ${({ theme }) => theme.color.secondary[900]}; + text-overflow: ellipsis; + white-space: nowrap; +`; + +export const NavigatorText = styled.p` + overflow: hidden; + + max-width: 10rem; + + color: ${({ theme }) => theme.color.primary[800]}; + text-overflow: ellipsis; + white-space: nowrap; +`; diff --git a/frontend/src/components/PairRoom/PairRoleCard/PairRoleCard.tsx b/frontend/src/components/PairRoom/PairRoleCard/PairRoleCard.tsx new file mode 100644 index 00000000..aa1b96c1 --- /dev/null +++ b/frontend/src/components/PairRoom/PairRoleCard/PairRoleCard.tsx @@ -0,0 +1,47 @@ +import Tooltip from '@/components/common/Tooltip/Tooltip'; +import { PairRoomCard } from '@/components/PairRoom/PairRoomCard'; + +import * as S from './PairRoleCard.styles'; + +interface PairRoleCardProps { + driver: string; + navigator: string; +} + +const PairRoleCard = ({ driver, navigator }: PairRoleCardProps) => { + return ( + + + + + πŸ’» + + + λ“œλΌμ΄λ²„ + + + {driver} + + + + + + λ‚΄λΉ„κ²Œμ΄ν„° + + {navigator} + + 🧭 + + + + + ); +}; + +export default PairRoleCard; diff --git a/frontend/src/components/PairRoom/PairRoomCard/Header/Header.stories.tsx b/frontend/src/components/PairRoom/PairRoomCard/Header/Header.stories.tsx new file mode 100644 index 00000000..ae7a7fe8 --- /dev/null +++ b/frontend/src/components/PairRoom/PairRoomCard/Header/Header.stories.tsx @@ -0,0 +1,23 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { IoPeople } from 'react-icons/io5'; + +import { theme } from '@/styles/theme'; + +import Header from './Header'; + +const meta = { + title: 'component/PairRoom/PairRoomCard/Header', + component: Header, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + icon: , + title: '제λͺ©', + children:
μΆ”κ°€ 컨텐츠
, + }, +}; diff --git a/frontend/src/components/PairRoom/PairRoomCard/Header/Header.styles.ts b/frontend/src/components/PairRoom/PairRoomCard/Header/Header.styles.ts new file mode 100644 index 00000000..f08cbd65 --- /dev/null +++ b/frontend/src/components/PairRoom/PairRoomCard/Header/Header.styles.ts @@ -0,0 +1,19 @@ +import styled from 'styled-components'; + +export const Layout = styled.div<{ $isOpen: boolean }>` + display: flex; + justify-content: space-between; + align-items: center; + + width: 100%; + height: 6rem; + padding: 2rem; + + font-size: ${({ theme }) => theme.fontSize.lg}; +`; + +export const TitleContainer = styled.div` + display: flex; + align-items: center; + gap: 1rem; +`; diff --git a/frontend/src/components/PairRoom/PairRoomCard/Header/Header.tsx b/frontend/src/components/PairRoom/PairRoomCard/Header/Header.tsx new file mode 100644 index 00000000..dde5bcfa --- /dev/null +++ b/frontend/src/components/PairRoom/PairRoomCard/Header/Header.tsx @@ -0,0 +1,31 @@ +import * as S from './Header.styles'; + +interface HeaderProps { + icon: React.ReactNode; + secondIcon?: React.ReactNode; + title: string; + isOpen?: boolean; + toggleIsOpen?: () => void; +} + +const Header = ({ + icon, + secondIcon, + title, + children, + isOpen = true, + toggleIsOpen, +}: React.PropsWithChildren) => { + return ( + + + {icon} +

{title}

+ {secondIcon} +
+ {children} +
+ ); +}; + +export default Header; diff --git a/frontend/src/components/PairRoom/PairRoomCard/PairRoomCard.stories.tsx b/frontend/src/components/PairRoom/PairRoomCard/PairRoomCard.stories.tsx new file mode 100644 index 00000000..bea710f6 --- /dev/null +++ b/frontend/src/components/PairRoom/PairRoomCard/PairRoomCard.stories.tsx @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { IoPeople } from 'react-icons/io5'; + +import { theme } from '@/styles/theme'; + +import Header from './Header/Header'; +import PairRoomCard from './PairRoomCard'; + +const meta = { + title: 'component/PairRoom/PairRoomCard', + component: PairRoomCard, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const CHILDREN_EXAMPLE = ( + <> +
} title="제λͺ©" /> +
λ³Έλ¬Έ
+ +); + +export const Default: Story = { + args: { + children: CHILDREN_EXAMPLE, + }, +}; diff --git a/frontend/src/components/PairRoom/PairRoomCard/PairRoomCard.styles.ts b/frontend/src/components/PairRoom/PairRoomCard/PairRoomCard.styles.ts new file mode 100644 index 00000000..ae1d6d6b --- /dev/null +++ b/frontend/src/components/PairRoom/PairRoomCard/PairRoomCard.styles.ts @@ -0,0 +1,12 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + flex: 1; + + position: relative; + + width: 100%; + border-radius: 1.5rem; + + background: ${({ theme }) => theme.color.black[10]}; +`; diff --git a/frontend/src/components/PairRoom/PairRoomCard/PairRoomCard.tsx b/frontend/src/components/PairRoom/PairRoomCard/PairRoomCard.tsx new file mode 100644 index 00000000..b23de42f --- /dev/null +++ b/frontend/src/components/PairRoom/PairRoomCard/PairRoomCard.tsx @@ -0,0 +1,7 @@ +import * as S from './PairRoomCard.styles'; + +const PairRoomCard = ({ children }: React.PropsWithChildren) => { + return {children}; +}; + +export default PairRoomCard; diff --git a/frontend/src/components/PairRoom/PairRoomCard/index.ts b/frontend/src/components/PairRoom/PairRoomCard/index.ts new file mode 100644 index 00000000..e21f60e1 --- /dev/null +++ b/frontend/src/components/PairRoom/PairRoomCard/index.ts @@ -0,0 +1,6 @@ +import Header from '@/components/PairRoom/PairRoomCard/Header/Header'; +import Layout from '@/components/PairRoom/PairRoomCard/PairRoomCard'; + +export const PairRoomCard = Object.assign(Layout, { + Header, +}); diff --git a/frontend/src/components/PairRoom/ReferenceCard/AddReferenceForm/AddReferenceForm.styles.ts b/frontend/src/components/PairRoom/ReferenceCard/AddReferenceForm/AddReferenceForm.styles.ts new file mode 100644 index 00000000..34777756 --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/AddReferenceForm/AddReferenceForm.styles.ts @@ -0,0 +1,35 @@ +import styled, { css } from 'styled-components'; + +export const Layout = styled.div` + display: flex; + align-items: center; + gap: 1rem; + + width: 100%; + height: 6rem; + padding: 0 2rem; +`; + +export const Form = styled.form` + display: flex; + align-items: center; + gap: 0.6rem; + + width: 80%; +`; + +export const ButtonContainer = styled.div` + display: flex; + gap: 0.6rem; +`; + +export const inputStyles = css` + height: 4rem; + border-radius: 0.6rem; +`; + +export const buttonStyles = css` + width: 4.4rem; + height: 4rem; + border-radius: 0.6rem; +`; diff --git a/frontend/src/components/PairRoom/ReferenceCard/AddReferenceForm/AddReferenceForm.tsx b/frontend/src/components/PairRoom/ReferenceCard/AddReferenceForm/AddReferenceForm.tsx new file mode 100644 index 00000000..38152f95 --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/AddReferenceForm/AddReferenceForm.tsx @@ -0,0 +1,59 @@ +import { LuPlus } from 'react-icons/lu'; + +import Button from '@/components/common/Button/Button'; +import Input from '@/components/common/Input/Input'; +import CategoryDropdown from '@/components/PairRoom/ReferenceCard/AddReferenceForm/CategoryDropdown/CategoryDropdown'; +import { Category } from '@/components/PairRoom/ReferenceCard/ReferenceCard.type'; + +import useInput from '@/hooks/common/useInput'; +import useReference from '@/hooks/PairRoom/useReference'; + +import * as S from './AddReferenceForm.styles'; + +interface ReferenceFormProps { + getCategoryNameById: (categoryId: string) => string; + categories: Category[]; + accessCode: string; + isCategoryExist: (categoryName: string) => boolean; +} + +const AddReferenceForm = ({ accessCode, categories, getCategoryNameById, isCategoryExist }: ReferenceFormProps) => { + const { value, status, message, handleChange, resetValue } = useInput(); + const { currentCategoryId, handleCurrentCategory, handleSubmit } = useReference(accessCode, value, () => + resetValue(), + ); + + return ( + + + + + + + + ); +}; + +export default AddReferenceForm; diff --git a/frontend/src/components/PairRoom/ReferenceCard/AddReferenceForm/CategoryDropdown/CategoryDropdown.tsx b/frontend/src/components/PairRoom/ReferenceCard/AddReferenceForm/CategoryDropdown/CategoryDropdown.tsx new file mode 100644 index 00000000..dc059aff --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/AddReferenceForm/CategoryDropdown/CategoryDropdown.tsx @@ -0,0 +1,74 @@ +/* eslint-disable jsx-a11y/no-autofocus */ + +import { validateCategory } from '@/validations/validateCategory'; + +import Dropdown from '@/components/common/Dropdown/Dropdown/Dropdown'; +import Input from '@/components/common/Input/Input'; +import { Category } from '@/components/PairRoom/ReferenceCard/ReferenceCard.type'; + +import useToastStore from '@/stores/toastStore'; + +import useInput from '@/hooks/common/useInput'; +import { DEFAULT_CATEGORY_VALUE } from '@/hooks/PairRoom/useCategories'; + +import { useAddCategory } from '@/queries/PairRoom/category/mutation'; + +interface CategoryDropdownProp { + categories: Category[]; + accessCode: string; + currentCategoryId: string | null; + handleCurrentCategory: (category: string) => void; + getCategoryNameById: (categoryId: string) => string; + isCategoryExist: (categoryName: string) => boolean; +} + +const CategoryDropdown = ({ + categories, + accessCode, + currentCategoryId, + handleCurrentCategory, + getCategoryNameById, + isCategoryExist, +}: CategoryDropdownProp) => { + const { value, status, message, handleChange, resetValue } = useInput(); + const { addToast } = useToastStore(); + + const addCategory = useAddCategory().mutateAsync; + const handleCategory = (option: string) => { + handleCurrentCategory(option); + }; + return ( + handleCategory(option)} + > +
{ + event.preventDefault(); + if (status === 'ERROR') { + addToast({ status, message }); + return; + } + addCategory({ category: value, accessCode }).then(() => resetValue()); + }} + > + handleChange(event, validateCategory(event.target.value, isCategoryExist))} + maxLength={15} + height="4rem" + status={status} + placeholder="+ μƒˆ μΉ΄ν…Œκ³ λ¦¬ μΆ”κ°€" + autoFocus + /> +
+
+ ); +}; + +export default CategoryDropdown; diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoriesEditor.styles.ts b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoriesEditor.styles.ts new file mode 100644 index 00000000..c8f9d7a6 --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoriesEditor.styles.ts @@ -0,0 +1,14 @@ +import styled from 'styled-components'; + +export const CategoryList = styled.ul` + display: flex; + flex-direction: column-reverse; + gap: 1rem; + + width: 100%; +`; + +export const CategoryInput = styled.input` + width: 100%; + border: 1px solid; +`; diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoriesEditor.tsx b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoriesEditor.tsx new file mode 100644 index 00000000..97b2369e --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoriesEditor.tsx @@ -0,0 +1,38 @@ +import CategoryItem from '@/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/CategoryItem'; +import { Category } from '@/components/PairRoom/ReferenceCard/ReferenceCard.type'; + +import * as S from './CategoriesEditor.styles'; + +interface CategoryFilterProps { + categories: Category[]; + selectedCategory: string; + handleSelectCategory: (categoryId: string) => void; + accessCode: string; + closeModal: () => void; +} + +const CategoriesEditor = ({ + closeModal, + accessCode, + categories, + selectedCategory, + handleSelectCategory, +}: CategoryFilterProps) => { + return ( + + {categories.map((category) => ( + + ))} + + ); +}; + +export default CategoriesEditor; diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/CategoryItem.styles.ts b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/CategoryItem.styles.ts new file mode 100644 index 00000000..f5863627 --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/CategoryItem.styles.ts @@ -0,0 +1,40 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + justify-content: space-between; + gap: 1rem; + + width: 100%; +`; + +export const Container = styled.div` + display: flex; + flex-direction: column; + gap: 0.3rem; + + width: 100%; + height: 6rem; + + cursor: pointer; + + img { + width: 2rem; + } +`; + +export const CategoryIconsContainer = styled.div` + display: flex; + align-items: center; + gap: 0.2rem; + + height: 4.4rem; +`; + +export const EditForm = styled.form` + display: flex; + justify-content: space-between; + gap: 1rem; + + width: 100%; +`; diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/CategoryItem.tsx b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/CategoryItem.tsx new file mode 100644 index 00000000..85474189 --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/CategoryItem.tsx @@ -0,0 +1,85 @@ +import Input from '@/components/common/Input/Input'; +import { Message } from '@/components/common/Input/Input.styles'; +import IconButton from '@/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/IconButton/IconButton'; +import ReadonlyCategoryItem from '@/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/ReadonlyCategoryItem/ReadonlyCategoryItem'; + +import { DEFAULT_CATEGORY_ID } from '@/hooks/PairRoom/useCategories'; +import useEditCategory from '@/hooks/PairRoom/useEditCategory'; + +import * as S from './CategoryItem.styles'; + +interface CategoryItemProps { + accessCode: string; + categoryName: string; + categoryId: string; + isChecked: boolean; + closeModal: () => void; + handleSelectCategory: (categoryId: string) => void; +} + +const CategoryItem = ({ + closeModal, + categoryName, + categoryId, + isChecked, + handleSelectCategory, + accessCode, +}: CategoryItemProps) => { + const { isEditing, categoryInputData, actions } = useEditCategory(accessCode, categoryName, categoryId); + + const handleUpdateCategory = async (event: React.FormEvent) => { + event.preventDefault(); + await actions.updateCategory(); + }; + + const handleDeleteCategory = async () => { + await actions.deleteCategory(); + if (isChecked) handleSelectCategory(DEFAULT_CATEGORY_ID); + }; + + return ( + + {isEditing ? ( + + + actions.editCategory(event, categoryName)} + status={categoryInputData.status} + height="4.4rem" + width="28rem" + /> + {categoryInputData.message && ( + {categoryInputData.message} + )} + + + + + + + ) : ( + <> + + + + {categoryId !== DEFAULT_CATEGORY_ID && ( + + + + + )} + + )} + + ); +}; + +export default CategoryItem; diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/IconButton/IconButton.styles.ts b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/IconButton/IconButton.styles.ts new file mode 100644 index 00000000..5f5d551f --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/IconButton/IconButton.styles.ts @@ -0,0 +1,21 @@ +import styled from 'styled-components'; + +export const IconsButton = styled.button` + display: flex; + justify-content: center; + align-items: center; + + padding: 0.5rem; + border-radius: 0.3rem; + + color: ${({ theme }) => theme.color.primary[700]}; + + transition: all 0.3s; + + cursor: pointer; + + &:hover { + background-color: ${({ theme }) => theme.color.black[30]}; + color: ${({ theme }) => theme.color.primary[800]}; + } +`; diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/IconButton/IconButton.tsx b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/IconButton/IconButton.tsx new file mode 100644 index 00000000..1951b4bf --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/IconButton/IconButton.tsx @@ -0,0 +1,35 @@ +import { AiFillDelete } from 'react-icons/ai'; +import { FaPencilAlt, FaCheck } from 'react-icons/fa'; +import { GiCancel } from 'react-icons/gi'; + +import * as S from './IconButton.styles'; + +type Icon = 'CHECK' | 'EDIT' | 'DELETE' | 'CANCEL'; + +interface IconButtonProps { + onClick?: () => void; + icon: Icon; + type?:'button'|'submit'|'reset'; +} + +const IconButton = ({ onClick, icon, type="button" }: IconButtonProps) => { + const GET_ICON = { + CHECK: , + EDIT: , + DELETE: , + CANCEL: , + }; + return ( + { + event.stopPropagation(); + onClick && onClick(); + }} + type={type} + > + {GET_ICON[icon]} + + ); +}; + +export default IconButton; diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/ReadonlyCategoryItem/ReadonlyCategoryItem.styles.ts b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/ReadonlyCategoryItem/ReadonlyCategoryItem.styles.ts new file mode 100644 index 00000000..4b5b2d04 --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/ReadonlyCategoryItem/ReadonlyCategoryItem.styles.ts @@ -0,0 +1,37 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + align-items: center; + gap: 1rem; + + width: 28rem; +`; + +export const ReadonlyCategoryItem = styled.li<{ $isChecked: boolean }>` + display: flex; + justify-content: space-between; + align-items: center; + + width: 100%; + height: 4.4rem; + padding: 0 1rem; + border: 1px solid ${({ theme }) => theme.color.black[50]}; + border-radius: 0.5rem; + + background-color: ${({ theme, $isChecked }) => ($isChecked ? theme.color.primary[700] : theme.color.black[10])}; + color: ${({ theme, $isChecked }) => ($isChecked ? theme.color.black[10] : theme.color.black[70])}; + font-size: ${({ theme }) => theme.fontSize.md}; + + transition: all 0.2s ease-out; + + &:hover { + background-color: ${({ theme }) => theme.color.primary[700]}; + color: ${({ theme }) => theme.color.black[10]}; + } + + &:active { + background-color: ${({ theme }) => theme.color.primary[800]}; + color: ${({ theme }) => theme.color.black[10]}; + } +`; diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/ReadonlyCategoryItem/ReadonlyCategoryItem.tsx b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/ReadonlyCategoryItem/ReadonlyCategoryItem.tsx new file mode 100644 index 00000000..c4bfb1c3 --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoryItem/ReadonlyCategoryItem/ReadonlyCategoryItem.tsx @@ -0,0 +1,41 @@ +import { CheckBoxChecked, CheckBoxUnchecked } from '@/assets'; + +import * as S from './ReadonlyCategoryItem.styles'; + +interface ReadonlyCategoryItemProps { + isChecked: boolean; + category: string; + categoryId: string; + closeModal: () => void; + handleSelectCategory: (categoryId: string) => void; +} + +const ReadonlyCategoryItem = ({ + closeModal, + categoryId, + isChecked, + category, + handleSelectCategory, +}: ReadonlyCategoryItemProps) => { + return ( + ) => { + if (event.currentTarget.id === 'μΉ΄ν…Œκ³ λ¦¬') return; + if (isChecked) return; + handleSelectCategory(event.currentTarget.id); + closeModal(); + }} + > + {isChecked + + +

{category}

+
+
+ ); +}; +export default ReadonlyCategoryItem; diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.styles.ts b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.styles.ts new file mode 100644 index 00000000..4499303d --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.styles.ts @@ -0,0 +1,42 @@ +import styled, { css } from 'styled-components'; + +export const inputStyles = css` + width: 100%; + + font-size: ${({ theme }) => theme.fontSize.md}; +`; + +export const CategoryBox = styled.div` + display: flex; + gap: 1rem; +`; + +export const Header = styled.div` + display: flex; + align-items: center; + gap: 1rem; + + color: ${({ theme }) => theme.color.black[80]}; + font-size: ${({ theme }) => theme.fontSize.lg}; +`; + +export const Footer = styled.form` + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 1rem; + + width: 100%; + height: 7rem; +`; + +export const AddNewCategoryInput = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; +`; +export const buttonStyles = css` + width: 4.4rem; + height: 4rem; + border-radius: 0.6rem; +`; diff --git a/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.tsx b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.tsx new file mode 100644 index 00000000..e52658f6 --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal.tsx @@ -0,0 +1,96 @@ +import { FaFilter } from 'react-icons/fa'; +import { LuPlus } from 'react-icons/lu'; + +import { validateCategory } from '@/validations/validateCategory'; + +import Button from '@/components/common/Button/Button'; +import Input from '@/components/common/Input/Input'; +import { Message } from '@/components/common/Input/Input.styles'; +import { Modal } from '@/components/common/Modal'; +import CategoriesEditor from '@/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoriesEditor/CategoriesEditor'; +import { Category } from '@/components/PairRoom/ReferenceCard/ReferenceCard.type'; + +import useInput from '@/hooks/common/useInput'; + +import { useAddCategory } from '@/queries/PairRoom/category/mutation'; + +import * as S from './CategoryManagementModal.styles'; + +interface CategoryManagementModalProps { + accessCode: string; + isOpen: boolean; + closeModal: () => void; + categories: Category[]; + isCategoryExist: (categoryName: string) => boolean; + selectedCategory: string; + handleSelectCategory: (categoryId: string) => void; +} + +const CategoryManagementModal = ({ + accessCode, + isOpen, + closeModal, + categories, + isCategoryExist, + selectedCategory, + handleSelectCategory, +}: CategoryManagementModalProps) => { + const { value, handleChange, resetValue, message, status } = useInput(''); + const addCategory = useAddCategory(); + + const closeCategoryManagementModal = () => { + resetValue(); + closeModal(); + }; + + const handleAddCategorySubmit = (event: React.FormEvent) => { + event.preventDefault(); + if (status === 'ERROR') return; + addCategory.mutateAsync({ category: value, accessCode }).then(() => resetValue()); + }; + + return ( + + + + +

μΉ΄ν…Œκ³ λ¦¬ 선택

+
+
+ + + + + + + + handleChange(event, validateCategory(event.target.value, isCategoryExist))} + status={status} + $css={S.inputStyles} + /> + + + {message} + +
+ ); +}; + +export default CategoryManagementModal; diff --git a/frontend/src/components/PairRoom/ReferenceCard/Header/Header.styles.ts b/frontend/src/components/PairRoom/ReferenceCard/Header/Header.styles.ts new file mode 100644 index 00000000..fa7b7dac --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/Header/Header.styles.ts @@ -0,0 +1,27 @@ +import styled, { css } from 'styled-components'; + +export const buttonStyles = css` + width: fit-content; + min-width: 6rem; + padding: 0 1rem; +`; + +export const Layout = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + + width: 100%; + height: 6rem; + padding: 2rem; + + font-size: ${({ theme }) => theme.fontSize.lg}; + + cursor: pointer; +`; + +export const Container = styled.div` + display: flex; + align-items: center; + gap: 0.8rem; +`; diff --git a/frontend/src/components/PairRoom/ReferenceCard/Header/Header.tsx b/frontend/src/components/PairRoom/ReferenceCard/Header/Header.tsx new file mode 100644 index 00000000..8a6f3abf --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/Header/Header.tsx @@ -0,0 +1,54 @@ +import { IoIosLink, IoIosArrowUp } from 'react-icons/io'; + +import Button from '@/components/common/Button/Button'; +import ToolTipQuestionBox from '@/components/common/ToolTipQuestionBox/ToolTipQuestionBox'; + +import { theme } from '@/styles/theme'; + +import * as S from './Header.styles'; + +interface HeaderProps { + isOpen: boolean; + selectedFilteringCategoryName: string; + toggleIsOpen: () => void; + onButtonClick: () => void; +} + +const Header = ({ + isOpen, + selectedFilteringCategoryName, + toggleIsOpen, + onButtonClick, +}: React.PropsWithChildren) => { + return ( + + + {isOpen ? ( + + ) : ( + + )} +

링크

+ +
+ +
+ ); +}; + +export default Header; diff --git a/frontend/src/components/PairRoom/ReferenceCard/ReferenceCard.stories.tsx b/frontend/src/components/PairRoom/ReferenceCard/ReferenceCard.stories.tsx new file mode 100644 index 00000000..a9b24df9 --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/ReferenceCard.stories.tsx @@ -0,0 +1,16 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import ReferenceCard from './ReferenceCard'; + +const meta = { + title: 'component/PairRoom/ReferenceCard', + component: ReferenceCard, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => {}} />, +}; diff --git a/frontend/src/components/PairRoom/ReferenceCard/ReferenceCard.styles.ts b/frontend/src/components/PairRoom/ReferenceCard/ReferenceCard.styles.ts new file mode 100644 index 00000000..15fa8050 --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/ReferenceCard.styles.ts @@ -0,0 +1,30 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + min-width: 49rem; + max-height: calc(100vh - 23rem); +`; + +export const Body = styled.div<{ $isOpen: boolean }>` + display: flex; + flex-direction: column; + overflow: hidden; + + height: ${({ $isOpen }) => ($isOpen ? 'calc(100vh - 25rem)' : '0')}; + + transition: height 0.3s; + + border-top: ${({ $isOpen, theme }) => $isOpen && `1px solid ${theme.color.black[30]}`}; +`; + +export const Footer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + + width: 100%; + min-height: 6rem; + + border-top: 1px solid ${({ theme }) => theme.color.black[30]}; +`; diff --git a/frontend/src/components/PairRoom/ReferenceCard/ReferenceCard.tsx b/frontend/src/components/PairRoom/ReferenceCard/ReferenceCard.tsx new file mode 100644 index 00000000..36987c30 --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/ReferenceCard.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react'; + +import { PairRoomCard } from '@/components/PairRoom/PairRoomCard'; +import AddReferenceForm from '@/components/PairRoom/ReferenceCard/AddReferenceForm/AddReferenceForm'; +import CategoryManagementModal from '@/components/PairRoom/ReferenceCard/CategoryManagementModal/CategoryManagementModal'; +import Header from '@/components/PairRoom/ReferenceCard/Header/Header'; +import ReferenceList from '@/components/PairRoom/ReferenceCard/ReferenceList/ReferenceList'; + +import useModal from '@/hooks/common/useModal'; +import useCategories, { DEFAULT_CATEGORY_ID, DEFAULT_CATEGORY_VALUE } from '@/hooks/PairRoom/useCategories'; + +import { useGetReference } from '@/queries/PairRoom/reference/query'; + +import * as S from './ReferenceCard.styles'; + +interface ReferenceCardProps { + accessCode: string; + isOpen: boolean; + toggleIsOpen: () => void; +} + +const ReferenceCard = ({ accessCode, isOpen, toggleIsOpen }: ReferenceCardProps) => { + const [selectedFilteringCategoryId, setSelectedFilteringCategoryId] = useState(DEFAULT_CATEGORY_ID); + const { isModalOpen, openModal, closeModal } = useModal(); + + const { categories, isCategoryExist, getCategoryNameById } = useCategories(accessCode); + + const { data: references } = useGetReference(selectedFilteringCategoryId, accessCode); + const selectedFilteringCategoryName = getCategoryNameById(selectedFilteringCategoryId) || DEFAULT_CATEGORY_VALUE; + + return ( + <> + + +
+ + + + + + + + + + setSelectedFilteringCategoryId(categoryId)} + /> + + ); +}; + +export default ReferenceCard; diff --git a/frontend/src/components/PairRoom/ReferenceCard/ReferenceCard.type.ts b/frontend/src/components/PairRoom/ReferenceCard/ReferenceCard.type.ts new file mode 100644 index 00000000..8772eafc --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/ReferenceCard.type.ts @@ -0,0 +1,4 @@ +export interface Category { + id: string; + value: string; +} diff --git a/frontend/src/components/PairRoom/ReferenceCard/ReferenceList/Reference/Reference.styles.ts b/frontend/src/components/PairRoom/ReferenceCard/ReferenceList/Reference/Reference.styles.ts new file mode 100644 index 00000000..348d1dde --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/ReferenceList/Reference/Reference.styles.ts @@ -0,0 +1,118 @@ +import { MdClose } from 'react-icons/md'; +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + + position: relative; + + width: 17rem; + height: 20rem; + border: 1px solid ${({ theme }) => theme.color.black[30]}; + border-radius: 1.5rem; +`; + +export const Image = styled.img` + width: 100%; + height: 10rem; + + object-fit: cover; + border-top-left-radius: 1.5rem; + border-top-right-radius: 1.5rem; +`; + +export const NoneImage = styled.div` + display: flex; + justify-content: center; + align-items: center; + + width: 100%; + height: 10rem; + + background-color: ${({ theme }) => theme.color.black[40]}; + color: ${({ theme }) => theme.color.black[70]}; + font-size: ${({ theme }) => theme.fontSize.sm}; + line-height: 1.3; + + border-top-left-radius: 1.5rem; + border-top-right-radius: 1.5rem; +`; + +export const Box = styled.div` + display: flex; + flex-direction: column; + gap: 0.6rem; + overflow: hidden; + + width: 100%; + height: 10rem; + max-height: 12rem; + padding: 1.5rem; + + cursor: pointer; +`; + +export const Title = styled.p` + overflow: hidden; + + width: 100%; + + font-size: ${({ theme }) => theme.fontSize.md}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; + text-overflow: ellipsis; + white-space: nowrap; + word-break: break-all; +`; + +export const Content = styled.p` + display: -webkit-box; + overflow: hidden; + + color: ${({ theme }) => theme.color.black[60]}; + font-size: ${({ theme }) => theme.fontSize.xs}; + line-height: 1.5; + text-overflow: ellipsis; + white-space: normal; + word-break: break-all; + + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; +`; + +export const DeleteButton = styled(MdClose)` + position: absolute; + top: 1rem; + right: 1rem; + + width: 2rem; + height: 2rem; + padding: 0.3rem; + border-radius: 100%; + + background-color: ${({ theme }) => theme.color.black[90]}; + opacity: 0.6; + color: ${({ theme }) => theme.color.black[20]}; + + cursor: pointer; + + &:hover { + opacity: 1; + } +`; + +export const Header = styled.div` + display: flex; + justify-content: space-between; + + position: absolute; + top: 1.2rem; + + width: 100%; + padding: 0 1rem; + + button { + width: fit-content; + padding: 0 1rem; + } +`; diff --git a/frontend/src/components/PairRoom/ReferenceCard/ReferenceList/Reference/Reference.tsx b/frontend/src/components/PairRoom/ReferenceCard/ReferenceList/Reference/Reference.tsx new file mode 100644 index 00000000..f5c197ab --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/ReferenceList/Reference/Reference.tsx @@ -0,0 +1,36 @@ +import { Link } from 'react-router-dom'; + +import * as S from './Reference.styles'; + +interface ReferenceProps { + url: string; + image?: string; + title: string; + description: string; + onDeleteReference: () => void; +} + +const Reference = ({ url, image, title, description, onDeleteReference }: ReferenceProps) => { + return ( + + + + {image ? ( + + ) : ( + + 이미지가 +
+ μ—†μŠ΅λ‹ˆλ‹€ +
+ )} + + {title} + {description} + + +
+ ); +}; + +export default Reference; diff --git a/frontend/src/components/PairRoom/ReferenceCard/ReferenceList/ReferenceList.styles.ts b/frontend/src/components/PairRoom/ReferenceCard/ReferenceList/ReferenceList.styles.ts new file mode 100644 index 00000000..1d5c9e85 --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/ReferenceList/ReferenceList.styles.ts @@ -0,0 +1,46 @@ +import styled, { css } from 'styled-components'; + +export const Layout = styled.div<{ $columns: number }>` + display: flex; + flex-grow: 1; + flex-direction: column; + align-items: ${({ $columns }) => ($columns > 2 ? 'center' : '')}; + gap: 1rem; + overflow-y: auto; + + padding: 3rem; +`; + +export const List = styled.ul<{ $columns: number }>` + gap: 3rem 0; + + width: 100%; + padding: 0; + + ${({ $columns }) => + $columns <= 2 + ? css` + display: flex; + flex-wrap: wrap; + gap: 3rem; + ` + : css` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); + place-items: center; + `} + + li { + list-style-type: none; + } +`; + +export const EmptyLayout = styled.div` + flex-grow: 1; + + height: 0; + padding: 2rem; + + color: ${({ theme }) => theme.color.black[60]}; + font-size: ${({ theme }) => theme.fontSize.md}; +`; diff --git a/frontend/src/components/PairRoom/ReferenceCard/ReferenceList/ReferenceList.tsx b/frontend/src/components/PairRoom/ReferenceCard/ReferenceList/ReferenceList.tsx new file mode 100644 index 00000000..c700342f --- /dev/null +++ b/frontend/src/components/PairRoom/ReferenceCard/ReferenceList/ReferenceList.tsx @@ -0,0 +1,39 @@ +import Reference from '@/components/PairRoom/ReferenceCard/ReferenceList/Reference/Reference'; + +import type { Link } from '@/apis/referenceLink'; + +import { useDeleteReferenceLink } from '@/queries/PairRoom/reference/mutation'; + +import * as S from './ReferenceList.styles'; + +interface ReferenceListProps { + references?: Link[]; + accessCode: string; +} + +const ReferenceList = ({ references, accessCode }: ReferenceListProps) => { + const deleteReference = useDeleteReferenceLink().mutate; + + if (!references || references.length < 1) return μ €μž₯된 링크가 μ—†μŠ΅λ‹ˆλ‹€.; + + return ( + + + {references.map((reference) => { + return ( + deleteReference({ id: reference.id, accessCode })} + /> + ); + })} + + + ); +}; + +export default ReferenceList; diff --git a/frontend/src/components/PairRoom/TimerCard/TimerCard.stories.tsx b/frontend/src/components/PairRoom/TimerCard/TimerCard.stories.tsx new file mode 100644 index 00000000..9f030241 --- /dev/null +++ b/frontend/src/components/PairRoom/TimerCard/TimerCard.stories.tsx @@ -0,0 +1,16 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import TimerCard from './TimerCard'; + +const meta = { + title: 'component/PairRoom/TimerCard', + component: TimerCard, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; diff --git a/frontend/src/components/PairRoom/TimerCard/TimerCard.styles.ts b/frontend/src/components/PairRoom/TimerCard/TimerCard.styles.ts new file mode 100644 index 00000000..e81d309a --- /dev/null +++ b/frontend/src/components/PairRoom/TimerCard/TimerCard.styles.ts @@ -0,0 +1,86 @@ +import { FaPause, FaPlay } from 'react-icons/fa6'; +import styled, { css } from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 3rem; + + min-width: 60rem; + height: 100%; + padding: 2rem; +`; + +export const ProgressBar = styled.div.attrs<{ $progress: number }>(({ theme, $progress }) => ({ + style: { + backgroundImage: `linear-gradient(white, white), + conic-gradient(${theme.color.primary[500]} ${$progress}%, ${theme.color.black[30]} ${$progress}%)`, + }, +}))` + display: flex; + justify-content: center; + align-items: center; + + width: 45vh; + min-width: 28rem; + height: 45vh; + min-height: 28rem; + border: 0.8rem solid transparent; + border-radius: 50%; + + transition: background-image 1s ease-in; + + aspect-ratio: 1; + background-clip: content-box, border-box; + background-origin: border-box; +`; + +export const Timer = styled.div` + display: flex; + align-items: center; + gap: 3rem; +`; + +export const TimerTextContainer = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 0.4rem; + + width: 10rem; + + font-size: ${({ theme }) => theme.fontSize.sm}; +`; + +export const TimerText = styled.p` + font-size: 7rem; +`; + +export const IconContainer = styled.div` + display: flex; + gap: 5rem; +`; + +export const IconButton = styled.button` + width: ${({ theme }) => theme.fontSize.h4}; + + background: transparent; + font-size: ${({ theme }) => theme.fontSize.h4}; +`; + +const iconStyle = css<{ $isActive: boolean }>` + color: ${({ $isActive, theme }) => ($isActive ? theme.color.secondary[500] : theme.color.black[50])}; + + cursor: ${({ $isActive }) => ($isActive ? 'pointer' : 'default')}; +`; + +export const PlayIcon = styled(FaPlay)<{ $isActive: boolean }>` + ${iconStyle} +`; + +export const PauseIcon = styled(FaPause)<{ $isActive: boolean }>` + ${iconStyle} +`; diff --git a/frontend/src/components/PairRoom/TimerCard/TimerCard.tsx b/frontend/src/components/PairRoom/TimerCard/TimerCard.tsx new file mode 100644 index 00000000..59eaa3ed --- /dev/null +++ b/frontend/src/components/PairRoom/TimerCard/TimerCard.tsx @@ -0,0 +1,64 @@ +import { useRef } from 'react'; + +import { PairRoomCard } from '@/components/PairRoom/PairRoomCard'; +import TimerEditPanel from '@/components/PairRoom/TimerCard/TimerEditPanel/TimerEditPanel'; + +import useTitleTime from '@/hooks/common/useTitleTime'; +import useTimer from '@/hooks/PairRoom/useTimer'; + +import { formatTime } from '@/utils/Timer/formatTime'; + +import * as S from './TimerCard.styles'; + +interface TimerCardProps { + accessCode: string; + defaultTime: number; + defaultTimeleft: number; + onTimerStop: () => void; +} + +const TimerCard = ({ accessCode, defaultTime, defaultTimeleft, onTimerStop }: TimerCardProps) => { + const { timeLeft, isActive, handleStart, handlePause } = useTimer( + accessCode, + defaultTime, + defaultTimeleft, + onTimerStop, + ); + + const timeLeftRef = useRef(timeLeft); + timeLeftRef.current = timeLeft; + + const { minutes, seconds } = formatTime(timeLeft); + useTitleTime(minutes, seconds); + + return ( + + + + + + + {minutes} + λΆ„(m) + + : + + {seconds} + 초(s) + + + + + + + + + + + + + + ); +}; + +export default TimerCard; diff --git a/frontend/src/components/PairRoom/TimerCard/TimerEditPanel/TimerEditPanel.styles.ts b/frontend/src/components/PairRoom/TimerCard/TimerEditPanel/TimerEditPanel.styles.ts new file mode 100644 index 00000000..62c91662 --- /dev/null +++ b/frontend/src/components/PairRoom/TimerCard/TimerEditPanel/TimerEditPanel.styles.ts @@ -0,0 +1,74 @@ +import { IoSettingsOutline } from 'react-icons/io5'; +import styled, { keyframes } from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 1rem; + + position: absolute; + top: 1.5rem; + right: 1.5rem; +`; + +export const Icon = styled(IoSettingsOutline)` + width: 2rem; + height: 2rem; + + color: ${({ theme }) => theme.color.black[70]}; + + transition: color 0.2s ease; + + cursor: pointer; + + &:hover { + color: ${({ theme }) => theme.color.secondary[600]}; + } +`; + +const slideDown = keyframes` + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +`; + +export const Panel = styled.div` + padding: 1.5rem 2rem; + border: 1px solid ${({ theme }) => theme.color.black[30]}; + border-radius: 1rem; + + background: ${({ theme }) => theme.color.black[10]}; + + animation: ${slideDown} 0.3s ease-out; +`; + +export const Title = styled.p` + margin-bottom: 0.5rem; + + color: ${({ theme }) => theme.color.black[70]}; + font-size: ${({ theme }) => theme.fontSize.sm}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; +`; + +export const Form = styled.form` + display: flex; + align-items: center; + gap: 1rem; + + p { + font-size: ${({ theme }) => theme.fontSize.sm}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; + } +`; + +export const ButtonContainer = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; +`; diff --git a/frontend/src/components/PairRoom/TimerCard/TimerEditPanel/TimerEditPanel.tsx b/frontend/src/components/PairRoom/TimerCard/TimerEditPanel/TimerEditPanel.tsx new file mode 100644 index 00000000..f8ab04f5 --- /dev/null +++ b/frontend/src/components/PairRoom/TimerCard/TimerEditPanel/TimerEditPanel.tsx @@ -0,0 +1,80 @@ +import { useRef } from 'react'; +import { useParams } from 'react-router-dom'; + +import { validateTimerDuration } from '@/validations/validateTimerDuration'; + +import Button from '@/components/common/Button/Button'; +import Input from '@/components/common/Input/Input'; + +import useToastStore from '@/stores/toastStore'; + +import useClickOutside from '@/hooks/common/useClickOutside'; +import useInput from '@/hooks/common/useInput'; +import useModal from '@/hooks/common/useModal'; + +import useUpdateDuration from '@/queries/PairRoom/useUpdateDuration'; + +import { BUTTON_TEXT } from '@/constants/button'; + +import * as S from './TimerEditPanel.styles'; + +interface TimerEditPanelProps { + isActive: boolean; +} + +const TimerEditPanel = ({ isActive }: TimerEditPanelProps) => { + const panelRef = useRef(null); + const { accessCode } = useParams(); + const { addToast } = useToastStore(); + + const { isModalOpen: isPanelOpen, openModal: openPanel, closeModal: closePanel } = useModal(); + const { value, handleChange, resetValue } = useInput(); + const { handleUpdateTimerDuration } = useUpdateDuration(); + + const handleButtonClick = () => { + if (isActive) { + addToast({ status: 'ERROR', message: '타이머 μž‘λ™ μ€‘μ—λŠ” 타이머 μ‹œκ°„μ„ λ³€κ²½ν•  수 μ—†μŠ΅λ‹ˆλ‹€.' }); + return; + } + + openPanel(); + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + if (!value || !accessCode) return; + handleUpdateTimerDuration(value, accessCode); + + resetValue(); + closePanel(); + }; + + useClickOutside(panelRef, () => closePanel()); + + const isButtonDisabled = value === '' || !validateTimerDuration(value); + + return ( + + + {isPanelOpen && ( + + 타이머 μ‹œκ°„ λ³€κ²½ + + + + + + + + + )} + + ); +}; + +export default TimerEditPanel; diff --git a/frontend/src/components/PairRoom/TodoListCard/Header/Header.styles.ts b/frontend/src/components/PairRoom/TodoListCard/Header/Header.styles.ts new file mode 100644 index 00000000..61c33612 --- /dev/null +++ b/frontend/src/components/PairRoom/TodoListCard/Header/Header.styles.ts @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + align-items: center; + gap: 0.8rem; + + width: 100%; + height: 6rem; + padding: 2rem; + + font-size: ${({ theme }) => theme.fontSize.lg}; + + cursor: pointer; +`; diff --git a/frontend/src/components/PairRoom/TodoListCard/Header/Header.tsx b/frontend/src/components/PairRoom/TodoListCard/Header/Header.tsx new file mode 100644 index 00000000..85411246 --- /dev/null +++ b/frontend/src/components/PairRoom/TodoListCard/Header/Header.tsx @@ -0,0 +1,32 @@ +import { IoIosCheckbox, IoIosArrowDown } from 'react-icons/io'; + +import ToolTipQuestionBox from '@/components/common/ToolTipQuestionBox/ToolTipQuestionBox'; + +import { theme } from '@/styles/theme'; + +import * as S from './Header.styles'; + +interface HeaderProps { + isOpen: boolean; + toggleIsOpen: () => void; +} + +const Header = ({ isOpen, toggleIsOpen }: React.PropsWithChildren) => { + return ( + + {isOpen ? ( + + ) : ( + + )} +

νˆ¬λ‘ 리슀트

+ +
+ ); +}; + +export default Header; diff --git a/frontend/src/components/PairRoom/TodoListCard/TodoItem/TodoItem.styles.ts b/frontend/src/components/PairRoom/TodoListCard/TodoItem/TodoItem.styles.ts new file mode 100644 index 00000000..d6a527b7 --- /dev/null +++ b/frontend/src/components/PairRoom/TodoListCard/TodoItem/TodoItem.styles.ts @@ -0,0 +1,76 @@ +import { AiFillDelete, AiFillCopy } from 'react-icons/ai'; +import styled from 'styled-components'; + +export const Layout = styled.div<{ $isChecked: boolean; $isIconHovered: boolean; $isDraggedOver: boolean }>` + display: flex; + justify-content: space-between; + align-items: center; + gap: 1.2rem; + + padding: 1.6rem; + border-radius: 1rem; + + background: ${({ $isChecked, $isDraggedOver, theme }) => + $isChecked + ? $isDraggedOver + ? theme.color.black[40] + : theme.color.black[30] + : $isDraggedOver + ? theme.color.secondary[200] + : theme.color.secondary[100]}; + font-size: ${({ theme }) => theme.fontSize.md}; + + transition: background 0.1s ease; + + cursor: pointer; + + &:hover { + background: ${({ $isChecked, $isIconHovered, theme }) => + !$isIconHovered && ($isChecked ? theme.color.black[40] : theme.color.secondary[150])}; + } +`; + +export const TodoContainer = styled.div<{ $isChecked: boolean }>` + display: flex; + align-items: center; + gap: 1.2rem; + + p { + text-decoration: ${({ $isChecked }) => $isChecked && 'line-through'}; + word-break: break-all; + + transition: text-decoration 0.1s ease; + } +`; + +export const IconContainer = styled.div` + display: flex; + align-items: center; + gap: 0.3rem; +`; + +export const CopyIcon = styled(AiFillCopy)<{ $isChecked: boolean }>` + width: 1.7rem; + height: 1.7rem; + + color: ${({ $isChecked, theme }) => ($isChecked ? theme.color.black[50] : theme.color.secondary[500])}; + + transition: color 0.1s ease; + + &:hover { + color: ${({ $isChecked, theme }) => ($isChecked ? theme.color.black[60] : theme.color.secondary[600])}; + } +`; + +export const DeleteIcon = styled(AiFillDelete)<{ $isChecked: boolean }>` + width: ${({ theme }) => theme.fontSize.lg}; + height: ${({ theme }) => theme.fontSize.lg}; + + color: ${({ $isChecked, theme }) => ($isChecked ? theme.color.black[50] : theme.color.secondary[500])}; + + transition: color 0.1s ease; + + &:hover { + color: ${({ $isChecked, theme }) => ($isChecked ? theme.color.black[60] : theme.color.secondary[600])}; + } +`; diff --git a/frontend/src/components/PairRoom/TodoListCard/TodoItem/TodoItem.tsx b/frontend/src/components/PairRoom/TodoListCard/TodoItem/TodoItem.tsx new file mode 100644 index 00000000..03e6f98b --- /dev/null +++ b/frontend/src/components/PairRoom/TodoListCard/TodoItem/TodoItem.tsx @@ -0,0 +1,65 @@ +import { useState } from 'react'; +import { useParams } from 'react-router-dom'; + +import CheckBox from '@/components/common/CheckBox/CheckBox'; + +import { Todo } from '@/apis/todo'; + +import useCopyClipBoard from '@/hooks/common/useCopyClipboard'; + +import useTodos from '@/queries/PairRoom/useTodos'; + +import * as S from './TodoItem.styles'; + +interface TodoItemProps { + todo: Todo; + isDraggedOver: boolean; + onDragStart: (position: number) => void; + onDragEnter: (position: number) => void; + onDrop: (event: React.DragEvent) => void; +} + +const TodoItem = ({ todo, isDraggedOver, onDragStart, onDragEnter, onDrop }: TodoItemProps) => { + const { accessCode } = useParams(); + + const [isIconHovered, setIsIconHovered] = useState(false); + const [, onCopy] = useCopyClipBoard(); + + const { handleUpdateChecked, handleDeleteTodo } = useTodos(accessCode || ''); + + const { id, isChecked, content } = todo; + + return ( + onDragStart(id)} + onDragEnter={() => onDragEnter(id)} + onDragOver={(event) => event.preventDefault()} + onDragEnd={onDrop} + > + + handleUpdateChecked(id)} /> +

{content}

+
+ + setIsIconHovered(true)} + onMouseLeave={() => setIsIconHovered(false)} + onClick={() => onCopy(content)} + /> + setIsIconHovered(true)} + onMouseLeave={() => setIsIconHovered(false)} + onClick={() => handleDeleteTodo(id)} + /> + +
+ ); +}; + +export default TodoItem; diff --git a/frontend/src/components/PairRoom/TodoListCard/TodoList/TodoList.styles.ts b/frontend/src/components/PairRoom/TodoListCard/TodoList/TodoList.styles.ts new file mode 100644 index 00000000..8b3e073b --- /dev/null +++ b/frontend/src/components/PairRoom/TodoListCard/TodoList/TodoList.styles.ts @@ -0,0 +1,29 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-grow: 1; + flex-direction: column; + gap: 1.5rem; + overflow-y: auto; + + padding: 2rem; +`; + +export const TodoListContainer = styled.div` + display: flex; + flex-grow: 1; + flex-direction: column; + gap: 1rem; + overflow-y: auto; +`; + +export const CountText = styled.p` + color: ${({ theme }) => theme.color.black[70]}; + font-size: ${({ theme }) => theme.fontSize.sm}; +`; + +export const EmptyText = styled.p` + color: ${({ theme }) => theme.color.black[60]}; + font-size: ${({ theme }) => theme.fontSize.md}; +`; diff --git a/frontend/src/components/PairRoom/TodoListCard/TodoList/TodoList.tsx b/frontend/src/components/PairRoom/TodoListCard/TodoList/TodoList.tsx new file mode 100644 index 00000000..52902dcd --- /dev/null +++ b/frontend/src/components/PairRoom/TodoListCard/TodoList/TodoList.tsx @@ -0,0 +1,42 @@ +import { useParams } from 'react-router-dom'; + +import TodoItem from '@/components/PairRoom/TodoListCard/TodoItem/TodoItem'; + +import useDragAndDrop from '@/hooks/common/useDragAndDrop'; + +import useTodos from '@/queries/PairRoom/useTodos'; + +import * as S from './TodoList.styles'; + +const TodoList = () => { + const { accessCode } = useParams(); + + const { todos, handleUpdateOrder } = useTodos(accessCode || ''); + const { dragOverItem, handleDragStart, handleDragEnter, handleDrop } = useDragAndDrop(todos, handleUpdateOrder); + + return ( + + {todos.length > 0 ? ( + <> + 총 {todos.length}개 + + {todos.map((todo) => ( + + ))} + + + ) : ( + μ €μž₯된 νˆ¬λ‘ λ¦¬μŠ€νŠΈκ°€ μ—†μŠ΅λ‹ˆλ‹€. + )} + + ); +}; + +export default TodoList; diff --git a/frontend/src/components/PairRoom/TodoListCard/TodoListCard.styles.ts b/frontend/src/components/PairRoom/TodoListCard/TodoListCard.styles.ts new file mode 100644 index 00000000..c89ae627 --- /dev/null +++ b/frontend/src/components/PairRoom/TodoListCard/TodoListCard.styles.ts @@ -0,0 +1,70 @@ +import styled, { css } from 'styled-components'; + +export const inputStyles = css` + height: 4rem; + border-radius: 0.6rem; +`; + +export const buttonStyles = css` + width: 4.4rem; + height: 4rem; + border-radius: 0.6rem; +`; + +export const Layout = styled.div` + min-width: 49rem; +`; + +export const Body = styled.div<{ $isOpen: boolean }>` + display: flex; + flex-direction: column; + overflow: hidden; + + height: ${({ $isOpen }) => ($isOpen ? 'calc(100vh - 25rem)' : '0')}; + + transition: height 0.3s; + + border-top: ${({ $isOpen, theme }) => $isOpen && `1px solid ${theme.color.black[30]}`}; +`; + +export const Footer = styled.div` + display: flex; + align-items: center; + + width: 100%; + height: 6rem; + min-height: 6rem; + border-radius: 0 0 1.5rem 1.5rem; + + background-color: ${({ theme }) => theme.color.black[10]}; + border-top: 1px solid ${({ theme }) => theme.color.black[30]}; +`; + +export const Form = styled.form` + display: flex; + align-items: center; + gap: 0.6rem; + + width: 100%; + padding: 0 2rem; +`; + +export const FooterButton = styled.button` + display: flex; + align-items: center; + gap: 1rem; + + width: 100%; + height: 6rem; + padding: 2rem; + border-radius: 0 0 1.5rem 1.5rem; + + color: ${({ theme }) => theme.color.black[70]}; + font-size: ${({ theme }) => theme.fontSize.base}; + + transition: all 0.2s ease 0s; + + &:hover { + background-color: ${({ theme }) => theme.color.black[20]}; + } +`; diff --git a/frontend/src/components/PairRoom/TodoListCard/TodoListCard.tsx b/frontend/src/components/PairRoom/TodoListCard/TodoListCard.tsx new file mode 100644 index 00000000..f571d656 --- /dev/null +++ b/frontend/src/components/PairRoom/TodoListCard/TodoListCard.tsx @@ -0,0 +1,60 @@ +import { useParams } from 'react-router-dom'; + +import { LuPlus } from 'react-icons/lu'; + +import Button from '@/components/common/Button/Button'; +import Input from '@/components/common/Input/Input'; +import { PairRoomCard } from '@/components/PairRoom/PairRoomCard'; +import Header from '@/components/PairRoom/TodoListCard/Header/Header'; +import TodoList from '@/components/PairRoom/TodoListCard/TodoList/TodoList'; + +import useInput from '@/hooks/common/useInput'; + +import useTodos from '@/queries/PairRoom/useTodos'; + +import * as S from './TodoListCard.styles'; + +interface TodoListCardProps { + isOpen: boolean; + toggleIsOpen: () => void; +} + +const TodoListCard = ({ isOpen, toggleIsOpen }: TodoListCardProps) => { + const { accessCode } = useParams(); + + const { value, handleChange, resetValue } = useInput(); + const { handleAddTodos } = useTodos(accessCode || ''); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + handleAddTodos(value); + resetValue(); + }; + + return ( + + +
+ + + + + + + + + + + + ); +}; + +export default TodoListCard; diff --git a/frontend/src/components/PairRoom/TodoListCard/TodoListCard.type.ts b/frontend/src/components/PairRoom/TodoListCard/TodoListCard.type.ts new file mode 100644 index 00000000..08d86cc8 --- /dev/null +++ b/frontend/src/components/PairRoom/TodoListCard/TodoListCard.type.ts @@ -0,0 +1,6 @@ +export interface Todo { + id: number; + content: string; + isChecked: boolean; + sort: number; +} diff --git a/frontend/src/components/PairRoomOnboarding/CreateBranchInput/CreateBranchInput.styles.ts b/frontend/src/components/PairRoomOnboarding/CreateBranchInput/CreateBranchInput.styles.ts new file mode 100644 index 00000000..f239e3b2 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/CreateBranchInput/CreateBranchInput.styles.ts @@ -0,0 +1,71 @@ +import { BsArrowReturnRight } from 'react-icons/bs'; +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 3rem; +`; + +export const TitleContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; +`; + +export const Title = styled.div` + color: ${({ theme }) => theme.color.primary[700]}; + font-size: ${({ theme }) => theme.fontSize.h5}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; +`; + +export const SubTitle = styled.div` + color: ${({ theme }) => theme.color.primary[600]}; + font-size: ${({ theme }) => theme.fontSize.lg}; + font-weight: ${({ theme }) => theme.fontWeight.normal}; +`; + +export const InputContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1.4rem; +`; + +export const RepositoryNameBox = styled.div` + display: flex; + justify-content: flex-end; + align-items: center; + + position: relative; + + width: 30rem; + height: 4rem; + padding: 0 1.5rem; + border-radius: 0.5rem; + + background-color: ${({ theme }) => theme.color.black[80]}; + color: ${({ theme }) => theme.color.black[10]}; + font-size: ${({ theme }) => theme.fontSize.md}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; +`; + +export const GithubLogo = styled.img` + position: absolute; + left: -2rem; + + width: 5rem; + object-fit: cover; +`; + +export const InputWrapper = styled.div` + display: flex; + justify-content: flex-end; + gap: 1rem; +`; + +export const ArrowIcon = styled(BsArrowReturnRight)` + margin-top: 0.8rem; + + color: ${({ theme }) => theme.color.black[80]}; + font-size: ${({ theme }) => theme.fontSize.lg}; +`; diff --git a/frontend/src/components/PairRoomOnboarding/CreateBranchInput/CreateBranchInput.tsx b/frontend/src/components/PairRoomOnboarding/CreateBranchInput/CreateBranchInput.tsx new file mode 100644 index 00000000..9474e953 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/CreateBranchInput/CreateBranchInput.tsx @@ -0,0 +1,45 @@ +import { GithubLogoWhite } from '@/assets'; + +import Input from '@/components/common/Input/Input'; +import { InputType } from '@/components/common/Input/Input.type'; + +import useGetBranches from '@/queries/PairRoomOnboarding/useGetBranches'; + +import * as S from './CreateBranchInput.styles'; + +interface CreateBranchInputProps { + repositoryName: string; + branchName: InputType; + onBranchName: (event: React.ChangeEvent, branches: string[]) => void; +} + +const CreateBranchInput = ({ repositoryName, branchName, onBranchName }: CreateBranchInputProps) => { + const { branches } = useGetBranches(repositoryName); + + return ( + + + {repositoryName} + λ―Έμ…˜μ„ μ‹œμž‘ν•  브랜치 이름을 μž…λ ₯ν•΄ μ£Όμ„Έμš”. + + + + + {repositoryName} + + + + onBranchName(event, branches)} + /> + + + + ); +}; + +export default CreateBranchInput; diff --git a/frontend/src/components/PairRoomOnboarding/HowToPairModal/HowToPairModal.styles.ts b/frontend/src/components/PairRoomOnboarding/HowToPairModal/HowToPairModal.styles.ts new file mode 100644 index 00000000..9096f82d --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/HowToPairModal/HowToPairModal.styles.ts @@ -0,0 +1,32 @@ +import styled from 'styled-components'; + +export const RoleTitle = styled.p` + margin-bottom: 0.5em; + + color: ${({ theme }) => theme.color.primary[800]}; + font-size: 1.2em; + font-weight: bold; +`; + +export const RoleDescription = styled.p` + margin-bottom: 1em; + + color: ${({ theme }) => theme.color.black[80]}; + line-height: 1.5; +`; + +export const GeneralAdvice = styled.p` + margin-top: 1em; + margin-bottom: 0.5em; + + color: ${({ theme }) => theme.color.primary[800]}; + font-size: ${({ theme }) => theme.fontSize.h5}; + font-weight: ${({ theme }) => theme.fontWeight.semibold}; +`; + +export const AdviceDescription = styled.p` + margin-bottom: 1em; + + font-weight: ${({ theme }) => theme.fontWeight.normal}; + line-height: 1.5; +`; diff --git a/frontend/src/components/PairRoomOnboarding/HowToPairModal/HowToPairModal.tsx b/frontend/src/components/PairRoomOnboarding/HowToPairModal/HowToPairModal.tsx new file mode 100644 index 00000000..aa403254 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/HowToPairModal/HowToPairModal.tsx @@ -0,0 +1,29 @@ +import Button from '@/components/common/Button/Button'; +import { Modal } from '@/components/common/Modal'; + +import * as S from './HowToPairModal.styles'; + +const HowToPairModal = ({ isOpen, closeModal }: { isOpen: boolean; closeModal: () => void }) => { + return ( + + + + λ“œλΌμ΄λ²„μ˜ μ—­ν•  + λ“œλΌμ΄λ²„λŠ” μ½”λ“œ μž‘μ„±μ— μ§‘μ€‘ν•˜λ˜, νŽ˜μ–΄μ˜ ν”Όλ“œλ°±κ³Ό μ˜λ„λ₯Ό κ²½μ²­ν•΄μ•Ό ν•©λ‹ˆλ‹€. + λ‚΄λΉ„κ²Œμ΄ν„°μ˜ μ—­ν•  + λ‚΄λΉ„κ²Œμ΄ν„°λŠ” λ“œλΌμ΄λ²„μ—κ²Œ λͺ…ν™•ν•˜κ³  ꡬ체적인 ν”Όλ“œλ°±μ„ μ œκ³΅ν•΄μ•Ό ν•©λ‹ˆλ‹€. + λͺ¨λ‘μ—κ²Œ + + μ μ ˆν•œ νœ΄μ‹κ³Ό 회고 μ£ΌκΈ°λ₯Ό μ •ν•˜κ³ , μ„œλ‘œμ˜ ν”Όλ“œλ°±μ„ λ°˜μ˜ν•˜λ©° 효율적인 ν˜‘λ ₯을 μœ μ§€ν•΄μ•Ό ν•©λ‹ˆλ‹€. + + + + + + + ); +}; + +export default HowToPairModal; diff --git a/frontend/src/components/PairRoomOnboarding/InformationBox/InformationBox.styles.ts b/frontend/src/components/PairRoomOnboarding/InformationBox/InformationBox.styles.ts new file mode 100644 index 00000000..cdbe3322 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/InformationBox/InformationBox.styles.ts @@ -0,0 +1,29 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 1rem 0; + + padding: 2rem; + border-radius: 1rem; + + background-color: ${({ theme }) => theme.color.primary[100]}; +`; + +export const Title = styled.p` + display: flex; + align-items: center; + gap: 0.8rem; + + color: ${({ theme }) => theme.color.primary[700]}; + font-size: ${({ theme }) => theme.fontSize.base}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; +`; + +export const Description = styled.p` + color: ${({ theme }) => theme.color.black[70]}; + font-size: ${({ theme }) => theme.fontSize.md}; + font-weight: ${({ theme }) => theme.fontWeight.normal}; + line-height: 1.6; +`; diff --git a/frontend/src/components/PairRoomOnboarding/InformationBox/InformationBox.tsx b/frontend/src/components/PairRoomOnboarding/InformationBox/InformationBox.tsx new file mode 100644 index 00000000..6a9e4a65 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/InformationBox/InformationBox.tsx @@ -0,0 +1,22 @@ +import { RiInformation2Line } from 'react-icons/ri'; + +import * as S from './InformationBox.styles'; + +interface InformationBoxProps { + title: string; + description: string; +} + +const InformationBox = ({ title, description }: InformationBoxProps) => { + return ( + + + + {title} + + {description} + + ); +}; + +export default InformationBox; diff --git a/frontend/src/components/PairRoomOnboarding/MissionSelectInput/MissionSelectInput.styles.ts b/frontend/src/components/PairRoomOnboarding/MissionSelectInput/MissionSelectInput.styles.ts new file mode 100644 index 00000000..ca0a6c83 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/MissionSelectInput/MissionSelectInput.styles.ts @@ -0,0 +1,101 @@ +import styled, { css } from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 3rem; +`; + +export const TitleContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; +`; + +export const Title = styled.div` + color: ${({ theme }) => theme.color.primary[700]}; + font-size: ${({ theme }) => theme.fontSize.h5}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; +`; + +export const SubTitle = styled.div` + color: ${({ theme }) => theme.color.primary[600]}; + font-size: ${({ theme }) => theme.fontSize.lg}; + font-weight: ${({ theme }) => theme.fontWeight.normal}; +`; + +export const HeaderContainer = styled.div` + display: flex; + flex-direction: column; + gap: 2rem; +`; + +export const RepositoryContainer = styled.div` + display: flex; + flex-wrap: wrap; + gap: 3rem; + + width: 100%; +`; + +export const MissionButton = css` + border-width: 2px; + + &:hover { + border-width: 2px; + } + + &:active { + border-width: 2px; + } + + &:disabled { + border-width: 2px; + } +`; + +export const MissionRepository = styled.div` + display: flex; + justify-content: center; + align-items: center; + + width: 100%; + height: 5rem; + border-radius: 1rem; + + background-color: ${({ theme }) => theme.color.black[80]}; + color: ${({ theme }) => theme.color.black[10]}; +`; + +export const MissionBranch = styled.input` + display: flex; + justify-content: center; + align-items: center; + + width: 80%; + height: 4rem; + padding: 0 1rem; + border: 2px solid ${({ theme }) => theme.color.black[70]}; + border-radius: 1rem; + + background-color: ${({ theme }) => theme.color.black[10]}; + color: ${({ theme }) => theme.color.black[70]}; +`; + +export const ModalContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; +`; + +export const MissionBranchBox = styled.div` + display: flex; + justify-content: flex-end; + align-items: center; + gap: 1rem; +`; + +export const Message = styled.p` + color: ${({ theme }) => theme.color.danger[500]}; + font-size: ${({ theme }) => theme.fontSize.sm}; +`; diff --git a/frontend/src/components/PairRoomOnboarding/MissionSelectInput/MissionSelectInput.tsx b/frontend/src/components/PairRoomOnboarding/MissionSelectInput/MissionSelectInput.tsx new file mode 100644 index 00000000..8da6b686 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/MissionSelectInput/MissionSelectInput.tsx @@ -0,0 +1,48 @@ +import Spinner from '@/components/common/Spinner/Spinner'; +import InformationBox from '@/components/PairRoomOnboarding/InformationBox/InformationBox'; +import RepositoryButton from '@/components/PairRoomOnboarding/RepositoryButton/RepositoryButton'; + +import useGetRepositories from '@/queries/PairRoomOnboarding/useGetRepositories'; + +import * as S from './MissionSelectInput.styles'; + +interface MissionSelectInputProps { + onRepositoryName: (repositoryName: string) => void; +} + +const MissionSelectInput = ({ onRepositoryName }: MissionSelectInputProps) => { + const { repositories, isFetching } = useGetRepositories(); + + return ( + + + + λ―Έμ…˜ 선택 + κ΅¬ν˜„ν•΄ λ³Ό λ―Έμ…˜ λ ˆν¬μ§€ν† λ¦¬λ₯Ό 선택해 μ£Όμ„Έμš”. + + + + + {isFetching ? ( + + ) : ( + repositories.map((repository) => { + return ( + + ); + }) + )} + + + ); +}; + +export default MissionSelectInput; diff --git a/frontend/src/components/PairRoomOnboarding/MissionSettingSection/MissionSettingSection.styles.ts b/frontend/src/components/PairRoomOnboarding/MissionSettingSection/MissionSettingSection.styles.ts new file mode 100644 index 00000000..4ba16db2 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/MissionSettingSection/MissionSettingSection.styles.ts @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 8rem; +`; + +export const ButtonWrapper = styled.div` + display: flex; + justify-content: center; + + width: 100%; + margin-top: 5rem; +`; diff --git a/frontend/src/components/PairRoomOnboarding/MissionSettingSection/MissionSettingSection.tsx b/frontend/src/components/PairRoomOnboarding/MissionSettingSection/MissionSettingSection.tsx new file mode 100644 index 00000000..bb19d996 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/MissionSettingSection/MissionSettingSection.tsx @@ -0,0 +1,44 @@ +import Button from '@/components/common/Button/Button'; +import CreateBranchInput from '@/components/PairRoomOnboarding/CreateBranchInput/CreateBranchInput'; +import MissionSelectInput from '@/components/PairRoomOnboarding/MissionSelectInput/MissionSelectInput'; + +import useDebounce from '@/hooks/common/useDebounce'; +import useAutoMoveIndex from '@/hooks/PairRoomOnboarding/useAutoMoveIndex'; +import usePairRoomMission from '@/hooks/PairRoomOnboarding/usePairRoomMission'; + +import * as S from './MissionSettingSection.styles'; + +interface MissionSettingSectionProps { + onCreateBranch: (repositoryName: string, branchName: string) => void; +} + +const MissionSettingSection = ({ onCreateBranch }: MissionSettingSectionProps) => { + const { + repositoryName, + branchName, + isRepositorySelected, + isValidBranchName, + handleRepositoryName, + handleBranchName, + } = usePairRoomMission(); + + const { moveIndex } = useAutoMoveIndex(0, [isRepositorySelected, useDebounce(isValidBranchName, 500)]); + + return ( + + + {moveIndex >= 1 && ( + + )} + {moveIndex >= 2 && ( + + + + )} + + ); +}; + +export default MissionSettingSection; diff --git a/frontend/src/components/PairRoomOnboarding/PairNameInput/PairNameInput.styles.ts b/frontend/src/components/PairRoomOnboarding/PairNameInput/PairNameInput.styles.ts new file mode 100644 index 00000000..63e5e812 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/PairNameInput/PairNameInput.styles.ts @@ -0,0 +1,31 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 3rem; +`; + +export const TitleContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; +`; + +export const Title = styled.div` + color: ${({ theme }) => theme.color.primary[700]}; + font-size: ${({ theme }) => theme.fontSize.h5}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; +`; + +export const SubTitle = styled.div` + color: ${({ theme }) => theme.color.primary[600]}; + font-size: ${({ theme }) => theme.fontSize.lg}; + font-weight: ${({ theme }) => theme.fontWeight.normal}; +`; + +export const InputContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; +`; diff --git a/frontend/src/components/PairRoomOnboarding/PairNameInput/PairNameInput.tsx b/frontend/src/components/PairRoomOnboarding/PairNameInput/PairNameInput.tsx new file mode 100644 index 00000000..07218d6c --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/PairNameInput/PairNameInput.tsx @@ -0,0 +1,44 @@ +import Input from '@/components/common/Input/Input'; +import { InputType } from '@/components/common/Input/Input.type'; + +import * as S from './PairNameInput.styles'; + +interface PairNameInputProps { + firstPairName: InputType; + secondPairName: InputType; + onFirstPair: (event: React.ChangeEvent) => void; + onSecondPair: (event: React.ChangeEvent) => void; +} + +const PairNameInput = ({ firstPairName, secondPairName, onFirstPair, onSecondPair }: PairNameInputProps) => { + return ( + + + 이름 μž…λ ₯ + λ‚˜μ™€ νŽ˜μ–΄μ˜ 이름을 μž…λ ₯ν•΄ μ£Όμ„Έμš”. + + + + + + + + + ); +}; + +export default PairNameInput; diff --git a/frontend/src/components/PairRoomOnboarding/PairRoleInput/PairRoleInput.styles.ts b/frontend/src/components/PairRoomOnboarding/PairRoleInput/PairRoleInput.styles.ts new file mode 100644 index 00000000..faf63022 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/PairRoleInput/PairRoleInput.styles.ts @@ -0,0 +1,52 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 3rem; +`; + +export const TitleContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; +`; + +export const Title = styled.div` + color: ${({ theme }) => theme.color.primary[700]}; + font-size: ${({ theme }) => theme.fontSize.h5}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; +`; + +export const SubTitle = styled.div` + color: ${({ theme }) => theme.color.primary[600]}; + font-size: ${({ theme }) => theme.fontSize.lg}; + font-weight: ${({ theme }) => theme.fontWeight.normal}; +`; + +export const HeaderContainer = styled.div` + display: flex; + flex-direction: column; + gap: 2rem; +`; + +export const DropdownContainer = styled.div` + display: flex; + gap: 2rem; + + width: 100%; +`; + +export const DropdownWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; + + width: 50%; +`; + +export const DropdownLabel = styled.p` + color: ${({ theme }) => theme.color.primary[700]}; + font-size: ${({ theme }) => theme.fontSize.base}; + font-weight: 500; +`; diff --git a/frontend/src/components/PairRoomOnboarding/PairRoleInput/PairRoleInput.tsx b/frontend/src/components/PairRoomOnboarding/PairRoleInput/PairRoleInput.tsx new file mode 100644 index 00000000..f5be7641 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/PairRoleInput/PairRoleInput.tsx @@ -0,0 +1,54 @@ +import Dropdown from '@/components/common/Dropdown/Dropdown/Dropdown'; +import InformationBox from '@/components/PairRoomOnboarding/InformationBox/InformationBox'; + +import type { Role } from '@/hooks/PairRoomOnboarding/usePairRoomInformation'; + +import * as S from './PairRoleInput.styles'; + +interface PairRoleInputProps { + firstPair: string; + secondPair: string; + driver: string; + navigator: string; + onRole: (pairName: string, role: Role) => void; +} + +const PairRoleInput = ({ firstPair, secondPair, driver, navigator, onRole }: PairRoleInputProps) => { + return ( + + + + μ—­ν•  μ„€μ • + λ“œλΌμ΄λ²„ / λ‚΄λΉ„κ²Œμ΄ν„°λ₯Ό μ„€μ •ν•΄ μ£Όμ„Έμš”. + + + + + + λ“œλΌμ΄λ²„ + onRole(name, 'DRIVER')} + /> + + + λ‚΄λΉ„κ²Œμ΄ν„° + onRole(name, 'NAVIGATOR')} + /> + + + + ); +}; + +export default PairRoleInput; diff --git a/frontend/src/components/PairRoomOnboarding/PairRoomSettingSection/PairRoomSettingSection.styles.ts b/frontend/src/components/PairRoomOnboarding/PairRoomSettingSection/PairRoomSettingSection.styles.ts new file mode 100644 index 00000000..4ba16db2 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/PairRoomSettingSection/PairRoomSettingSection.styles.ts @@ -0,0 +1,15 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 8rem; +`; + +export const ButtonWrapper = styled.div` + display: flex; + justify-content: center; + + width: 100%; + margin-top: 5rem; +`; diff --git a/frontend/src/components/PairRoomOnboarding/PairRoomSettingSection/PairRoomSettingSection.tsx b/frontend/src/components/PairRoomOnboarding/PairRoomSettingSection/PairRoomSettingSection.tsx new file mode 100644 index 00000000..a4a2a86e --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/PairRoomSettingSection/PairRoomSettingSection.tsx @@ -0,0 +1,69 @@ +import Button from '@/components/common/Button/Button'; +import PairNameInput from '@/components/PairRoomOnboarding/PairNameInput/PairNameInput'; +import PairRoleInput from '@/components/PairRoomOnboarding/PairRoleInput/PairRoleInput'; +import TimerDurationInput from '@/components/PairRoomOnboarding/TimerDurationInput/TimerDurationInput'; + +import useDebounce from '@/hooks/common/useDebounce'; +import useAutoMoveIndex from '@/hooks/PairRoomOnboarding/useAutoMoveIndex'; +import usePairRoomInformation from '@/hooks/PairRoomOnboarding/usePairRoomInformation'; + +import useAddPairRoom from '@/queries/Main/useAddPairRoom'; + +import { BUTTON_TEXT } from '@/constants/button'; + +import * as S from './PairRoomSettingSection.styles'; + +const PairRoomSettingSection = () => { + const { + firstPairName, + secondPairName, + driver, + navigator, + timerDuration, + isPairNameValid, + isPairRoleValid, + isTimerDurationValid, + handleFirstPairName, + handleSecondPairName, + handlePairRole, + handleTimerDuration, + } = usePairRoomInformation(); + + const validationList = [useDebounce(isPairNameValid, 500), isPairRoleValid, isTimerDurationValid]; + + const { moveIndex } = useAutoMoveIndex(0, validationList); + + const { handleAddPairRoom } = useAddPairRoom(); + + const handleSuccess = () => handleAddPairRoom(driver, navigator, timerDuration); + + return ( + + + {moveIndex >= 1 && ( + + )} + {moveIndex >= 2 && } + {moveIndex >= 3 && ( + + + + )} + + ); +}; + +export default PairRoomSettingSection; diff --git a/frontend/src/components/PairRoomOnboarding/ProgressBar/ProgressBar.stories.tsx b/frontend/src/components/PairRoomOnboarding/ProgressBar/ProgressBar.stories.tsx new file mode 100644 index 00000000..4374a54d --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/ProgressBar/ProgressBar.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import ProgressBar from './ProgressBar'; + +const meta = { + title: 'component/PairRoomOnboarding/ProgressBar', + component: ProgressBar, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + step: 'ROLE', + }, +}; diff --git a/frontend/src/components/PairRoomOnboarding/ProgressBar/ProgressBar.styles.ts b/frontend/src/components/PairRoomOnboarding/ProgressBar/ProgressBar.styles.ts new file mode 100644 index 00000000..e09bc303 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/ProgressBar/ProgressBar.styles.ts @@ -0,0 +1,51 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + justify-content: center; + align-items: center; + + button { + width: 3rem; + height: 3rem; + + cursor: default; + + &:hover { + background-color: ${({ theme }) => theme.color.primary[500]}; + } + + &:active { + background-color: ${({ theme }) => theme.color.primary[500]}; + } + } +`; + +export const ButtonContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; +`; + +export const ButtonWrapper = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 1.4rem; +`; + +export const ButtonLabel = styled.p` + min-width: 5rem; + + color: ${({ theme }) => theme.color.primary[800]}; + font-size: 1.4rem; + font-weight: ${({ theme }) => theme.fontWeight.light}; + text-align: center; +`; + +export const ProgressLine = styled.div` + width: 20rem; + height: 3.5rem; + border-top: 0.15rem dashed ${({ theme }) => theme.color.primary[500]}; +`; diff --git a/frontend/src/components/PairRoomOnboarding/ProgressBar/ProgressBar.tsx b/frontend/src/components/PairRoomOnboarding/ProgressBar/ProgressBar.tsx new file mode 100644 index 00000000..db132d6b --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/ProgressBar/ProgressBar.tsx @@ -0,0 +1,31 @@ +import Button from '@/components/common/Button/Button'; + +import * as S from './ProgressBar.styles'; + +interface ProgressBarProps { + step: string; +} + +const OPTIONS = [ + { id: '1', label: '' }, + { id: '1', label: '' }, +]; +//TODO: 좔후에 제거 ν˜Ήμ€ λ¦¬νŒ©ν† λ§ + +const ProgressBar = ({ step }: ProgressBarProps) => ( + + {OPTIONS.map((option, idx) => ( + + + + {option.label} + + {idx === 0 && } + + ))} + +); + +export default ProgressBar; diff --git a/frontend/src/components/PairRoomOnboarding/RepositoryButton/RepositoryButton.styles.ts b/frontend/src/components/PairRoomOnboarding/RepositoryButton/RepositoryButton.styles.ts new file mode 100644 index 00000000..19e692f1 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/RepositoryButton/RepositoryButton.styles.ts @@ -0,0 +1,62 @@ +import { Link } from 'react-router-dom'; + +import styled, { css } from 'styled-components'; + +export const buttonStyles = css` + width: 30rem; + border: 0; + border-radius: 5px; + + background: ${({ theme }) => theme.color.black[80]}; + color: ${({ theme }) => theme.color.black[10]}; + font-size: ${({ theme }) => theme.fontSize.md}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; + text-align: right; + + &:hover { + border: 0; + + background: ${({ theme }) => theme.color.black[75]}; + color: ${({ theme }) => theme.color.black[10]}; + } +`; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.6rem; +`; + +export const InfoContainer = styled.div` + display: flex; + justify-content: flex-end; + align-items: center; + + position: relative; + + width: 100%; + height: 4rem; + padding: 0 1.5rem; +`; + +export const GithubLogo = styled.img` + position: absolute; + left: -2rem; + + width: 5rem; + object-fit: cover; +`; + +export const RepositoryLink = styled(Link)` + display: flex; + align-items: center; + + color: ${({ theme }) => theme.color.black[60]}; + font-size: ${({ theme }) => theme.fontSize.sm}; + text-decoration: underline; + + &:hover { + color: ${({ theme }) => theme.color.black[65]}; + } +`; diff --git a/frontend/src/components/PairRoomOnboarding/RepositoryButton/RepositoryButton.tsx b/frontend/src/components/PairRoomOnboarding/RepositoryButton/RepositoryButton.tsx new file mode 100644 index 00000000..6d655ca5 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/RepositoryButton/RepositoryButton.tsx @@ -0,0 +1,39 @@ +import { IoIosArrowForward } from 'react-icons/io'; + +import { GithubLogoWhite } from '@/assets'; + +import Button from '@/components/common/Button/Button'; + +import * as S from './RepositoryButton.styles'; + +interface RepositoryButtonProps { + id: string; + name: string; + onSelect: (currentRepo: string) => void; +} + +const RepositoryButton = ({ id, name, onSelect }: RepositoryButtonProps) => { + return ( + + + + λ ˆν¬μ§€ν† λ¦¬λ‘œ μ΄λ™ν•˜κΈ° + + + + ); +}; + +export default RepositoryButton; diff --git a/frontend/src/components/PairRoomOnboarding/TimerDurationInput/TimerDurationInput.styles.ts b/frontend/src/components/PairRoomOnboarding/TimerDurationInput/TimerDurationInput.styles.ts new file mode 100644 index 00000000..bcda9305 --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/TimerDurationInput/TimerDurationInput.styles.ts @@ -0,0 +1,48 @@ +import styled, { css } from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 3rem; +`; + +export const TitleContainer = styled.div` + display: flex; + flex-direction: column; + gap: 1rem; +`; + +export const Title = styled.div` + color: ${({ theme }) => theme.color.primary[700]}; + font-size: ${({ theme }) => theme.fontSize.h5}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; +`; + +export const SubTitle = styled.div` + color: ${({ theme }) => theme.color.primary[600]}; + font-size: ${({ theme }) => theme.fontSize.lg}; + font-weight: ${({ theme }) => theme.fontWeight.normal}; +`; + +export const HeaderContainer = styled.div` + display: flex; + flex-direction: column; + gap: 2rem; +`; + +export const ButtonContainer = styled.div` + display: flex; + gap: 2rem; + + width: 100%; +`; + +export const InputContainer = styled.div` + display: flex; + gap: 0.5rem; +`; + +export const inputStyles = css` + height: 4rem; + border-radius: 1rem; +`; diff --git a/frontend/src/components/PairRoomOnboarding/TimerDurationInput/TimerDurationInput.tsx b/frontend/src/components/PairRoomOnboarding/TimerDurationInput/TimerDurationInput.tsx new file mode 100644 index 00000000..96a8c13a --- /dev/null +++ b/frontend/src/components/PairRoomOnboarding/TimerDurationInput/TimerDurationInput.tsx @@ -0,0 +1,86 @@ +import { useState } from 'react'; + +import { validateTimerDuration } from '@/validations/validateTimerDuration'; + +import Button from '@/components/common/Button/Button'; +import Input from '@/components/common/Input/Input'; +import InformationBox from '@/components/PairRoomOnboarding/InformationBox/InformationBox'; + +import * as S from './TimerDurationInput.styles'; + +const OPTIONS = [ + { label: '10λΆ„', value: '10' }, + { label: '15λΆ„', value: '15' }, + { label: '30λΆ„', value: '30' }, +]; + +interface TimerDurationInputProps { + timerDuration: string; + onTimerDuration: (timerDuration: string) => void; +} + +const TimerDurationInput = ({ timerDuration, onTimerDuration }: TimerDurationInputProps) => { + const [isCustom, setIsCustom] = useState(false); + + const handleIsCustomTime = () => { + if (!isCustom) setIsCustom(true); + onTimerDuration(''); + }; + + const handleOptionTime = (option: string) => { + if (isCustom) setIsCustom(false); + onTimerDuration(option); + }; + + const handleCustomTime = (event: React.ChangeEvent) => { + onTimerDuration(event.target.value); + }; + + return ( + + + + 타이머 μ„€μ • + 타이머 μ‹œκ°„μ„ μ„€μ •ν•΄ μ£Όμ„Έμš”. + + + + + {OPTIONS.map((option) => ( + + ))} + + + {isCustom && ( + + )} + + + + ); +}; + +export default TimerDurationInput; diff --git a/frontend/src/components/common/Animation/ScrollAnimationContainer.styles.ts b/frontend/src/components/common/Animation/ScrollAnimationContainer.styles.ts new file mode 100644 index 00000000..3fd127f6 --- /dev/null +++ b/frontend/src/components/common/Animation/ScrollAnimationContainer.styles.ts @@ -0,0 +1,49 @@ +import styled, { css, keyframes } from 'styled-components'; + +interface FrameInAnimationProps { + $animationDirection: 'left' | 'right' | 'top' | 'bottom'; + $animationDuration: number; + $animationDelay: number; +} + +const frameInAnimation = (direction: 'left' | 'right' | 'top' | 'bottom') => keyframes` + 0% { + opacity: 0; + transform: ${ + direction === 'left' + ? 'translateX(100%)' + : direction === 'right' + ? 'translateX(-100%)' + : direction === 'top' + ? 'translateY(100%)' + : 'translateY(-100%)' + }; + } + + 100%{ + opacity: 1; + transform: translateX(0%) translateY(0%); + } +`; + +export const Container = styled.div` + display: flex; + justify-content: center; + align-items: center; + visibility: hidden; + + width: fit-content; + height: fit-content; + + opacity: 0; + + &.frame-in { + visibility: visible; + + animation: ${(props) => css` + ${frameInAnimation(props.$animationDirection)} + `} + ${(props) => props.$animationDuration}s forwards; + animation-delay: ${(props) => props.$animationDelay}s; + } +`; diff --git a/frontend/src/components/common/Animation/ScrollAnimationContainer.tsx b/frontend/src/components/common/Animation/ScrollAnimationContainer.tsx new file mode 100644 index 00000000..7394c8be --- /dev/null +++ b/frontend/src/components/common/Animation/ScrollAnimationContainer.tsx @@ -0,0 +1,33 @@ +import { IntersectionObserverOptions, useScrollAnimation } from '@/hooks/common/useScrollAnimation'; + +import * as S from './ScrollAnimationContainer.styles'; + +interface ScrollAnimationContainerProps { + animationDirection: 'left' | 'right' | 'top' | 'bottom'; + animationDuration?: number; + animationDelay?: number; + intersectionObserverOptions?: IntersectionObserverOptions; + children: React.ReactNode; +} + +export const ScrollAnimationContainer = ({ + animationDirection = 'bottom', + animationDuration = 1.2, + animationDelay = 0, + intersectionObserverOptions = {}, + children, +}: ScrollAnimationContainerProps) => { + const { ref, isInViewport } = useScrollAnimation(intersectionObserverOptions); + + return ( + + {children} + + ); +}; diff --git a/frontend/src/components/common/Background/WaveBackground.styles.ts b/frontend/src/components/common/Background/WaveBackground.styles.ts new file mode 100644 index 00000000..98b76f41 --- /dev/null +++ b/frontend/src/components/common/Background/WaveBackground.styles.ts @@ -0,0 +1,47 @@ +import styled, { keyframes } from 'styled-components'; + +const drift = keyframes` + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +`; + +export const WaveBackground = styled.div` + overflow: hidden; + + position: fixed; + z-index: -1; + + width: 100vw; + height: calc(100vh - 7rem); +`; + +const WaveBase = styled.div` + position: absolute; + bottom: calc(30vh); + left: calc(-40vw); + + width: 150%; + border-radius: 43%; + + opacity: 0.3; + + aspect-ratio: 1 / 1; + + transform-origin: 50% 48%; + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + width: 180%; + } +`; + +export const Wave = styled(WaveBase)` + background: ${({ theme }) => theme.color.primary[300]}; + + animation: ${drift} 40s infinite linear; +`; + +export const WaveTwo = styled(WaveBase)` + background: ${({ theme }) => theme.color.primary[200]}; + + animation: ${drift} 13s infinite linear; +`; diff --git a/frontend/src/components/common/Background/WaveBackground.tsx b/frontend/src/components/common/Background/WaveBackground.tsx new file mode 100644 index 00000000..76f724a6 --- /dev/null +++ b/frontend/src/components/common/Background/WaveBackground.tsx @@ -0,0 +1,10 @@ +import * as S from './WaveBackground.styles'; + +const WaveBackground = () => ( + + + + +); + +export default WaveBackground; diff --git a/frontend/src/components/common/Button/Button.stories.tsx b/frontend/src/components/common/Button/Button.stories.tsx new file mode 100644 index 00000000..12c54c84 --- /dev/null +++ b/frontend/src/components/common/Button/Button.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { css } from 'styled-components'; + +import Button from '@/components/common/Button/Button'; + +const meta = { + title: 'component/common/Button', + component: Button, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const CustomButton = css` + background-color: red; + + &:hover { + background-color: #d80000; + } + + &:active { + background-color: #9e0000; + } +`; + +export const Default: Story = { + args: { + onClick: () => console.log(), + children: '확인', + }, +}; + +export const UsedCss: Story = { + args: { + onClick: () => console.log(), + children: '확인', + css: CustomButton, + }, +}; diff --git a/frontend/src/components/common/Button/Button.styles.ts b/frontend/src/components/common/Button/Button.styles.ts new file mode 100644 index 00000000..b2ca648a --- /dev/null +++ b/frontend/src/components/common/Button/Button.styles.ts @@ -0,0 +1,84 @@ +import styled, { css } from 'styled-components'; + +import type { ButtonColor, ButtonSize } from '@/components/common/Button/Button.type'; + +interface ButtonStyleProp { + $color: ButtonColor; + $animation: boolean; + $size: ButtonSize; + $filled: boolean; + $rounded: boolean; + disabled: boolean; + $css?: ReturnType; +} +const buttonShapes = { + sm: css` + width: 6rem; + height: 3rem; + + font-size: ${({ theme }) => theme.fontSize.sm}; + `, + md: css` + width: 10rem; + height: 4rem; + + font-size: ${({ theme }) => theme.fontSize.base}; + `, + lg: css` + width: 15rem; + height: 4rem; + + font-size: ${({ theme }) => theme.fontSize.base}; + `, + xl: css` + width: 24.5rem; + height: 6.5rem; + + font-size: ${({ theme }) => theme.fontSize.h5}; + `, +}; + +export const Button = styled.button` + display: flex; + justify-content: center; + align-items: center; + + border: 1px solid ${({ theme, $color }) => theme.color[$color][500]}; + border-radius: ${({ $rounded }) => ($rounded ? '50rem' : '1rem')}; + + background-color: ${({ $filled, theme, $color }) => ($filled ? theme.color[$color][500] : theme.color.black[10])}; + color: ${({ $filled, theme, $color }) => ($filled ? theme.color.black[10] : theme.color[$color][500])}; + + transition: all 0.2s; + + ${({ $size }) => buttonShapes[$size]} + + &:hover { + border: 1px solid ${({ theme, $color }) => theme.color[$color][600]}; + + background-color: ${({ $filled, theme, $color }) => ($filled ? theme.color[$color][600] : theme.color.black[10])}; + color: ${({ $filled, theme, $color }) => ($filled ? theme.color.black[10] : theme.color[$color][600])}; + + transform: ${({ $animation }) => $animation && 'scale(1.01)'}; + } + + &:active { + border: 1px solid ${({ theme, $color }) => theme.color[$color][700]}; + + background-color: ${({ $filled, theme, $color }) => ($filled ? theme.color[$color][700] : theme.color.black[10])}; + color: ${({ $filled, theme, $color }) => ($filled ? theme.color.black[10] : theme.color[$color][700])}; + + transform: ${($animation) => $animation && 'scale(1.02)'}; + } + + &:disabled { + border: 1px solid ${({ theme }) => theme.color.black[50]}; + + background-color: ${({ $filled, theme }) => ($filled ? theme.color.black[50] : theme.color.black[10])}; + color: ${({ $filled, theme }) => ($filled ? 'white' : theme.color.black[50])}; + } + + cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')}; + + ${(props) => props.$css} +`; diff --git a/frontend/src/components/common/Button/Button.tsx b/frontend/src/components/common/Button/Button.tsx new file mode 100644 index 00000000..bfeb21f8 --- /dev/null +++ b/frontend/src/components/common/Button/Button.tsx @@ -0,0 +1,47 @@ +import { ButtonHTMLAttributes } from 'react'; + +import { css } from 'styled-components'; + +import * as S from '@/components/common/Button/Button.styles'; +import type { ButtonColor, ButtonSize } from '@/components/common/Button/Button.type'; + +interface ButtonProp extends ButtonHTMLAttributes { + size?: ButtonSize; + fontSize?: string; + + color?: ButtonColor; + filled?: boolean; + rounded?: boolean; + animation?: boolean; + + css?: ReturnType; +} + +const Button = ({ + size = 'lg', + filled = true, + rounded = false, + animation = true, + color = 'primary', + children, + css, + disabled = false, + ...props +}: React.PropsWithChildren) => { + return ( + + {children} + + ); +}; + +export default Button; diff --git a/frontend/src/components/common/Button/Button.type.ts b/frontend/src/components/common/Button/Button.type.ts new file mode 100644 index 00000000..3be34e45 --- /dev/null +++ b/frontend/src/components/common/Button/Button.type.ts @@ -0,0 +1,2 @@ +export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl'; +export type ButtonColor = 'primary' | 'secondary'; diff --git a/frontend/src/components/common/CheckBox/CheckBox.styles.ts b/frontend/src/components/common/CheckBox/CheckBox.styles.ts new file mode 100644 index 00000000..640b8508 --- /dev/null +++ b/frontend/src/components/common/CheckBox/CheckBox.styles.ts @@ -0,0 +1,30 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + + cursor: pointer; +`; + +export const Input = styled.input` + display: none; +`; + +export const CheckMark = styled.div<{ $isChecked: boolean }>` + display: flex; + justify-content: center; + align-items: center; + + width: 2rem; + height: 2rem; + border: 1px solid ${({ $isChecked, theme }) => ($isChecked ? theme.color.black[60] : theme.color.secondary[500])}; + border-radius: 4px; + + background-color: ${({ $isChecked, theme }) => ($isChecked ? theme.color.black[50] : theme.color.secondary[200])}; + + transition: all 0.1s ease 0s; + + &:hover { + background-color: ${({ theme, $isChecked }) => ($isChecked ? theme.color.black[60] : theme.color.secondary[300])}; + } +`; diff --git a/frontend/src/components/common/CheckBox/CheckBox.tsx b/frontend/src/components/common/CheckBox/CheckBox.tsx new file mode 100644 index 00000000..45bcc4f8 --- /dev/null +++ b/frontend/src/components/common/CheckBox/CheckBox.tsx @@ -0,0 +1,23 @@ +import { MdCheck } from 'react-icons/md'; + +import { theme } from '@/styles/theme'; + +import * as S from './CheckBox.styles'; + +interface CheckBoxProps { + isChecked: boolean; + onClick: () => void; +} + +const CheckBox = ({ isChecked, onClick }: CheckBoxProps) => { + return ( + + + + {isChecked && } + + + ); +}; + +export default CheckBox; diff --git a/frontend/src/components/common/Dropdown/Dropdown/Dropdown.stories.tsx b/frontend/src/components/common/Dropdown/Dropdown/Dropdown.stories.tsx new file mode 100644 index 00000000..d99b29e9 --- /dev/null +++ b/frontend/src/components/common/Dropdown/Dropdown/Dropdown.stories.tsx @@ -0,0 +1,22 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Dropdown from '@/components/common/Dropdown/Dropdown/Dropdown'; + +const meta = { + title: 'component/common/Dropdown', + component: Dropdown, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const EXAMPLE_OPTIONS = ['μ˜΅μ…˜1', 'μ˜΅μ…˜2', 'μ˜΅μ…˜3']; + +export const Default: Story = { + args: { + placeholder: 'This is Dropdown', + options: EXAMPLE_OPTIONS, + onSelect: (option) => alert(`${option}λ₯Ό μ„ νƒν–ˆμŠ΅λ‹ˆλ‹€.`), + }, +}; diff --git a/frontend/src/components/common/Dropdown/Dropdown/Dropdown.styles.tsx b/frontend/src/components/common/Dropdown/Dropdown/Dropdown.styles.tsx new file mode 100644 index 00000000..ba1fa352 --- /dev/null +++ b/frontend/src/components/common/Dropdown/Dropdown/Dropdown.styles.tsx @@ -0,0 +1,105 @@ +import { RiArrowDropDownLine } from 'react-icons/ri'; +import styled from 'styled-components'; + +import Button from '@/components/common/Button/Button'; +import { Direction } from '@/components/common/Dropdown/Dropdown/Dropdown'; + +const getDirection = { + lower: { + open: 180, + close: 0, + }, + upper: { + open: 0, + close: 180, + }, +}; + +export const Layout = styled.div<{ $width: string; $height: string }>` + display: flex; + flex-direction: column; + gap: 1rem; + + position: relative; + + width: ${({ $width }) => $width}; + height: fit-content; + + button { + display: flex; + justify-content: space-between; + align-items: center; + + width: 100%; + height: ${({ $height }) => $height}; + padding: 1rem; + padding-left: 1.7rem; + border-radius: 0.8rem; + + font-size: ${({ theme }) => theme.fontSize.md}; + + &:hover { + background-color: ${({ theme }) => theme.color.black[40]}; + + transform: none; + } + + &:active { + background-color: ${({ theme }) => theme.color.black[60]}; + + transform: none; + } + } +`; + +export const OpenButton = styled(Button)<{ $isSelected: boolean; $isOpen: boolean }>` + border: 1px solid + ${({ $isSelected, $isOpen, theme }) => ($isSelected || $isOpen ? theme.color.primary[700] : theme.color.black[50])}; + + background-color: white; + color: ${({ $isSelected, theme }) => ($isSelected ? theme.color.primary[700] : theme.color.black[50])}; +`; + +export const Icon = styled(RiArrowDropDownLine)<{ $isOpen: boolean; $direction: Direction }>` + transform: rotate(${({ $isOpen, $direction }) => getDirection[$direction][$isOpen ? 'open' : 'close']}deg); + transition: transform 0.2s ease-in-out; +`; + +export const DropdownContainer = styled.div<{ $direction: Direction }>` + display: flex; + flex-direction: ${({ $direction }) => ($direction === 'lower' ? 'column' : 'column-reverse')}; +`; + +export const ItemList = styled.ul<{ $height: string; $direction: Direction }>` + display: flex; + flex-direction: ${({ $direction }) => ($direction === 'lower' ? 'column' : 'column-reverse')}; + overflow-y: auto; + + position: absolute; + top: ${({ $direction }) => ($direction === 'lower' ? '5rem' : '')}; + bottom: ${({ $direction }) => ($direction === 'lower' ? '' : '5rem')}; + left: 0; + z-index: 1000; + + width: 100%; + max-height: 20rem; + border-radius: 0.8rem; + + background-color: white; + box-shadow: + 0 0 2px grey, + 1px 1px 3px lightgrey; +`; + +export const Item = styled(Button)` + justify-content: flex-start; + + height: 4.8rem; + border: none; + + color: ${({ theme }) => theme.color.primary[700]}; + + &:hover { + border: none; + } +`; diff --git a/frontend/src/components/common/Dropdown/Dropdown/Dropdown.tsx b/frontend/src/components/common/Dropdown/Dropdown/Dropdown.tsx new file mode 100644 index 00000000..87925281 --- /dev/null +++ b/frontend/src/components/common/Dropdown/Dropdown/Dropdown.tsx @@ -0,0 +1,123 @@ +import { useState, useRef } from 'react'; + +import * as S from '@/components/common/Dropdown/Dropdown/Dropdown.styles'; +import HiddenDropdown from '@/components/common/Dropdown/HiddenDropdown/HiddenDropdown'; + +import useClickOutside from '@/hooks/common/useClickOutside'; + +import { theme } from '@/styles/theme'; + +export type Direction = 'lower' | 'upper'; + +export interface Option { + id: string; + value: string; +} +interface DropdownProps { + placeholder: string; + valueOptions?: Option[]; + options?: string[]; + selectedOption?: string; + width?: string; + height?: string; + direction?: Direction; + onSelect: (option: string) => void; + children?: React.ReactNode; +} + +const Dropdown = ({ + placeholder, + options, + selectedOption = '', + width = '100%', + height = '4.8rem', + direction = 'lower', + onSelect, + children, + valueOptions, +}: DropdownProps) => { + const dropdownRef = useRef(null); + + const [isOpen, setIsOpen] = useState(false); + + useClickOutside(dropdownRef, () => setIsOpen(false)); + + const handleSelect = (option: string) => { + onSelect(option); + setIsOpen(false); + }; + + const toggleDropdown = (event: React.MouseEvent) => { + event.stopPropagation(); + setIsOpen((prev) => !prev); + }; + + return ( + + + + {children && isOpen ? ( + children + ) : ( + + {selectedOption || placeholder} + + + )} + + {options && !options.some((option) => option === '') && isOpen && ( + + {options.map((option, index) => ( +
  • + ) => { + event.stopPropagation(); + handleSelect(option); + }} + > + {option} + +
  • + ))} +
    + )} + + {valueOptions && !valueOptions.some((option) => option.value === '') && isOpen && ( + + {valueOptions.map((option, index) => ( +
  • + ) => { + event.stopPropagation(); + handleSelect(option.id); + }} + > + {option.value} + +
  • + ))} +
    + )} +
    +
    + ); +}; + +export default Dropdown; diff --git a/frontend/src/components/common/Dropdown/HiddenDropdown/HiddenDropdown.tsx b/frontend/src/components/common/Dropdown/HiddenDropdown/HiddenDropdown.tsx new file mode 100644 index 00000000..1faee377 --- /dev/null +++ b/frontend/src/components/common/Dropdown/HiddenDropdown/HiddenDropdown.tsx @@ -0,0 +1,39 @@ +import { Option } from '@/components/common/Dropdown/Dropdown/Dropdown'; + +interface HiddenDropdownProps extends React.HTMLAttributes { + options?: string[]; + selectedOption?: string; + valueOptions?: Option[]; + + handleSelect: (value: string) => void; +} + +const HiddenDropdown = ({ options, selectedOption, handleSelect, valueOptions }: HiddenDropdownProps) => { + return ( + + ); +}; + +// μ›Ή μ ‘κ·Όμ„± ν–₯상을 μœ„ν•΄ μˆ¨κ²¨μ§„ select νƒœκ·Έ κ΅¬ν˜„ + +export default HiddenDropdown; diff --git a/frontend/src/components/common/Header/Header.stories.tsx b/frontend/src/components/common/Header/Header.stories.tsx new file mode 100644 index 00000000..738db9b4 --- /dev/null +++ b/frontend/src/components/common/Header/Header.stories.tsx @@ -0,0 +1,35 @@ +import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport'; +import type { Meta, StoryObj } from '@storybook/react'; + +import Header from '@/components/common/Header/Header'; + +const meta = { + title: 'component/common/Header', + component: Header, + parameters: { + viewport: { + viewports: INITIAL_VIEWPORTS, + defaultViewport: 'iphonese2', + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + parameters: { + viewport: { + defaultViewport: 'responsive', + }, + }, +}; + +export const Mobile: Story = { + parameters: { + viewport: { + defaultViewport: 'iphonese2', + }, + }, +}; diff --git a/frontend/src/components/common/Header/Header.styles.ts b/frontend/src/components/common/Header/Header.styles.ts new file mode 100644 index 00000000..d0779f16 --- /dev/null +++ b/frontend/src/components/common/Header/Header.styles.ts @@ -0,0 +1,70 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + + height: 7rem; + padding: 0 5rem; + + border-bottom: 0.1rem solid ${({ theme }) => theme.color.black[30]}; + + color: ${({ theme }) => theme.color.black[80]}; + font-size: ${({ theme }) => theme.fontSize.base}; + + a { + display: flex; + justify-content: center; + align-items: center; + } + + button { + transition: all 0.1s; + + cursor: pointer; + + &:hover { + opacity: 0.7; + text-decoration: underline; + } + + &:active { + opacity: 0.5; + text-decoration: underline; + } + } + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + padding: 0 8vw; + } +`; + +export const Logo = styled.img` + width: 3.6rem; + height: 3.6rem; +`; + +export const LinkContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + gap: 1.4rem; +`; + +export const HowToPairText = styled.button` + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + display: none; + } +`; + +export const HowToPairIcon = styled.div` + display: none; + + color: ${({ theme }) => theme.color.primary[800]}; + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + display: flex; + align-items: center; + } +`; diff --git a/frontend/src/components/common/Header/Header.tsx b/frontend/src/components/common/Header/Header.tsx new file mode 100644 index 00000000..c35b4f30 --- /dev/null +++ b/frontend/src/components/common/Header/Header.tsx @@ -0,0 +1,51 @@ +import { Link } from 'react-router-dom'; + +import { FaBook } from 'react-icons/fa'; + +import { LogoIcon } from '@/assets'; + +import useUserStore from '@/stores/userStore'; + +import useSignInHandler from '@/hooks/member/useSignInHandler'; +import useSignOutHandler from '@/hooks/member/useSignOutHandler'; + +import { theme } from '@/styles/theme'; + +import * as S from './Header.styles'; + +const Header = () => { + const { username, userStatus } = useUserStore(); + + const { handleSignInGithub } = useSignInHandler(); + const { handleSignOut } = useSignOutHandler(); + + return ( + + + + + + + μ½”λ”©ν•΄λ“€μ˜€ μ‹œμž‘ν•˜κΈ° + + + + + + + {userStatus === 'SIGNED_IN' ? ( + <> + + + + + + ) : ( + + )} + + + ); +}; + +export default Header; diff --git a/frontend/src/components/common/Input/Input.stories.tsx b/frontend/src/components/common/Input/Input.stories.tsx new file mode 100644 index 00000000..ed7d4993 --- /dev/null +++ b/frontend/src/components/common/Input/Input.stories.tsx @@ -0,0 +1,22 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Input from '@/components/common/Input/Input'; + +const meta = { + title: 'component/common/Input', + component: Input, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + id: 'default', + placeholder: 'μž…λ ₯ν•΄μ£Όμ„Έμš”', + message: 'λ©”μ„Έμ§€μž…λ‹ˆλ‹€', + label: '라벨', + disabled: false, + }, +}; diff --git a/frontend/src/components/common/Input/Input.styles.ts b/frontend/src/components/common/Input/Input.styles.ts new file mode 100644 index 00000000..14a5c202 --- /dev/null +++ b/frontend/src/components/common/Input/Input.styles.ts @@ -0,0 +1,92 @@ +import styled, { css } from 'styled-components'; + +import type { InputStatus } from '@/components/common/Input/Input.type'; + +interface InputProps { + $status: InputStatus; + $height: string; + $css?: ReturnType; +} + +interface LayoutProps { + $width: string; +} + +const inputStatusCss = { + DEFAULT: css` + border: 1px solid ${({ theme }) => theme.color.black[40]}; + + background-color: ${({ theme }) => theme.color.black[10]}; + `, + ERROR: css` + border: 1px solid ${({ theme }) => theme.color.danger[600]}; + + background-color: ${({ theme }) => theme.color.danger[50]}; + `, + SUCCESS: css` + border: 1px solid ${({ theme }) => theme.color.success[600]}; + + background-color: ${({ theme }) => theme.color.success[100]}; + `, +}; + +const inputStatusMessageCss = { + DEFAULT: css` + color: ${({ theme }) => theme.color.black[80]}; + `, + ERROR: css` + color: ${({ theme }) => theme.color.danger[600]}; + `, + + SUCCESS: css` + color: ${({ theme }) => theme.color.success[700]}; + `, +}; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 0.8rem; + + width: ${({ $width }) => $width}; +`; + +export const Label = styled.label` + color: ${({ theme }) => theme.color.primary[700]}; + font-size: ${({ theme }) => theme.fontSize.base}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; +`; + +export const Message = styled.p<{ $status: InputStatus }>` + font-size: ${({ theme }) => theme.fontSize.sm}; + ${({ $status }) => inputStatusMessageCss[$status]}; +`; + +export const Input = styled.input` + ${({ $status }) => inputStatusCss[$status]}; + ${({ $status }) => inputStatusCss[$status]}; + width: 100%; + height: ${({ $height }) => $height}; + padding: 0 1rem; + border-radius: 0.5rem; + + font-size: ${({ theme }) => theme.fontSize.md}; + + &::placeholder { + color: ${({ theme }) => theme.color.black[50]}; + } + + &:focus { + border: 1px solid ${({ theme }) => theme.color.primary[700]}; + + background-color: ${({ theme }) => theme.color.black[10]}; + } + + &:disabled { + border: 1px solid ${({ theme }) => theme.color.black[40]}; + + background-color: ${({ theme }) => theme.color.black[30]}; + } + + ${(props) => props.$css} +`; diff --git a/frontend/src/components/common/Input/Input.tsx b/frontend/src/components/common/Input/Input.tsx new file mode 100644 index 00000000..7f8b33d1 --- /dev/null +++ b/frontend/src/components/common/Input/Input.tsx @@ -0,0 +1,30 @@ +import { forwardRef, InputHTMLAttributes } from 'react'; + +import { css } from 'styled-components'; + +import * as S from '@/components/common/Input/Input.styles'; +import type { InputStatus } from '@/components/common/Input/Input.type'; + +interface InputProps extends InputHTMLAttributes { + width?: string; + height?: string; + status?: InputStatus; + label?: string; + message?: string; + $css?: ReturnType; +} +const Input = forwardRef( + ({ width = '100%', status = 'DEFAULT', message, label, height = '4.8rem', ...props }: InputProps, ref) => { + return ( + + {label && {label}} + + {message && {message}} + + ); + }, +); + +Input.displayName = 'InputComponent'; + +export default Input; diff --git a/frontend/src/components/common/Input/Input.type.ts b/frontend/src/components/common/Input/Input.type.ts new file mode 100644 index 00000000..aa51fa66 --- /dev/null +++ b/frontend/src/components/common/Input/Input.type.ts @@ -0,0 +1,7 @@ +export interface InputType { + value: string; + status: InputStatus; + message: string; +} + +export type InputStatus = 'DEFAULT' | 'ERROR' | 'SUCCESS'; diff --git a/frontend/src/components/common/Modal/Body/Body.stories.ts b/frontend/src/components/common/Modal/Body/Body.stories.ts new file mode 100644 index 00000000..e59d2753 --- /dev/null +++ b/frontend/src/components/common/Modal/Body/Body.stories.ts @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Body from './Body'; + +const meta = { + title: 'component/common/modal/Body', + component: Body, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + children: 'λ³Έλ¬Έ ν‘œμ‹œ', + }, +}; diff --git a/frontend/src/components/common/Modal/Body/Body.styles.ts b/frontend/src/components/common/Modal/Body/Body.styles.ts new file mode 100644 index 00000000..53abbb7b --- /dev/null +++ b/frontend/src/components/common/Modal/Body/Body.styles.ts @@ -0,0 +1,10 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + overflow-y: auto; + + margin: 4rem 0; + + font-size: ${({ theme }) => theme.fontSize.base}; + line-height: 1.5; +`; diff --git a/frontend/src/components/common/Modal/Body/Body.tsx b/frontend/src/components/common/Modal/Body/Body.tsx new file mode 100644 index 00000000..8e67723b --- /dev/null +++ b/frontend/src/components/common/Modal/Body/Body.tsx @@ -0,0 +1,7 @@ +import * as S from './Body.styles'; + +const Body = ({ children }: React.PropsWithChildren) => { + return {children}; +}; + +export default Body; diff --git a/frontend/src/components/common/Modal/CloseButton/CloseButton.stories.ts b/frontend/src/components/common/Modal/CloseButton/CloseButton.stories.ts new file mode 100644 index 00000000..26842845 --- /dev/null +++ b/frontend/src/components/common/Modal/CloseButton/CloseButton.stories.ts @@ -0,0 +1,14 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import CloseButton from './CloseButton'; + +const meta = { + title: 'component/common/modal/CloseButton', + component: CloseButton, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/frontend/src/components/common/Modal/CloseButton/CloseButton.styles.ts b/frontend/src/components/common/Modal/CloseButton/CloseButton.styles.ts new file mode 100644 index 00000000..f4e0bdd2 --- /dev/null +++ b/frontend/src/components/common/Modal/CloseButton/CloseButton.styles.ts @@ -0,0 +1,9 @@ +import styled from 'styled-components'; + +export const Button = styled.button` + position: absolute; + top: 3.5rem; + right: 3.5rem; + + cursor: pointer; +`; diff --git a/frontend/src/components/common/Modal/CloseButton/CloseButton.tsx b/frontend/src/components/common/Modal/CloseButton/CloseButton.tsx new file mode 100644 index 00000000..01dd6127 --- /dev/null +++ b/frontend/src/components/common/Modal/CloseButton/CloseButton.tsx @@ -0,0 +1,17 @@ +import { MdClose } from 'react-icons/md'; + +import * as S from './CloseButton.styles'; + +interface CloseButtonProps { + close: () => void; +} + +const CloseButton = ({ close }: CloseButtonProps) => { + return ( + + + + ); +}; + +export default CloseButton; diff --git a/frontend/src/components/common/Modal/Footer/Footer.stories.tsx b/frontend/src/components/common/Modal/Footer/Footer.stories.tsx new file mode 100644 index 00000000..3fb001a6 --- /dev/null +++ b/frontend/src/components/common/Modal/Footer/Footer.stories.tsx @@ -0,0 +1,40 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Button from '@/components/common/Button/Button'; + +import Footer from './Footer'; + +const meta = { + title: 'component/common/modal/Footer', + component: Footer, + parameters: { + controls: { exclude: 'children' }, + }, + argTypes: { + direction: { + options: ['row', 'column'], + control: { type: 'radio' }, + }, + position: { + options: ['left', 'center', 'right'], + control: { type: 'radio' }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const CHILDREN_EXAMPLE = ( + <> + + + +); + +export const Default: Story = { + args: { + children: CHILDREN_EXAMPLE, + }, +}; diff --git a/frontend/src/components/common/Modal/Footer/Footer.styles.ts b/frontend/src/components/common/Modal/Footer/Footer.styles.ts new file mode 100644 index 00000000..f5ac6d83 --- /dev/null +++ b/frontend/src/components/common/Modal/Footer/Footer.styles.ts @@ -0,0 +1,17 @@ +import styled from 'styled-components'; + +import { Position, Direction } from '@/components/common/Modal/Footer/Footer'; + +const positionMapper = { + LEFT: 'start', + CENTER: 'center', + RIGHT: 'end', +}; + +export const Layout = styled.div<{ $direction: Direction; $position: Position }>` + display: flex; + flex-direction: ${({ $direction }) => $direction}; + justify-content: ${({ $direction, $position }) => $direction === 'ROW' && positionMapper[$position]}; + align-items: ${({ $direction, $position }) => $direction === 'COLUMN' && positionMapper[$position]}; + gap: 1.6rem; +`; diff --git a/frontend/src/components/common/Modal/Footer/Footer.tsx b/frontend/src/components/common/Modal/Footer/Footer.tsx new file mode 100644 index 00000000..7233abf9 --- /dev/null +++ b/frontend/src/components/common/Modal/Footer/Footer.tsx @@ -0,0 +1,19 @@ +import * as S from './Footer.styles'; + +export type Direction = 'ROW' | 'COLUMN'; +export type Position = 'LEFT' | 'CENTER' | 'RIGHT'; + +interface FooterProps { + direction?: Direction; + position?: Position; +} + +const Footer = ({ direction = 'ROW', position = 'RIGHT', children }: React.PropsWithChildren) => { + return ( + + {children} + + ); +}; + +export default Footer; diff --git a/frontend/src/components/common/Modal/Header/Header.stories.tsx b/frontend/src/components/common/Modal/Header/Header.stories.tsx new file mode 100644 index 00000000..848485ec --- /dev/null +++ b/frontend/src/components/common/Modal/Header/Header.stories.tsx @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Header from './Header'; + +const meta = { + title: 'component/common/modal/Header', + component: Header, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + title: '제λͺ© ν‘œμ‹œ', + subTitle: 'λΆ€μ œλͺ© ν‘œμ‹œ', + }, +}; diff --git a/frontend/src/components/common/Modal/Header/Header.styles.ts b/frontend/src/components/common/Modal/Header/Header.styles.ts new file mode 100644 index 00000000..1bc6fa8a --- /dev/null +++ b/frontend/src/components/common/Modal/Header/Header.styles.ts @@ -0,0 +1,18 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 0.5rem; +`; + +export const Title = styled.h3` + color: ${({ theme }) => theme.color.primary[700]}; + font-size: ${({ theme }) => theme.fontSize.h3}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; +`; + +export const SubTitle = styled.p` + color: ${({ theme }) => theme.color.primary[600]}; + font-size: ${({ theme }) => theme.fontSize.h6}; +`; diff --git a/frontend/src/components/common/Modal/Header/Header.tsx b/frontend/src/components/common/Modal/Header/Header.tsx new file mode 100644 index 00000000..0872df1f --- /dev/null +++ b/frontend/src/components/common/Modal/Header/Header.tsx @@ -0,0 +1,19 @@ +import * as S from './Header.styles'; + +interface HeaderProps { + title?: string; + subTitle?: string; + children?: React.ReactNode; +} + +const Header = ({ title, subTitle, children }: HeaderProps) => { + return ( + + {title} + {subTitle && {subTitle}} + {children} + + ); +}; + +export default Header; diff --git a/frontend/src/components/common/Modal/Modal.stories.tsx b/frontend/src/components/common/Modal/Modal.stories.tsx new file mode 100644 index 00000000..6e9ba5d7 --- /dev/null +++ b/frontend/src/components/common/Modal/Modal.stories.tsx @@ -0,0 +1,81 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Button from '@/components/common/Button/Button'; +import Body from '@/components/common/Modal/Body/Body'; +import CloseButton from '@/components/common/Modal/CloseButton/CloseButton'; +import Footer from '@/components/common/Modal/Footer/Footer'; +import Header from '@/components/common/Modal/Header/Header'; + +import Modal from './Modal'; + +const meta = { + title: 'component/common/Modal', + component: Modal, + parameters: { + controls: { exclude: ['close', 'children'] }, + }, + argTypes: { + position: { + options: ['CENTER', 'BOTTOM'], + control: { type: 'radio' }, + }, + size: { + options: ['sm', 'md', 'lg'], + control: { type: 'radio' }, + }, + height: { + control: { type: 'text' }, + }, + backdropType: { + options: ['TRANSPARENT', 'BLUR', 'OPAQUE'], + control: { type: 'radio' }, + }, + shadow: { + control: { type: 'boolean' }, + }, + animation: { + control: { type: 'boolean' }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const CHILDREN_EXAMPLE = ( + <> + {}} /> +
    + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. + Pellentesque sit amet hendrerit risus, sed porttitor quam. Lorem ipsum dolor sit amet, consectetur adipiscing + elit. Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor + quam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit + venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam. Lorem ipsum dolor sit amet, consectetur + adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed + porttitor quam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit + venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam.Lorem ipsum dolor sit amet, consectetur + adipiscing elit. + +
    + + +
    + +); + +export const Center: Story = { + args: { + isOpen: true, + children: CHILDREN_EXAMPLE, + }, +}; + +export const Bottom: Story = { + args: { + isOpen: true, + children: CHILDREN_EXAMPLE, + position: 'BOTTOM', + }, +}; diff --git a/frontend/src/components/common/Modal/Modal.styles.ts b/frontend/src/components/common/Modal/Modal.styles.ts new file mode 100644 index 00000000..25008c92 --- /dev/null +++ b/frontend/src/components/common/Modal/Modal.styles.ts @@ -0,0 +1,125 @@ +import styled, { keyframes, css } from 'styled-components'; + +import type { Size, Position, BackdropType } from './Modal.type'; + +const fadeIn = keyframes` + from { + opacity: 0; + } + to { + opacity: 1; + } +`; + +const slideIn = keyframes` + from { + transform: translateY(-0.2rem); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +`; + +const slideOut = keyframes` + from { + transform: translateY(0.2rem); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +`; + +export const Layout = styled.div<{ $position: Position }>` + display: flex; + justify-content: center; + align-items: ${({ $position }) => ($position === 'BOTTOM' ? 'flex-end' : 'center')}; + + position: fixed; + top: 0; + + width: 100%; + height: 100%; + + animation: ${fadeIn} 0.3s ease; +`; + +const backdropMapper = { + OPAQUE: css` + background: ${({ theme }) => theme.color.black[90]}; + opacity: 0.36; + `, + BLUR: css` + background: #00000080; + backdrop-filter: blur(10px); + `, + TRANSPARENT: css` + background: transparent; + `, +}; + +export const Backdrop = styled.div<{ $backdropType: BackdropType }>` + position: fixed; + top: 0; + + width: 100%; + height: 100%; + ${({ $backdropType }) => backdropMapper[$backdropType]} +`; + +const sizeMapper: Record = { + sm: '30%', + md: '60%', + lg: '90%', +}; + +const positionMapper = { + BOTTOM: css` + max-height: 90vh; + margin: 0; + border-radius: 2rem 2rem 0 0; + `, + CENTER: css` + max-height: 70vh; + margin: 0 3rem; + border-radius: 2rem; + `, +}; + +const animationMapper = { + BOTTOM: css` + animation: ${slideIn} 0.3s ease-in forwards; + `, + CENTER: css` + animation: ${slideOut} 0.3s ease-in forwards; + `, +}; + +export const Container = styled.div<{ + $size: Size | string; + $height: string; + $position: Position; + $shadow: boolean; + $animation: boolean; +}>` + display: flex; + flex-direction: column; + + position: relative; + + width: ${({ $size }) => sizeMapper[$size as Size] ?? $size}; + height: ${({ $height }) => $height && $height}; + padding: 4rem; + + background: ${({ theme }) => theme.color.black[10]}; + box-shadow: ${({ $shadow }) => + $shadow && + ` 0 3px 6px rgb(0 0 0 / 10%), + 0 3px 6px rgb(0 0 0 / 10%)`}; + + ${({ $position }) => positionMapper[$position]} + ${({ $position, $animation }) => $animation && animationMapper[$position]} +`; diff --git a/frontend/src/components/common/Modal/Modal.tsx b/frontend/src/components/common/Modal/Modal.tsx new file mode 100644 index 00000000..7b13e4b9 --- /dev/null +++ b/frontend/src/components/common/Modal/Modal.tsx @@ -0,0 +1,50 @@ +import { createPortal } from 'react-dom'; + +import useEscapeKey from '@/hooks/common/useEscapeKey'; +import useFocusTrap from '@/hooks/common/useFocusTrap'; +import usePreventScroll from '@/hooks/common/usePreventScroll'; + +import * as S from './Modal.styles'; +import type { Position, Size, BackdropType } from './Modal.type'; + +interface ModalProps { + isOpen: boolean; + close: () => void; + size?: Size | string; + height?: string; + position?: Position; + shadow?: boolean; + animation?: boolean; + backdropType?: BackdropType; +} + +const Modal = ({ + isOpen, + close, + size = 'md', + height = '', + position = 'CENTER', + shadow = true, + animation = true, + backdropType = 'OPAQUE', + children, +}: React.PropsWithChildren) => { + const modalRef = useFocusTrap(isOpen); + + useEscapeKey(isOpen, close); + usePreventScroll(isOpen); + + if (!isOpen) return null; + + return createPortal( + + + + {children} + + , + document.body, + ); +}; + +export default Modal; diff --git a/frontend/src/components/common/Modal/Modal.type.ts b/frontend/src/components/common/Modal/Modal.type.ts new file mode 100644 index 00000000..685ca004 --- /dev/null +++ b/frontend/src/components/common/Modal/Modal.type.ts @@ -0,0 +1,3 @@ +export type Size = 'sm' | 'md' | 'lg'; +export type Position = 'CENTER' | 'BOTTOM'; +export type BackdropType = 'TRANSPARENT' | 'BLUR' | 'OPAQUE'; diff --git a/frontend/src/components/common/Modal/index.ts b/frontend/src/components/common/Modal/index.ts new file mode 100644 index 00000000..9efeeacb --- /dev/null +++ b/frontend/src/components/common/Modal/index.ts @@ -0,0 +1,12 @@ +import Body from '@/components/common/Modal/Body/Body'; +import CloseButton from '@/components/common/Modal/CloseButton/CloseButton'; +import Footer from '@/components/common/Modal/Footer/Footer'; +import Header from '@/components/common/Modal/Header/Header'; +import Layout from '@/components/common/Modal/Modal'; + +export const Modal = Object.assign(Layout, { + CloseButton, + Header, + Body, + Footer, +}); diff --git a/frontend/src/components/common/ScrollIcon/ScrollIcon.styles.ts b/frontend/src/components/common/ScrollIcon/ScrollIcon.styles.ts new file mode 100644 index 00000000..f96305e0 --- /dev/null +++ b/frontend/src/components/common/ScrollIcon/ScrollIcon.styles.ts @@ -0,0 +1,63 @@ +import { RiArrowDownDoubleLine } from 'react-icons/ri'; +import styled, { css, keyframes } from 'styled-components'; + +const bounce = keyframes` + 0%, 20%, 50%, 80%, 100% { + transform: translateY(0); + } + 40% { + transform: translateY(-10px); + } + 60% { + transform: translateY(-5px); + } +`; + +interface ScrollIconProps { + $isBottom: boolean; +} + +const getTransformStyle = ({ $isBottom }: ScrollIconProps) => { + return $isBottom + ? css` + transform: rotate(180deg); + ` + : css` + transform: rotate(0deg); + `; +}; + +export const Layout = styled.div<{ $isBottom: boolean }>` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 0.5rem; + + position: fixed; + bottom: 2rem; + left: calc(50% - 3rem); + z-index: 10; + + padding: 1.5rem; + border-radius: 3rem; + + opacity: 0.7; + font-size: 2rem; + + animation: ${bounce} 1.5s infinite; + transition: opacity 0.2s ease-in-out; + + cursor: pointer; + + &:hover { + opacity: 1; + } +`; + +export const ScrollIcon = styled(RiArrowDownDoubleLine)<{ $isBottom: boolean }>` + color: ${({ theme }) => theme.color.primary[800]}; + + transition: transform 0.3s ease-in-out; + ${getTransformStyle} +`; diff --git a/frontend/src/components/common/ScrollIcon/ScrollIcon.tsx b/frontend/src/components/common/ScrollIcon/ScrollIcon.tsx new file mode 100644 index 00000000..6f106385 --- /dev/null +++ b/frontend/src/components/common/ScrollIcon/ScrollIcon.tsx @@ -0,0 +1,27 @@ +import useScrollIcon from '@/hooks/common/useScrollIcon'; + +import * as S from './ScrollIcon.styles'; + +export interface TargetSection { + id: string; + position: 'top' | 'bottom'; +} +interface ScrollIconProps { + targetSections?: TargetSection[]; +} + +const ScrollIcon = ({ targetSections }: ScrollIconProps) => { + const { currentSection, handleClick } = useScrollIcon({ targetSections }); + + const isBottom = + currentSection === + (targetSections && targetSections.length > 0 ? targetSections[targetSections.length - 1].id : 'bottom'); + + return ( + + + + ); +}; + +export default ScrollIcon; diff --git a/frontend/src/components/common/Spinner/Spinner.stories.tsx b/frontend/src/components/common/Spinner/Spinner.stories.tsx new file mode 100644 index 00000000..6f2beca6 --- /dev/null +++ b/frontend/src/components/common/Spinner/Spinner.stories.tsx @@ -0,0 +1,14 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Spinner from './Spinner'; + +const meta = { + title: 'component/common/Spinner', + component: Spinner, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/frontend/src/components/common/Spinner/Spinner.styles.ts b/frontend/src/components/common/Spinner/Spinner.styles.ts new file mode 100644 index 00000000..da4cc50b --- /dev/null +++ b/frontend/src/components/common/Spinner/Spinner.styles.ts @@ -0,0 +1,62 @@ +import styled, { keyframes } from 'styled-components'; + +import { SpinnerColor, SpinnerSize } from '@/components/common/Spinner/Spinner.type'; + +const bounce = keyframes` + 0%, + 100% { + transform: scale(0); + } + 50% { + transform: scale(1); + } +`; + +const spinnerSizes = { + sm: '4rem', + md: '8rem', + lg: '12rem', + xl: '16rem', +}; + +export const Spinner = styled.div<{ $size: SpinnerSize }>` + display: flex; + justify-content: center; + align-items: center; + + position: relative; + + min-width: ${({ $size }) => spinnerSizes[$size]}; + min-height: ${({ $size }) => spinnerSizes[$size]}; +`; + +export const FirstBounce = styled.div<{ $size: SpinnerSize; $color: SpinnerColor }>` + position: absolute; + + width: ${({ $size }) => spinnerSizes[$size]}; + margin: auto; + border-radius: 50%; + + background-color: ${({ theme, $color }) => theme.color[$color][700]}; + opacity: 0.6; + + animation: ${bounce} 2s infinite ease-in-out; + + aspect-ratio: 1; +`; + +export const SecondBounce = styled.div<{ $size: SpinnerSize; $color: SpinnerColor }>` + position: absolute; + + width: ${({ $size }) => spinnerSizes[$size]}; + margin: auto; + border-radius: 50%; + + background-color: ${({ theme, $color }) => theme.color[$color][700]}; + opacity: 0.6; + + animation: ${bounce} 2s infinite ease-in-out; + + aspect-ratio: 1; + animation-delay: -1s; +`; diff --git a/frontend/src/components/common/Spinner/Spinner.tsx b/frontend/src/components/common/Spinner/Spinner.tsx new file mode 100644 index 00000000..1e3e61fe --- /dev/null +++ b/frontend/src/components/common/Spinner/Spinner.tsx @@ -0,0 +1,19 @@ +import { SpinnerColor, SpinnerSize } from '@/components/common/Spinner/Spinner.type'; + +import * as S from './Spinner.styles'; + +interface SpinnerProps { + size?: SpinnerSize; + color?: SpinnerColor; +} + +const Spinner = ({ size = 'md', color = 'primary' }: SpinnerProps) => { + return ( + + + + + ); +}; + +export default Spinner; diff --git a/frontend/src/components/common/Spinner/Spinner.type.ts b/frontend/src/components/common/Spinner/Spinner.type.ts new file mode 100644 index 00000000..947f5e14 --- /dev/null +++ b/frontend/src/components/common/Spinner/Spinner.type.ts @@ -0,0 +1,2 @@ +export type SpinnerSize = 'sm' | 'md' | 'lg' | 'xl'; +export type SpinnerColor = 'primary' | 'secondary'; diff --git a/frontend/src/components/common/Toast/Toast.stories.tsx b/frontend/src/components/common/Toast/Toast.stories.tsx new file mode 100644 index 00000000..031fb16b --- /dev/null +++ b/frontend/src/components/common/Toast/Toast.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import Toast from './Toast'; + +const meta = { + title: 'component/common/Toast', + component: Toast, + argTypes: { + isOpen: { + control: { type: 'boolean' }, + }, + status: { + options: ['SUCCESS', 'INFO', 'WARNING', 'ERROR'], + control: { type: 'radio' }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + message: 'μ—λŸ¬ λ©”μ‹œμ§€', + }, +}; diff --git a/frontend/src/components/common/Toast/Toast.styles.ts b/frontend/src/components/common/Toast/Toast.styles.ts new file mode 100644 index 00000000..5c3f9bf5 --- /dev/null +++ b/frontend/src/components/common/Toast/Toast.styles.ts @@ -0,0 +1,69 @@ +import styled, { css, keyframes, RuleSet } from 'styled-components'; + +import type { Status } from './Toast'; + +const slideIn = keyframes` + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +`; + +const slideOut = keyframes` + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } +`; + +const pushDown = keyframes` + from { + transform: translateY(-50%); + } + to { + transform: translateY(0); + } +`; + +const backgroundMapper: Record> = { + SUCCESS: css` + background-color: ${({ theme }) => theme.color.success[500]}; + `, + INFO: css` + background-color: ${({ theme }) => theme.color.info[500]}; + `, + WARNING: css` + background-color: ${({ theme }) => theme.color.warning[500]}; + `, + ERROR: css` + background-color: ${({ theme }) => theme.color.danger[500]}; + `, +}; + +export const Layout = styled.div<{ $isOpen: boolean; $isPush: boolean; $status: Status }>` + display: flex; + align-items: center; + + width: 30rem; + min-height: 5rem; + padding: 1.2rem 1.8rem; + border-radius: 1.5rem; + + color: ${({ theme }) => theme.color.black[10]}; + font-size: ${({ theme }) => theme.fontSize.md}; + line-height: 1.5; + + animation: + ${({ $isOpen }) => ($isOpen ? slideIn : slideOut)} 0.8s none, + ${({ $isPush }) => $isPush && pushDown} 0.5s none; + + ${({ $status }) => backgroundMapper[$status]}; +`; diff --git a/frontend/src/components/common/Toast/Toast.tsx b/frontend/src/components/common/Toast/Toast.tsx new file mode 100644 index 00000000..d2c42176 --- /dev/null +++ b/frontend/src/components/common/Toast/Toast.tsx @@ -0,0 +1,27 @@ +import * as S from './Toast.styles'; + +export type Status = 'SUCCESS' | 'INFO' | 'WARNING' | 'ERROR'; + +interface ToastProps { + isOpen: boolean; + isPush: boolean; + message: string; + status?: Status; +} + +const TOAST_IMOJI: Record = { + SUCCESS: 'βœ…', + INFO: 'πŸ“–', + WARNING: 'πŸ‘€', + ERROR: '⛔️', +}; + +const Toast = ({ isOpen, isPush, message, status = 'ERROR' }: ToastProps) => { + return ( + + {`${TOAST_IMOJI[status]} ${message}`} + + ); +}; + +export default Toast; diff --git a/frontend/src/components/common/ToastList/ToastList.styles.ts b/frontend/src/components/common/ToastList/ToastList.styles.ts new file mode 100644 index 00000000..a905ed2a --- /dev/null +++ b/frontend/src/components/common/ToastList/ToastList.styles.ts @@ -0,0 +1,12 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 0.6rem; + + position: fixed; + top: 9rem; + right: 2rem; + z-index: 9999; +`; diff --git a/frontend/src/components/common/ToastList/ToastList.tsx b/frontend/src/components/common/ToastList/ToastList.tsx new file mode 100644 index 00000000..a6ad96f1 --- /dev/null +++ b/frontend/src/components/common/ToastList/ToastList.tsx @@ -0,0 +1,22 @@ +import { createPortal } from 'react-dom'; + +import Toast from '@/components/common/Toast/Toast'; + +import useToastStore from '@/stores/toastStore'; + +import * as S from './ToastList.styles'; + +const ToastList = () => { + const { toastList } = useToastStore(); + + return createPortal( + + {toastList.map((item) => ( + + ))} + , + document.body, + ); +}; + +export default ToastList; diff --git a/frontend/src/components/common/ToolTipQuestionBox/ToolTipQuestionBox.styles.ts b/frontend/src/components/common/ToolTipQuestionBox/ToolTipQuestionBox.styles.ts new file mode 100644 index 00000000..aa13d698 --- /dev/null +++ b/frontend/src/components/common/ToolTipQuestionBox/ToolTipQuestionBox.styles.ts @@ -0,0 +1,16 @@ +import { AiFillQuestionCircle } from 'react-icons/ai'; +import styled from 'styled-components'; + +export const QuestionIcon = styled(AiFillQuestionCircle)<{ $color: string }>` + width: 2rem; + height: 2rem; + + color: ${({ $color }) => $color}; + + cursor: help; + + &:hover { + transform: scale(1.1); + transition: all 0.1s ease-out; + } +`; diff --git a/frontend/src/components/common/ToolTipQuestionBox/ToolTipQuestionBox.tsx b/frontend/src/components/common/ToolTipQuestionBox/ToolTipQuestionBox.tsx new file mode 100644 index 00000000..b254a196 --- /dev/null +++ b/frontend/src/components/common/ToolTipQuestionBox/ToolTipQuestionBox.tsx @@ -0,0 +1,28 @@ +import Tooltip from '@/components/common/Tooltip/Tooltip'; +import { Direction } from '@/components/common/Tooltip/Tooltip.type'; + +import { theme } from '@/styles/theme'; + +import * as S from './ToolTipQuestionBox.styles'; + +interface ToolTipQuestionBoxProps { + message: string; + color?: string; + boxColor?: string; + boxDirection?: Direction; +} + +const ToolTipQuestionBox = ({ + color = theme.color.primary[800], + boxColor = theme.color.primary[800], + boxDirection = 'bottom', + ...props +}: ToolTipQuestionBoxProps) => { + return ( + + + + ); +}; + +export default ToolTipQuestionBox; diff --git a/frontend/src/components/common/Tooltip/Tooltip.styles.ts b/frontend/src/components/common/Tooltip/Tooltip.styles.ts new file mode 100644 index 00000000..b6615940 --- /dev/null +++ b/frontend/src/components/common/Tooltip/Tooltip.styles.ts @@ -0,0 +1,134 @@ +import styled, { css, keyframes } from 'styled-components'; + +import { Direction } from '@/components/common/Tooltip/Tooltip.type'; + +const fadeIn = keyframes` + 0% { + opacity: 0; + } + 100% { + opacity: 0.8; + } +`; + +export const Box = styled.div` + display: flex; + align-items: center; + + position: relative; + top: 0.1rem; + + width: fit-content; + height: fit-content; + + &:hover > .tooltip, + &:active > .tooltip { + display: block; + + transition: all 0.75s ease; + } +`; + +const arrowStyle = css` + content: ''; + + position: absolute; + border-width: 0.8rem; + border-style: solid; + filter: drop-shadow(0 0.2rem 0.2rem rgb(0 0 0 / 30%)); +`; + +const directionStyle = (direction: Direction, color: string) => { + switch (direction) { + case 'top': + return css` + bottom: 3.5rem; + left: 50%; + + transform: translateX(-50%); + + &::before { + ${arrowStyle} + top: 100%; + left: 50%; + + transform: translateX(-50%); + border-color: ${color} transparent transparent transparent; + } + `; + case 'bottom': + return css` + top: 3.5rem; + left: 50%; + + transform: translateX(-50%); + + &::before { + ${arrowStyle} + bottom: 100%; + left: 50%; + + transform: translateX(-50%); + border-color: transparent transparent ${color} transparent; + } + `; + case 'left': + return css` + top: 50%; + right: 3.5rem; + + transform: translateY(-50%); + + &::before { + ${arrowStyle} + top: 50%; + left: 100%; + + transform: translateY(-50%); + border-color: transparent transparent transparent ${color}; + } + `; + case 'right': + return css` + top: 50%; + left: 3.5rem; + + transform: translateY(-50%); + + &::before { + ${arrowStyle} + top: 50%; + right: 100%; + + transform: translateY(-50%); + border-color: transparent ${color} transparent transparent; + } + `; + } +}; + +export const Content = styled.div<{ $color: string; $direction: Direction }>` + display: none; + + position: absolute; + z-index: 100; + + width: fit-content; + min-width: 20rem; + padding: 1rem; + border-radius: 0.5rem; + + background-color: ${({ $color }) => $color}; + box-shadow: 0 0.2rem 0.4rem rgb(0 0 0 / 30%); + color: white; + font-size: ${({ theme }) => theme.fontSize.sm}; + line-height: 150%; + text-align: center; + word-break: keep-all; + + animation: ${fadeIn} 0.3s ease-in-out forwards; + + cursor: help; + + ${({ $color, $direction }) => directionStyle($direction, $color)}; +`; diff --git a/frontend/src/components/common/Tooltip/Tooltip.tsx b/frontend/src/components/common/Tooltip/Tooltip.tsx new file mode 100644 index 00000000..8aaff2e1 --- /dev/null +++ b/frontend/src/components/common/Tooltip/Tooltip.tsx @@ -0,0 +1,29 @@ +import { Direction } from '@/components/common/Tooltip/Tooltip.type'; + +import { theme } from '@/styles/theme'; + +import * as S from './Tooltip.styles'; + +interface ToolTipProps { + message: string; + direction?: Direction; + color?: string; +} + +const Tooltip = ({ + message, + direction = 'bottom', + color = theme.color.primary[800], + children, +}: React.PropsWithChildren) => { + return ( + + {children} + + {message} + + + ); +}; + +export default Tooltip; diff --git a/frontend/src/components/common/Tooltip/Tooltip.type.ts b/frontend/src/components/common/Tooltip/Tooltip.type.ts new file mode 100644 index 00000000..e6e61a74 --- /dev/null +++ b/frontend/src/components/common/Tooltip/Tooltip.type.ts @@ -0,0 +1 @@ +export type Direction = 'top' | 'bottom' | 'left' | 'right'; diff --git a/frontend/src/constants/button.ts b/frontend/src/constants/button.ts new file mode 100644 index 00000000..86f0772b --- /dev/null +++ b/frontend/src/constants/button.ts @@ -0,0 +1,8 @@ +export const BUTTON_TEXT = { + CLOSE: 'λ‹«κΈ°', + BACK: '이전', + NEXT: 'λ‹€μŒ', + COMPLETE: 'μ™„λ£Œ', + CANCEL: 'μ·¨μ†Œ', + CONFIRM: '확인', +}; diff --git a/frontend/src/constants/coduoDocs.ts b/frontend/src/constants/coduoDocs.ts new file mode 100644 index 00000000..e63de5c9 --- /dev/null +++ b/frontend/src/constants/coduoDocs.ts @@ -0,0 +1,104 @@ +import checkBranchCreated from '@/assets/images/docs/check-branch-created.png'; +import checkBranchCreatedWebp from '@/assets/images/docs/check-branch-created.webp'; +import clone from '@/assets/images/docs/clone.png'; +import cloneWebp from '@/assets/images/docs/clone.webp'; +import createBranch from '@/assets/images/docs/create-branch.png'; +import createBranchWebp from '@/assets/images/docs/create-branch.webp'; +import createFork from '@/assets/images/docs/create-fork.png'; +import createForkWebp from '@/assets/images/docs/create-fork.webp'; +import createRoom from '@/assets/images/docs/create-room.png'; +import createRoomWebp from '@/assets/images/docs/create-room.webp'; +import forkRepository from '@/assets/images/docs/fork-repository.png'; +import forkRepositoryWebp from '@/assets/images/docs/fork-repository.webp'; +import inputName from '@/assets/images/docs/input-name.png'; +import inputNameWebp from '@/assets/images/docs/input-name.webp'; +import inputPairName from '@/assets/images/docs/input-pair-name.png'; +import inputPairNameWebp from '@/assets/images/docs/input-pair-name.webp'; +import selectDriver from '@/assets/images/docs/select-driver.png'; +import selectDriverWebp from '@/assets/images/docs/select-driver.webp'; +import selectMission from '@/assets/images/docs/select-mission.png'; +import selectMissionWebp from '@/assets/images/docs/select-mission.webp'; +import setRole from '@/assets/images/docs/set-role.png'; +import setRoleWebp from '@/assets/images/docs/set-role.webp'; +import setTimer from '@/assets/images/docs/set-timer.png'; +import setTimerWebp from '@/assets/images/docs/set-timer.webp'; +import startFree from '@/assets/images/docs/start-free.png'; +import startFreeWebp from '@/assets/images/docs/start-free.webp'; +import startWithMission from '@/assets/images/docs/start-with-mission.png'; +import startWithMissionWebp from '@/assets/images/docs/start-with-mission.webp'; + +export const START_CONTENT = [ + { + id: 'start-coduo', + subtitle: 'μ½”λ”©ν•΄λ“€μ˜€ μ‹œμž‘ν•˜κΈ°', + }, + { + id: 'start-with-mission', + subtitle: 'λ―Έμ…˜κ³Ό ν•¨κ»˜ μ‹œμž‘ν•˜κΈ°', + }, + { + id: 'start-free', + subtitle: '자유둭게 μ‹œμž‘ν•˜κΈ°', + }, + { + id: 'input-name', + subtitle: '이름 μž…λ ₯ν•˜κΈ°', + }, + { + id: 'set-role', + subtitle: 'μ—­ν•  μ„€μ •ν•˜κΈ°', + }, + { + id: 'set-timer', + subtitle: '타이머 μ„€μ •ν•˜κΈ°', + }, +]; + +export const ABOUT_PAIR_PROGRAMMING = [ + { + id: 'what-is-pair-programming', + subtitle: 'νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ΄λž€?', + }, + { + id: 'what-is-pair-room', + subtitle: 'νŽ˜μ–΄λ£Έμ΄λž€?', + }, +]; + +/** + * + * export const DOCS_IMAGES = { + checkBranchCreated: `${S3_BASE_URL}/check-branch-created.png`, + μΆ”ν›„ 이미지 배포 μ‹œ λ‹€μŒκ³Ό 같이 url을 λ„£μ–΄μ£Όμ–΄ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. +}; + */ +export const DOCS_IMAGES = { + checkBranchCreated, + createBranch, + createRoom, + selectMission, + startWithMission, + checkBranchCreatedWebp, + createBranchWebp, + createRoomWebp, + selectMissionWebp, + startWithMissionWebp, + forkRepository, + forkRepositoryWebp, + createFork, + createForkWebp, + clone, + cloneWebp, + inputName, + inputNameWebp, + inputPairName, + inputPairNameWebp, + selectDriver, + selectDriverWebp, + setRole, + setRoleWebp, + setTimer, + setTimerWebp, + startFree, + startFreeWebp, +}; diff --git a/frontend/src/constants/message.ts b/frontend/src/constants/message.ts new file mode 100644 index 00000000..ab15aedf --- /dev/null +++ b/frontend/src/constants/message.ts @@ -0,0 +1,18 @@ +export const ERROR_MESSAGES = { + GET_REFERENCE_LINKS: '레퍼런슀 링크λ₯Ό λΆˆλŸ¬μ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.', + ADD_REFERENCE_LINKS: '레퍼런슀 링크λ₯Ό μ €μž₯ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.', + DELETE_REFERENCE_LINKS: '레퍼런슀 링크 μ‚­μ œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.', + GET_PAIR_ROOM: 'νŽ˜μ–΄λ£Έ 정보λ₯Ό λΆˆλŸ¬μ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.', + ADD_PAIR_NAMES: 'νŽ˜μ–΄λ£Έ 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.', + GET_TODOS: 'νˆ¬λ‘ 리슀트λ₯Ό λΆˆλŸ¬μ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.', + ADD_TODO: 'νˆ¬λ‘ μ•„μ΄ν…œμ„ μ €μž₯ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.', + UPDATE_TODO: 'νˆ¬λ‘ μ•„μ΄ν…œμ„ μˆ˜μ •ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.', + DELETE_TODO: 'νˆ¬λ‘ μ•„μ΄ν…œμ„ μ‚­μ œν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.', + GET_CATEGORIES: 'μΉ΄ν…Œκ³ λ¦¬ 정보λ₯Ό κ°€μ Έμ˜€μ§€ λͺ»ν–ˆμ–΄μš” πŸ₯²', + ADD_CATEGORY: 'μΉ΄ν…Œκ³ λ¦¬λ₯Ό μΆ”κ°€ν•˜μ§€ λͺ»ν–ˆμ–΄μš” πŸ₯²', + SIGN_IN: 'λ‘œκ·ΈμΈμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.', + SIGN_UP: 'νšŒμ›κ°€μž…μ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.', + SIGN_OUT: 'λ‘œκ·Έμ•„μ›ƒμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.', + CHECK_USER_LOGIN: 'μœ μ € 둜그인 μ—¬λΆ€λ₯Ό ν™•μΈν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.', + GET_MEMBER: 'μœ μ € 정보λ₯Ό κ°€μ Έμ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.', +}; diff --git a/frontend/src/constants/queryKeys.ts b/frontend/src/constants/queryKeys.ts new file mode 100644 index 00000000..8f48ef90 --- /dev/null +++ b/frontend/src/constants/queryKeys.ts @@ -0,0 +1,13 @@ +export const QUERY_KEYS = { + GET_REFERENCE_LINKS: 'getReferenceLinks', + GET_PAIR_ROOM: 'getPairRoom', + GET_PAIR_ROOM_TIMER: 'getPairRoomTimer', + GET_PAIR_ROOM_HISTORY: 'getPairRoomHistory', + GET_MY_PAIR_ROOMS: 'getMyPairRooms', + GET_REPOSITORIES: 'getRepositories', + GET_BRANCHES: 'getBranches', + GET_CATEGORIES: 'getCategories', + GET_SIGN_IN: 'getSignIn', + GET_SIGN_OUT: 'getSignOut', + GET_TODOS: 'getTodos', +}; diff --git a/frontend/src/hooks/PairRoom/useCategories.ts b/frontend/src/hooks/PairRoom/useCategories.ts new file mode 100644 index 00000000..9a9145d1 --- /dev/null +++ b/frontend/src/hooks/PairRoom/useCategories.ts @@ -0,0 +1,40 @@ +import { useGetCategories } from '@/queries/PairRoom/category/query'; + +export const DEFAULT_CATEGORY_ID = '0'; +export const DEFAULT_CATEGORY_VALUE = '전체'; + +const DEFAULT_CATEGORY = { + id: DEFAULT_CATEGORY_ID, + value: DEFAULT_CATEGORY_VALUE, +}; + +const useCategories = (accessCode: string) => { + const { data } = useGetCategories(accessCode); + + const categoryNameList = data?.map((category) => category.value); + + const isCategoryExist = (categoryName: string) => { + if (!categoryNameList) return false; + return categoryNameList.includes(categoryName); + }; + + const getCategoryNameById = (categoryId: string): string => { + const category = data?.find((category) => category.id === categoryId); + + if (!category) { + return ''; + } + + return category.value; + }; + + const categories = [DEFAULT_CATEGORY, ...(data || [])]; + + return { + categories, + isCategoryExist, + getCategoryNameById, + }; +}; + +export default useCategories; diff --git a/frontend/src/hooks/PairRoom/useEditCategory.ts b/frontend/src/hooks/PairRoom/useEditCategory.ts new file mode 100644 index 00000000..e948214a --- /dev/null +++ b/frontend/src/hooks/PairRoom/useEditCategory.ts @@ -0,0 +1,62 @@ +import { useState } from 'react'; + +import { validateCategory } from '@/validations/validateCategory'; + +import useToastStore from '@/stores/toastStore'; + +import useInput from '@/hooks/common/useInput'; +import useCategories from '@/hooks/PairRoom/useCategories'; + +import { useDeleteCategory, useUpdateCategory } from '@/queries/PairRoom/category/mutation'; + +const useEditCategory = (accessCode: string, categoryName: string, categoryId: string) => { + const { addToast } = useToastStore(); + + const [isEditing, setIsEditing] = useState(false); + const { value, handleChange, resetValue, message, status } = useInput(categoryName); + const { isCategoryExist } = useCategories(accessCode); + + const updateCategoryMutation = useUpdateCategory(); + const deleteCategoryMutation = useDeleteCategory(); + + const startEditing = () => setIsEditing(true); + const cancelEditing = () => { + setIsEditing(false); + resetValue(); + }; + + const editCategory = (event: React.ChangeEvent, prevCategoryName: string) => { + handleChange(event, validateCategory(event.target.value, isCategoryExist, prevCategoryName)); + }; + + const updateCategory = async () => { + if (value === categoryName) return; + if (status === 'ERROR') return; + await updateCategoryMutation.mutateAsync({ + categoryId, + updatedCategoryName: value, + accessCode, + }); + setIsEditing(false); + addToast({ status: 'SUCCESS', message: 'μΉ΄ν…Œκ³ λ¦¬ 이름이 μˆ˜μ •λ˜μ—ˆμ–΄μš”.' }); + }; + + const deleteCategory = async () => { + await deleteCategoryMutation.mutateAsync({ categoryId, accessCode }); + addToast({ status: 'SUCCESS', message: 'μΉ΄ν…Œκ³ λ¦¬κ°€ μ‚­μ œλ˜μ—ˆμ–΄μš”.' }); + }; + + return { + isEditing, + categoryInputData: { value, message, status }, + actions: { + startEditing, + cancelEditing, + editCategory, + updateCategory, + deleteCategory, + }, + }; +}; + +export default useEditCategory; diff --git a/frontend/src/hooks/PairRoom/useReference.ts b/frontend/src/hooks/PairRoom/useReference.ts new file mode 100644 index 00000000..530c5ebb --- /dev/null +++ b/frontend/src/hooks/PairRoom/useReference.ts @@ -0,0 +1,23 @@ +import { useState } from 'react'; + +import { DEFAULT_CATEGORY_ID } from '@/hooks/PairRoom/useCategories'; + +import { useAddReferenceLink } from '@/queries/PairRoom/reference/mutation'; + +import { formatLink } from '@/utils/Reference/formatLink'; + +const useReference = (accessCode: string, reference: string, success: () => void) => { + const addReference = useAddReferenceLink().mutateAsync; + const [currentCategoryId, setCurrentCategoryId] = useState(null); + const handleCurrentCategory = (category: string | null) => setCurrentCategoryId(category); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const url = formatLink(reference); + const categoryId = currentCategoryId === DEFAULT_CATEGORY_ID ? null : currentCategoryId; + addReference({ url, accessCode, categoryId }).then(() => success()); + }; + return { currentCategoryId, handleCurrentCategory, handleSubmit }; +}; + +export default useReference; diff --git a/frontend/src/hooks/PairRoom/useTimer.ts b/frontend/src/hooks/PairRoom/useTimer.ts new file mode 100644 index 00000000..58a6a55c --- /dev/null +++ b/frontend/src/hooks/PairRoom/useTimer.ts @@ -0,0 +1,119 @@ +import { useRef, useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { useQueryClient } from '@tanstack/react-query'; + +import { AlarmSound } from '@/assets'; + +import useToastStore from '@/stores/toastStore'; + +import { getSSEConnection, startTimer, stopTimer } from '@/apis/timer'; + +import useNotification from '@/hooks/common/useNotification'; + +import { QUERY_KEYS } from '@/constants/queryKeys'; + +const STATUS_SSE_KEY = 'timer'; +const TIME_SSE_KEY = 'remaining-time'; +const TIMEOUT_LIMIT = 100; + +const useTimer = (accessCode: string, defaultTime: number, defaultTimeleft: number, onTimerStop: () => void) => { + const navigate = useNavigate(); + + const queryClient = useQueryClient(); + + const alarmAudio = useRef(new Audio(AlarmSound)); + const timeoutCount = useRef(0); + + const [timeLeft, setTimeLeft] = useState(defaultTimeleft); + const [isActive, setIsActive] = useState(false); + + const { addToast } = useToastStore(); + const { fireNotification } = useNotification(); + + const handleStart = () => { + if (!isActive) startTimer(accessCode); + }; + + const handlePause = () => { + stopTimer(accessCode); + }; + + const handleStop = () => { + addToast({ status: 'SUCCESS', message: '타이머가 μ’…λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.' }); + + setIsActive(false); + setTimeLeft(defaultTime); + onTimerStop(); + + addToast({ status: 'INFO', message: 'λ“œλΌμ΄λ²„ / λ‚΄λΉ„κ²Œμ΄ν„° 역할을 λ°”κΏ” μ£Όμ„Έμš”!' }); + }; + + useEffect(() => { + const sse = getSSEConnection(accessCode); + + const handleStatus = (event: MessageEvent) => { + if (event.data === 'start') { + setIsActive(true); + addToast({ status: 'SUCCESS', message: '타이머가 μ‹œμž‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€.' }); + return; + } + + if (event.data === 'running') { + setIsActive(true); + addToast({ status: 'SUCCESS', message: '타이머가 진행 μ€‘μž…λ‹ˆλ‹€.' }); + return; + } + + if (event.data === 'pause') { + setIsActive(false); + addToast({ status: 'WARNING', message: '타이머가 μΌμ‹œ μ •μ§€λ˜μ—ˆμŠ΅λ‹ˆλ‹€.' }); + return; + } + + if (event.data === 'update') { + queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_PAIR_ROOM_TIMER] }); + addToast({ status: 'WARNING', message: '타이머 μ‹œκ°„μ΄ λ³€κ²½λ˜μ—ˆμŠ΅λ‹ˆλ‹€.' }); + return; + } + }; + + const handleTimeLeft = (event: MessageEvent) => { + if (event.data === '0') { + handleStop(); + alarmAudio.current.play(); + fireNotification('타이머가 λλ‚¬μ–΄μš”!', 'λ“œλΌμ΄λ²„ / λ‚΄λΉ„κ²Œμ΄ν„° 역할을 λ°”κΏ” μ£Όμ„Έμš”!', { + requireInteraction: true, + }); + } else { + setTimeLeft(event.data); + } + }; + + const handleBeforeUnload = (event: BeforeUnloadEvent) => { + event.preventDefault(); + }; + + sse.addEventListener(TIME_SSE_KEY, handleTimeLeft); + sse.addEventListener(STATUS_SSE_KEY, handleStatus); + + sse.onerror = () => { + timeoutCount.current += 1; + if (timeoutCount.current >= TIMEOUT_LIMIT) navigate('/error'); + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + + return () => { + sse.removeEventListener(STATUS_SSE_KEY, handleStatus); + sse.removeEventListener(TIME_SSE_KEY, handleTimeLeft); + sse.close(); + + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, []); + + return { timeLeft, isActive, handleStart, handlePause }; +}; + +export default useTimer; diff --git a/frontend/src/hooks/PairRoomOnboarding/useAutoMoveIndex.ts b/frontend/src/hooks/PairRoomOnboarding/useAutoMoveIndex.ts new file mode 100644 index 00000000..e4b837ad --- /dev/null +++ b/frontend/src/hooks/PairRoomOnboarding/useAutoMoveIndex.ts @@ -0,0 +1,20 @@ +import { useState, useEffect } from 'react'; + +const useAutoMoveIndex = (defaultIndex: number, validationList: boolean[]) => { + const [index, setIndex] = useState(defaultIndex); + + const handleIndex = (nextIndex: number) => nextIndex > index && setIndex(nextIndex); + + if (validationList.some(Boolean)) { + const nextIndex = defaultIndex + validationList.filter(Boolean).length; + handleIndex(nextIndex); + } + + useEffect(() => { + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); + }, [index]); + + return { moveIndex: index }; +}; + +export default useAutoMoveIndex; diff --git a/frontend/src/hooks/PairRoomOnboarding/usePairRoomInformation.ts b/frontend/src/hooks/PairRoomOnboarding/usePairRoomInformation.ts new file mode 100644 index 00000000..c41fe3bd --- /dev/null +++ b/frontend/src/hooks/PairRoomOnboarding/usePairRoomInformation.ts @@ -0,0 +1,103 @@ +import { useState } from 'react'; + +import { validateName, validateDuplicateName } from '@/validations/validatePairName'; +import { validateTimerDuration } from '@/validations/validateTimerDuration'; + +import { InputType, InputStatus } from '@/components/common/Input/Input.type'; + +export type Role = 'DRIVER' | 'NAVIGATOR'; + +const usePairRoomInformation = () => { + const [firstPairName, setFirstPairName] = useState({ + value: '', + status: 'DEFAULT' as InputStatus, + message: '', + }); + const [secondPairName, setSecondPairName] = useState({ + value: '', + status: 'DEFAULT' as InputStatus, + message: '', + }); + + const [driver, setDriver] = useState(''); + const [navigator, setNavigator] = useState(''); + const [timerDuration, setTimerDuration] = useState(''); + + const isPairNameValid = + firstPairName.value !== '' && + secondPairName.value !== '' && + firstPairName.status !== 'ERROR' && + secondPairName.status !== 'ERROR'; + + const isPairRoleValid = driver !== '' && navigator !== ''; + + const isTimerDurationValid = timerDuration !== '' && validateTimerDuration(timerDuration); + + const handlePairName = (firstPairName: string, secondPairName: string) => { + const isValidFirstPairName = validateName(firstPairName); + const isValidSecondPairName = validateName(secondPairName); + + const isDuplicateName = validateDuplicateName(firstPairName, secondPairName); + + setFirstPairName({ + value: firstPairName, + status: isValidFirstPairName.status !== 'ERROR' ? isDuplicateName.status : isValidFirstPairName.status, + message: isValidFirstPairName.status !== 'ERROR' ? isDuplicateName.message : isValidFirstPairName.message, + }); + + setSecondPairName({ + value: secondPairName, + status: isValidSecondPairName.status !== 'ERROR' ? isDuplicateName.status : isValidSecondPairName.status, + message: isValidSecondPairName.status !== 'ERROR' ? isDuplicateName.message : isValidSecondPairName.message, + }); + }; + + const handleFirstPairName = (event: React.ChangeEvent) => { + if (firstPairName.value === driver || firstPairName.value === navigator) { + setDriver(''); + setNavigator(''); + } + + handlePairName(event.target.value, secondPairName.value); + }; + + const handleSecondPairName = (event: React.ChangeEvent) => { + if (secondPairName.value === driver || secondPairName.value === navigator) { + setDriver(''); + setNavigator(''); + } + + handlePairName(firstPairName.value, event.target.value); + }; + + const handlePairRole = (pairName: string, role: Role) => { + const otherPair = firstPairName.value === pairName ? secondPairName.value : firstPairName.value; + + if (role === 'DRIVER') { + setDriver(pairName); + setNavigator(otherPair); + } else { + setDriver(otherPair); + setNavigator(pairName); + } + }; + + const handleTimerDuration = (timerDuration: string) => setTimerDuration(timerDuration); + + return { + firstPairName, + secondPairName, + driver, + navigator, + timerDuration, + isPairNameValid, + isPairRoleValid, + isTimerDurationValid, + handleFirstPairName, + handleSecondPairName, + handlePairRole, + handleTimerDuration, + }; +}; + +export default usePairRoomInformation; diff --git a/frontend/src/hooks/PairRoomOnboarding/usePairRoomMission.ts b/frontend/src/hooks/PairRoomOnboarding/usePairRoomMission.ts new file mode 100644 index 00000000..8e774f9a --- /dev/null +++ b/frontend/src/hooks/PairRoomOnboarding/usePairRoomMission.ts @@ -0,0 +1,33 @@ +import { useState } from 'react'; + +import { validateBranchName } from '@/validations/validateBranchName'; + +import useInput from '@/hooks/common/useInput'; + +const usePairRoomMission = () => { + const [repositoryName, setRepositoryName] = useState(''); + + const { value, status, message, handleChange, resetValue } = useInput(); + + const isRepositorySelected = repositoryName !== ''; + const isValidBranchName = status === 'DEFAULT' && value !== '' && value.length <= 30; + + const handleRepositoryName = (name: string) => { + setRepositoryName(name); + resetValue(); + }; + + const handleBranchName = (event: React.ChangeEvent, existingBranches: string[]) => + handleChange(event, validateBranchName(event.target.value, existingBranches)); + + return { + repositoryName, + branchName: { value, status, message }, + isRepositorySelected, + isValidBranchName, + handleRepositoryName, + handleBranchName, + }; +}; + +export default usePairRoomMission; diff --git a/frontend/src/hooks/common/useClickOutside.ts b/frontend/src/hooks/common/useClickOutside.ts new file mode 100644 index 00000000..08a67bdb --- /dev/null +++ b/frontend/src/hooks/common/useClickOutside.ts @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; + +const useClickOutside = (ref: React.RefObject, callback: () => void) => { + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (ref.current && !ref.current.contains(event.target as Node)) { + callback(); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [ref, callback]); +}; + +export default useClickOutside; diff --git a/frontend/src/hooks/common/useCopyClipboard.test.ts b/frontend/src/hooks/common/useCopyClipboard.test.ts new file mode 100644 index 00000000..cc91cbec --- /dev/null +++ b/frontend/src/hooks/common/useCopyClipboard.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, jest, beforeEach } from '@jest/globals'; +import { renderHook, act } from '@testing-library/react'; + +import useToastStore from '@/stores/toastStore'; + +import useCopyClipBoard from '@/hooks/common/useCopyClipboard'; + +jest.mock('@/stores/toastStore'); + +describe('useCopyClipBoard', () => { + let addToastMock: jest.Mock; + + beforeEach(() => { + addToastMock = jest.fn(); + (useToastStore as unknown as jest.Mock).mockReturnValue({ addToast: addToastMock }); + }); + + it('ν…μŠ€νŠΈκ°€ ν΄λ¦½λ³΄λ“œμ— μ„±κ³΅μ μœΌλ‘œ λ³΅μ‚¬λ˜λ©΄ 성곡 ν† μŠ€νŠΈ λ©”μ‹œμ§€λ₯Ό μΆ”κ°€ν•œλ‹€.', async () => { + const writeTextMock = jest.fn().mockReturnValue(Promise.resolve()); + Object.assign(navigator, { + clipboard: { + writeText: writeTextMock, + }, + }); + + const { result } = renderHook(() => useCopyClipBoard()); + + await act(async () => { + await result.current[1]('test text'); + }); + + expect(writeTextMock).toHaveBeenCalledWith('test text'); + expect(result.current[0]).toBe(true); + expect(addToastMock).toHaveBeenCalledWith({ status: 'SUCCESS', message: 'ν΄λ¦½λ³΄λ“œμ— λ³΅μ‚¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€.' }); + }); + + it('ν…μŠ€νŠΈκ°€ ν΄λ¦½λ³΄λ“œμ— 볡사 μ‹€νŒ¨ν•˜λ©΄ μ‹€νŒ¨ ν† μŠ€νŠΈ λ©”μ‹œμ§€λ₯Ό μΆ”κ°€ν•œλ‹€.', async () => { + const writeTextMock = jest.fn().mockRejectedValue(new Error('Failed to copy') as unknown as never); + Object.assign(navigator, { + clipboard: { + writeText: writeTextMock, + }, + }); + + const { result } = renderHook(() => useCopyClipBoard()); + + await act(async () => { + await result.current[1]('test text'); + }); + + expect(writeTextMock).toHaveBeenCalledWith('test text'); + expect(result.current[0]).toBe(false); + expect(addToastMock).toHaveBeenCalledWith({ status: 'ERROR', message: 'ν΄λ¦½λ³΄λ“œμ— 볡사에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.' }); + }); +}); diff --git a/frontend/src/hooks/common/useCopyClipboard.ts b/frontend/src/hooks/common/useCopyClipboard.ts new file mode 100644 index 00000000..570d829f --- /dev/null +++ b/frontend/src/hooks/common/useCopyClipboard.ts @@ -0,0 +1,26 @@ +import { useState } from 'react'; + +import useToastStore from '@/stores/toastStore'; + +type onCopyFn = (text: string) => Promise; + +const useCopyClipBoard = (): [boolean, onCopyFn] => { + const [isCopy, setIsCopy] = useState(false); + + const { addToast } = useToastStore(); + + const onCopy: onCopyFn = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + setIsCopy(true); + addToast({ status: 'SUCCESS', message: 'ν΄λ¦½λ³΄λ“œμ— λ³΅μ‚¬λ˜μ—ˆμŠ΅λ‹ˆλ‹€.' }); + } catch (error) { + setIsCopy(false); + addToast({ status: 'ERROR', message: 'ν΄λ¦½λ³΄λ“œμ— 볡사에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.' }); + } + }; + + return [isCopy, onCopy]; +}; + +export default useCopyClipBoard; diff --git a/frontend/src/hooks/common/useDebounce.ts b/frontend/src/hooks/common/useDebounce.ts new file mode 100644 index 00000000..c8ff2672 --- /dev/null +++ b/frontend/src/hooks/common/useDebounce.ts @@ -0,0 +1,14 @@ +import { useState, useEffect } from 'react'; + +const useDebounce = (value: T, delay: number): T => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => setDebouncedValue(value), delay); + return () => clearTimeout(handler); + }, [value]); + + return debouncedValue; +}; + +export default useDebounce; diff --git a/frontend/src/hooks/common/useDragAndDrop.ts b/frontend/src/hooks/common/useDragAndDrop.ts new file mode 100644 index 00000000..5a05a33f --- /dev/null +++ b/frontend/src/hooks/common/useDragAndDrop.ts @@ -0,0 +1,29 @@ +import { useState } from 'react'; + +import { Todo } from '@/apis/todo'; + +export type DragPosition = 'ABOVE' | 'BELOW'; + +const useDragAndDrop = (list: Todo[], handleOrder: (todoId: number, order: number) => void) => { + const [dragItem, setDragItem] = useState(null); + const [dragOverItem, setDragOverItem] = useState(null); + + const handleDragStart = (id: number) => setDragItem(list.find((item) => item.id === id) || null); + + const handleDragEnter = (id: number) => setDragOverItem(list.find((item) => item.id === id) || null); + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault(); + + if (!dragItem || !dragOverItem || dragItem.id === dragOverItem.id) return; + + handleOrder(dragItem.id, dragOverItem.order); + + setDragItem(null); + setDragOverItem(null); + }; + + return { dragItem, dragOverItem, handleDragStart, handleDragEnter, handleDrop }; +}; + +export default useDragAndDrop; diff --git a/frontend/src/hooks/common/useEscapeKey.ts b/frontend/src/hooks/common/useEscapeKey.ts new file mode 100644 index 00000000..c76cbd50 --- /dev/null +++ b/frontend/src/hooks/common/useEscapeKey.ts @@ -0,0 +1,17 @@ +import { useEffect } from 'react'; + +const useEscapeKey = (isOpen: boolean, close: () => void) => { + useEffect(() => { + if (!isOpen) return; + + const handleEscapePress = (event: KeyboardEvent) => { + if (event.key === 'Escape') close(); + }; + + document.addEventListener('keydown', handleEscapePress, true); + + return () => document.removeEventListener('keydown', handleEscapePress, true); + }, [isOpen, close]); +}; + +export default useEscapeKey; diff --git a/frontend/src/hooks/common/useFocusTrap.ts b/frontend/src/hooks/common/useFocusTrap.ts new file mode 100644 index 00000000..faf24e6e --- /dev/null +++ b/frontend/src/hooks/common/useFocusTrap.ts @@ -0,0 +1,42 @@ +import { useEffect, useRef } from 'react'; + +const FOCUSABLE_SELECTORS = + 'button:not([disabled]), input:not([disabled]), select:not([disabled], textarea:not([disabled]), a[href], [tabindex]:not([tabindex="-1"])'; + +const useFocusTrap = (isOpen: boolean) => { + const mainRef = useRef(null); + const focusableElements = useRef([]); + + useEffect(() => { + if (!isOpen || !mainRef.current) return; + + focusableElements.current = Array.from(mainRef.current.querySelectorAll(FOCUSABLE_SELECTORS)); + + if (focusableElements.current.length === 0) return; + + const moveFocusIndexPrev = (currentIndex: number) => + currentIndex === 0 ? focusableElements.current.length - 1 : currentIndex - 1; + + const moveFocusIndexNext = (currentIndex: number) => + currentIndex === focusableElements.current.length - 1 ? 0 : currentIndex + 1; + + const handleKeyPress = (event: KeyboardEvent) => { + if (event.key === 'Tab') { + event.preventDefault(); + + const currentIndex = focusableElements.current.findIndex((el) => el === document.activeElement); + const nextIndex = event.shiftKey ? moveFocusIndexPrev(currentIndex) : moveFocusIndexNext(currentIndex); + + focusableElements.current[nextIndex].focus(); + } + }; + + document.addEventListener('keydown', handleKeyPress); + + return () => document.removeEventListener('keydown', handleKeyPress); + }, [isOpen, mainRef]); + + return mainRef; +}; + +export default useFocusTrap; diff --git a/frontend/src/hooks/common/useHashScroll.ts b/frontend/src/hooks/common/useHashScroll.ts new file mode 100644 index 00000000..d7c3d6ea --- /dev/null +++ b/frontend/src/hooks/common/useHashScroll.ts @@ -0,0 +1,63 @@ +import { useEffect, useRef, useState } from 'react'; +import { useLocation } from 'react-router-dom'; + +const useHashScroll = () => { + const location = useLocation(); + const currentHash = location.hash.replace('#', ''); + const [activeSection, setActiveSection] = useState(currentHash); + const isProcessingRef = useRef(false); + + const handleActiveSection = (section: string) => setActiveSection(section); + + /** + * 슀크둀 이벀트λ₯Ό κ°μ§€ν•˜μ—¬ ν˜„μž¬ λ·°ν¬νŠΈμ— λ³΄μ΄λŠ” μ„Ήμ…˜μ˜ id 값에 따라 activeSection μ—…λ°μ΄νŠΈ + * - μš”μ†Œκ°€ λ·°ν¬νŠΈμ— μ§„μž…ν•˜λŠ” μˆœκ°„ ν™œμ„±ν™” + * - μš”μ†Œκ°€ 뷰포트 μƒλ‹¨μ—μ„œ 보이기 μ‹œμž‘ν•˜λŠ” μˆœκ°„λΆ€ν„° ν•˜λ‹¨μ— λ„λ‹¬ν•˜κΈ° μ§μ „κΉŒμ§€μ˜ λͺ¨λ“  μœ„μΉ˜λ₯Ό 포함 + * - isProcessingRef λ₯Ό 톡해 μ§„ν–‰μ€‘μΌλ•Œ λΆˆν•„μš”ν•œ 계산을 ν•˜μ§€ μ•Šλ„λ‘ 함. + */ + useEffect(() => { + const handleScroll = () => { + if (isProcessingRef.current) return; + + requestAnimationFrame(() => { + const sections = Array.from(document.querySelectorAll('section[id], div[id], p[id]')); + const viewportHeight = window.innerHeight; + + const visibleSection = sections.find((section) => { + const rect = section.getBoundingClientRect(); + return rect.top >= 0 && rect.top <= viewportHeight; + }); + + if (visibleSection && visibleSection.id !== activeSection) { + handleActiveSection(visibleSection.id); + } + + isProcessingRef.current = false; + }); + + isProcessingRef.current = true; + }; + + window.addEventListener('scroll', handleScroll); + + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, [activeSection]); + + /** + * url 의 ν•΄μ‹œκ°’μ„ 기반으둜 슀크둀 이동 + */ + useEffect(() => { + if (location.hash) { + const element = document.getElementById(currentHash); + if (element) { + element.scrollIntoView({ behavior: 'smooth' }); + } + } + }, [location, currentHash]); + + return { activeSection }; +}; + +export default useHashScroll; diff --git a/frontend/src/hooks/common/useInput.test.ts b/frontend/src/hooks/common/useInput.test.ts new file mode 100644 index 00000000..e16aa2f0 --- /dev/null +++ b/frontend/src/hooks/common/useInput.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from '@jest/globals'; +import { renderHook, act } from '@testing-library/react'; + +import type { InputStatus } from '@/components/common/Input/Input.type'; + +import useInput from '@/hooks/common/useInput'; + +describe('useInput', () => { + it('초기 μƒνƒœκ°€ μ˜¬λ°”λ₯Έμ§€ ν™•μΈν•œλ‹€.', () => { + const { result } = renderHook(() => useInput('initial')); + expect(result.current.value).toBe('initial'); + expect(result.current.status).toBe('DEFAULT'); + expect(result.current.message).toBe(''); + }); + + it('handleChangeκ°€ 값을 μ—…λ°μ΄νŠΈν•˜λŠ”μ§€ ν™•μΈν•œλ‹€.', () => { + const { result } = renderHook(() => useInput('initial')); + + act(() => { + result.current.handleChange({ target: { value: 'new value' } } as React.ChangeEvent); + }); + + expect(result.current.value).toBe('new value'); + }); + + it('handleChangeκ°€ μœ νš¨μ„± 검사λ₯Ό μˆ˜ν–‰ν•˜λŠ”μ§€ ν™•μΈν•œλ‹€.', () => { + const validateValue = (value: string) => { + if (value.length < 5) { + return { status: 'ERROR' as InputStatus, message: 'Too short' }; + } + return { status: 'DEFAULT' as InputStatus, message: '' }; + }; + + const { result } = renderHook(() => useInput('initial')); + + act(() => { + result.current.handleChange( + { target: { value: 'new' } } as React.ChangeEvent, + validateValue('new'), + ); + }); + + expect(result.current.status).toBe('ERROR'); + expect(result.current.message).toBe('Too short'); + + act(() => { + result.current.handleChange( + { target: { value: 'new value' } } as React.ChangeEvent, + validateValue('new value'), + ); + }); + + expect(result.current.status).toBe('DEFAULT'); + expect(result.current.message).toBe(''); + }); + + it('resetValueκ°€ 값을 μ΄ˆκΈ°ν™”ν•˜λŠ”μ§€ ν™•μΈν•œλ‹€.', () => { + const { result } = renderHook(() => useInput('initial')); + + act(() => { + result.current.handleChange({ target: { value: 'new value' } } as React.ChangeEvent); + }); + + act(() => { + result.current.resetValue(); + }); + + expect(result.current.value).toBe('initial'); + }); +}); diff --git a/frontend/src/hooks/common/useInput.ts b/frontend/src/hooks/common/useInput.ts new file mode 100644 index 00000000..12087b06 --- /dev/null +++ b/frontend/src/hooks/common/useInput.ts @@ -0,0 +1,32 @@ +import { useState } from 'react'; + +import type { InputStatus } from '@/components/common/Input/Input.type'; + +const useInput = (initialValue: string = '') => { + const [value, setValue] = useState(initialValue); + const [status, setStatus] = useState('DEFAULT'); + const [message, setMessage] = useState(''); + + const handleChange = ( + event: React.ChangeEvent, + validateValue?: { status: InputStatus; message: string }, + ) => { + if (validateValue) { + const { status, message } = validateValue; + setStatus(status); + setMessage(message); + } + + setValue(event.target.value); + }; + + const resetValue = () => { + setValue(initialValue); + setStatus('DEFAULT'); + setMessage(''); + }; + + return { value, status, message, handleChange, resetValue } as const; +}; + +export default useInput; diff --git a/frontend/src/hooks/common/useModal.ts b/frontend/src/hooks/common/useModal.ts new file mode 100644 index 00000000..3ccc6e49 --- /dev/null +++ b/frontend/src/hooks/common/useModal.ts @@ -0,0 +1,15 @@ +import { useState } from 'react'; + +const useModal = (defaultValue: boolean = false) => { + const [isModalOpen, setIsModalOpen] = useState(defaultValue); + + const openModal = () => setIsModalOpen(true); + + const closeModal = () => setIsModalOpen(false); + + const modalToggle = () => setIsModalOpen(!isModalOpen); + + return { isModalOpen, openModal, closeModal, modalToggle }; +}; + +export default useModal; diff --git a/frontend/src/hooks/common/useNotification.ts b/frontend/src/hooks/common/useNotification.ts new file mode 100644 index 00000000..b02c71c1 --- /dev/null +++ b/frontend/src/hooks/common/useNotification.ts @@ -0,0 +1,43 @@ +import { useRef, useEffect } from 'react'; + +import { LogoIcon } from '@/assets'; + +const useNotification = () => { + const notificationRef = useRef(null); + + const handleNotificationClick = (event: Event) => { + event.preventDefault(); + window.focus(); + notificationRef.current?.close(); + }; + + const requestPermission = async () => { + if (Notification.permission !== 'granted') { + await Notification.requestPermission(); + } + }; + + const fireNotification = (title: string, body: string, options?: NotificationOptions) => { + if (Notification.permission === 'granted' && !document.hasFocus()) { + const newOption = { + body: body || ' ', + badge: LogoIcon, + icon: LogoIcon, + ...options, + }; + const notification = new Notification(title, newOption); + notificationRef.current = notification; + notification.onclick = handleNotificationClick; + } else { + console.warn('μ•Œλ¦Ό κΆŒν•œμ΄ ν—ˆμš©λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.'); + } + }; + + useEffect(() => { + requestPermission(); + }, []); + + return { fireNotification }; +}; + +export default useNotification; diff --git a/frontend/src/hooks/common/usePreventBackNavigation.ts b/frontend/src/hooks/common/usePreventBackNavigation.ts new file mode 100644 index 00000000..93648b8d --- /dev/null +++ b/frontend/src/hooks/common/usePreventBackNavigation.ts @@ -0,0 +1,23 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +const usePreventBackNavigation = () => { + const navigate = useNavigate(); + + useEffect(() => { + const handlePopState = (event: PopStateEvent) => { + event.preventDefault(); + navigate(1); + }; + + window.addEventListener('popstate', handlePopState); + + window.history.pushState(null, '', window.location.href); + + return () => { + window.removeEventListener('popstate', handlePopState); + }; + }, [navigate]); +}; + +export default usePreventBackNavigation; diff --git a/frontend/src/hooks/common/usePreventScroll.ts b/frontend/src/hooks/common/usePreventScroll.ts new file mode 100644 index 00000000..08b6beb2 --- /dev/null +++ b/frontend/src/hooks/common/usePreventScroll.ts @@ -0,0 +1,33 @@ +import { useEffect } from 'react'; + +const preventScroll = () => { + const currentScrollY = window.scrollY; + + document.body.style.position = 'fixed'; + document.body.style.width = '100%'; + document.body.style.top = `-${currentScrollY}px`; + document.body.style.overflowY = 'auto'; + + return currentScrollY; +}; + +const allowScroll = (prevScrollY: number) => { + document.body.style.position = ''; + document.body.style.width = ''; + document.body.style.top = ''; + document.body.style.overflowY = ''; + + window.scrollTo(0, prevScrollY); +}; + +const usePreventScroll = (isOpen: boolean) => { + useEffect(() => { + if (!isOpen) return; + + const prevScrollY = preventScroll(); + + return () => allowScroll(prevScrollY); + }, [isOpen]); +}; + +export default usePreventScroll; diff --git a/frontend/src/hooks/common/useScrollAnimation.ts b/frontend/src/hooks/common/useScrollAnimation.ts new file mode 100644 index 00000000..118b12db --- /dev/null +++ b/frontend/src/hooks/common/useScrollAnimation.ts @@ -0,0 +1,38 @@ +import { useEffect, useRef, useState } from 'react'; + +export interface IntersectionObserverOptions { + root?: Element | null; + rootMargin?: string; + threshold?: number | number[]; +} + +export const useScrollAnimation = (options: IntersectionObserverOptions = {}) => { + const [isInViewport, setIsInViewport] = useState(false); + const [hasAnimated, setHasAnimated] = useState(false); + const ref = useRef(null); + + useEffect(() => { + if (!ref.current) return; + + const callback = (entries: IntersectionObserverEntry[]) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setIsInViewport(true); + setHasAnimated(true); + } else if (!hasAnimated) { + setIsInViewport(false); + } + }); + }; + const observerOptions = { ...options }; + + const observer = new IntersectionObserver(callback, observerOptions); + observer.observe(ref.current); + + return () => { + observer.disconnect(); + }; + }, [options, hasAnimated]); + + return { isInViewport: hasAnimated || isInViewport, ref }; +}; diff --git a/frontend/src/hooks/common/useScrollIcon.ts b/frontend/src/hooks/common/useScrollIcon.ts new file mode 100644 index 00000000..af5f2592 --- /dev/null +++ b/frontend/src/hooks/common/useScrollIcon.ts @@ -0,0 +1,75 @@ +import { useState, useEffect } from 'react'; + +import { TargetSection } from '@/components/common/ScrollIcon/ScrollIcon'; + +interface UseScrollIconProps { + targetSections?: TargetSection[]; +} + +const useScrollIcon = ({ targetSections }: UseScrollIconProps) => { + const defaultSections: TargetSection[] = [ + { id: 'top', position: 'top' }, + { id: 'bottom', position: 'bottom' }, + ]; + + const sections = targetSections && targetSections.length > 0 ? targetSections : defaultSections; + + const [currentSection, setCurrentSection] = useState(sections[0].id); + + const handleScroll = () => { + const scrollPosition = window.scrollY + window.innerHeight / 2; + if (!targetSections && window.scrollY < 50) { + setCurrentSection('top'); + return; + } + if (!targetSections && window.innerHeight + window.scrollY >= document.body.scrollHeight - 50) { + setCurrentSection('bottom'); + return; + } + + const isInView = (element: HTMLElement, scrollPosition: number) => { + const elementTop = element.offsetTop; + const elementBottom = elementTop + element.offsetHeight; + return scrollPosition >= elementTop && scrollPosition < elementBottom; + }; + + sections.some((section) => { + const element = document.getElementById(section.id); + if (element && isInView(element, scrollPosition)) { + setCurrentSection(section.id); + return true; + } + return false; + }); + }; + + const handleClick = () => { + const currentIndex = sections.findIndex((section) => section.id === currentSection); + const nextIndex = (currentIndex + 1) % sections.length; + const nextSection = sections[nextIndex]; + + switch (nextSection.id) { + case 'top': + window.scrollTo({ top: 0, behavior: 'smooth' }); + break; + case 'bottom': + window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); + break; + default: + document.getElementById(nextSection.id)?.scrollIntoView({ behavior: 'smooth' }); + } + + setCurrentSection(nextSection.id); + }; + + useEffect(() => { + window.addEventListener('scroll', handleScroll); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, []); + + return { currentSection, handleClick }; +}; + +export default useScrollIcon; diff --git a/frontend/src/hooks/common/useTitleTime.ts b/frontend/src/hooks/common/useTitleTime.ts new file mode 100644 index 00000000..94b72e2b --- /dev/null +++ b/frontend/src/hooks/common/useTitleTime.ts @@ -0,0 +1,22 @@ +import { useEffect, useState } from 'react'; + +const useTitleTime = (minutes?: string, seconds?: string) => { + const [titleTime, setTitleTime] = useState(''); + + const updateTitle = () => { + const htmlTitle = document.querySelector('title'); + if (htmlTitle) htmlTitle.innerHTML = `${titleTime} μ½”λ”©ν•΄λ“€μ˜€`; + }; + + const handleTitle = (title: string) => { + setTitleTime(`${title} -`); + }; + + useEffect(updateTitle, [titleTime]); + useEffect(() => { + if (!minutes || !seconds) return setTitleTime(''); + handleTitle(`${minutes}:${seconds}`); + }, [minutes, seconds]); +}; + +export default useTitleTime; diff --git a/frontend/src/hooks/member/useSignInHandler.ts b/frontend/src/hooks/member/useSignInHandler.ts new file mode 100644 index 00000000..1670106d --- /dev/null +++ b/frontend/src/hooks/member/useSignInHandler.ts @@ -0,0 +1,12 @@ +import { getSignInGithub } from '@/apis/oauth'; + +const useSignInHandler = () => { + const handleSignInGithub = async () => { + const response = await getSignInGithub(); + window.location.href = response.endpoint; + }; + + return { handleSignInGithub }; +}; + +export default useSignInHandler; diff --git a/frontend/src/hooks/member/useSignOutHandler.ts b/frontend/src/hooks/member/useSignOutHandler.ts new file mode 100644 index 00000000..6f451360 --- /dev/null +++ b/frontend/src/hooks/member/useSignOutHandler.ts @@ -0,0 +1,21 @@ +import { useNavigate } from 'react-router-dom'; + +import useUserStore from '@/stores/userStore'; + +import { getSignOut } from '@/apis/member'; + +const useSignOutHandler = () => { + const navigate = useNavigate(); + const { setUser } = useUserStore(); + + const handleSignOut = async () => { + await getSignOut(); + setUser('', 'SIGNED_OUT'); + document.cookie = 'whoami=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; + navigate('/'); + }; + + return { handleSignOut }; +}; + +export default useSignOutHandler; diff --git a/frontend/src/hooks/member/useSignUpHandler.ts b/frontend/src/hooks/member/useSignUpHandler.ts new file mode 100644 index 00000000..bac0252a --- /dev/null +++ b/frontend/src/hooks/member/useSignUpHandler.ts @@ -0,0 +1,22 @@ +import { useNavigate } from 'react-router-dom'; + +import useUserStore from '@/stores/userStore'; + +import { addSignUp } from '@/apis/oauth'; + +const useSignUpHandler = () => { + const navigate = useNavigate(); + + const { setUser } = useUserStore(); + + const handleSignUp = async (username: string) => { + await addSignUp(username); + + setUser(username, 'SIGNED_IN'); + + navigate('/main'); + }; + + return { handleSignUp }; +}; +export default useSignUpHandler; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx new file mode 100644 index 00000000..ca4a820e --- /dev/null +++ b/frontend/src/index.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import ReactGA from 'react-ga4'; + +import * as Sentry from '@sentry/react'; +import ReactDOM from 'react-dom/client'; + +import App from './App'; + +import './styles/font.css'; + +if (process.env.NODE_ENV === 'production') { + Sentry.init({ + dsn: process.env.SENTRY_DSN, + integrations: [Sentry.browserTracingIntegration(), Sentry.replayIntegration()], + tracesSampleRate: 1.0, + tracePropagationTargets: ['localhost:3001', 'https://coduo.site', /^\/api\//], + replaysSessionSampleRate: 0.1, + replaysOnErrorSampleRate: 1.0, + }); + if (process.env.GA_TRACKING_ID) ReactGA.initialize(process.env.GA_TRACKING_ID); +} + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/frontend/src/pages/Callback/Callback.styles.ts b/frontend/src/pages/Callback/Callback.styles.ts new file mode 100644 index 00000000..d2a6f33c --- /dev/null +++ b/frontend/src/pages/Callback/Callback.styles.ts @@ -0,0 +1,26 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 6rem; + + height: calc(100vh - 7rem); + padding: 15rem 5rem; + + background-color: ${({ theme }) => theme.color.black[20]}; +`; + +export const LogoIcon = styled.img` + width: 30rem; + max-width: 40rem; +`; + +export const Title = styled.h1` + color: ${({ theme }) => theme.color.primary[800]}; + font-size: ${({ theme }) => theme.fontSize.h5}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; + line-height: 1.5; + text-align: center; +`; diff --git a/frontend/src/pages/Callback/Callback.tsx b/frontend/src/pages/Callback/Callback.tsx new file mode 100644 index 00000000..eb763990 --- /dev/null +++ b/frontend/src/pages/Callback/Callback.tsx @@ -0,0 +1,48 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { LogoIconWithTitle } from '@/assets'; + +import Spinner from '@/components/common/Spinner/Spinner'; + +import useUserStore from '@/stores/userStore'; + +import { getMember } from '@/apis/member'; +import { getSignInCallback } from '@/apis/oauth'; + +import * as S from './Callback.styles'; + +const Callback = () => { + const navigate = useNavigate(); + + const { setUser } = useUserStore(); + + useEffect(() => { + const handleCallBack = async () => { + const { signedUp } = await getSignInCallback(); + + if (signedUp) { + const { username } = await getMember(); + + setUser(username, 'SIGNED_IN'); + navigate('/main'); + + return; + } + + navigate('/sign-up'); + }; + + handleCallBack(); + }, []); + + return ( + + + 둜그인 μ€‘μž…λ‹ˆλ‹€. μž μ‹œλ§Œ κΈ°λ‹€λ €μ£Όμ„Έμš” 😊 + + + ); +}; + +export default Callback; diff --git a/frontend/src/pages/CoduoDocs/CoduoDocs.stories.tsx b/frontend/src/pages/CoduoDocs/CoduoDocs.stories.tsx new file mode 100644 index 00000000..eb1bf863 --- /dev/null +++ b/frontend/src/pages/CoduoDocs/CoduoDocs.stories.tsx @@ -0,0 +1,14 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import CoduoDocs from '@/pages/CoduoDocs/CoduoDocs'; + +const meta = { + title: 'component/CoduoDocs/CoduoDocs', + component: CoduoDocs, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/frontend/src/pages/CoduoDocs/CoduoDocs.styles.ts b/frontend/src/pages/CoduoDocs/CoduoDocs.styles.ts new file mode 100644 index 00000000..cc06e1aa --- /dev/null +++ b/frontend/src/pages/CoduoDocs/CoduoDocs.styles.ts @@ -0,0 +1,56 @@ +import styled from 'styled-components'; + +export const Title = styled.p` + color: ${({ theme }) => theme.color.black[90]}; + font-size: ${({ theme }) => theme.fontSize.h2}; + font-weight: ${({ theme }) => theme.fontWeight.semibold}; +`; + +export const Subtitle = styled.p` + color: ${({ theme }) => theme.color.black[90]}; + font-size: ${({ theme }) => theme.fontSize.h4}; + font-weight: ${({ theme }) => theme.fontWeight.semibold}; +`; + +export const Content = styled.p` + gap: 1rem; + + color: ${({ theme }) => theme.color.black[90]}; + font-size: ${({ theme }) => theme.fontSize.base}; + line-height: 1.9; +`; + +export const Strong = styled.strong` + color: ${({ theme }) => theme.color.primary[700]}; + font-weight: ${({ theme }) => theme.fontWeight.semibold}; +`; + +export const Sentence = styled.div` + display: flex; + flex-direction: column; + gap: 0.8rem; +`; + +export const ParagraphContainer = styled.section` + display: flex; + flex-direction: column; + gap: 1.7rem; +`; + +export const ImageContainer = styled.section` + display: flex; + flex-direction: column; + gap: 5rem; +`; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + gap: 8.5rem; + + margin: 8% 10% 8% 25%; + + @media (width <= 1000px) { + margin: 8% 17%; + } +`; diff --git a/frontend/src/pages/CoduoDocs/CoduoDocs.tsx b/frontend/src/pages/CoduoDocs/CoduoDocs.tsx new file mode 100644 index 00000000..a6e0e315 --- /dev/null +++ b/frontend/src/pages/CoduoDocs/CoduoDocs.tsx @@ -0,0 +1,277 @@ +import DocsImage from '@/components/CoduoDocs/DocsImage/DocsImage'; +import ContentBox from '@/components/CoduoDocs/FloatingSidebar/ContentBox'; +import FloatingSidebar from '@/components/CoduoDocs/FloatingSidebar/FloatingSidebar'; +import Quote from '@/components/CoduoDocs/Quote/Quote'; +import SourceCode from '@/components/CoduoDocs/SourceCode/SourceCode'; + +import useHashScroll from '@/hooks/common/useHashScroll'; + +import { ABOUT_PAIR_PROGRAMMING, DOCS_IMAGES, START_CONTENT } from '@/constants/coduoDocs'; + +import * as S from './CoduoDocs.styles'; + +const CoduoDocs = () => { + const { activeSection } = useHashScroll(); + + return ( + <> + + + + + + + + μ½”λ”©ν•΄λ“€μ˜€ μ‹œμž‘ν•˜κΈ° + + μ½”λ”©ν•΄λ“€μ˜€ λ¬Έμ„œμ— μ˜€μ‹  것을 ν™˜μ˜ν•©λ‹ˆλ‹€! μ½”λ”©ν•΄λ“€μ˜€λŠ” νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ„ 처음 μ ‘ν•˜λŠ” μ‚¬μš©μžκ°€ νŽ˜μ–΄ + ν”„λ‘œκ·Έλž˜λ°μ„ μ‹œμž‘ν•˜κΈ° μœ„ν•΄ ν•„μš”ν•œ λͺ¨λ“  것을 μ œκ³΅ν•˜λŠ” μ„œλΉ„μŠ€μž…λ‹ˆλ‹€. μ—¬κΈ°μ—μ„œλŠ” μ½”λ”©ν•΄λ“€μ˜€λ₯Ό μ–΄λ–»κ²Œ μ‹œμž‘ν•  수 + μžˆλŠ”μ§€ μ†Œκ°œν•©λ‹ˆλ‹€. + + + + + + + + μ½”λ”©ν•΄λ“€μ˜€λŠ” μ›ν™œν•œ νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ° 진행을 μœ„ν•΄ μ—°μŠ΅ λ―Έμ…˜μ„ μ œκ³΅ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. + + + + + + + + λ―Έμ…˜κ³Ό ν•¨κ»˜ μ‹œμž‘ν•˜κΈ° + + + + + + + + + + + + + + + + + + + + + + + 이제 κ°€μ €μ˜¨ ν”„λ‘œμ νŠΈλ₯Ό 본인이 μ‚¬μš©ν•˜λŠ” 톡합 개발 ν™˜κ²½(IDE)으둜 μ—΄μ–΄ λ―Έμ…˜μ„ μ§„ν–‰ν•©λ‹ˆλ‹€. + + + + + 자유둭게 μ‹œμž‘ν•˜κΈ° + + + + + + + + + + + + + + + + + + + + + + + + + νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ— λŒ€ν•΄ + νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ΄λž€? + + + + νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°(Pair Programming)은 두 λͺ…μ˜ ν”„λ‘œκ·Έλž˜λ¨Έκ°€ ν•œ μ»΄ν“¨ν„°μ—μ„œ ν•¨κ»˜ μž‘μ—…ν•˜λ©° μ†Œν”„νŠΈμ›¨μ–΄ μ½”λ“œλ₯Ό + μž‘μ„±ν•˜λŠ” ν˜‘μ—… λ°©μ‹μž…λ‹ˆλ‹€. νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ—μ„œλŠ” 두 μ‚¬λžŒμ΄ 각각 λ“œλΌμ΄λ²„(Driver) 와 λ‚΄λΉ„κ²Œμ΄ν„°(Navigator) + 역할을 λ²ˆκ°ˆμ•„ κ°€λ©° μˆ˜ν–‰ν•©λ‹ˆλ‹€. + + + + + + +
    + + +
    + + 지속적인 μ˜μ‚¬μ†Œν†΅ μœ μ§€ν•˜κΈ° +

    + νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ˜ 핡심은 지속적인 μ˜μ‚¬μ†Œν†΅μž…λ‹ˆλ‹€. λ§Œμ•½ 두 개발자 사이에 λŒ€ν™”κ°€ μ—†λ‹€λ©΄, 그듀은 μ•„λ§ˆλ„ + 사고 과정을 κ³΅μœ ν•˜κ³  μžˆμ§€ μ•Šμ„ κ²ƒμž…λ‹ˆλ‹€. λ“œλΌμ΄λ²„λŠ” μ½”λ“œλ₯Ό μž‘μ„±ν•˜λ©΄μ„œ μžμ‹ μ˜ 생각을 말둜 ν‘œν˜„ν•΄μ•Ό + ν•˜λ©°, λ‚΄λΉ„κ²Œμ΄ν„°λŠ” 적극적으둜 μ˜κ²¬μ„ μ œμ‹œν•˜κ³  μ§ˆλ¬Έν•΄μ•Ό ν•©λ‹ˆλ‹€. 이λ₯Ό 톡해 아이디어λ₯Ό 효과적으둜 + κ΅ν™˜ν•˜κ³  문제λ₯Ό ν•¨κ»˜ ν•΄κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€. +

    +
    + μ—­ν•  μ •κΈ°μ μœΌλ‘œ κ΅λŒ€ν•˜κΈ°μ™€ μ½”λ“œ 자주 μ»€λ°‹ν•˜κΈ° +

    + λ“œλΌμ΄λ²„μ™€ λ‚΄λΉ„κ²Œμ΄ν„° 역할을 일정 κ°„κ²©μœΌλ‘œ λ°”κΎΈλŠ” 것이 μ€‘μš”ν•©λ‹ˆλ‹€. μ΄λŠ” 개발자 κ°„μ˜ 기술 곡유λ₯Ό + μ΄‰μ§„ν•˜κ³  집쀑λ ₯을 μœ μ§€ν•˜λŠ” 데 도움이 λ©λ‹ˆλ‹€. 역할을 μ „ν™˜ν•  λ•Œλ§ˆλ‹€ μ½”λ“œλ₯Ό μ»€λ°‹ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€. μž‘μ€ + λ‹¨μœ„λ‘œ 자주 μ»€λ°‹ν•˜λ©΄ μž‘μ—… 진행 상황을 λͺ…ν™•νžˆ ν•  수 있고, λ‚˜μ€‘μ— μ½”λ“œλ₯Ό λ¦¬λ·°ν•˜κ±°λ‚˜ 문제λ₯Ό ν•΄κ²°ν•  λ•Œ + μœ μš©ν•©λ‹ˆλ‹€. +

    +
    + μ μ ˆν•œ νŽ˜μ–΄ ꡬ성과 μ΅μˆ™ν•œ 개발 ν™˜κ²½ μ‚¬μš©ν•˜κΈ° +

    + νŽ˜μ–΄λ₯Ό 이룰 λ•ŒλŠ” 두 κ°œλ°œμžκ°€ μ›ν™œν•˜κ²Œ ν˜‘λ ₯ν•  수 μžˆλŠ”μ§€ μ‹ μ€‘νžˆ κ³ λ €ν•΄μ•Ό ν•©λ‹ˆλ‹€. μ„±κ²©μ΄λ‚˜ μž‘μ—… μŠ€νƒ€μΌμ΄ + λ§žμ§€ μ•ŠμœΌλ©΄ 생산성이 λ–¨μ–΄μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ, 두 개발자 λͺ¨λ‘ μ‚¬μš©ν•˜λŠ” 개발 ν™˜κ²½μ— μ΅μˆ™ν•΄μ•Ό ν•©λ‹ˆλ‹€. + 그렇지 μ•ŠμœΌλ©΄ νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°μ˜ κ· ν˜•μ΄ 깨질 수 μžˆμŠ΅λ‹ˆλ‹€. ν•„μš”ν•œ 경우, μ‹œμž‘ 전에 개발 ν™˜κ²½μ„ ν•¨κ»˜ + μ„€μ •ν•˜κ³  μ΅νžˆλŠ” μ‹œκ°„μ„ κ°€μ§€λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€. +

    +
    + ν•„μš”ν•  λ•Œ λͺ…ν™•νžˆ μ„€λͺ… μš”μ²­ν•˜κΈ°μ™€ μ μ ˆν•œ νœ΄μ‹ μ·¨ν•˜κΈ° +

    + 특히 κ²½ν—˜μ΄ 적은 κ°œλ°œμžκ°€ 전문가와 μž‘μ—…ν•  λ•ŒλŠ” 배울 수 μžˆλŠ” λͺ¨λ“  기회λ₯Ό ν™œμš©ν•΄μ•Ό ν•©λ‹ˆλ‹€. 이해가 λ˜μ§€ + μ•ŠλŠ” 뢀뢄이 μžˆλ‹€λ©΄ μ£Όμ €ν•˜μ§€ 말고 μ„€λͺ…을 μš”μ²­ν•΄μ•Ό ν•©λ‹ˆλ‹€. λ™μ‹œμ—, 두 개발자 λͺ¨λ‘μ—κ²Œ μ ν•©ν•œ 페이슀둜 + μž‘μ—…ν•΄μ•Ό ν•©λ‹ˆλ‹€. ν•„μš”ν•  λ•ŒλŠ” νœ΄μ‹μ„ μ·¨ν•˜μ—¬ 집쀑λ ₯을 μœ μ§€ν•˜κ³  생산성을 높일 수 μžˆμŠ΅λ‹ˆλ‹€. 정기적인 + νœ΄μ‹μ€ μž₯기적으둜 더 효율적인 μž‘μ—…μ„ κ°€λŠ₯ν•˜κ²Œ ν•©λ‹ˆλ‹€. +

    +
    +
    +
    +
    + + νŽ˜μ–΄λ£Έμ΄λž€? + + + + +
    + νˆ¬λ‘ 리슀트 +

    + νˆ¬λ‘ 리슀트λ₯Ό μ œκ³΅ν•˜μ—¬, νŽ˜μ–΄κ°€ ν•¨κ»˜ λͺ©ν‘œμ™€ ν•  일을 μ„€μ •ν•˜κ³  관리할 수 μžˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό 톡해 μž‘μ—…μ˜ + λ°©ν–₯성을 λͺ…ν™•νžˆ ν•˜κ³  진행 상황을 μ‹€μ‹œκ°„μœΌλ‘œ νŒŒμ•…ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ, μš°μ„ μˆœμœ„λ₯Ό μ„€μ •ν•˜μ—¬ μ€‘μš”ν•œ + μž‘μ—…μ— 집쀑할 수 있으며, μ™„λ£Œλœ μž‘μ—…μ„ μ²΄ν¬ν•˜λ©΄μ„œ 성취감을 λŠλ‚„ 수 μžˆμŠ΅λ‹ˆλ‹€. +

    +
    + 레퍼런슀 +

    + νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ° 도쀑 μ°Έκ³ ν•œ 레퍼런슀λ₯Ό μ €μž₯ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ €μž₯된 λ ˆνΌλŸ°μŠ€λŠ” μΉ΄ν…Œκ³ λ¦¬λ³„λ‘œ μ •λ¦¬λ˜μ–΄ + λ‚˜μ€‘μ— μ‰½κ²Œ μ°Ύμ•„λ³Ό 수 μžˆμ–΄ ν”„λ‘œμ νŠΈ μ™„λ£Œ 후에도 ν•™μŠ΅ 자료둜 ν™œμš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. +

    +
    + μ•ŒλžŒ κΈ°λŠ₯ +

    + μ„€μ •ν•œ μ‹œκ°„μ΄ μ§€λ‚˜λ©΄ 역할을 λ³€κ²½ν•  수 μžˆλ„λ‘ μ•ŒλžŒμ΄ 울렀 μ μ ˆν•œ μ‹œμ μ— 역할을 ꡐ체할 수 μžˆλ„λ‘ + λ•μŠ΅λ‹ˆλ‹€. +

    +
    + 회고 +

    + νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ° μ„Έμ…˜μ΄ μ’…λ£Œλ˜λ©΄ 회고λ₯Ό μ§„ν–‰ν•©λ‹ˆλ‹€. 이 κ³Όμ •μ—μ„œ 잘된 점, κ°œμ„ ν•  점, 배운 점 등을 ν•¨κ»˜ + λ…Όμ˜ν•˜κ³  κΈ°λ‘ν•©λ‹ˆλ‹€. 회고 κΈ°λŠ₯은 κ΅¬μ‘°ν™”λœ ν…œν”Œλ¦Ώμ„ μ œκ³΅ν•˜μ—¬ 효과적인 회고λ₯Ό κ°€λŠ₯ν•˜κ²Œ ν•˜λ©°, 이전 회고 + λ‚΄μš©μ„ μ‰½κ²Œ μ°Έμ‘°ν•  수 μžˆμ–΄ 지속적인 κ°œμ„ μ΄ κ°€λŠ₯ν•©λ‹ˆλ‹€. +

    +
    +
    +
    +
    +
    +
    + + ); +}; + +export default CoduoDocs; diff --git a/frontend/src/pages/CoduoDocs/CoduoDocs.type.ts b/frontend/src/pages/CoduoDocs/CoduoDocs.type.ts new file mode 100644 index 00000000..218f2adc --- /dev/null +++ b/frontend/src/pages/CoduoDocs/CoduoDocs.type.ts @@ -0,0 +1,4 @@ +export interface Content { + id: string; + subtitle: string; +} diff --git a/frontend/src/pages/Error/Error.styles.ts b/frontend/src/pages/Error/Error.styles.ts new file mode 100644 index 00000000..5ddf33d5 --- /dev/null +++ b/frontend/src/pages/Error/Error.styles.ts @@ -0,0 +1,36 @@ +import styled, { css } from 'styled-components'; + +export const buttonStyles = css` + font-size: ${({ theme }) => theme.fontSize.md}; +`; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 6rem; + + height: calc(100vh - 7rem); + padding: 15rem; + + background-color: ${({ theme }) => theme.color.primary[100]}; +`; + +export const TitleContainer = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 2rem; +`; + +export const Title = styled.h1` + color: ${({ theme }) => theme.color.primary[800]}; + font-size: ${({ theme }) => theme.fontSize.h1}; + font-weight: bold; +`; + +export const SubTitle = styled.p` + font-size: ${({ theme }) => theme.fontSize.md}; + line-height: 1.5; + text-align: center; +`; diff --git a/frontend/src/pages/Error/Error.tsx b/frontend/src/pages/Error/Error.tsx new file mode 100644 index 00000000..534e9f07 --- /dev/null +++ b/frontend/src/pages/Error/Error.tsx @@ -0,0 +1,23 @@ +import { Link } from 'react-router-dom'; + +import Button from '@/components/common/Button/Button'; + +import * as S from './Error.styles'; + +const Error = () => { + return ( + + + 404 + νŽ˜μ΄μ§€λ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑 λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. + + + + + + ); +}; + +export default Error; diff --git a/frontend/src/pages/Landing/Landing.styles.ts b/frontend/src/pages/Landing/Landing.styles.ts new file mode 100644 index 00000000..51d1c7b2 --- /dev/null +++ b/frontend/src/pages/Landing/Landing.styles.ts @@ -0,0 +1,91 @@ +import styled, { css } from 'styled-components'; + +export const buttonStyles = css` + width: 26rem; + height: 6rem; + + font-size: ${({ theme }) => theme.fontSize.h6}; +`; + +export const githubButtonStyles = css` + justify-content: space-evenly; + + width: 26rem; + height: 6rem; + border: 1px solid ${({ theme }) => theme.color.black[80]}; + + background-color: ${({ theme }) => theme.color.black[90]}; + color: ${({ theme }) => theme.color.black[10]}; + font-size: ${({ theme }) => theme.fontSize.h6}; + + img { + width: 3rem; + height: 3rem; + } + + &:hover { + border: 1px solid ${({ theme }) => theme.color.black[70]}; + + background-color: ${({ theme }) => theme.color.black[80]}; + color: ${({ theme }) => theme.color.black[10]}; + } + + &:active { + border: 1px solid ${({ theme }) => theme.color.black[60]}; + + background-color: ${({ theme }) => theme.color.black[70]}; + color: ${({ theme }) => theme.color.black[10]}; + } +`; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 12rem; + overflow: hidden; + + height: calc(100vh - 7rem); + padding: 10rem; + + background: linear-gradient( + 140deg, + ${({ theme }) => theme.color.secondary[100]}, + ${({ theme }) => theme.color.primary[200]} + ); + background-color: ${({ theme }) => theme.color.black[10]}; + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + padding: 4rem; + } +`; + +export const SubTitle = styled.h2` + color: ${({ theme }) => theme.color.primary[700]}; + font-size: ${({ theme }) => theme.fontSize.h3}; + font-weight: ${({ theme }) => theme.fontWeight.normal}; + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + font-size: ${({ theme }) => theme.fontSize.h4}; + } +`; + +export const Logo = styled.img` + width: 50rem; + filter: drop-shadow(0 0 2rem ${({ theme }) => theme.color.black[10]}); + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + width: 40rem; + } +`; + +export const ButtonContainer = styled.div` + display: flex; + gap: 4rem; + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + flex-direction: column; + gap: 2rem; + } +`; diff --git a/frontend/src/pages/Landing/Landing.tsx b/frontend/src/pages/Landing/Landing.tsx new file mode 100644 index 00000000..a793eb27 --- /dev/null +++ b/frontend/src/pages/Landing/Landing.tsx @@ -0,0 +1,64 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { GithubLogoWhite, LogoIconWithTitle } from '@/assets'; + +import * as S from '@/pages/Landing/Landing.styles'; + +import { ScrollAnimationContainer } from '@/components/common/Animation/ScrollAnimationContainer'; +import Button from '@/components/common/Button/Button'; +import ScrollIcon, { TargetSection } from '@/components/common/ScrollIcon/ScrollIcon'; +import HowToPair from '@/components/Landing/HowToPair/HowToPair'; + +import useUserStore from '@/stores/userStore'; + +import usePreventBackNavigation from '@/hooks/common/usePreventBackNavigation'; +import useTitleTime from '@/hooks/common/useTitleTime'; +import useSignInHandler from '@/hooks/member/useSignInHandler'; + +const Landing = () => { + const navigate = useNavigate(); + const targetSections: TargetSection[] = [ + { id: 'landing', position: 'top' }, + { id: 'how-to-pair', position: 'bottom' }, + ]; + useTitleTime(); + usePreventBackNavigation(); + + const { userStatus } = useUserStore(); + const { handleSignInGithub } = useSignInHandler(); + + useEffect(() => { + if (userStatus === 'SIGNED_IN') navigate('/main'); + }, [userStatus]); + + return ( + <> + + + + λ‹Ήμ‹ μ˜ 첫 번째 νŽ˜μ–΄ ν”„λ‘œκ·Έλž˜λ°, + + + + + + + + + + + + + + + + ); +}; + +export default Landing; diff --git a/frontend/src/pages/Layout.styles.ts b/frontend/src/pages/Layout.styles.ts new file mode 100644 index 00000000..fdbdd727 --- /dev/null +++ b/frontend/src/pages/Layout.styles.ts @@ -0,0 +1,8 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + + min-width: fit-content; +`; diff --git a/frontend/src/pages/Layout.tsx b/frontend/src/pages/Layout.tsx new file mode 100644 index 00000000..7515fd33 --- /dev/null +++ b/frontend/src/pages/Layout.tsx @@ -0,0 +1,20 @@ +import { Outlet } from 'react-router-dom'; + +import Header from '@/components/common/Header/Header'; +import ToastList from '@/components/common/ToastList/ToastList'; + +import * as S from './Layout.styles'; + +const Layout = () => { + return ( + +
    +
    + +
    + + + ); +}; + +export default Layout; diff --git a/frontend/src/pages/Loading/Loading.styles.ts b/frontend/src/pages/Loading/Loading.styles.ts new file mode 100644 index 00000000..5e0eaeab --- /dev/null +++ b/frontend/src/pages/Loading/Loading.styles.ts @@ -0,0 +1,21 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + height: calc(100vh - 7rem); + padding: 20px; + + background-color: ${({ theme }) => theme.color.black[20]}; +`; + +export const Title = styled.h1` + margin-bottom: 2rem; + + color: ${({ theme }) => theme.color.primary[800]}; + font-size: ${({ theme }) => theme.fontSize.h5}; + font-weight: ${({ theme }) => theme.fontWeight.light}; +`; diff --git a/frontend/src/pages/Loading/Loading.tsx b/frontend/src/pages/Loading/Loading.tsx new file mode 100644 index 00000000..6e39462b --- /dev/null +++ b/frontend/src/pages/Loading/Loading.tsx @@ -0,0 +1,14 @@ +import Spinner from '@/components/common/Spinner/Spinner'; + +import * as S from './Loading.styles'; + +const Loading = () => { + return ( + + νŽ˜μ΄μ§€λ₯Ό λΆˆλŸ¬μ˜€λŠ” μ€‘μž…λ‹ˆλ‹€. μž μ‹œλ§Œ κΈ°λ‹€λ €μ£Όμ„Έμš” ☺️ + + + ); +}; + +export default Loading; diff --git a/frontend/src/pages/Main/Main.styles.ts b/frontend/src/pages/Main/Main.styles.ts new file mode 100644 index 00000000..8b4888ac --- /dev/null +++ b/frontend/src/pages/Main/Main.styles.ts @@ -0,0 +1,107 @@ +import styled, { css } from 'styled-components'; + +export const Layout = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + gap: 4rem; + overflow: hidden; + + position: relative; + + min-height: calc(100vh - 7rem); + padding: 8rem 10.8vw; + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + flex-direction: column; + justify-content: center; + gap: 8rem; + + padding: 8rem 5.4vw; + } +`; + +export const TextContainer = styled.div` + display: flex; + flex-direction: column; + gap: 6rem; +`; + +export const TitleContainer = styled.div` + display: flex; + flex-direction: column; + gap: 2rem; + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + align-items: center; + gap: 4rem; + + text-align: center; + } +`; + +export const SubTitle = styled.h2` + color: ${({ theme }) => theme.color.primary[800]}; + font-size: ${({ theme }) => theme.fontSize.h3}; + font-weight: ${({ theme }) => theme.fontWeight.light}; + line-height: 1.4; + + span { + font-weight: ${({ theme }) => theme.fontWeight.medium}; + } + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + font-size: ${({ theme }) => theme.fontSize.h4}; + } +`; + +export const Title = styled.h1` + color: ${({ theme }) => theme.color.primary[500]}; + font-size: 9rem; + font-weight: ${({ theme }) => theme.fontWeight.medium}; + + span { + color: ${({ theme }) => theme.color.secondary[500]}; + } + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + font-size: 8rem; + } +`; + +export const Info = styled.p` + opacity: 0.5; + color: ${({ theme }) => theme.color.primary[700]}; + font-size: ${({ theme }) => theme.fontSize.lg}; + line-height: 1.6; + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + display: none; + } +`; + +export const ButtonContainer = styled.div` + display: flex; + flex-direction: column; + gap: 3rem; + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + } +`; + +export const buttonStyles = css` + width: 24rem; + height: 6rem; + + font-size: ${({ theme }) => theme.fontSize.h6}; + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + width: 100%; + min-width: 18rem; + + font-size: ${({ theme }) => theme.fontSize.lg}; + } +`; diff --git a/frontend/src/pages/Main/Main.tsx b/frontend/src/pages/Main/Main.tsx new file mode 100644 index 00000000..fc9db072 --- /dev/null +++ b/frontend/src/pages/Main/Main.tsx @@ -0,0 +1,69 @@ +import * as S from '@/pages/Main/Main.styles'; + +import { ScrollAnimationContainer } from '@/components/common/Animation/ScrollAnimationContainer'; +import WaveBackground from '@/components/common/Background/WaveBackground'; +import Button from '@/components/common/Button/Button'; +import PairRoomCreateModal from '@/components/Main/PairRoomCreateModal/PairRoomCreateModal'; +import PairRoomEntryModal from '@/components/Main/PairRoomEntryModal/PairRoomEntryModal'; + +import useModal from '@/hooks/common/useModal'; +import usePreventBackNavigation from '@/hooks/common/usePreventBackNavigation'; + +const Main = () => { + usePreventBackNavigation(); + + const { + isModalOpen: isPairRoomCreateModalOpen, + openModal: openPairRoomCreateModal, + closeModal: closePairRoomCreateModal, + } = useModal(); + + const { + isModalOpen: isPairRoomEntryModalOpen, + openModal: openPairRoomEntryModal, + closeModal: closePairRoomEntryModal, + } = useModal(); + + return ( + <> + + + + + + + ν˜‘μ—…κ³Ό μ„±μž₯을 μœ„ν•œ +
    + νŽ˜μ–΄ν”„λ‘œκ·Έλž˜λ°- +
    + + μ½”λ”©ν•΄λ“€μ˜€ + +
    + + μ½”λ”©ν•΄λ“€μ˜€λŠ” νŽ˜μ–΄ν”„λ‘œκ·Έλž˜λ°μ„ 톡해 더 λ‚˜μ€ κ²°κ³Όλ₯Ό λ§Œλ“€μ–΄λ‚΄λŠ” 것을 λͺ©ν‘œλ‘œ ν•©λ‹ˆλ‹€. +
    + 직관적인 μΈν„°νŽ˜μ΄μŠ€μ™€ μ‹€μ‹œκ°„ ν˜‘μ—… λ„κ΅¬λ‘œ, λˆ„κ΅¬λ‚˜ μ‰½κ²Œ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. +
    +
    +
    + + + + + + + + + + +
    + + ); +}; + +export default Main; diff --git a/frontend/src/pages/MyPage/MyPage.styles.ts b/frontend/src/pages/MyPage/MyPage.styles.ts new file mode 100644 index 00000000..6da414d3 --- /dev/null +++ b/frontend/src/pages/MyPage/MyPage.styles.ts @@ -0,0 +1,90 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + justify-content: center; +`; + +export const Container = styled.div` + display: flex; + flex-direction: column; + gap: 5rem; + + width: 70rem; + margin: 5rem 10rem; + + h2 { + font-size: ${({ theme }) => theme.fontSize.lg}; + font-weight: ${({ theme }) => theme.fontWeight.normal}; + } +`; + +export const TitleContainer = styled.div` + display: flex; + flex-direction: column; + gap: 0.8rem; +`; + +export const Title = styled.h1` + color: ${({ theme }) => theme.color.black[90]}; + font-size: ${({ theme }) => theme.fontSize.h2}; + font-weight: ${({ theme }) => theme.fontWeight.semibold}; +`; + +export const SubTitle = styled.p` + color: ${({ theme }) => theme.color.black[90]}; + font-size: ${({ theme }) => theme.fontSize.h6}; + + span { + color: ${({ theme }) => theme.color.primary[600]}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; + } +`; + +export const ListWrapper = styled.div` + display: flex; + flex-direction: column; + gap: 2.4rem; + + h2 { + font-size: ${({ theme }) => theme.fontSize.h4}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; + } +`; + +export const List = styled.div` + display: flex; + flex-direction: column; + gap: 1.6rem; +`; + +export const AllText = styled.p` + padding-bottom: 1.6rem; + + color: ${({ theme }) => theme.color.black[70]}; + font-size: ${({ theme }) => theme.fontSize.md}; +`; + +export const EmptyText = styled.p` + color: ${({ theme }) => theme.color.black[70]}; + font-size: ${({ theme }) => theme.fontSize.base}; +`; + +export const LeaveButton = styled.button` + display: flex; + justify-content: flex-end; + align-items: center; + gap: 0.4rem; + + color: ${({ theme }) => theme.color.black[60]}; + font-size: ${({ theme }) => theme.fontSize.md}; + + transition: all 0.2s ease; + + cursor: pointer; + + &:hover { + color: ${({ theme }) => theme.color.black[70]}; + font-size: ${({ theme }) => theme.fontSize.md}; + } +`; diff --git a/frontend/src/pages/MyPage/MyPage.tsx b/frontend/src/pages/MyPage/MyPage.tsx new file mode 100644 index 00000000..58926a61 --- /dev/null +++ b/frontend/src/pages/MyPage/MyPage.tsx @@ -0,0 +1,58 @@ +import { IoIosArrowForward } from 'react-icons/io'; + +import Spinner from '@/components/common/Spinner/Spinner'; +import PairRoomButton from '@/components/MyPage/PairRoomButton/PairRoomButton'; + +import useUserStore from '@/stores/userStore'; + +import useMyPairRooms from '@/queries/MyPage/useMyPairRooms'; + +import * as S from './MyPage.styles'; + +const MyPage = () => { + const { username } = useUserStore(); + + const { data: pairRooms, isFetching } = useMyPairRooms(); + + return ( + + + + 마이 νŽ˜μ΄μ§€ + + {username} λ‹˜μ˜ 마이 νŽ˜μ΄μ§€μ— μ˜€μ‹  κ±Έ ν™˜μ˜ν•©λ‹ˆλ‹€! + + + +

    λ‚˜μ˜ νŽ˜μ–΄λ£Έ λͺ©λ‘

    +
    + 총 {pairRooms && pairRooms.length}개 + + {isFetching && } + {!isFetching && pairRooms?.length === 0 ? ( + μƒμ„±ν•œ νŽ˜μ–΄λ£Έμ΄ μ—†μŠ΅λ‹ˆλ‹€. + ) : ( + pairRooms && + pairRooms.map((pairRoom) => ( + + )) + )} + +
    +
    + + νšŒμ› νƒˆν‡΄ν•˜κΈ° + + +
    +
    + ); +}; + +export default MyPage; diff --git a/frontend/src/pages/PairRoom/PairRoom.styles.tsx b/frontend/src/pages/PairRoom/PairRoom.styles.tsx new file mode 100644 index 00000000..3582bcee --- /dev/null +++ b/frontend/src/pages/PairRoom/PairRoom.styles.tsx @@ -0,0 +1,35 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + gap: 2rem; + + min-width: fit-content; + height: calc(100vh - 7rem); + min-height: 60rem; + padding: 2rem; + + background: ${({ theme }) => theme.color.primary[100]}; +`; + +export const SpinnerContainer = styled.div` + display: flex; + gap: 2rem; + + min-width: fit-content; + height: calc(100vh - 7rem); + min-height: 60rem; + padding: 2rem; + + background: ${({ theme }) => theme.color.primary[100]}; +`; + +export const Container = styled.div` + display: flex; + flex-direction: column; + gap: 2rem; + + width: 100%; + min-height: 56rem; + max-height: calc(100vh - 11rem); +`; diff --git a/frontend/src/pages/PairRoom/PairRoom.tsx b/frontend/src/pages/PairRoom/PairRoom.tsx new file mode 100644 index 00000000..07a376c0 --- /dev/null +++ b/frontend/src/pages/PairRoom/PairRoom.tsx @@ -0,0 +1,78 @@ +import { useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; + +import Loading from '@/pages/Loading/Loading'; + +import PairListCard from '@/components/PairRoom/PairListCard/PairListCard'; +import PairRoleCard from '@/components/PairRoom/PairRoleCard/PairRoleCard'; +import ReferenceCard from '@/components/PairRoom/ReferenceCard/ReferenceCard'; +import TimerCard from '@/components/PairRoom/TimerCard/TimerCard'; +import TodoListCard from '@/components/PairRoom/TodoListCard/TodoListCard'; + +import { getPairRoomExists } from '@/apis/pairRoom'; + +import useGetPairRoom from '@/queries/PairRoom/useGetPairRoom'; +import useUpdatePairRoom from '@/queries/PairRoom/useUpdatePairRoom'; + +import * as S from './PairRoom.styles'; + +const PairRoom = () => { + const navigate = useNavigate(); + const { accessCode } = useParams(); + + useEffect(() => { + const checkPairRoomExists = async () => { + if (!accessCode) navigate('/error'); + + const { exists } = await getPairRoomExists(accessCode || ''); + + if (!exists) navigate('/error'); + }; + + checkPairRoomExists(); + }, [accessCode]); + + const [driver, setDriver] = useState(''); + const [navigator, setNavigator] = useState(''); + + const { + driver: latestDriver, + navigator: latestNavigator, + duration, + remainingTime, + isFetching, + } = useGetPairRoom(accessCode || ''); + const { handleUpdatePairRole } = useUpdatePairRoom(accessCode || ''); + + useEffect(() => { + setDriver(latestDriver); + setNavigator(latestNavigator); + }, [latestDriver, latestNavigator]); + + const [isCardOpen, setIsCardOpen] = useState(false); + + if (isFetching) { + return ; + } + + return ( + + {}} /> + + + + + + setIsCardOpen(false)} /> + setIsCardOpen(true)} /> + + + ); +}; + +export default PairRoom; diff --git a/frontend/src/pages/PairRoomOnboarding/PairRoomOnboarding.styles.ts b/frontend/src/pages/PairRoomOnboarding/PairRoomOnboarding.styles.ts new file mode 100644 index 00000000..9a04487a --- /dev/null +++ b/frontend/src/pages/PairRoomOnboarding/PairRoomOnboarding.styles.ts @@ -0,0 +1,49 @@ +import styled from 'styled-components'; + +export const Layout = styled.div` + display: flex; + justify-content: center; + + width: 100%; + min-height: calc(100vh - 7rem); + + background-color: ${({ theme }) => theme.color.primary[100]}; +`; + +export const Title = styled.h2` + color: ${({ theme }) => theme.color.primary[800]}; + font-size: ${({ theme }) => theme.fontSize.h3}; + font-weight: ${({ theme }) => theme.fontWeight.semibold}; +`; + +export const Container = styled.div` + display: flex; + flex-direction: column; + gap: 5rem; + + width: 60%; + min-width: 76.8rem; + padding: 4rem 4rem 12rem; + + background-color: ${({ theme }) => theme.color.black[10]}; + + @media (max-width: ${({ theme }) => theme.deviceWidth.mobile}) { + width: 100%; + min-width: 0; + padding: 4rem; + } +`; + +export const InputContainer = styled.div` + display: flex; + flex-direction: column; + gap: 8rem; +`; + +export const ButtonWrapper = styled.div` + display: flex; + justify-content: center; + + width: 100%; + margin-top: 5rem; +`; diff --git a/frontend/src/pages/PairRoomOnboarding/PairRoomOnboarding.tsx b/frontend/src/pages/PairRoomOnboarding/PairRoomOnboarding.tsx new file mode 100644 index 00000000..7ebf3654 --- /dev/null +++ b/frontend/src/pages/PairRoomOnboarding/PairRoomOnboarding.tsx @@ -0,0 +1,28 @@ +import { useLocation } from 'react-router-dom'; + +import MissionSettingSection from '@/components/PairRoomOnboarding/MissionSettingSection/MissionSettingSection'; +import PairRoomSettingSection from '@/components/PairRoomOnboarding/PairRoomSettingSection/PairRoomSettingSection'; + +import useCreateBranch from '@/queries/PairRoomOnboarding/useCreateBranch'; + +import * as S from './PairRoomOnboarding.styles'; + +const PairRoomOnboarding = () => { + const location = useLocation(); + const searchParams = new URLSearchParams(location.search); + const mission = searchParams.get('mission'); + + const { handleCreateBranch, isSuccess } = useCreateBranch(); + + return ( + + + {mission === 'true' ? 'λ―Έμ…˜κ³Ό ν•¨κ»˜ μ‹œμž‘ν•˜κΈ°' : 'κ·Έλƒ₯ μ‹œμž‘ν•˜κΈ°'} + {mission === 'true' && !isSuccess && } + {((mission === 'true' && isSuccess) || mission === 'false') && } + + + ); +}; + +export default PairRoomOnboarding; diff --git a/frontend/src/pages/SignUp/SignUp.styles.ts b/frontend/src/pages/SignUp/SignUp.styles.ts new file mode 100644 index 00000000..faf310ab --- /dev/null +++ b/frontend/src/pages/SignUp/SignUp.styles.ts @@ -0,0 +1,35 @@ +import styled, { css } from 'styled-components'; + +export const buttonStyles = css` + font-size: ${({ theme }) => theme.fontSize.md}; +`; + +export const Layout = styled.div` + display: flex; + flex-direction: column; + align-items: center; + gap: 6rem; + + height: calc(100vh - 7rem); + padding: 15rem 5rem; + + background-color: ${({ theme }) => theme.color.black[20]}; +`; + +export const LogoIconWithTitle = styled.img` + width: 30rem; + max-width: 40rem; +`; + +export const Form = styled.form` + display: flex; + flex-direction: column; + align-items: center; + gap: 3.6rem; +`; + +export const Title = styled.h1` + color: ${({ theme }) => theme.color.primary[800]}; + font-size: ${({ theme }) => theme.fontSize.h5}; + font-weight: ${({ theme }) => theme.fontWeight.medium}; +`; diff --git a/frontend/src/pages/SignUp/SignUp.tsx b/frontend/src/pages/SignUp/SignUp.tsx new file mode 100644 index 00000000..c829ed45 --- /dev/null +++ b/frontend/src/pages/SignUp/SignUp.tsx @@ -0,0 +1,67 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { LogoIconWithTitle } from '@/assets'; + +import { validateName } from '@/validations/validatePairName'; + +import Button from '@/components/common/Button/Button'; +import Input from '@/components/common/Input/Input'; + +import useUserStore from '@/stores/userStore'; + +import useInput from '@/hooks/common/useInput'; +import useSignUpHandler from '@/hooks/member/useSignUpHandler'; + +import * as S from './SignUp.styles'; + +const SignUp = () => { + const navigate = useNavigate(); + + const { userStatus } = useUserStore(); + + useEffect(() => { + if (userStatus === 'SIGNED_IN') navigate('/main', { replace: true }); + }, [userStatus]); + + const { + value: username, + status: usernameStatus, + message: usernameMessage, + handleChange: onUsernameChange, + } = useInput(); + + const { handleSignUp } = useSignUpHandler(); + + const handleChange = (event: React.ChangeEvent) => { + onUsernameChange(event, validateName(event.target.value)); + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + handleSignUp(username); + }; + + return ( + + + + 첫 λ°©λ¬Έμ΄μ‹œλ„€μš”! 당신을 μ–΄λ–»κ²Œ λΆˆλŸ¬μ•Ό ν• κΉŒμš”? + + + + + ); +}; + +export default SignUp; diff --git a/frontend/src/queries/Main/useAddPairRoom.ts b/frontend/src/queries/Main/useAddPairRoom.ts new file mode 100644 index 00000000..5fa7f1a0 --- /dev/null +++ b/frontend/src/queries/Main/useAddPairRoom.ts @@ -0,0 +1,32 @@ +import { useNavigate } from 'react-router-dom'; + +import { useMutation } from '@tanstack/react-query'; + +import useToastStore from '@/stores/toastStore'; + +import { addPairRoom } from '@/apis/pairRoom'; + +const useAddPairRoom = () => { + const navigate = useNavigate(); + + const { addToast } = useToastStore(); + + const { mutate, isPending } = useMutation({ + mutationFn: addPairRoom, + onError: (error) => addToast({ status: 'ERROR', message: error.message }), + onSuccess: (accessCode) => navigate(`/room/${accessCode}`), + }); + + const handleAddPairRoom = async (driver: string, navigator: string, timerDuration: string) => { + return mutate({ + driver, + navigator, + timerDuration: Number(timerDuration) * 60 * 1000, + timerRemainingTime: Number(timerDuration) * 60 * 1000, + }); + }; + + return { handleAddPairRoom, isPending }; +}; + +export default useAddPairRoom; diff --git a/frontend/src/queries/MyPage/useMyPairRooms.ts b/frontend/src/queries/MyPage/useMyPairRooms.ts new file mode 100644 index 00000000..4a522e8b --- /dev/null +++ b/frontend/src/queries/MyPage/useMyPairRooms.ts @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getMyPairRooms } from '@/apis/member'; + +import { QUERY_KEYS } from '@/constants/queryKeys'; + +const useMyPairRooms = () => { + const { data, isFetching } = useQuery({ + queryKey: [QUERY_KEYS.GET_MY_PAIR_ROOMS], + queryFn: getMyPairRooms, + refetchOnWindowFocus: false, + }); + + return { data, isFetching }; +}; + +export default useMyPairRooms; diff --git a/frontend/src/queries/PairRoom/category/mutation.ts b/frontend/src/queries/PairRoom/category/mutation.ts new file mode 100644 index 00000000..c451b32c --- /dev/null +++ b/frontend/src/queries/PairRoom/category/mutation.ts @@ -0,0 +1,49 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import useToastStore from '@/stores/toastStore'; + +import { addCategory, deleteCategory, updateCategory } from '@/apis/category'; + +import { QUERY_KEYS } from '@/constants/queryKeys'; + +export const useAddCategory = (onSuccess?: () => void) => { + const queryClient = useQueryClient(); + const { addToast } = useToastStore(); + + return useMutation({ + mutationFn: addCategory, + onSuccess: () => { + onSuccess && onSuccess(); + queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_CATEGORIES] }); + }, + onError: (error) => addToast({ status: 'ERROR', message: error.message }), + }); +}; + +export const useDeleteCategory = () => { + const queryClient = useQueryClient(); + + const { addToast } = useToastStore(); + + return useMutation({ + mutationFn: deleteCategory, + onSuccess: () => queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_CATEGORIES] }), + onError: (error) => addToast({ status: 'ERROR', message: error.message }), + }); +}; + +export const useUpdateCategory = () => { + const queryClient = useQueryClient(); + + const { addToast } = useToastStore(); + + return useMutation({ + mutationFn: updateCategory, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_CATEGORIES] }); + }, + onError: (error) => { + addToast({ status: 'ERROR', message: error.message }); + }, + }); +}; diff --git a/frontend/src/queries/PairRoom/category/query.ts b/frontend/src/queries/PairRoom/category/query.ts new file mode 100644 index 00000000..3fdb67ad --- /dev/null +++ b/frontend/src/queries/PairRoom/category/query.ts @@ -0,0 +1,12 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getCategories } from '@/apis/category'; + +import { QUERY_KEYS } from '@/constants/queryKeys'; + +export const useGetCategories = (accessCode: string) => + useQuery({ + queryKey: [QUERY_KEYS.GET_CATEGORIES], + queryFn: () => getCategories(accessCode), + retry: 0, + }); diff --git a/frontend/src/queries/PairRoom/reference/mutation.ts b/frontend/src/queries/PairRoom/reference/mutation.ts new file mode 100644 index 00000000..2d958419 --- /dev/null +++ b/frontend/src/queries/PairRoom/reference/mutation.ts @@ -0,0 +1,29 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import useToastStore from '@/stores/toastStore'; + +import { addReferenceLink, deleteReferenceLink } from '@/apis/referenceLink'; + +import { QUERY_KEYS } from '@/constants/queryKeys'; + +export const useAddReferenceLink = () => { + const queryClient = useQueryClient(); + const { addToast } = useToastStore(); + + return useMutation({ + mutationFn: addReferenceLink, + onSuccess: () => queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_REFERENCE_LINKS] }), + onError: (error) => addToast({ status: 'ERROR', message: error.message }), + }); +}; + +export const useDeleteReferenceLink = () => { + const queryClient = useQueryClient(); + const { addToast } = useToastStore(); + + return useMutation({ + mutationFn: deleteReferenceLink, + onSuccess: () => queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_REFERENCE_LINKS] }), + onError: (error) => addToast({ status: 'ERROR', message: error.message }), + }); +}; diff --git a/frontend/src/queries/PairRoom/reference/query.ts b/frontend/src/queries/PairRoom/reference/query.ts new file mode 100644 index 00000000..8c5b0f9e --- /dev/null +++ b/frontend/src/queries/PairRoom/reference/query.ts @@ -0,0 +1,11 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getReferenceLinks } from '@/apis/referenceLink'; + +import { QUERY_KEYS } from '@/constants/queryKeys'; + +export const useGetReference = (categoryId: string, accessCode: string) => + useQuery({ + queryKey: [QUERY_KEYS.GET_REFERENCE_LINKS, categoryId], + queryFn: () => getReferenceLinks({ accessCode, categoryId }), + }); diff --git a/frontend/src/queries/PairRoom/useGetPairRoom.ts b/frontend/src/queries/PairRoom/useGetPairRoom.ts new file mode 100644 index 00000000..16607663 --- /dev/null +++ b/frontend/src/queries/PairRoom/useGetPairRoom.ts @@ -0,0 +1,38 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getPairRoom } from '@/apis/pairRoom'; +import { getTimer } from '@/apis/timer'; + +import { QUERY_KEYS } from '@/constants/queryKeys'; + +const useGetPairRoom = (accessCode: string) => { + const { + data: pairRoom, + isFetching: isPairRoomFetching, + isRefetching: isPairRoomReFetching, + refetch, + } = useQuery({ + queryKey: [QUERY_KEYS.GET_PAIR_ROOM], + queryFn: () => getPairRoom(accessCode), + enabled: !!accessCode, + refetchOnWindowFocus: false, + }); + + const { data: timer, isFetching: isTimerFetching } = useQuery({ + queryKey: [QUERY_KEYS.GET_PAIR_ROOM_TIMER], + queryFn: () => getTimer(accessCode), + enabled: !!accessCode, + refetchOnWindowFocus: false, + }); + + return { + driver: pairRoom?.driver || '', + navigator: pairRoom?.navigator || '', + duration: timer?.duration || 0, + remainingTime: timer?.remainingTime || 0, + isFetching: (isPairRoomFetching && !isPairRoomReFetching) || isTimerFetching, + refetch, + }; +}; + +export default useGetPairRoom; diff --git a/frontend/src/queries/PairRoom/useTodos.ts b/frontend/src/queries/PairRoom/useTodos.ts new file mode 100644 index 00000000..bf83843c --- /dev/null +++ b/frontend/src/queries/PairRoom/useTodos.ts @@ -0,0 +1,65 @@ +import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'; + +import useToastStore from '@/stores/toastStore'; + +import { getTodos, addTodos, updateOrder, updateChecked, updateContents, deleteTodo } from '@/apis/todo'; + +import { QUERY_KEYS } from '@/constants/queryKeys'; + +const useTodos = (accessCode: string) => { + const queryClient = useQueryClient(); + + const { addToast } = useToastStore(); + + const { data } = useQuery({ + queryKey: [QUERY_KEYS.GET_TODOS], + queryFn: () => getTodos(accessCode), + }); + + const { mutate: addTodosMutation } = useMutation({ + mutationFn: addTodos, + onSuccess: () => queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_TODOS] }), + onError: (error) => addToast({ status: 'ERROR', message: error.message }), + }); + + const { mutate: updateContentsMutation } = useMutation({ + mutationFn: updateContents, + onSuccess: () => queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_TODOS] }), + onError: (error) => addToast({ status: 'ERROR', message: error.message }), + }); + + const { mutate: updateOrderMutation } = useMutation({ + mutationFn: updateOrder, + onSuccess: () => queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_TODOS] }), + onError: (error) => addToast({ status: 'ERROR', message: error.message }), + }); + + const { mutate: updateCheckedMutation } = useMutation({ + mutationFn: updateChecked, + onSuccess: () => queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_TODOS] }), + onError: (error) => addToast({ status: 'ERROR', message: error.message }), + }); + + const { mutate: deleteTodoMutation } = useMutation({ + mutationFn: deleteTodo, + onSuccess: () => queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_TODOS] }), + onError: (error) => addToast({ status: 'ERROR', message: error.message }), + }); + + const handleAddTodos = (content: string) => addTodosMutation({ content, accessCode }); + const handleUpdateContents = (todoId: number, contents: string) => updateContentsMutation({ todoId, contents }); + const handleUpdateOrder = (todoId: number, order: number) => updateOrderMutation({ todoId, order }); + const handleUpdateChecked = (todoId: number) => updateCheckedMutation({ todoId }); + const handleDeleteTodo = (todoId: number) => deleteTodoMutation({ todoId }); + + return { + todos: data || [], + handleAddTodos, + handleUpdateContents, + handleUpdateOrder, + handleUpdateChecked, + handleDeleteTodo, + }; +}; + +export default useTodos; diff --git a/frontend/src/queries/PairRoom/useUpdateDuration.ts b/frontend/src/queries/PairRoom/useUpdateDuration.ts new file mode 100644 index 00000000..9af3064f --- /dev/null +++ b/frontend/src/queries/PairRoom/useUpdateDuration.ts @@ -0,0 +1,20 @@ +import { useMutation } from '@tanstack/react-query'; + +import useToastStore from '@/stores/toastStore'; + +import { updateDuration } from '@/apis/timer'; + +const useUpdateDuration = () => { + const { addToast } = useToastStore(); + + const { mutate, isPending } = useMutation({ + mutationFn: updateDuration, + onError: (error) => addToast({ status: 'ERROR', message: error.message }), + }); + + const handleUpdateTimerDuration = (duration: string, accessCode: string) => mutate({ duration, accessCode }); + + return { handleUpdateTimerDuration, isPending }; +}; + +export default useUpdateDuration; diff --git a/frontend/src/queries/PairRoom/useUpdatePairRoom.ts b/frontend/src/queries/PairRoom/useUpdatePairRoom.ts new file mode 100644 index 00000000..dcdf1fed --- /dev/null +++ b/frontend/src/queries/PairRoom/useUpdatePairRoom.ts @@ -0,0 +1,20 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { updatePairRole } from '@/apis/pairRoom'; + +import { QUERY_KEYS } from '@/constants/queryKeys'; + +const useUpdatePairRoom = (accessCode: string) => { + const queryClient = useQueryClient(); + + const { mutate: updatePairRoleMutation } = useMutation({ + mutationFn: updatePairRole, + onSuccess: () => queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.GET_PAIR_ROOM] }), + }); + + const handleUpdatePairRole = () => updatePairRoleMutation({ accessCode }); + + return { handleUpdatePairRole }; +}; + +export default useUpdatePairRoom; diff --git a/frontend/src/queries/PairRoomOnboarding/useCreateBranch.ts b/frontend/src/queries/PairRoomOnboarding/useCreateBranch.ts new file mode 100644 index 00000000..0861c1c3 --- /dev/null +++ b/frontend/src/queries/PairRoomOnboarding/useCreateBranch.ts @@ -0,0 +1,32 @@ +import { useMutation } from '@tanstack/react-query'; + +import useToastStore from '@/stores/toastStore'; + +import { createBranch, getSHAforMain } from '@/apis/github'; + +const useCreateBranch = () => { + const { addToast } = useToastStore(); + + const { mutate, isSuccess } = useMutation({ + mutationFn: createBranch, + onSuccess: () => { + addToast({ status: 'SUCCESS', message: '브랜치 생성에 μ„±κ³΅ν–ˆμŠ΅λ‹ˆλ‹€.' }); + }, + onError: () => { + addToast({ status: 'ERROR', message: '브랜치 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.' }); + //TODO: 좔후에 status λΆ„κΈ°μ²˜λ¦¬ν•˜κΈ° + }, + }); + + const handleCreateBranch = async (currentRepository: string, branchName: string) => { + const sha = await getSHAforMain(currentRepository); + + if (sha && currentRepository != '') { + mutate({ repositoryName: currentRepository, branchName, sha }); + } + }; + + return { handleCreateBranch, isSuccess }; +}; + +export default useCreateBranch; diff --git a/frontend/src/queries/PairRoomOnboarding/useGetBranches.ts b/frontend/src/queries/PairRoomOnboarding/useGetBranches.ts new file mode 100644 index 00000000..1addb281 --- /dev/null +++ b/frontend/src/queries/PairRoomOnboarding/useGetBranches.ts @@ -0,0 +1,16 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getBranches } from '@/apis/github'; + +import { QUERY_KEYS } from '@/constants/queryKeys'; + +const useGetBranches = (repositoryName: string) => { + const { data: branches } = useQuery({ + queryKey: [QUERY_KEYS.GET_BRANCHES, repositoryName], + queryFn: () => getBranches(repositoryName), + }); + + return { branches: branches?.map((branch) => branch.name) || [] }; +}; + +export default useGetBranches; diff --git a/frontend/src/queries/PairRoomOnboarding/useGetRepositories.ts b/frontend/src/queries/PairRoomOnboarding/useGetRepositories.ts new file mode 100644 index 00000000..d5d9abe1 --- /dev/null +++ b/frontend/src/queries/PairRoomOnboarding/useGetRepositories.ts @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getRepositories } from '@/apis/github'; + +import { QUERY_KEYS } from '@/constants/queryKeys'; + +const FILTER_KEYWORD = 'coduo'; + +const useGetRepositories = () => { + const { data, isFetching, error } = useQuery({ + queryKey: [QUERY_KEYS.GET_REPOSITORIES], + queryFn: () => getRepositories(), + refetchOnWindowFocus: false, + }); + + return { repositories: data?.filter((el) => el.name.startsWith(FILTER_KEYWORD)) || [], isFetching, error }; +}; + +export default useGetRepositories; diff --git a/frontend/src/stores/toastStore.ts b/frontend/src/stores/toastStore.ts new file mode 100644 index 00000000..056de690 --- /dev/null +++ b/frontend/src/stores/toastStore.ts @@ -0,0 +1,44 @@ +import { create } from 'zustand'; + +import type { Status } from '@/components/common/Toast/Toast'; + +interface Toast { + status: Status; + message: string; +} + +interface ToastItem extends Toast { + id: number; + isOpen: boolean; + isPush: boolean; +} + +interface ToastStore { + toastList: ToastItem[]; + addToast: (toast: Toast) => void; +} + +const useToastStore = create((set) => ({ + toastList: [], + addToast: (toast: Toast) => { + const id = Date.now(); + const toastItem = { ...toast, id, isOpen: true, isPush: false }; + + set((state) => { + return { toastList: [toastItem, ...state.toastList.map((item) => ({ ...item, isPush: true }))].slice(0, 3) }; + }); + + setTimeout(() => { + set((state) => ({ + toastList: state.toastList.map((item) => (item.id === id ? { ...item, isOpen: false } : item)), + })); + setTimeout(() => { + set((state) => ({ + toastList: state.toastList.filter((item) => item.id !== id), + })); + }, 750); + }, 3000); + }, +})); + +export default useToastStore; diff --git a/frontend/src/stores/userStore.ts b/frontend/src/stores/userStore.ts new file mode 100644 index 00000000..e682ab25 --- /dev/null +++ b/frontend/src/stores/userStore.ts @@ -0,0 +1,19 @@ +import { create } from 'zustand'; + +type UserStatus = 'SIGNED_IN' | 'SIGNED_OUT'; + +interface UserStore { + username: string; + userStatus: UserStatus; + setUser: (username: string, userStatus: UserStatus) => void; +} + +const useUserStore = create((set) => ({ + username: '', + userStatus: 'SIGNED_OUT', + setUser: (username, userStatus) => { + set(() => ({ username, userStatus })); + }, +})); + +export default useUserStore; diff --git a/frontend/src/styles/Global.style.tsx b/frontend/src/styles/Global.style.tsx new file mode 100644 index 00000000..7d6c9c45 --- /dev/null +++ b/frontend/src/styles/Global.style.tsx @@ -0,0 +1,121 @@ +import { createGlobalStyle } from 'styled-components'; + +const GlobalStyles = createGlobalStyle` + /* + Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property + - The "symbol *" part is to solve Firefox SVG sprite bug + - The "html" element is excluded, otherwise a bug in Chrome breaks the CSS hyphens property (https://github.com/elad2412/the-new-css-reset/issues/36) + */ + *:where(:not(html, iframe, canvas, img, svg, video, audio, svg *, symbol *)) { + all: unset; + + display: revert; + } + + /* Preferred box-sizing value */ + *, + *::before, + *::after { + box-sizing: border-box; + } + + /* Fix mobile Safari increase font-size on landscape mode */ + html { + text-size-adjust: none; + } + + /* Reapply the pointer cursor for anchor tags */ + a, + button { + cursor: revert; + } + + /* Remove list styles (bullets/numbers) */ + ol, + ul, + menu, + summary { + list-style: none; + } + + /* For images to not be able to exceed their container */ + img { + max-inline-size: 100%; + max-block-size: 100%; + } + + /* Removes spacing between cells in tables */ + table { + border-collapse: collapse; + } + + /* Safari - solving issue when using user-select:none on the text input doesn't working */ + input, + textarea { + user-select: auto; + } + + /* Revert the 'white-space' property for textarea elements on Safari */ + textarea { + white-space: revert; + } + + /* Minimum style to allow to style meter element */ + meter { + appearance: revert; + } + + /* Preformatted text - use only for this feature */ + :where(pre) { + all: revert; + box-sizing: border-box; + } + + /* Fix the feature of 'hidden' attribute. + display: revert; revert to element instead of attribute */ + :where([hidden]) { + display: none; + } + + /* Revert for bug in Chromium browsers + - Fix for the content editable attribute will work properly. + - webkit-user-select: auto; added for Safari in case of using user-select:none on wrapper element */ + :where([contenteditable]:not([contenteditable='false'])) { + -moz-user-modify: read-write; + -webkit-user-modify: read-write; + overflow-wrap: break-word; + line-break: after-white-space; + user-select: auto; + } + + /* Apply back the draggable feature - exist only in Chromium and Safari */ + :where([draggable='true']) { + -webkit-user-drag: element; + } + + /* Revert Modal native behavior */ + :where(dialog:modal) { + all: revert; + box-sizing: border-box; + } + + /* Remove details summary webkit styles */ + ::-webkit-details-marker { + display: none; + } + + /* Chrome, Safari, Edge, Opera */ + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + appearance: none; + + margin: 0; + } + + /* Firefox */ + input[type='number'] { + appearance: textfield; + } +`; + +export default GlobalStyles; diff --git a/frontend/src/styles/font.css b/frontend/src/styles/font.css new file mode 100644 index 00000000..6f773b38 --- /dev/null +++ b/frontend/src/styles/font.css @@ -0,0 +1,12 @@ +html { + font-family: 'Pretendard Variable'; + font-size: 62.5%; + + @media screen and (max-width: 768px) { + font-size: 50%; + } +} + +body { + font-family: 'Pretendard Variable'; +} diff --git a/frontend/src/styles/styled.d.ts b/frontend/src/styles/styled.d.ts new file mode 100644 index 00000000..f5a7786b --- /dev/null +++ b/frontend/src/styles/styled.d.ts @@ -0,0 +1,11 @@ +import { ColorTypes, FontSizeTypes, FontWeightTypes } from './theme'; + +import 'styled-components'; + +declare module 'styled-components' { + export interface DefaultTheme { + color: ColorTypes; + fontSize: FontSizeTypes; + fontWeight: FontWeightTypes; + } +} diff --git a/frontend/src/styles/theme.ts b/frontend/src/styles/theme.ts new file mode 100644 index 00000000..3a8f156d --- /dev/null +++ b/frontend/src/styles/theme.ts @@ -0,0 +1,136 @@ +import { DefaultTheme } from 'styled-components'; + +const color = { + primary: { + 100: '#F1FBFA', + 200: '#D8FFFB', + 300: '#97FBD5', + 400: '#62F5CA', + 500: '#00E0C8', + 600: '#00C0BD', + 700: '#0094A0', + 800: '#00506B', + }, + secondary: { + 100: '#FFF8DC', + 150: '#FFF3CB', + 200: '#FFEEBA', + 300: '#FFE296', + 400: '#FFD67D', + 500: '#FFC453', + 600: '#FFA800', + 700: '#B87C2A', + 800: '#935D1A', + 900: '#7A4510', + }, + success: { + 100: '#F1FBD1', + 200: '#E0F8A4', + 300: '#C4EB74', + 400: '#A5D84F', + 500: '#7CBF1E', + 600: '#63A415', + 700: '#4C890F', + 800: '#376E09', + 900: '#295B05', + }, + info: { + 100: '#DEEAFF', + 200: '#BED5FF', + 300: '#9EBDFF', + 400: '#86A9FF', + 500: '#5E89FF', + 600: '#4468DB', + 700: '#2F4CB7', + 800: '#1D3393', + 900: '#001666', + }, + warning: { + 100: '#FFF8CC', + 200: '#FFEF99', + 300: '#FFE466', + 400: '#FFD93F', + 500: '#FFC700', + 600: '#DBA600', + 700: '#B78600', + 800: '#936900', + 900: '#7A5400', + }, + danger: { + 50: '#FFF4F1', + 100: '#FFEEDD', + 200: '#FFD9BB', + 300: '#FFBF99', + 400: '#FFA680', + 500: '#FF7D56', + 600: '#DB573E', + 700: '#B7372B', + 800: '#931D1B', + 900: '#7A1016', + }, + black: { + 10: '#FFF', + 20: '#F9F9F9', + 30: '#EEE', + 40: '#E5E5E5', + 50: '#CCC', + 60: '#AAA', + 65: '#909090', + 70: '#757575', + 75: '#545454', + 80: '#333', + 85: '#1A1A1A', + 90: '#000', + }, +}; + +const fontSize = { + h1: '4.8rem', + h2: '4.0rem', + h3: '3.2rem', + h4: '2.8rem', + h5: '2.4rem', + h6: '2.0rem', + lg: '1.8rem', + base: '1.6rem', + md: '1.4rem', + sm: '1.2rem', + xs: '1.0rem', +}; + +const deviceWidth = { + mobile: '768px', +}; + +const fontWeight = { + thin: '100', + extraLight: '200', + light: '300', + normal: '400', + medium: '500', + semibold: '600', + bold: '700', + extraBold: '800', + black: '900', +}; + +const iconSize = { + sm: '2.5rem', + md: '3.5rem', + lg: '4.5rem', + xl: '5.5rem', +}; + +export type ColorTypes = typeof color; +export type FontSizeTypes = typeof fontSize; +export type FontWeightTypes = typeof fontWeight; +export type DeviceWidthTypes = typeof deviceWidth; +export type IconSizeTypes = typeof iconSize; + +export const theme: DefaultTheme = { + color, + fontSize, + fontWeight, + deviceWidth, + iconSize, +}; diff --git a/frontend/src/types/types.d.ts b/frontend/src/types/types.d.ts new file mode 100644 index 00000000..d7d96622 --- /dev/null +++ b/frontend/src/types/types.d.ts @@ -0,0 +1,7 @@ +declare module '*.ttf'; +declare module '*.svg'; +declare module '*.png'; +declare module '*.mp3'; +declare module '*.webp'; +declare module '*.jpg'; +declare module '*.ico'; diff --git a/frontend/src/utils/Reference/formatLink.ts b/frontend/src/utils/Reference/formatLink.ts new file mode 100644 index 00000000..cd307533 --- /dev/null +++ b/frontend/src/utils/Reference/formatLink.ts @@ -0,0 +1,12 @@ +export const formatLink = (link: string): string => { + if (!link) { + return ''; + } + const trimmedLink = link.trim(); + + if (trimmedLink.startsWith('http://') || trimmedLink.startsWith('https://')) { + return trimmedLink; + } + + return `https://${trimmedLink}`; +}; diff --git a/frontend/src/utils/Timer/formatTime.ts b/frontend/src/utils/Timer/formatTime.ts new file mode 100644 index 00000000..eb6ca3b2 --- /dev/null +++ b/frontend/src/utils/Timer/formatTime.ts @@ -0,0 +1,10 @@ +const formatMinutes = (minutes: number) => (minutes < 10 ? `0${minutes}` : `${minutes}`); + +const formatSeconds = (seconds: number) => (seconds < 10 ? `0${seconds}` : `${seconds}`); + +export const formatTime = (time: number) => { + const minutes = Math.floor(time / (60 * 1000)); + const seconds = Math.floor((time % 60000) / 1000); + + return { minutes: formatMinutes(minutes), seconds: formatSeconds(seconds) }; +}; diff --git a/frontend/src/validations/validateBranchName.ts b/frontend/src/validations/validateBranchName.ts new file mode 100644 index 00000000..4c262bfa --- /dev/null +++ b/frontend/src/validations/validateBranchName.ts @@ -0,0 +1,9 @@ +import type { InputStatus } from '@/components/common/Input/Input.type'; + +export const validateBranchName = (name: string, branches: string[]) => { + if (name.trim() === '') return { status: 'ERROR' as InputStatus, message: '값을 μž…λ ₯ν•΄ μ£Όμ„Έμš”.' }; + if (name.length > 30) return { status: 'ERROR' as InputStatus, message: '30자 μ΄ν•˜λ‘œ μž…λ ₯ν•΄ μ£Όμ„Έμš”.' }; + if (branches.includes(name)) return { status: 'ERROR' as InputStatus, message: 'μ€‘λ³΅λœ 브랜치 이름 μž…λ‹ˆλ‹€.' }; + + return { status: 'DEFAULT' as InputStatus, message: '' }; +}; diff --git a/frontend/src/validations/validateCategory.ts b/frontend/src/validations/validateCategory.ts new file mode 100644 index 00000000..7dfdfd76 --- /dev/null +++ b/frontend/src/validations/validateCategory.ts @@ -0,0 +1,21 @@ +import { InputStatus } from '@/components/common/Input/Input.type'; + +import { DEFAULT_CATEGORY_VALUE } from '@/hooks/PairRoom/useCategories'; + +const MAX_CATEGORY_NAME_LENGTH = 10; + +export const validateCategory = ( + category: string, + isCategoryExist: (categoryName: string) => boolean, + prevCategoryName?: string, +) => { + if (category.length > MAX_CATEGORY_NAME_LENGTH) + return { status: 'ERROR' as InputStatus, message: `${MAX_CATEGORY_NAME_LENGTH}자 μ΄ν•˜λ‘œ μž…λ ₯ν•΄μ£Όμ„Έμš”` }; + if (prevCategoryName === category) + return { status: 'ERROR' as InputStatus, message: '이전과 λ™μΌν•œ μΉ΄ν…Œκ³ λ¦¬ μ΄λ¦„μž…λ‹ˆλ‹€. λ‹€λ₯Έ 이름을 μž…λ ₯ν•΄μ£Όμ„Έμš”.' }; + + if (isCategoryExist(category) || category === DEFAULT_CATEGORY_VALUE) + return { status: 'ERROR' as InputStatus, message: 'μ€‘λ³΅λœ μΉ΄ν…Œκ³ λ¦¬ μž…λ‹ˆλ‹€.' }; + + return { status: 'DEFAULT' as InputStatus, message: '' }; +}; diff --git a/frontend/src/validations/validatePairName.ts b/frontend/src/validations/validatePairName.ts new file mode 100644 index 00000000..92cdd76c --- /dev/null +++ b/frontend/src/validations/validatePairName.ts @@ -0,0 +1,16 @@ +import type { InputStatus } from '@/components/common/Input/Input.type'; + +export const validateName = (name: string) => { + if (name.trim() === '') return { status: 'ERROR' as InputStatus, message: '값을 μž…λ ₯ν•΄ μ£Όμ„Έμš”.' }; + if (name.length > 10) + return { status: 'ERROR' as InputStatus, message: '이름(λ˜λŠ” λ‹‰λ„€μž„)은 10자 μ΄ν•˜λ‘œ μž…λ ₯ν•΄ μ£Όμ„Έμš”.' }; + + return { status: 'DEFAULT' as InputStatus, message: '' }; +}; + +export const validateDuplicateName = (firstPairName: string, secondPairName: string) => { + if (firstPairName.trim() !== '' && secondPairName.trim() !== '' && firstPairName.trim() === secondPairName.trim()) + return { status: 'ERROR' as InputStatus, message: 'μ€‘λ³΅λœ 이름(λ˜λŠ” λ‹‰λ„€μž„)μž…λ‹ˆλ‹€. ' }; + + return { status: 'DEFAULT' as InputStatus, message: '' }; +}; diff --git a/frontend/src/validations/validateTimerDuration.ts b/frontend/src/validations/validateTimerDuration.ts new file mode 100644 index 00000000..e310e086 --- /dev/null +++ b/frontend/src/validations/validateTimerDuration.ts @@ -0,0 +1,6 @@ +export const validateTimerDuration = (timerDuration: string) => { + if (!Number.isInteger(Number(timerDuration))) return false; + if (Number(timerDuration) <= 0 || Number(timerDuration) >= 100) return false; + + return true; +}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 00000000..44e8b9e3 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "allowJs": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Testing */ + "esModuleInterop": true, + "types": ["jest", "@testing-library/jest-dom"], + + /* Path */ + "baseUrl": "src", + "paths": { + "@/*": ["./*"], + "@/pages/*": ["./pages/*"], + "@/components/*": ["./components/*"], + "@/hooks/*": ["./hooks/*"], + "@/assets/*": ["./assets/*"], + "@/styles/*": ["./styles/*"], + "@/validations/*": ["./validations/*"] + } + }, + "include": ["src"] +} diff --git a/frontend/webpack.common.config.js b/frontend/webpack.common.config.js new file mode 100644 index 00000000..a2e31403 --- /dev/null +++ b/frontend/webpack.common.config.js @@ -0,0 +1,53 @@ +import path from 'path'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import CopyWebpackPlugin from 'copy-webpack-plugin'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const config = { + entry: './src/index.tsx', + output: { + path: path.resolve(__dirname, 'dist'), + publicPath: '/', + clean: true, + }, + module: { + rules: [ + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + { + test: /\.(png|svg|jpg|gif|mp3|webp|ico)$/, + type: 'asset/resource', + }, + { + test: /\.(ts|js)x?$/, + exclude: /node_modules/, + use: ['ts-loader'], + }, + ], + }, + plugins: [ + new HtmlWebpackPlugin({ + filename: 'index.html', + template: './public/index.html', + }), + new CopyWebpackPlugin({ + patterns: [ + { from: 'public/coduo_metadata.jpg', to: 'coduo_metadata.jpg' }, + { from: 'public/favicon.ico', to: 'favicon.ico' }, + { from: 'src/assets', to: 'assets/' }, + ], + }), + ], + resolve: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + alias: { + '@': path.resolve(__dirname, 'src'), + }, + }, +}; + +export default config; diff --git a/frontend/webpack.development.config.js b/frontend/webpack.development.config.js new file mode 100644 index 00000000..0c01f2e4 --- /dev/null +++ b/frontend/webpack.development.config.js @@ -0,0 +1,32 @@ +import { merge } from 'webpack-merge'; +import common from './webpack.common.config.js'; +import dotenv from 'dotenv'; +import webpack from 'webpack'; + +const env = dotenv.config({ path: '.env.development' }).parsed; + +const envKeys = env + ? Object.keys(env).reduce((prev, next) => { + prev[`process.env.${next}`] = JSON.stringify(env[next]); + return prev; + }, {}) + : {}; + +export default merge(common, { + mode: 'development', + plugins: [new webpack.DefinePlugin(envKeys)], + module: { + rules: [], + }, + devServer: { + client: { + overlay: true, + progress: true, + }, + compress: true, + hot: true, + open: true, + port: 3000, + historyApiFallback: true, + }, +}); diff --git a/frontend/webpack.production.config.js b/frontend/webpack.production.config.js new file mode 100644 index 00000000..383dd22b --- /dev/null +++ b/frontend/webpack.production.config.js @@ -0,0 +1,45 @@ +import { merge } from 'webpack-merge'; +import common from './webpack.common.config.js'; +import { sentryWebpackPlugin } from '@sentry/webpack-plugin'; +import dotenv from 'dotenv'; +import webpack from 'webpack'; +import pkg from './package.json' with { type: 'json' }; + +const env = dotenv.config({ path: '.env.production' }).parsed; + +const envKeys = env + ? Object.keys(env).reduce((prev, next) => { + prev[`process.env.${next}`] = JSON.stringify(env[next]); + return prev; + }, {}) + : {}; + +export default merge(common, { + mode: 'production', + devtool: 'source-map', + plugins: [ + sentryWebpackPlugin({ + authToken: process.env.SENTRY_AUTH_TOKEN, + org: '2024-coduo', + project: 'coduo2024', + release: 'Coduo' + process.env.REACT_APP_VERSION, + telemetry: false, + sourcemaps: { + assets: './dist/**', + filesToDeleteAfterUpload: './dist/**/*.map', + }, + include: './dist', + ignore: ['node_modules'], + }), + new webpack.DefinePlugin({ + ...envKeys, + REACT_APP_VERSION: pkg.version, + }), + ], + optimization: { + minimize: true, + splitChunks: { + chunks: 'all', + }, + }, +}); diff --git a/frontend/yarn.lock b/frontend/yarn.lock new file mode 100644 index 00000000..d3d09746 --- /dev/null +++ b/frontend/yarn.lock @@ -0,0 +1,10342 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@adobe/css-tools@^4.3.2": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63" + integrity sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ== + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" + integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== + dependencies: + "@babel/highlight" "^7.24.7" + picocolors "^1.0.0" + +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.24.8": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.9.tgz#53eee4e68f1c1d0282aa0eb05ddb02d033fc43a0" + integrity sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng== + +"@babel/compat-data@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.2.tgz#e41928bd33475305c586f6acbbb7e3ade7a6f7f5" + integrity sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.18.9", "@babel/core@^7.23.0", "@babel/core@^7.23.9", "@babel/core@^7.24.4": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.9.tgz#dc07c9d307162c97fa9484ea997ade65841c7c82" + integrity sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.24.9" + "@babel/helper-compilation-targets" "^7.24.8" + "@babel/helper-module-transforms" "^7.24.9" + "@babel/helpers" "^7.24.8" + "@babel/parser" "^7.24.8" + "@babel/template" "^7.24.7" + "@babel/traverse" "^7.24.8" + "@babel/types" "^7.24.9" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/core@^7.18.5": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.2.tgz#ed8eec275118d7613e77a352894cd12ded8eba77" + integrity sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.25.0" + "@babel/helper-compilation-targets" "^7.25.2" + "@babel/helper-module-transforms" "^7.25.2" + "@babel/helpers" "^7.25.0" + "@babel/parser" "^7.25.0" + "@babel/template" "^7.25.0" + "@babel/traverse" "^7.25.2" + "@babel/types" "^7.25.2" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.24.8", "@babel/generator@^7.24.9", "@babel/generator@^7.7.2": + version "7.24.10" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.10.tgz#a4ab681ec2a78bbb9ba22a3941195e28a81d8e76" + integrity sha512-o9HBZL1G2129luEUlG1hB4N/nlYNWHnpwlND9eOMclRqqu1YDy2sSYVCFUZwl8I1Gxh+QSRrP2vD7EpUmFVXxg== + dependencies: + "@babel/types" "^7.24.9" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + +"@babel/generator@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.0.tgz#f858ddfa984350bc3d3b7f125073c9af6988f18e" + integrity sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw== + dependencies: + "@babel/types" "^7.25.0" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" + integrity sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz#37d66feb012024f2422b762b9b2a7cfe27c7fba3" + integrity sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.24.7", "@babel/helper-compilation-targets@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz#b607c3161cd9d1744977d4f97139572fe778c271" + integrity sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw== + dependencies: + "@babel/compat-data" "^7.24.8" + "@babel/helper-validator-option" "^7.24.8" + browserslist "^4.23.1" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-compilation-targets@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz#e1d9410a90974a3a5a66e84ff55ef62e3c02d06c" + integrity sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw== + dependencies: + "@babel/compat-data" "^7.25.2" + "@babel/helper-validator-option" "^7.24.8" + browserslist "^4.23.1" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.24.7", "@babel/helper-create-class-features-plugin@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.8.tgz#47f546408d13c200c0867f9d935184eaa0851b09" + integrity sha512-4f6Oqnmyp2PP3olgUMmOwC3akxSm5aBYraQ6YDdKy7NcAMkDECHWG0DEnV6M2UAkERgIBhYt8S27rURPg7SxWA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-function-name" "^7.24.7" + "@babel/helper-member-expression-to-functions" "^7.24.8" + "@babel/helper-optimise-call-expression" "^7.24.7" + "@babel/helper-replace-supers" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/helper-split-export-declaration" "^7.24.7" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz#be4f435a80dc2b053c76eeb4b7d16dd22cfc89da" + integrity sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + regexpu-core "^5.3.1" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.1", "@babel/helper-define-polyfill-provider@^0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz#18594f789c3594acb24cfdb4a7f7b7d2e8bd912d" + integrity sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + +"@babel/helper-environment-visitor@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz#4b31ba9551d1f90781ba83491dd59cf9b269f7d9" + integrity sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-function-name@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz#75f1e1725742f39ac6584ee0b16d94513da38dd2" + integrity sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA== + dependencies: + "@babel/template" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-hoist-variables@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz#b4ede1cde2fd89436397f30dc9376ee06b0f25ee" + integrity sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-member-expression-to-functions@^7.24.7", "@babel/helper-member-expression-to-functions@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz#6155e079c913357d24a4c20480db7c712a5c3fb6" + integrity sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA== + dependencies: + "@babel/traverse" "^7.24.8" + "@babel/types" "^7.24.8" + +"@babel/helper-module-imports@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" + integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-module-transforms@^7.24.7", "@babel/helper-module-transforms@^7.24.8", "@babel/helper-module-transforms@^7.24.9": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.9.tgz#e13d26306b89eea569180868e652e7f514de9d29" + integrity sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw== + dependencies: + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-simple-access" "^7.24.7" + "@babel/helper-split-export-declaration" "^7.24.7" + "@babel/helper-validator-identifier" "^7.24.7" + +"@babel/helper-module-transforms@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz#ee713c29768100f2776edf04d4eb23b8d27a66e6" + integrity sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ== + dependencies: + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-simple-access" "^7.24.7" + "@babel/helper-validator-identifier" "^7.24.7" + "@babel/traverse" "^7.25.2" + +"@babel/helper-optimise-call-expression@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz#8b0a0456c92f6b323d27cfd00d1d664e76692a0f" + integrity sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.24.8", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878" + integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg== + +"@babel/helper-remap-async-to-generator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz#b3f0f203628522713849d49403f1a414468be4c7" + integrity sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-wrap-function" "^7.24.7" + +"@babel/helper-replace-supers@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz#f933b7eed81a1c0265740edc91491ce51250f765" + integrity sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg== + dependencies: + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-member-expression-to-functions" "^7.24.7" + "@babel/helper-optimise-call-expression" "^7.24.7" + +"@babel/helper-simple-access@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" + integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-skip-transparent-expression-wrappers@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz#5f8fa83b69ed5c27adc56044f8be2b3ea96669d9" + integrity sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helper-split-export-declaration@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856" + integrity sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA== + dependencies: + "@babel/types" "^7.24.7" + +"@babel/helper-string-parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" + integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== + +"@babel/helper-validator-identifier@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== + +"@babel/helper-validator-option@^7.24.7", "@babel/helper-validator-option@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d" + integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q== + +"@babel/helper-wrap-function@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz#52d893af7e42edca7c6d2c6764549826336aae1f" + integrity sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw== + dependencies: + "@babel/helper-function-name" "^7.24.7" + "@babel/template" "^7.24.7" + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/helpers@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.8.tgz#2820d64d5d6686cca8789dd15b074cd862795873" + integrity sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ== + dependencies: + "@babel/template" "^7.24.7" + "@babel/types" "^7.24.8" + +"@babel/helpers@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.0.tgz#e69beb7841cb93a6505531ede34f34e6a073650a" + integrity sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw== + dependencies: + "@babel/template" "^7.25.0" + "@babel/types" "^7.25.0" + +"@babel/highlight@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" + integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== + dependencies: + "@babel/helper-validator-identifier" "^7.24.7" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.0", "@babel/parser@^7.23.9", "@babel/parser@^7.24.7", "@babel/parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.8.tgz#58a4dbbcad7eb1d48930524a3fd93d93e9084c6f" + integrity sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w== + +"@babel/parser@^7.25.0", "@babel/parser@^7.25.3": + version "7.25.3" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.3.tgz#91fb126768d944966263f0657ab222a642b82065" + integrity sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw== + dependencies: + "@babel/types" "^7.25.2" + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz#fd059fd27b184ea2b4c7e646868a9a381bbc3055" + integrity sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ== + dependencies: + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz#468096ca44bbcbe8fcc570574e12eb1950e18107" + integrity sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz#e4eabdd5109acc399b38d7999b2ef66fc2022f89" + integrity sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/plugin-transform-optional-chaining" "^7.24.7" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz#71b21bb0286d5810e63a1538aa901c58e87375ec" + integrity sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg== + dependencies: + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13", "@babel/plugin-syntax-class-properties@^7.8.3": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-flow@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.7.tgz#d1759e84dd4b437cf9fae69b4c06c41d7625bfb7" + integrity sha512-9G8GYT/dxn/D1IIKOUBmGX0mnmj46mGH9NnZyJLwtCpgh5f7D2VbuKodb+2s9m1Yavh1s7ASQN8lf0eqrb1LTw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-syntax-import-assertions@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz#2a0b406b5871a20a841240586b1300ce2088a778" + integrity sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-syntax-import-attributes@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz#b4f9ea95a79e6912480c4b626739f86a076624ca" + integrity sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-syntax-import-meta@^7.10.4", "@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.24.7", "@babel/plugin-syntax-jsx@^7.7.2": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz#39a1fa4a7e3d3d7f34e2acc6be585b718d30e02d" + integrity sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5", "@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.24.7", "@babel/plugin-syntax-typescript@^7.7.2": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz#58d458271b4d3b6bb27ee6ac9525acbb259bad1c" + integrity sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz#4f6886c11e423bd69f3ce51dbf42424a5f275514" + integrity sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-async-generator-functions@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz#7330a5c50e05181ca52351b8fd01642000c96cfd" + integrity sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g== + dependencies: + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-remap-async-to-generator" "^7.24.7" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-transform-async-to-generator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz#72a3af6c451d575842a7e9b5a02863414355bdcc" + integrity sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA== + dependencies: + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-remap-async-to-generator" "^7.24.7" + +"@babel/plugin-transform-block-scoped-functions@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz#a4251d98ea0c0f399dafe1a35801eaba455bbf1f" + integrity sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-block-scoping@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz#42063e4deb850c7bd7c55e626bf4e7ab48e6ce02" + integrity sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-class-properties@^7.22.5", "@babel/plugin-transform-class-properties@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz#256879467b57b0b68c7ddfc5b76584f398cd6834" + integrity sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-class-static-block@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz#c82027ebb7010bc33c116d4b5044fbbf8c05484d" + integrity sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-transform-classes@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.8.tgz#ad23301fe5bc153ca4cf7fb572a9bc8b0b711cf7" + integrity sha512-VXy91c47uujj758ud9wx+OMgheXm4qJfyhj1P18YvlrQkNOSrwsteHk+EFS3OMGfhMhpZa0A+81eE7G4QC+3CA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-compilation-targets" "^7.24.8" + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-function-name" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-replace-supers" "^7.24.7" + "@babel/helper-split-export-declaration" "^7.24.7" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz#4cab3214e80bc71fae3853238d13d097b004c707" + integrity sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/template" "^7.24.7" + +"@babel/plugin-transform-destructuring@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz#c828e814dbe42a2718a838c2a2e16a408e055550" + integrity sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.8" + +"@babel/plugin-transform-dotall-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz#5f8bf8a680f2116a7207e16288a5f974ad47a7a0" + integrity sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-duplicate-keys@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz#dd20102897c9a2324e5adfffb67ff3610359a8ee" + integrity sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-dynamic-import@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz#4d8b95e3bae2b037673091aa09cd33fecd6419f4" + integrity sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-transform-exponentiation-operator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz#b629ee22645f412024297d5245bce425c31f9b0d" + integrity sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-export-namespace-from@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz#176d52d8d8ed516aeae7013ee9556d540c53f197" + integrity sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-transform-flow-strip-types@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.24.7.tgz#ae454e62219288fbb734541ab00389bfb13c063e" + integrity sha512-cjRKJ7FobOH2eakx7Ja+KpJRj8+y+/SiB3ooYm/n2UJfxu0oEaOoxOinitkJcPqv9KxS0kxTGPUaR7L2XcXDXA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-flow" "^7.24.7" + +"@babel/plugin-transform-for-of@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz#f25b33f72df1d8be76399e1b8f3f9d366eb5bc70" + integrity sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + +"@babel/plugin-transform-function-name@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz#6d8601fbffe665c894440ab4470bc721dd9131d6" + integrity sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w== + dependencies: + "@babel/helper-compilation-targets" "^7.24.7" + "@babel/helper-function-name" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-json-strings@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz#f3e9c37c0a373fee86e36880d45b3664cedaf73a" + integrity sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-transform-literals@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz#36b505c1e655151a9d7607799a9988fc5467d06c" + integrity sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-logical-assignment-operators@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz#a58fb6eda16c9dc8f9ff1c7b1ba6deb7f4694cb0" + integrity sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-transform-member-expression-literals@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz#3b4454fb0e302e18ba4945ba3246acb1248315df" + integrity sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-modules-amd@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz#65090ed493c4a834976a3ca1cde776e6ccff32d7" + integrity sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg== + dependencies: + "@babel/helper-module-transforms" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-modules-commonjs@^7.23.0", "@babel/plugin-transform-modules-commonjs@^7.24.7", "@babel/plugin-transform-modules-commonjs@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz#ab6421e564b717cb475d6fff70ae7f103536ea3c" + integrity sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA== + dependencies: + "@babel/helper-module-transforms" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-simple-access" "^7.24.7" + +"@babel/plugin-transform-modules-systemjs@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz#f8012316c5098f6e8dee6ecd58e2bc6f003d0ce7" + integrity sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw== + dependencies: + "@babel/helper-hoist-variables" "^7.24.7" + "@babel/helper-module-transforms" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-validator-identifier" "^7.24.7" + +"@babel/plugin-transform-modules-umd@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz#edd9f43ec549099620df7df24e7ba13b5c76efc8" + integrity sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A== + dependencies: + "@babel/helper-module-transforms" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz#9042e9b856bc6b3688c0c2e4060e9e10b1460923" + integrity sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-new-target@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz#31ff54c4e0555cc549d5816e4ab39241dfb6ab00" + integrity sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.22.11", "@babel/plugin-transform-nullish-coalescing-operator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz#1de4534c590af9596f53d67f52a92f12db984120" + integrity sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-transform-numeric-separator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz#bea62b538c80605d8a0fac9b40f48e97efa7de63" + integrity sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-transform-object-rest-spread@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz#d13a2b93435aeb8a197e115221cab266ba6e55d6" + integrity sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q== + dependencies: + "@babel/helper-compilation-targets" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.24.7" + +"@babel/plugin-transform-object-super@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz#66eeaff7830bba945dd8989b632a40c04ed625be" + integrity sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-replace-supers" "^7.24.7" + +"@babel/plugin-transform-optional-catch-binding@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz#00eabd883d0dd6a60c1c557548785919b6e717b4" + integrity sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-transform-optional-chaining@^7.23.0", "@babel/plugin-transform-optional-chaining@^7.24.7", "@babel/plugin-transform-optional-chaining@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz#bb02a67b60ff0406085c13d104c99a835cdf365d" + integrity sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-transform-parameters@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz#5881f0ae21018400e320fc7eb817e529d1254b68" + integrity sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-private-methods@^7.22.5", "@babel/plugin-transform-private-methods@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz#e6318746b2ae70a59d023d5cc1344a2ba7a75f5e" + integrity sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-private-property-in-object@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz#4eec6bc701288c1fab5f72e6a4bbc9d67faca061" + integrity sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-create-class-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-transform-property-literals@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz#f0d2ed8380dfbed949c42d4d790266525d63bbdc" + integrity sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-regenerator@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz#021562de4534d8b4b1851759fd7af4e05d2c47f8" + integrity sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + regenerator-transform "^0.15.2" + +"@babel/plugin-transform-reserved-words@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz#80037fe4fbf031fc1125022178ff3938bb3743a4" + integrity sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-shorthand-properties@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz#85448c6b996e122fa9e289746140aaa99da64e73" + integrity sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-spread@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz#e8a38c0fde7882e0fb8f160378f74bd885cc7bb3" + integrity sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + +"@babel/plugin-transform-sticky-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz#96ae80d7a7e5251f657b5cf18f1ea6bf926f5feb" + integrity sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-template-literals@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz#a05debb4a9072ae8f985bcf77f3f215434c8f8c8" + integrity sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-typeof-symbol@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz#383dab37fb073f5bfe6e60c654caac309f92ba1c" + integrity sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.8" + +"@babel/plugin-transform-typescript@^7.24.7": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.8.tgz#c104d6286e04bf7e44b8cba1b686d41bad57eb84" + integrity sha512-CgFgtN61BbdOGCP4fLaAMOPkzWUh6yQZNMr5YSt8uz2cZSSiQONCQFWqsE4NeVfOIhqDOlS9CR3WD91FzMeB2Q== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-create-class-features-plugin" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/plugin-syntax-typescript" "^7.24.7" + +"@babel/plugin-transform-unicode-escapes@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz#2023a82ced1fb4971630a2e079764502c4148e0e" + integrity sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-unicode-property-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz#9073a4cd13b86ea71c3264659590ac086605bbcd" + integrity sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-unicode-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz#dfc3d4a51127108099b19817c0963be6a2adf19f" + integrity sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-transform-unicode-sets-regex@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz#d40705d67523803a576e29c63cef6e516b858ed9" + integrity sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.24.7" + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/preset-env@^7.24.4": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.8.tgz#e0db94d7f17d6f0e2564e8d29190bc8cdacec2d1" + integrity sha512-vObvMZB6hNWuDxhSaEPTKCwcqkAIuDtE+bQGn4XMXne1DSLzFVY8Vmj1bm+mUQXYNN8NmaQEO+r8MMbzPr1jBQ== + dependencies: + "@babel/compat-data" "^7.24.8" + "@babel/helper-compilation-targets" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-validator-option" "^7.24.8" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.24.7" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.24.7" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.24.7" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.24.7" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.24.7" + "@babel/plugin-syntax-import-attributes" "^7.24.7" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.24.7" + "@babel/plugin-transform-async-generator-functions" "^7.24.7" + "@babel/plugin-transform-async-to-generator" "^7.24.7" + "@babel/plugin-transform-block-scoped-functions" "^7.24.7" + "@babel/plugin-transform-block-scoping" "^7.24.7" + "@babel/plugin-transform-class-properties" "^7.24.7" + "@babel/plugin-transform-class-static-block" "^7.24.7" + "@babel/plugin-transform-classes" "^7.24.8" + "@babel/plugin-transform-computed-properties" "^7.24.7" + "@babel/plugin-transform-destructuring" "^7.24.8" + "@babel/plugin-transform-dotall-regex" "^7.24.7" + "@babel/plugin-transform-duplicate-keys" "^7.24.7" + "@babel/plugin-transform-dynamic-import" "^7.24.7" + "@babel/plugin-transform-exponentiation-operator" "^7.24.7" + "@babel/plugin-transform-export-namespace-from" "^7.24.7" + "@babel/plugin-transform-for-of" "^7.24.7" + "@babel/plugin-transform-function-name" "^7.24.7" + "@babel/plugin-transform-json-strings" "^7.24.7" + "@babel/plugin-transform-literals" "^7.24.7" + "@babel/plugin-transform-logical-assignment-operators" "^7.24.7" + "@babel/plugin-transform-member-expression-literals" "^7.24.7" + "@babel/plugin-transform-modules-amd" "^7.24.7" + "@babel/plugin-transform-modules-commonjs" "^7.24.8" + "@babel/plugin-transform-modules-systemjs" "^7.24.7" + "@babel/plugin-transform-modules-umd" "^7.24.7" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.24.7" + "@babel/plugin-transform-new-target" "^7.24.7" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.7" + "@babel/plugin-transform-numeric-separator" "^7.24.7" + "@babel/plugin-transform-object-rest-spread" "^7.24.7" + "@babel/plugin-transform-object-super" "^7.24.7" + "@babel/plugin-transform-optional-catch-binding" "^7.24.7" + "@babel/plugin-transform-optional-chaining" "^7.24.8" + "@babel/plugin-transform-parameters" "^7.24.7" + "@babel/plugin-transform-private-methods" "^7.24.7" + "@babel/plugin-transform-private-property-in-object" "^7.24.7" + "@babel/plugin-transform-property-literals" "^7.24.7" + "@babel/plugin-transform-regenerator" "^7.24.7" + "@babel/plugin-transform-reserved-words" "^7.24.7" + "@babel/plugin-transform-shorthand-properties" "^7.24.7" + "@babel/plugin-transform-spread" "^7.24.7" + "@babel/plugin-transform-sticky-regex" "^7.24.7" + "@babel/plugin-transform-template-literals" "^7.24.7" + "@babel/plugin-transform-typeof-symbol" "^7.24.8" + "@babel/plugin-transform-unicode-escapes" "^7.24.7" + "@babel/plugin-transform-unicode-property-regex" "^7.24.7" + "@babel/plugin-transform-unicode-regex" "^7.24.7" + "@babel/plugin-transform-unicode-sets-regex" "^7.24.7" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.10.4" + babel-plugin-polyfill-regenerator "^0.6.1" + core-js-compat "^3.37.1" + semver "^6.3.1" + +"@babel/preset-flow@^7.22.15": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/preset-flow/-/preset-flow-7.24.7.tgz#eef5cb8e05e97a448fc50c16826f5612fe512c06" + integrity sha512-NL3Lo0NorCU607zU3NwRyJbpaB6E3t0xtd3LfAQKDfkeX4/ggcDXvkmkW42QWT5owUeW/jAe4hn+2qvkV1IbfQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-validator-option" "^7.24.7" + "@babel/plugin-transform-flow-strip-types" "^7.24.7" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/preset-typescript@^7.23.0": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz#66cd86ea8f8c014855671d5ea9a737139cbbfef1" + integrity sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-validator-option" "^7.24.7" + "@babel/plugin-syntax-jsx" "^7.24.7" + "@babel/plugin-transform-modules-commonjs" "^7.24.7" + "@babel/plugin-transform-typescript" "^7.24.7" + +"@babel/register@^7.22.15": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/register/-/register-7.24.6.tgz#59e21dcc79e1d04eed5377633b0f88029a6bef9e" + integrity sha512-WSuFCc2wCqMeXkz/i3yfAAsxwWflEgbVkZzivgAmXl/MxrXeoYFZOOPllbC8R8WTF7u61wSRQtDVZ1879cdu6w== + dependencies: + clone-deep "^4.0.1" + find-cache-dir "^2.0.0" + make-dir "^2.1.0" + pirates "^4.0.6" + source-map-support "^0.5.16" + +"@babel/regjsgen@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" + integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== + +"@babel/runtime@^7.12.5", "@babel/runtime@^7.17.8", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.8.tgz#5d958c3827b13cc6d05e038c07fb2e5e3420d82e" + integrity sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.24.7", "@babel/template@^7.3.3": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.7.tgz#02efcee317d0609d2c07117cb70ef8fb17ab7315" + integrity sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/parser" "^7.24.7" + "@babel/types" "^7.24.7" + +"@babel/template@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.0.tgz#e733dc3134b4fede528c15bc95e89cb98c52592a" + integrity sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/parser" "^7.25.0" + "@babel/types" "^7.25.0" + +"@babel/traverse@^7.18.9", "@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.8.tgz#6c14ed5232b7549df3371d820fbd9abfcd7dfab7" + integrity sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.24.8" + "@babel/helper-environment-visitor" "^7.24.7" + "@babel/helper-function-name" "^7.24.7" + "@babel/helper-hoist-variables" "^7.24.7" + "@babel/helper-split-export-declaration" "^7.24.7" + "@babel/parser" "^7.24.8" + "@babel/types" "^7.24.8" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/traverse@^7.25.2": + version "7.25.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.3.tgz#f1b901951c83eda2f3e29450ce92743783373490" + integrity sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.25.0" + "@babel/parser" "^7.25.3" + "@babel/template" "^7.25.0" + "@babel/types" "^7.25.2" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.18.9", "@babel/types@^7.20.7", "@babel/types@^7.24.0", "@babel/types@^7.24.7", "@babel/types@^7.24.8", "@babel/types@^7.24.9", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.24.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.9.tgz#228ce953d7b0d16646e755acf204f4cf3d08cc73" + integrity sha512-xm8XrMKz0IlUdocVbYJe0Z9xEgidU7msskG8BbhnTPK/HZ2z/7FP7ykqPgrUH+C+r414mNfNWam1f2vqOjqjYQ== + dependencies: + "@babel/helper-string-parser" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" + to-fast-properties "^2.0.0" + +"@babel/types@^7.25.0", "@babel/types@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.2.tgz#55fb231f7dc958cd69ea141a4c2997e819646125" + integrity sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q== + dependencies: + "@babel/helper-string-parser" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" + to-fast-properties "^2.0.0" + +"@base2/pretty-print-object@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz#371ba8be66d556812dc7fb169ebc3c08378f69d4" + integrity sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA== + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@chromatic-com/storybook@^1.6.1": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@chromatic-com/storybook/-/storybook-1.6.1.tgz#42173679c166ce982903b9e40bb457c037e9dac2" + integrity sha512-x1x1NB3j4xpfeSWKr96emc+7ZvfsvH+/WVb3XCjkB24PPbT8VZXb3mJSAQMrSzuQ8+eQE9kDogYHH9Fj3tb/Cw== + dependencies: + chromatic "^11.4.0" + filesize "^10.0.12" + jsonfile "^6.1.0" + react-confetti "^6.1.0" + strip-ansi "^7.1.0" + +"@csstools/css-parser-algorithms@^2.7.1": + version "2.7.1" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz#6d93a8f7d8aeb7cd9ed0868f946e46f021b6aa70" + integrity sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw== + +"@csstools/css-tokenizer@^2.4.1": + version "2.4.1" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz#1d8b2e200197cf5f35ceb07ca2dade31f3a00ae8" + integrity sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg== + +"@csstools/media-query-list-parser@^2.1.13": + version "2.1.13" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz#f00be93f6bede07c14ddf51a168ad2748e4fe9e5" + integrity sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA== + +"@csstools/selector-specificity@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-3.1.1.tgz#63085d2995ca0f0e55aa8b8a07d69bfd48b844fe" + integrity sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA== + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@dual-bundle/import-meta-resolve@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#519c1549b0e147759e7825701ecffd25e5819f7b" + integrity sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg== + +"@emotion/is-prop-valid@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz#d4175076679c6a26faa92b03bb786f9e52612337" + integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw== + dependencies: + "@emotion/memoize" "^0.8.1" + +"@emotion/memoize@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" + integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== + +"@emotion/unitless@0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" + integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== + +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1": + version "4.11.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae" + integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.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" + +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== + +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== + dependencies: + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" + integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + +"@jest/core@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" + integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== + dependencies: + "@jest/console" "^29.7.0" + "@jest/reporters" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.7.0" + jest-config "^29.7.0" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-resolve-dependencies "^29.7.0" + jest-runner "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + jest-watcher "^29.7.0" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" + integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== + dependencies: + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== + dependencies: + jest-get-type "^29.6.3" + +"@jest/expect@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" + integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== + dependencies: + expect "^29.7.0" + jest-snapshot "^29.7.0" + +"@jest/fake-timers@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== + dependencies: + "@jest/types" "^29.6.3" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +"@jest/globals@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" + integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/types" "^29.6.3" + jest-mock "^29.7.0" + +"@jest/reporters@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" + integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^6.0.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + jest-worker "^29.7.0" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/source-map@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" + integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" + integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== + dependencies: + "@jest/console" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" + integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== + dependencies: + "@jest/test-result" "^29.7.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + slash "^3.0.0" + +"@jest/transform@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" + integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@jsonjoy.com/base64@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" + integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== + +"@jsonjoy.com/json-pack@^1.0.3": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.0.4.tgz#ab59c642a2e5368e8bcfd815d817143d4f3035d0" + integrity sha512-aOcSN4MeAtFROysrbqG137b7gaDDSmVrl5mpo6sT/w+kcXpWnzhMjmY/Fh/sDx26NBxyIE7MB1seqLeCAzy9Sg== + dependencies: + "@jsonjoy.com/base64" "^1.1.1" + "@jsonjoy.com/util" "^1.1.2" + hyperdyperid "^1.2.0" + thingies "^1.20.0" + +"@jsonjoy.com/util@^1.1.2": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.2.0.tgz#0fe9a92de72308c566ebcebe8b5a3f01d3149df2" + integrity sha512-4B8B+3vFsY4eo33DMKyJPlQ3sBMpPFUZK2dr3O3rXrOGKKbYG44J0XSFkDo1VOQiri5HFEhIeVvItjR2xcazmg== + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" + integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== + +"@mdx-js/react@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-3.0.1.tgz#997a19b3a5b783d936c75ae7c47cfe62f967f746" + integrity sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A== + dependencies: + "@types/mdx" "^2.0.0" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@octokit/auth-token@^5.0.0": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-5.1.1.tgz#3bbfe905111332a17f72d80bd0b51a3e2fa2cf07" + integrity sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA== + +"@octokit/core@^6.1.2": + version "6.1.2" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-6.1.2.tgz#20442d0a97c411612da206411e356014d1d1bd17" + integrity sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg== + dependencies: + "@octokit/auth-token" "^5.0.0" + "@octokit/graphql" "^8.0.0" + "@octokit/request" "^9.0.0" + "@octokit/request-error" "^6.0.1" + "@octokit/types" "^13.0.0" + before-after-hook "^3.0.2" + universal-user-agent "^7.0.0" + +"@octokit/endpoint@^10.0.0": + version "10.1.1" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-10.1.1.tgz#1a9694e7aef6aa9d854dc78dd062945945869bcc" + integrity sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q== + dependencies: + "@octokit/types" "^13.0.0" + universal-user-agent "^7.0.2" + +"@octokit/graphql@^8.0.0": + version "8.1.1" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-8.1.1.tgz#3cacab5f2e55d91c733e3bf481d3a3f8a5f639c4" + integrity sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg== + dependencies: + "@octokit/request" "^9.0.0" + "@octokit/types" "^13.0.0" + universal-user-agent "^7.0.0" + +"@octokit/openapi-types@^22.2.0": + version "22.2.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-22.2.0.tgz#75aa7dcd440821d99def6a60b5f014207ae4968e" + integrity sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg== + +"@octokit/plugin-paginate-rest@^11.0.0": + version "11.3.3" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.3.tgz#efc97ba66aae6797e2807a082f99b9cfc0e05aba" + integrity sha512-o4WRoOJZlKqEEgj+i9CpcmnByvtzoUYC6I8PD2SA95M+BJ2x8h7oLcVOg9qcowWXBOdcTRsMZiwvM3EyLm9AfA== + dependencies: + "@octokit/types" "^13.5.0" + +"@octokit/plugin-request-log@^5.3.1": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz#ccb75d9705de769b2aa82bcd105cc96eb0c00f69" + integrity sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw== + +"@octokit/plugin-rest-endpoint-methods@^13.0.0": + version "13.2.4" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.4.tgz#543add032d3fe3f5d2839bfd619cf66d85469f01" + integrity sha512-gusyAVgTrPiuXOdfqOySMDztQHv6928PQ3E4dqVGEtOvRXAKRbJR4b1zQyniIT9waqaWk/UDaoJ2dyPr7Bk7Iw== + dependencies: + "@octokit/types" "^13.5.0" + +"@octokit/request-error@^6.0.1": + version "6.1.4" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-6.1.4.tgz#ad96e29148d19edc2ba8009fc2b5a24a36c90f16" + integrity sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg== + dependencies: + "@octokit/types" "^13.0.0" + +"@octokit/request@^9.0.0": + version "9.1.3" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-9.1.3.tgz#42b693bc06238f43af3c037ebfd35621c6457838" + integrity sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA== + dependencies: + "@octokit/endpoint" "^10.0.0" + "@octokit/request-error" "^6.0.1" + "@octokit/types" "^13.1.0" + universal-user-agent "^7.0.2" + +"@octokit/rest@^21.0.1": + version "21.0.1" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-21.0.1.tgz#b77d985ded81ac180f4efb26858311c0fcb8462c" + integrity sha512-RWA6YU4CqK0h0J6tfYlUFnH3+YgBADlxaHXaKSG+BVr2y4PTfbU2tlKuaQoQZ83qaTbi4CUxLNAmbAqR93A6mQ== + dependencies: + "@octokit/core" "^6.1.2" + "@octokit/plugin-paginate-rest" "^11.0.0" + "@octokit/plugin-request-log" "^5.3.1" + "@octokit/plugin-rest-endpoint-methods" "^13.0.0" + +"@octokit/types@^13.0.0", "@octokit/types@^13.1.0", "@octokit/types@^13.5.0": + version "13.5.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.5.0.tgz#4796e56b7b267ebc7c921dcec262b3d5bfb18883" + integrity sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ== + dependencies: + "@octokit/openapi-types" "^22.2.0" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@pkgr/core@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" + integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== + +"@remix-run/router@1.18.0": + version "1.18.0" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.18.0.tgz#20b033d1f542a100c1d57cfd18ecf442d1784732" + integrity sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw== + +"@sentry-internal/browser-utils@8.22.0": + version "8.22.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.22.0.tgz#29f35a617ec69d881141decdcdbbe3a777528392" + integrity sha512-R0u8KPaSivueIwUOhmYxcisKaJq3gx+I0xOcWoluDB3OI1Ds/QOSP/vmTsMg/mjwG/nUJ8RRM8pj0s8vlqCrjg== + dependencies: + "@sentry/core" "8.22.0" + "@sentry/types" "8.22.0" + "@sentry/utils" "8.22.0" + +"@sentry-internal/feedback@8.22.0": + version "8.22.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.22.0.tgz#d6b222ea08d72886b3efbc0760c8cf1f71b3e7fc" + integrity sha512-Sy2+v0xBmVnZ5LQ48603CvLy5vVQvAZ+hc9xQSAHexts07NkvApMU1qv26YNwxlAWfDha1wXiW6ryd4YDzaoVA== + dependencies: + "@sentry/core" "8.22.0" + "@sentry/types" "8.22.0" + "@sentry/utils" "8.22.0" + +"@sentry-internal/replay-canvas@8.22.0": + version "8.22.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.22.0.tgz#70c5951a9d54f2e814e930fbe0371ab83fee1e72" + integrity sha512-/gV8qN3JqWw0LXTMuCGB8RDI8Bx1VESNRBdh/7Cmc5+hxYBfcketuix3S8mHWcE/JO+Ed9g1Abzys6GphTB9LA== + dependencies: + "@sentry-internal/replay" "8.22.0" + "@sentry/core" "8.22.0" + "@sentry/types" "8.22.0" + "@sentry/utils" "8.22.0" + +"@sentry-internal/replay@8.22.0": + version "8.22.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.22.0.tgz#c76bbe4575f8ab478694fe54d4bae87010315d3a" + integrity sha512-sF8RyMPJP1fSIyyBDAbtybvKCu0dy8ZAfMwLP7ZqEnWrhZqktVuqM7/++EAFMlD5YaWJXm1IDuOXjgSQjUtSIQ== + dependencies: + "@sentry-internal/browser-utils" "8.22.0" + "@sentry/core" "8.22.0" + "@sentry/types" "8.22.0" + "@sentry/utils" "8.22.0" + +"@sentry/babel-plugin-component-annotate@2.21.1": + version "2.21.1" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.21.1.tgz#1fbf7ceca62fdc44957e37b60312808588c87350" + integrity sha512-u1L8gZ4He0WdyiIsohYkA/YOY1b6Oa5yIMRtfZZ9U5TiWYLgOfMWyb88X0GotZeghSbgxrse/yI4WeHnhAUQDQ== + +"@sentry/browser@8.22.0": + version "8.22.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.22.0.tgz#ee15b0ef7985732d56d01b62adbbc7b76bcc7ddc" + integrity sha512-t3b+/9WWcP9SQTWwrHrB57B33ENgmUjyFlW2+JSlCXkSJBSmAoquPZ/GPjOuPaSr3HIA0mu9uEr4A41d5diASQ== + dependencies: + "@sentry-internal/browser-utils" "8.22.0" + "@sentry-internal/feedback" "8.22.0" + "@sentry-internal/replay" "8.22.0" + "@sentry-internal/replay-canvas" "8.22.0" + "@sentry/core" "8.22.0" + "@sentry/types" "8.22.0" + "@sentry/utils" "8.22.0" + +"@sentry/bundler-plugin-core@2.21.1": + version "2.21.1" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.21.1.tgz#fa50e83742d23962dd647d2dc8faeb2a86799751" + integrity sha512-F8FdL/bS8cy1SY1Gw0Mfo3ROTqlrq9Lvt5QGvhXi22dpVcDkWmoTWE2k+sMEnXOa8SdThMc/gyC8lMwHGd3kFQ== + dependencies: + "@babel/core" "^7.18.5" + "@sentry/babel-plugin-component-annotate" "2.21.1" + "@sentry/cli" "^2.22.3" + dotenv "^16.3.1" + find-up "^5.0.0" + glob "^9.3.2" + magic-string "0.30.8" + unplugin "1.0.1" + +"@sentry/cli-darwin@2.33.1": + version "2.33.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.33.1.tgz#e4eb1dd01ee3ce2788025426b860ccc63759589c" + integrity sha512-+4/VIx/E1L2hChj5nGf5MHyEPHUNHJ/HoG5RY+B+vyEutGily1c1+DM2bum7RbD0xs6wKLIyup5F02guzSzG8A== + +"@sentry/cli-linux-arm64@2.33.1": + version "2.33.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.33.1.tgz#9ea1718c21ef32ca83b0852ca29fb461fd26d25a" + integrity sha512-DbGV56PRKOLsAZJX27Jt2uZ11QfQEMmWB4cIvxkKcFVE+LJP4MVA+MGGRUL6p+Bs1R9ZUuGbpKGtj0JiG6CoXw== + +"@sentry/cli-linux-arm@2.33.1": + version "2.33.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.33.1.tgz#e8a1dca4d008dd6a72ab5935304c104e98e2901c" + integrity sha512-zbxEvQju+tgNvzTOt635le4kS/Fbm2XC2RtYbCTs034Vb8xjrAxLnK0z1bQnStUV8BkeBHtsNVrG+NSQDym2wg== + +"@sentry/cli-linux-i686@2.33.1": + version "2.33.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.33.1.tgz#f1fe8dd4d6dde0812a94fba31de8054ddfb7284a" + integrity sha512-g2LS4oPXkPWOfKWukKzYp4FnXVRRSwBxhuQ9eSw2peeb58ZIObr4YKGOA/8HJRGkooBJIKGaAR2mH2Pk1TKaiA== + +"@sentry/cli-linux-x64@2.33.1": + version "2.33.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.33.1.tgz#6e086675356a9eb79731bf9e447d078bae1b5adf" + integrity sha512-IV3dcYV/ZcvO+VGu9U6kuxSdbsV2kzxaBwWUQxtzxJ+cOa7J8Hn1t0koKGtU53JVZNBa06qJWIcqgl4/pCuKIg== + +"@sentry/cli-win32-i686@2.33.1": + version "2.33.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.33.1.tgz#0e6b36c4a2f5f6e85a59247a123d276b3ef10f1a" + integrity sha512-F7cJySvkpzIu7fnLKNHYwBzZYYwlhoDbAUnaFX0UZCN+5DNp/5LwTp37a5TWOsmCaHMZT4i9IO4SIsnNw16/zQ== + +"@sentry/cli-win32-x64@2.33.1": + version "2.33.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.33.1.tgz#2d00b38a2dd9f3355df91825582ada3ea0034e86" + integrity sha512-8VyRoJqtb2uQ8/bFRKNuACYZt7r+Xx0k2wXRGTyH05lCjAiVIXn7DiS2BxHFty7M1QEWUCMNsb/UC/x/Cu2wuA== + +"@sentry/cli@^2.22.3": + version "2.33.1" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.33.1.tgz#cfbdffdd896b05b92a659baf435b5607037af928" + integrity sha512-dUlZ4EFh98VFRPJ+f6OW3JEYQ7VvqGNMa0AMcmvk07ePNeK/GicAWmSQE4ZfJTTl80ul6HZw1kY01fGQOQlVRA== + dependencies: + https-proxy-agent "^5.0.0" + node-fetch "^2.6.7" + progress "^2.0.3" + proxy-from-env "^1.1.0" + which "^2.0.2" + optionalDependencies: + "@sentry/cli-darwin" "2.33.1" + "@sentry/cli-linux-arm" "2.33.1" + "@sentry/cli-linux-arm64" "2.33.1" + "@sentry/cli-linux-i686" "2.33.1" + "@sentry/cli-linux-x64" "2.33.1" + "@sentry/cli-win32-i686" "2.33.1" + "@sentry/cli-win32-x64" "2.33.1" + +"@sentry/core@8.22.0": + version "8.22.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.22.0.tgz#755387b85c1f7d849fb80acd7e8d54ee73ee2853" + integrity sha512-fYPnxp7UkY2tckaOtivIySxnJvlbekuxs+Qi6rkUv9JpF+TYKpt7OPNUAbgVIhS0xazAEN6iKTfmnmpUbFRLmQ== + dependencies: + "@sentry/types" "8.22.0" + "@sentry/utils" "8.22.0" + +"@sentry/react@^8.22.0": + version "8.22.0" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.22.0.tgz#6ad8c50d22262f14b83373ade0d77cf1c48adf70" + integrity sha512-LcO8SPfjYsx3Zvg1mQwjreVvtriVxde+6njIJyXU9TArB0e8bFexvd4MGXdBExgW9aY449hNaStgKRWMNHeVHQ== + dependencies: + "@sentry/browser" "8.22.0" + "@sentry/core" "8.22.0" + "@sentry/types" "8.22.0" + "@sentry/utils" "8.22.0" + hoist-non-react-statics "^3.3.2" + +"@sentry/types@8.22.0": + version "8.22.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.22.0.tgz#98bfc8cebd37c93f5afe76c6df97d88116b32127" + integrity sha512-1MLK3xO+uF2oJaa+M98aLIrQsEHzV7xnVWPfE3MhejYLNQebj4rQnQKTut/xZNIF9W0Q+bRcakLarC3ce2a74g== + +"@sentry/utils@8.22.0": + version "8.22.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.22.0.tgz#0e4411b19add83f84097fbf9c88423e69b8f6a38" + integrity sha512-0ITG2+3EtyMtyc/nQG8aB9z9eIQ4L43nM/KuNgYSnM1vPl/zegbaLT0Ek/xkQB1OLIOLkEPQ6x9GWe+248/n3g== + dependencies: + "@sentry/types" "8.22.0" + +"@sentry/webpack-plugin@^2.21.1": + version "2.21.1" + resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-2.21.1.tgz#ea0fa50508b4bb3d7cbdb25379edbf7b251e7fdc" + integrity sha512-mhKWQq7/eC35qrhhD8oXm/37vZ1BQqmCD8dUngFIr4D24rc7dwlGwPGOYv59yiBqjTS0fGJ+o0xC5PTRKljGQQ== + dependencies: + "@sentry/bundler-plugin-core" "2.21.1" + unplugin "1.0.1" + uuid "^9.0.0" + +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@sindresorhus/merge-streams@^2.1.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz#719df7fb41766bc143369eaa0dd56d8dc87c9958" + integrity sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg== + +"@sinonjs/commons@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + dependencies: + "@sinonjs/commons" "^3.0.0" + +"@storybook/addon-actions@8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-8.2.4.tgz#5d752381d3113e573dc9916f43cc7405d28d3a23" + integrity sha512-l1dlzWBBkR/5aullsX8N1ZbYr2bkeHPAaMCRy1jG5BBA8IHbi55JFwmJ8XF2gXkT2GyAZnePzb43RuLXz4KxFQ== + dependencies: + "@storybook/global" "^5.0.0" + "@types/uuid" "^9.0.1" + dequal "^2.0.2" + polished "^4.2.2" + uuid "^9.0.0" + +"@storybook/addon-backgrounds@8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-backgrounds/-/addon-backgrounds-8.2.4.tgz#a82e3739ec55804b597b5a34b28af39f3e613fd9" + integrity sha512-4oU25rFyr4OgMxHe4RpLJ7lxVwUDfdTi1j/YVyHfYv8koTqjagso8bv0uj0ujP5C3dSsVO0sp3/JOfPDkEUtrA== + dependencies: + "@storybook/global" "^5.0.0" + memoizerific "^1.11.3" + ts-dedent "^2.0.0" + +"@storybook/addon-controls@8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-controls/-/addon-controls-8.2.4.tgz#850680957d20b6f1e320f60da6ca18aa5178ed71" + integrity sha512-e56aUYhxyR8zJJstRAUP3WILhWTcvgRf5bysTtiyjFAL7U47cuCr043+IYEsxLkXhuZTKX2pcYSrjBtT5bYkVA== + dependencies: + dequal "^2.0.2" + lodash "^4.17.21" + ts-dedent "^2.0.0" + +"@storybook/addon-docs@8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-docs/-/addon-docs-8.2.4.tgz#16b30faa76d0a3d888538f37178dce5f60ab0b2f" + integrity sha512-oyrDw4nGfntu5Hkhr2Qt1wUOyLaVVERQekYyejyir92QhM10UeA7ZarPXNLfCTj7rbTrWmM1Waka9Tsf8TGMrw== + dependencies: + "@babel/core" "^7.24.4" + "@mdx-js/react" "^3.0.0" + "@storybook/blocks" "8.2.4" + "@storybook/csf-plugin" "8.2.4" + "@storybook/global" "^5.0.0" + "@storybook/react-dom-shim" "8.2.4" + "@types/react" "^16.8.0 || ^17.0.0 || ^18.0.0" + fs-extra "^11.1.0" + react "^16.8.0 || ^17.0.0 || ^18.0.0" + react-dom "^16.8.0 || ^17.0.0 || ^18.0.0" + rehype-external-links "^3.0.0" + rehype-slug "^6.0.0" + ts-dedent "^2.0.0" + +"@storybook/addon-essentials@^8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-essentials/-/addon-essentials-8.2.4.tgz#db4804dbe89f88bb259a977e4cbaa0f9085d8917" + integrity sha512-4upNauDJAJxauxnoUpUvzDnLo18C2yTVxgg+Id9wrKpt9C+CYH2oXyXzxoYGucYWZEe7zgCO6rWrGrKEisiLPQ== + dependencies: + "@storybook/addon-actions" "8.2.4" + "@storybook/addon-backgrounds" "8.2.4" + "@storybook/addon-controls" "8.2.4" + "@storybook/addon-docs" "8.2.4" + "@storybook/addon-highlight" "8.2.4" + "@storybook/addon-measure" "8.2.4" + "@storybook/addon-outline" "8.2.4" + "@storybook/addon-toolbars" "8.2.4" + "@storybook/addon-viewport" "8.2.4" + ts-dedent "^2.0.0" + +"@storybook/addon-highlight@8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-highlight/-/addon-highlight-8.2.4.tgz#85eedd166457b47d9844d6872fa33c820949af08" + integrity sha512-Ll/2y0m/q9ko9jFt40qsiee4fds6vpcwwxi3mPAVwRV/J7PpMzPkoLxM54bKpeHiWdTeGCXRguXNvyeQMQf3pg== + dependencies: + "@storybook/global" "^5.0.0" + +"@storybook/addon-interactions@^8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-interactions/-/addon-interactions-8.2.4.tgz#b95f7e87b8654ad52c6769e3c4c56f8dad18c991" + integrity sha512-jGGTCKfqZzq3DSZF+cimD8FBcO8X9yu/cNTcxHtx6TN9McV69sTiSzOpGgbWkLjLjP0XU12NQGqFw38tIn7n9Q== + dependencies: + "@storybook/global" "^5.0.0" + "@storybook/instrumenter" "8.2.4" + "@storybook/test" "8.2.4" + polished "^4.2.2" + ts-dedent "^2.2.0" + +"@storybook/addon-links@^8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-links/-/addon-links-8.2.4.tgz#b52e96cc63404b3cb76fe3fb21231514bf6b3291" + integrity sha512-1FgD6YXdXXSEDrp2aO4LxYt/X7LnBYx7cLlFla+xbn1CZLGqWLLeOT+BFd29wxpzs3u1Tap9r1iz1vRYL5ziyg== + dependencies: + "@storybook/csf" "0.1.11" + "@storybook/global" "^5.0.0" + ts-dedent "^2.0.0" + +"@storybook/addon-measure@8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-measure/-/addon-measure-8.2.4.tgz#b03363d2836dcbe0c72dc6414af03c8592905dcb" + integrity sha512-bSyE3mGDaaIKoe6Kt/f20YXKsn8WSoJUHrfKA68gbb+H3tegVQaqeS2KY5YzLqvjHe1qSmrO132NJt8RixLOPQ== + dependencies: + "@storybook/global" "^5.0.0" + tiny-invariant "^1.3.1" + +"@storybook/addon-onboarding@^8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-onboarding/-/addon-onboarding-8.2.4.tgz#98388e3ccdc6bed36e3cf9d9e830ea9300a3152f" + integrity sha512-guFRNPoNpLTR6hReGClUZasyMstGR2XmM4fjKg1iVvodw0nI/sZE/8eG2J2pWUGnp5YzFYirLuIZ03QO7edEMg== + dependencies: + react-confetti "^6.1.0" + +"@storybook/addon-outline@8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-outline/-/addon-outline-8.2.4.tgz#20ad622d0c8e5f842ec9f7611f1807715c777d1e" + integrity sha512-1C6NrvSDREgCZ7o/1n7Ca81uDDzrSrzWiOkh4OeA7PPQ/445cAOX2OMvxzNkKDIT9GLCLNi9M5XIVyGxJVS4dQ== + dependencies: + "@storybook/global" "^5.0.0" + ts-dedent "^2.0.0" + +"@storybook/addon-toolbars@8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-toolbars/-/addon-toolbars-8.2.4.tgz#8032e75c4d2a5e6c867dd486aa9881068d2727dc" + integrity sha512-iPnSr+hdz40Uoqg2cimyWf01/Y8GdgdMKB+b47TGIxtn9SEFBXck00ZG8ttwBvEsecu9K9CDt20fIOnr6oK5tQ== + +"@storybook/addon-viewport@8.2.4", "@storybook/addon-viewport@^8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-viewport/-/addon-viewport-8.2.4.tgz#c4499d721e6b9627b5083a30144bb3a6f6c0db81" + integrity sha512-58DcoX0xGpWlJfc0iLDjggkVPYzT4JdCZA2ioK9SQXQMsUzGFwR5PAAJv1tivYp7467tNkXvcM3QTb3Q3g8p4g== + dependencies: + memoizerific "^1.11.3" + +"@storybook/addon-webpack5-compiler-swc@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@storybook/addon-webpack5-compiler-swc/-/addon-webpack5-compiler-swc-1.0.4.tgz#07fb85132afc82a25c976000af107ffe045f1b77" + integrity sha512-S/ypdAK9oqwUAt3ZOn44qi3RWdH5uBLbBgtfHSXckqTpQRu7F7A9bRzjK+H5ti4xVADRhxu/xzIBwxWgcCeIXA== + dependencies: + "@swc/core" "1.5.7" + swc-loader "^0.2.3" + +"@storybook/blocks@8.2.4", "@storybook/blocks@^8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/blocks/-/blocks-8.2.4.tgz#8c2ab2834ac766f7241492dd7c9479f0550088b1" + integrity sha512-Hl2Dpg41YiJLSVXxjEJPjgPShrDJM3RY6HEEOjqTcAADsheX1IHAWXMJSJGMmne3Sew6VdJXPuHBIOFV4suZxg== + dependencies: + "@storybook/csf" "0.1.11" + "@storybook/global" "^5.0.0" + "@storybook/icons" "^1.2.5" + "@types/lodash" "^4.14.167" + color-convert "^2.0.1" + dequal "^2.0.2" + lodash "^4.17.21" + markdown-to-jsx "^7.4.5" + memoizerific "^1.11.3" + polished "^4.2.2" + react-colorful "^5.1.2" + telejson "^7.2.0" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + +"@storybook/builder-webpack5@8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/builder-webpack5/-/builder-webpack5-8.2.4.tgz#66b45dc4b204243eb5f09ea60c05278f8ab62308" + integrity sha512-O+upj4dsqmQuOJ1MzxypOoVfZ/bN78em/Px6Dks9LUdvLe/bLLXBeB0HsXdmuUE3GZS5LnR8gQQncl5V3pRLkA== + dependencies: + "@storybook/core-webpack" "8.2.4" + "@types/node" "^18.0.0" + "@types/semver" "^7.3.4" + browser-assert "^1.2.1" + case-sensitive-paths-webpack-plugin "^2.4.0" + cjs-module-lexer "^1.2.3" + constants-browserify "^1.0.0" + css-loader "^6.7.1" + es-module-lexer "^1.5.0" + express "^4.19.2" + fork-ts-checker-webpack-plugin "^8.0.0" + fs-extra "^11.1.0" + html-webpack-plugin "^5.5.0" + magic-string "^0.30.5" + path-browserify "^1.0.1" + process "^0.11.10" + semver "^7.3.7" + style-loader "^3.3.1" + terser-webpack-plugin "^5.3.1" + ts-dedent "^2.0.0" + url "^0.11.0" + util "^0.12.4" + util-deprecate "^1.0.2" + webpack "5" + webpack-dev-middleware "^6.1.2" + webpack-hot-middleware "^2.25.1" + webpack-virtual-modules "^0.6.0" + +"@storybook/codemod@8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/codemod/-/codemod-8.2.4.tgz#5499a806d77b1f397cd9a0e4234c61e02b7f2e3b" + integrity sha512-QcZdqjX4NvkVcWR3yI9it3PfqmBOCR+3iY6j4PmG7p5IE0j9kXMKBbeFrBRprSijHKlwcjbc3bRx2SnKF6AFEg== + dependencies: + "@babel/core" "^7.24.4" + "@babel/preset-env" "^7.24.4" + "@babel/types" "^7.24.0" + "@storybook/core" "8.2.4" + "@storybook/csf" "0.1.11" + "@types/cross-spawn" "^6.0.2" + cross-spawn "^7.0.3" + globby "^14.0.1" + jscodeshift "^0.15.1" + lodash "^4.17.21" + prettier "^3.1.1" + recast "^0.23.5" + tiny-invariant "^1.3.1" + +"@storybook/components@^8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/components/-/components-8.2.4.tgz#3185c3d5c68700daaad4b8cb3078a2eb0a9e3047" + integrity sha512-JLT1RoR/RXX+ZTeFoY85CRHb9Zz3l0PRRUSetEjoIJdnBGeL5C38bs0s9QnYjpCDLUlhdYhTln+GzmbyH8ocpA== + +"@storybook/core-webpack@8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/core-webpack/-/core-webpack-8.2.4.tgz#b3b28371658d0ad26cdbbac70f3eafe55f90ba23" + integrity sha512-yJeBZ5EIcU1qtZxc4E/0tgNovBiDdMsCOmWTBi724sqlGscvYSGhsI2v9JBvg3fJhnU2whCakeq4IOLOtiMAeQ== + dependencies: + "@types/node" "^18.0.0" + ts-dedent "^2.0.0" + +"@storybook/core@8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/core/-/core-8.2.4.tgz#04547072b3b66bb142ccb3c28984159b7239767f" + integrity sha512-jePmsGZT2hhUNQs8ED6+hFVt2m4hrMseO8kkN7Mcsve1MIujzHUS7Gjo4uguBwHJJOtiXB2fw4OSiQCmsXscZA== + dependencies: + "@storybook/csf" "0.1.11" + "@types/express" "^4.17.21" + "@types/node" "^18.0.0" + browser-assert "^1.2.1" + esbuild "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0" + esbuild-register "^3.5.0" + express "^4.19.2" + process "^0.11.10" + recast "^0.23.5" + util "^0.12.4" + ws "^8.2.3" + +"@storybook/csf-plugin@8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/csf-plugin/-/csf-plugin-8.2.4.tgz#76fc4b8a487fe7129267810f91efc1041d43a345" + integrity sha512-7V2tmeyAwv4/AQiBpB+7fCpphnY1yhcz+Zv9esUOHKqFn5+7u9FKpEXFFcf6fcbqXr2KoNw2F1EnTv3K/SxXrg== + dependencies: + unplugin "^1.3.1" + +"@storybook/csf@0.1.11": + version "0.1.11" + resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.1.11.tgz#ad685a4fe564a47a6b73571c2e7c07b526f4f71b" + integrity sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg== + dependencies: + type-fest "^2.19.0" + +"@storybook/csf@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@storybook/csf/-/csf-0.0.1.tgz#95901507dc02f0bc6f9ac8ee1983e2fc5bb98ce6" + integrity sha512-USTLkZze5gkel8MYCujSRBVIrUQ3YPBrLOx7GNk/0wttvVtlzWXAq9eLbQ4p/NicGxP+3T7KPEMVV//g+yubpw== + dependencies: + lodash "^4.17.15" + +"@storybook/global@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@storybook/global/-/global-5.0.0.tgz#b793d34b94f572c1d7d9e0f44fac4e0dbc9572ed" + integrity sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ== + +"@storybook/icons@^1.2.5": + version "1.2.9" + resolved "https://registry.yarnpkg.com/@storybook/icons/-/icons-1.2.9.tgz#bb4a51a79e186b62e2dd0e04928b8617ac573838" + integrity sha512-cOmylsz25SYXaJL/gvTk/dl3pyk7yBFRfeXTsHvTA3dfhoU/LWSq0NKL9nM7WBasJyn6XPSGnLS4RtKXLw5EUg== + +"@storybook/instrumenter@8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/instrumenter/-/instrumenter-8.2.4.tgz#695edbb52af1b2884701eeafc507b6624029ce2c" + integrity sha512-szcRjg7XhtobDW4omexWqBRlmRyrKW9p8uF9k6hanJqhHl4iG9D8xbi3SdaRhcn5KN1Wqv6RDAB+kXzHlFfdKA== + dependencies: + "@storybook/global" "^5.0.0" + "@vitest/utils" "^1.3.1" + util "^0.12.4" + +"@storybook/manager-api@^8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/manager-api/-/manager-api-8.2.4.tgz#459eab9451263580a15297991099e2590c91a04e" + integrity sha512-ayiOtcGupSeLCi2doEsRpALNPo4MBWYruc+e3jjkeVJQIg9A1ipSogNQh8unuOmq9rezO4/vcNBd6MxLs3xLWg== + +"@storybook/preset-react-webpack@8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/preset-react-webpack/-/preset-react-webpack-8.2.4.tgz#e59c4e20e6bf2a14d479c437aa35ea190f2f468b" + integrity sha512-uu77sBOibgPGWhG84eJsQkGv/UwbVnG/gS4CqHvHeuivtZup5vWxwuqh3ifsU7+uX94ZZuFJ5DNuo6194x9CdA== + dependencies: + "@storybook/core-webpack" "8.2.4" + "@storybook/react" "8.2.4" + "@storybook/react-docgen-typescript-plugin" "1.0.6--canary.9.0c3f3b7.0" + "@types/node" "^18.0.0" + "@types/semver" "^7.3.4" + find-up "^5.0.0" + fs-extra "^11.1.0" + magic-string "^0.30.5" + react-docgen "^7.0.0" + resolve "^1.22.8" + semver "^7.3.7" + tsconfig-paths "^4.2.0" + webpack "5" + +"@storybook/preview-api@^8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/preview-api/-/preview-api-8.2.4.tgz#c1681e86f7a45b22ea2978c3fca654a3c7926f01" + integrity sha512-IxOiUYYzNnk1OOz3zQBhsa3P1fsgqeMBZcH7TjiQWs9osuWG20oqsFR6+Z3dxoW8IuQHvpnREGKvAbRsDsThcA== + +"@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0": + version "1.0.6--canary.9.0c3f3b7.0" + resolved "https://registry.yarnpkg.com/@storybook/react-docgen-typescript-plugin/-/react-docgen-typescript-plugin-1.0.6--canary.9.0c3f3b7.0.tgz#7f10f3c641f32e4513a8b6ffb5036933e7059534" + integrity sha512-KUqXC3oa9JuQ0kZJLBhVdS4lOneKTOopnNBK4tUAgoxWQ3u/IjzdueZjFr7gyBrXMoU6duutk3RQR9u8ZpYJ4Q== + dependencies: + debug "^4.1.1" + endent "^2.0.1" + find-cache-dir "^3.3.1" + flat-cache "^3.0.4" + micromatch "^4.0.2" + react-docgen-typescript "^2.2.2" + tslib "^2.0.0" + +"@storybook/react-dom-shim@8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/react-dom-shim/-/react-dom-shim-8.2.4.tgz#f5cfb92f2e6040441fc15fe11928df9c690c3ecf" + integrity sha512-p2ypPWuKKFY/ij7yYjvdnrOcfdpxnAJd9D4/2Hm2eVioE4y8HQSND54t9OfkW+498Ez7ph4zW9ez005XqzH/+w== + +"@storybook/react-webpack5@^8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/react-webpack5/-/react-webpack5-8.2.4.tgz#07f1d419975bf2b6859ac7c043832bfd30fe95b8" + integrity sha512-ZwJQF8vW6XcdHrmuEX+rNNV9/lmAFs+p/FoDGGhsiUD7fIUX/F9xak0Ug+uhBcCEniY2suXcNHVQIInaH5/B8Q== + dependencies: + "@storybook/builder-webpack5" "8.2.4" + "@storybook/preset-react-webpack" "8.2.4" + "@storybook/react" "8.2.4" + "@types/node" "^18.0.0" + +"@storybook/react@8.2.4", "@storybook/react@^8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/react/-/react-8.2.4.tgz#0202f4f36b22afddaa519ad55a8c1c4df3e511ff" + integrity sha512-tRkEeFhwq2GeRsPwFc8dINI5L4mXanXaa7/JreB6ZcUeOZD8d81TWXCH9QyGvxfe0LW+DeNujA91mx5Yja35Zw== + dependencies: + "@storybook/components" "^8.2.4" + "@storybook/global" "^5.0.0" + "@storybook/manager-api" "^8.2.4" + "@storybook/preview-api" "^8.2.4" + "@storybook/react-dom-shim" "8.2.4" + "@storybook/theming" "^8.2.4" + "@types/escodegen" "^0.0.6" + "@types/estree" "^0.0.51" + "@types/node" "^18.0.0" + acorn "^7.4.1" + acorn-jsx "^5.3.1" + acorn-walk "^7.2.0" + escodegen "^2.1.0" + html-tags "^3.1.0" + lodash "^4.17.21" + prop-types "^15.7.2" + react-element-to-jsx-string "^15.0.0" + semver "^7.3.7" + ts-dedent "^2.0.0" + type-fest "~2.19" + util-deprecate "^1.0.2" + +"@storybook/test@8.2.4", "@storybook/test@^8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/test/-/test-8.2.4.tgz#0618a2b7f9fc456c685de382fb25d52e0ab3979b" + integrity sha512-boFjNFja4BNSbQhvmMlTVdQmZh36iM9+8w0sb7IK2e9Xnoi4+utupPNwBLvSsw4bRayK8+mP4Vk46O8h3TaiMw== + dependencies: + "@storybook/csf" "0.1.11" + "@storybook/instrumenter" "8.2.4" + "@testing-library/dom" "10.1.0" + "@testing-library/jest-dom" "6.4.5" + "@testing-library/user-event" "14.5.2" + "@vitest/expect" "1.6.0" + "@vitest/spy" "1.6.0" + util "^0.12.4" + +"@storybook/theming@^8.2.4": + version "8.2.4" + resolved "https://registry.yarnpkg.com/@storybook/theming/-/theming-8.2.4.tgz#b8b1b16b60bba982265778069aecaadc7a135822" + integrity sha512-B4HQMzTeg1TgV9uPDIoDkMSnP839Y05I9+Tw60cilAD+jTqrCvMlccHfehsTzJk+gioAflunATcbU05TMZoeIQ== + +"@swc/core-darwin-arm64@1.5.7": + version "1.5.7" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.7.tgz#2b5cdbd34e4162e50de6147dd1a5cb12d23b08e8" + integrity sha512-bZLVHPTpH3h6yhwVl395k0Mtx8v6CGhq5r4KQdAoPbADU974Mauz1b6ViHAJ74O0IVE5vyy7tD3OpkQxL/vMDQ== + +"@swc/core-darwin-x64@1.5.7": + version "1.5.7" + resolved "https://registry.yarnpkg.com/@swc/core-darwin-x64/-/core-darwin-x64-1.5.7.tgz#6aa7e3c01ab8e5e41597f8a24ff24c4e50936a46" + integrity sha512-RpUyu2GsviwTc2qVajPL0l8nf2vKj5wzO3WkLSHAHEJbiUZk83NJrZd1RVbEknIMO7+Uyjh54hEh8R26jSByaw== + +"@swc/core-linux-arm-gnueabihf@1.5.7": + version "1.5.7" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.5.7.tgz#160108633b9e1d1ad05f815bedc7e9eb5d59fc2a" + integrity sha512-cTZWTnCXLABOuvWiv6nQQM0hP6ZWEkzdgDvztgHI/+u/MvtzJBN5lBQ2lue/9sSFYLMqzqff5EHKlFtrJCA9dQ== + +"@swc/core-linux-arm64-gnu@1.5.7": + version "1.5.7" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.5.7.tgz#cbfa512683c73227ad25552f3b3e722b0e7fbd1d" + integrity sha512-hoeTJFBiE/IJP30Be7djWF8Q5KVgkbDtjySmvYLg9P94bHg9TJPSQoC72tXx/oXOgXvElDe/GMybru0UxhKx4g== + +"@swc/core-linux-arm64-musl@1.5.7": + version "1.5.7" + resolved "https://registry.yarnpkg.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.5.7.tgz#80239cb58fe57f3c86b44617fe784530ec55ee2b" + integrity sha512-+NDhK+IFTiVK1/o7EXdCeF2hEzCiaRSrb9zD7X2Z7inwWlxAntcSuzZW7Y6BRqGQH89KA91qYgwbnjgTQ22PiQ== + +"@swc/core-linux-x64-gnu@1.5.7": + version "1.5.7" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.5.7.tgz#a699c1632de60b6a63b7fdb7abcb4fef317e57ca" + integrity sha512-25GXpJmeFxKB+7pbY7YQLhWWjkYlR+kHz5I3j9WRl3Lp4v4UD67OGXwPe+DIcHqcouA1fhLhsgHJWtsaNOMBNg== + +"@swc/core-linux-x64-musl@1.5.7": + version "1.5.7" + resolved "https://registry.yarnpkg.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.5.7.tgz#8e4c203d6bc41e7f85d7d34d0fdf4ef751fa626c" + integrity sha512-0VN9Y5EAPBESmSPPsCJzplZHV26akC0sIgd3Hc/7S/1GkSMoeuVL+V9vt+F/cCuzr4VidzSkqftdP3qEIsXSpg== + +"@swc/core-win32-arm64-msvc@1.5.7": + version "1.5.7" + resolved "https://registry.yarnpkg.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.5.7.tgz#31e3d42b8c0aa79f0ea1a980c0dd1a999d378ed7" + integrity sha512-RtoNnstBwy5VloNCvmvYNApkTmuCe4sNcoYWpmY7C1+bPR+6SOo8im1G6/FpNem8AR5fcZCmXHWQ+EUmRWJyuA== + +"@swc/core-win32-ia32-msvc@1.5.7": + version "1.5.7" + resolved "https://registry.yarnpkg.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.5.7.tgz#a235285f9f62850aefcf9abb03420f2c54f63638" + integrity sha512-Xm0TfvcmmspvQg1s4+USL3x8D+YPAfX2JHygvxAnCJ0EHun8cm2zvfNBcsTlnwYb0ybFWXXY129aq1wgFC9TpQ== + +"@swc/core-win32-x64-msvc@1.5.7": + version "1.5.7" + resolved "https://registry.yarnpkg.com/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.5.7.tgz#f84641393b5223450d00d97bfff877b8b69d7c9b" + integrity sha512-tp43WfJLCsKLQKBmjmY/0vv1slVywR5Q4qKjF5OIY8QijaEW7/8VwPyUyVoJZEnDgv9jKtUTG5PzqtIYPZGnyg== + +"@swc/core@1.5.7": + version "1.5.7" + resolved "https://registry.yarnpkg.com/@swc/core/-/core-1.5.7.tgz#e1db7b9887d5f34eb4a3256a738d0c5f1b018c33" + integrity sha512-U4qJRBefIJNJDRCCiVtkfa/hpiZ7w0R6kASea+/KLp+vkus3zcLSB8Ub8SvKgTIxjWpwsKcZlPf5nrv4ls46SQ== + dependencies: + "@swc/counter" "^0.1.2" + "@swc/types" "0.1.7" + optionalDependencies: + "@swc/core-darwin-arm64" "1.5.7" + "@swc/core-darwin-x64" "1.5.7" + "@swc/core-linux-arm-gnueabihf" "1.5.7" + "@swc/core-linux-arm64-gnu" "1.5.7" + "@swc/core-linux-arm64-musl" "1.5.7" + "@swc/core-linux-x64-gnu" "1.5.7" + "@swc/core-linux-x64-musl" "1.5.7" + "@swc/core-win32-arm64-msvc" "1.5.7" + "@swc/core-win32-ia32-msvc" "1.5.7" + "@swc/core-win32-x64-msvc" "1.5.7" + +"@swc/counter@^0.1.2", "@swc/counter@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" + integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== + +"@swc/types@0.1.7": + version "0.1.7" + resolved "https://registry.yarnpkg.com/@swc/types/-/types-0.1.7.tgz#ea5d658cf460abff51507ca8d26e2d391bafb15e" + integrity sha512-scHWahbHF0eyj3JsxG9CFJgFdFNaVQCNAimBlT6PzS3n/HptxqREjsm4OH6AN3lYcffZYSPxXW8ua2BEHp0lJQ== + dependencies: + "@swc/counter" "^0.1.3" + +"@tanstack/query-core@5.51.9": + version "5.51.9" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.51.9.tgz#eb2e02c715068d5664680b773e39eb44db6b48d8" + integrity sha512-HsAwaY5J19MD18ykZDS3aVVh+bAt0i7m6uQlFC2b77DLV9djo+xEN7MWQAQQTR8IM+7r/zbozTQ7P0xr0bHuew== + +"@tanstack/react-query@^5.51.11": + version "5.51.11" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.51.11.tgz#8ab2cf6a4baa7a99b5682addf31328525f43242c" + integrity sha512-4Kq2x0XpDlpvSnaLG+8pHNH60zEc3mBvb3B2tOMDjcPCi/o+Du3p/9qpPLwJOTliVxxPJAP27fuIhLrsRdCr7A== + dependencies: + "@tanstack/query-core" "5.51.9" + +"@testing-library/dom@10.1.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.1.0.tgz#2d073e49771ad614da999ca48f199919e5176fb6" + integrity sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^5.0.1" + aria-query "5.3.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.5.0" + pretty-format "^27.0.2" + +"@testing-library/jest-dom@6.4.5": + version "6.4.5" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.4.5.tgz#badb40296477149136dabef32b572ddd3b56adf1" + integrity sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A== + dependencies: + "@adobe/css-tools" "^4.3.2" + "@babel/runtime" "^7.9.2" + aria-query "^5.0.0" + chalk "^3.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.6.3" + lodash "^4.17.21" + redent "^3.0.0" + +"@testing-library/react@^16.0.0": + version "16.0.0" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.0.0.tgz#0a1e0c7a3de25841c3591b8cb7fb0cf0c0a27321" + integrity sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ== + dependencies: + "@babel/runtime" "^7.12.5" + +"@testing-library/user-event@14.5.2": + version "14.5.2" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.2.tgz#db7257d727c891905947bd1c1a99da20e03c2ebd" + integrity sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ== + +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + +"@types/aria-query@^5.0.1": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708" + integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== + +"@types/babel__core@^7.1.14", "@types/babel__core@^7.18.0": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.8" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab" + integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6", "@types/babel__traverse@^7.18.0": + version "7.20.6" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.6.tgz#8dc9f0ae0f202c08d8d4dab648912c8d6038e3f7" + integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg== + dependencies: + "@babel/types" "^7.20.7" + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" + integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== + dependencies: + "@types/node" "*" + +"@types/connect-history-api-fallback@^1.5.4": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" + integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/cross-spawn@^6.0.2": + version "6.0.6" + resolved "https://registry.yarnpkg.com/@types/cross-spawn/-/cross-spawn-6.0.6.tgz#0163d0b79a6f85409e0decb8dcca17147f81fd22" + integrity sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA== + dependencies: + "@types/node" "*" + +"@types/doctrine@^0.0.9": + version "0.0.9" + resolved "https://registry.yarnpkg.com/@types/doctrine/-/doctrine-0.0.9.tgz#d86a5f452a15e3e3113b99e39616a9baa0f9863f" + integrity sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA== + +"@types/emscripten@^1.39.6": + version "1.39.13" + resolved "https://registry.yarnpkg.com/@types/emscripten/-/emscripten-1.39.13.tgz#afeb1648648dc096efe57983e20387627306e2aa" + integrity sha512-cFq+fO/isvhvmuP/+Sl4K4jtU6E23DoivtbO4r50e3odaxAiVdbfSYRDdJ4gCdxx+3aRjhphS5ZMwIH4hFy/Cw== + +"@types/escodegen@^0.0.6": + version "0.0.6" + resolved "https://registry.yarnpkg.com/@types/escodegen/-/escodegen-0.0.6.tgz#5230a9ce796e042cda6f086dbf19f22ea330659c" + integrity sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig== + +"@types/eslint-scope@^3.7.3": + version "3.7.7" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.7.tgz#3108bd5f18b0cdb277c867b3dd449c9ed7079ac5" + integrity sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.56.10" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.10.tgz#eb2370a73bf04a901eeba8f22595c7ee0f7eb58d" + integrity sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +"@types/estree@^0.0.51": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": + version "4.19.5" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz#218064e321126fcf9048d1ca25dd2465da55d9c6" + integrity sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*", "@types/express@^4.17.21": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/graceful-fs@^4.1.3": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" + integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== + dependencies: + "@types/node" "*" + +"@types/hast@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-3.0.4.tgz#1d6b39993b82cea6ad783945b0508c25903e15aa" + integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ== + dependencies: + "@types/unist" "*" + +"@types/html-minifier-terser@^6.0.0": + version "6.1.0" + resolved "https://registry.yarnpkg.com/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#4fc33a00c1d0c16987b1a20cf92d20614c55ac35" + integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/http-proxy@^1.17.8": + version "1.17.14" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.14.tgz#57f8ccaa1c1c3780644f8a94f9c6b5000b5e2eec" + integrity sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + +"@types/istanbul-lib-report@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" + integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jsdom@^20.0.0": + version "20.0.1" + resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-20.0.1.tgz#07c14bc19bd2f918c1929541cdaacae894744808" + integrity sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ== + dependencies: + "@types/node" "*" + "@types/tough-cookie" "*" + parse5 "^7.0.0" + +"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + +"@types/lodash@^4.14.167": + version "4.17.7" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.7.tgz#2f776bcb53adc9e13b2c0dfd493dfcbd7de43612" + integrity sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA== + +"@types/mdx@^2.0.0": + version "2.0.13" + resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.13.tgz#68f6877043d377092890ff5b298152b0a21671bd" + integrity sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "20.14.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.10.tgz#a1a218290f1b6428682e3af044785e5874db469a" + integrity sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ== + dependencies: + undici-types "~5.26.4" + +"@types/node@^18.0.0": + version "18.19.40" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.40.tgz#e9213ba98122992dafd8d55a2196f2ebb56b2555" + integrity sha512-MIxieZHrm4Ee8XArBIc+Or9HINt2StOmCbgRcXGSJl8q14svRvkZPe7LJq9HKtTI1SK3wU8b91TjntUm7T69Pg== + dependencies: + undici-types "~5.26.4" + +"@types/parse-json@^4.0.0": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" + integrity sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw== + +"@types/prop-types@*": + version "15.7.12" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" + integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== + +"@types/qs@*": + version "6.9.15" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" + integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/react-dom@^18.3.0": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0" + integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^16.8.0 || ^17.0.0 || ^18.0.0", "@types/react@^18.3.3": + version "18.3.3" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.3.tgz#9679020895318b0915d7a3ab004d92d33375c45f" + integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + +"@types/resolve@^1.20.2": + version "1.20.6" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.6.tgz#e6e60dad29c2c8c206c026e6dd8d6d1bdda850b8" + integrity sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ== + +"@types/retry@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" + integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow== + +"@types/semver@^7.3.12", "@types/semver@^7.3.4": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.4": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" + integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== + dependencies: + "@types/express" "*" + +"@types/serve-static@*", "@types/serve-static@^1.15.5": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/sockjs@^0.3.36": + version "0.3.36" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" + integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== + dependencies: + "@types/node" "*" + +"@types/stack-utils@^2.0.0": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" + integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== + +"@types/stylis@4.2.5": + version "4.2.5" + resolved "https://registry.yarnpkg.com/@types/stylis/-/stylis-4.2.5.tgz#1daa6456f40959d06157698a653a9ab0a70281df" + integrity sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw== + +"@types/tough-cookie@*": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== + +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.2.tgz#6dd61e43ef60b34086287f83683a5c1b2dc53d20" + integrity sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ== + +"@types/uuid@^9.0.1": + version "9.0.8" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" + integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== + +"@types/ws@^8.5.10": + version "8.5.10" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" + integrity sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A== + dependencies: + "@types/node" "*" + +"@types/yargs-parser@*": + version "21.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== + +"@types/yargs@^17.0.8": + version "17.0.32" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.32.tgz#030774723a2f7faafebf645f4e5a48371dca6229" + integrity sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog== + dependencies: + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@^7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz#b3563927341eca15124a18c6f94215f779f5c02a" + integrity sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "7.16.0" + "@typescript-eslint/type-utils" "7.16.0" + "@typescript-eslint/utils" "7.16.0" + "@typescript-eslint/visitor-keys" "7.16.0" + graphemer "^1.4.0" + ignore "^5.3.1" + natural-compare "^1.4.0" + ts-api-utils "^1.3.0" + +"@typescript-eslint/parser@^7.16.1": + version "7.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.16.1.tgz#84c581cf86c8b2becd48d33ddc41a6303d57b274" + integrity sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA== + dependencies: + "@typescript-eslint/scope-manager" "7.16.1" + "@typescript-eslint/types" "7.16.1" + "@typescript-eslint/typescript-estree" "7.16.1" + "@typescript-eslint/visitor-keys" "7.16.1" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" + integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + +"@typescript-eslint/scope-manager@7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz#eb0757af5720c9c53c8010d7a0355ae27e17b7e5" + integrity sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw== + dependencies: + "@typescript-eslint/types" "7.16.0" + "@typescript-eslint/visitor-keys" "7.16.0" + +"@typescript-eslint/scope-manager@7.16.1": + version "7.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz#2b43041caabf8ddd74512b8b550b9fc53ca3afa1" + integrity sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw== + dependencies: + "@typescript-eslint/types" "7.16.1" + "@typescript-eslint/visitor-keys" "7.16.1" + +"@typescript-eslint/type-utils@7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz#ec52b1932b8fb44a15a3e20208e0bd49d0b6bd00" + integrity sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg== + dependencies: + "@typescript-eslint/typescript-estree" "7.16.0" + "@typescript-eslint/utils" "7.16.0" + debug "^4.3.4" + ts-api-utils "^1.3.0" + +"@typescript-eslint/types@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== + +"@typescript-eslint/types@7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.16.0.tgz#60a19d7e7a6b1caa2c06fac860829d162a036ed2" + integrity sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw== + +"@typescript-eslint/types@7.16.1": + version "7.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.16.1.tgz#bbab066276d18e398bc64067b23f1ce84dfc6d8c" + integrity sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ== + +"@typescript-eslint/typescript-estree@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/typescript-estree@7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz#98ac779d526fab2a781e5619c9250f3e33867c09" + integrity sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw== + dependencies: + "@typescript-eslint/types" "7.16.0" + "@typescript-eslint/visitor-keys" "7.16.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" + +"@typescript-eslint/typescript-estree@7.16.1": + version "7.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz#9b145ba4fd1dde1986697e1ce57dc501a1736dd3" + integrity sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ== + dependencies: + "@typescript-eslint/types" "7.16.1" + "@typescript-eslint/visitor-keys" "7.16.1" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" + +"@typescript-eslint/utils@7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.16.0.tgz#b38dc0ce1778e8182e227c98d91d3418449aa17f" + integrity sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "7.16.0" + "@typescript-eslint/types" "7.16.0" + "@typescript-eslint/typescript-estree" "7.16.0" + +"@typescript-eslint/utils@^5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" + integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" + eslint-scope "^5.1.1" + semver "^7.3.7" + +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== + dependencies: + "@typescript-eslint/types" "5.62.0" + eslint-visitor-keys "^3.3.0" + +"@typescript-eslint/visitor-keys@7.16.0": + version "7.16.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz#a1d99fa7a3787962d6e0efd436575ef840e23b06" + integrity sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg== + dependencies: + "@typescript-eslint/types" "7.16.0" + eslint-visitor-keys "^3.4.3" + +"@typescript-eslint/visitor-keys@7.16.1": + version "7.16.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz#4287bcf44c34df811ff3bb4d269be6cfc7d8c74b" + integrity sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg== + dependencies: + "@typescript-eslint/types" "7.16.1" + eslint-visitor-keys "^3.4.3" + +"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + +"@vitest/expect@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.6.0.tgz#0b3ba0914f738508464983f4d811bc122b51fb30" + integrity sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ== + dependencies: + "@vitest/spy" "1.6.0" + "@vitest/utils" "1.6.0" + chai "^4.3.10" + +"@vitest/spy@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.6.0.tgz#362cbd42ccdb03f1613798fde99799649516906d" + integrity sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw== + dependencies: + tinyspy "^2.2.0" + +"@vitest/utils@1.6.0", "@vitest/utils@^1.3.1": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.6.0.tgz#5c5675ca7d6f546a7b4337de9ae882e6c57896a1" + integrity sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw== + dependencies: + diff-sequences "^29.6.3" + estree-walker "^3.0.3" + loupe "^2.3.7" + pretty-format "^29.7.0" + +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.1.1.tgz#3b2f852e91dac6e3b85fb2a314fb8bef46d94646" + integrity sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw== + +"@webpack-cli/info@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.2.tgz#cc3fbf22efeb88ff62310cf885c5b09f44ae0fdd" + integrity sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A== + +"@webpack-cli/serve@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e" + integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +"@yarnpkg/fslib@2.10.3": + version "2.10.3" + resolved "https://registry.yarnpkg.com/@yarnpkg/fslib/-/fslib-2.10.3.tgz#a8c9893df5d183cf6362680b9f1c6d7504dd5717" + integrity sha512-41H+Ga78xT9sHvWLlFOZLIhtU6mTGZ20pZ29EiZa97vnxdohJD2AF42rCoAoWfqUz486xY6fhjMH+DYEM9r14A== + dependencies: + "@yarnpkg/libzip" "^2.3.0" + tslib "^1.13.0" + +"@yarnpkg/libzip@2.3.0", "@yarnpkg/libzip@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/libzip/-/libzip-2.3.0.tgz#fe1e762e47669f6e2c960fc118436608d834e3be" + integrity sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg== + dependencies: + "@types/emscripten" "^1.39.6" + tslib "^1.13.0" + +abab@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" + integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-globals@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-7.0.1.tgz#0dbf05c44fa7c94332914c02066d5beff62c40c3" + integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q== + dependencies: + acorn "^8.1.0" + acorn-walk "^8.0.2" + +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== + +acorn-jsx@^5.3.1, acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-walk@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" + integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== + +acorn-walk@^8.0.2: + version "8.3.3" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.3.tgz#9caeac29eefaa0c41e3d4c65137de4d6f34df43e" + integrity sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw== + dependencies: + acorn "^8.11.0" + +acorn@^7.4.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +acorn@^8.1.0, acorn@^8.11.0, acorn@^8.11.3, acorn@^8.7.1, acorn@^8.8.1, acorn@^8.8.2, acorn@^8.9.0: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.12.4, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + 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" + +ajv@^8.0.0, ajv@^8.9.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.16.0.tgz#22e2a92b94f005f7e0f9c9d39652ef0b8f6f0cb4" + integrity sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw== + dependencies: + fast-deep-equal "^3.1.3" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.4.1" + +ajv@^8.0.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-html-community@0.0.8, ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +anymatch@^3.0.3, anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +aria-query@5.3.0, aria-query@^5.0.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + +aria-query@~5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" + integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== + dependencies: + deep-equal "^2.0.5" + +array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-includes@^3.1.6, array-includes@^3.1.7, array-includes@^3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== + 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" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.findlast@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== + 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" + +array.prototype.findlastindex@^1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" + integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== + 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" + +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.toreversed@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz#b989a6bf35c4c5051e1dc0325151bf8088954eba" + integrity sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.tosorted@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" + integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== + 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" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" + +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + +ast-types-flow@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" + integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== + +ast-types@^0.16.1: + version "0.16.1" + resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.16.1.tgz#7a9da1617c9081bc121faafe91711b4c8bb81da2" + integrity sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg== + dependencies: + tslib "^2.0.1" + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +async@^3.2.3: + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +axe-core@^4.9.1: + version "4.9.1" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.9.1.tgz#fcd0f4496dad09e0c899b44f6c4bb7848da912ae" + integrity sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw== + +axobject-query@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" + integrity sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg== + dependencies: + deep-equal "^2.0.5" + +babel-core@^7.0.0-bridge.0: + version "7.0.0-bridge.0" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece" + integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg== + +babel-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" + integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== + dependencies: + "@jest/transform" "^29.7.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.6.3" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" + integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-plugin-polyfill-corejs2@^0.4.10: + version "0.4.11" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz#30320dfe3ffe1a336c15afdcdafd6fd615b25e33" + integrity sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.6.2" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.10.4: + version "0.10.4" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz#789ac82405ad664c20476d0233b485281deb9c77" + integrity sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.1" + core-js-compat "^3.36.1" + +babel-plugin-polyfill-regenerator@^0.6.1: + version "0.6.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz#addc47e240edd1da1058ebda03021f382bba785e" + integrity sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.2" + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + +babel-preset-jest@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" + integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== + dependencies: + babel-plugin-jest-hoist "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +balanced-match@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9" + integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +before-after-hook@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-3.0.2.tgz#d5665a5fa8b62294a5aa0a499f933f4a1016195d" + integrity sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" + integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw== + dependencies: + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browser-assert@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/browser-assert/-/browser-assert-1.2.1.tgz#9aaa5a2a8c74685c2ae05bfe46efd606f068c200" + integrity sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ== + +browserslist@^4.21.10, browserslist@^4.23.0, browserslist@^4.23.1: + version "4.23.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.2.tgz#244fe803641f1c19c28c48c4b6ec9736eb3d32ed" + integrity sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA== + dependencies: + caniuse-lite "^1.0.30001640" + electron-to-chromium "^1.4.820" + node-releases "^2.0.14" + update-browserslist-db "^1.1.0" + +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +bundle-name@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889" + integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q== + dependencies: + run-applescript "^7.0.0" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camel-case@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== + dependencies: + pascal-case "^3.1.2" + tslib "^2.0.3" + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +camelize@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.1.tgz#89b7e16884056331a35d6b5ad064332c91daa6c3" + integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ== + +caniuse-lite@^1.0.30001640: + version "1.0.30001641" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001641.tgz#3572862cd18befae3f637f2a1101cc033c6782ac" + integrity sha512-Phv5thgl67bHYo1TtMY/MurjkHhV4EDaCosezRXgZ8jzA/Ub+wjxAvbGvjoFENStinwi5kCyOYV3mi5tOGykwA== + +case-sensitive-paths-webpack-plugin@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz#db64066c6422eed2e08cc14b986ca43796dbc6d4" + integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw== + +chai@^4.3.10: + version "4.4.1" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.1.tgz#3603fa6eba35425b0f2ac91a009fe924106e50d1" + integrity sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" + pathval "^1.1.1" + type-detect "^4.0.8" + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +check-error@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" + integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== + dependencies: + get-func-name "^2.0.2" + +chokidar@^3.5.3, chokidar@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +chromatic@^11.4.0: + version "11.5.5" + resolved "https://registry.yarnpkg.com/chromatic/-/chromatic-11.5.5.tgz#1a656adac3fd9efc4341d3755af2f0a7515e3e3e" + integrity sha512-YS0GJwegF0vpMbwZE68/xJlI4SlUGMqI78V2ATAF19YwTHaq8jGP1CPQGKUSlgWUhzPtyu3ELy6Dvv/owYljAg== + +chromatic@^11.5.6: + version "11.5.6" + resolved "https://registry.yarnpkg.com/chromatic/-/chromatic-11.5.6.tgz#9d243235ff079307f5e8b9682ab5315ee48e5cf8" + integrity sha512-ycX/hlZLs69BltwwBNsEXr+As6x5/0rlwp6W/CiHMZ3tpm7dmkd+hQCsb8JGHb1h49W3qPOKQ/Lh9evqcJ1yeQ== + +chrome-trace-event@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + +ci-info@^3.2.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + +citty@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/citty/-/citty-0.1.6.tgz#0f7904da1ed4625e1a9ea7e0fa780981aab7c5e4" + integrity sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ== + dependencies: + consola "^3.2.3" + +cjs-module-lexer@^1.0.0, cjs-module-lexer@^1.2.3: + version "1.3.1" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz#c485341ae8fd999ca4ee5af2d7a1c9ae01e0099c" + integrity sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q== + +clean-css@^5.2.2: + version "5.3.3" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" + integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg== + dependencies: + source-map "~0.6.0" + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-spinners@^2.5.0: + version "2.9.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +collect-v8-coverage@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" + integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colord@^2.9.3: + version "2.9.3" + resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" + integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== + +colorette@^2.0.10, colorette@^2.0.14: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + +commander@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +confbox@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.7.tgz#ccfc0a2bcae36a84838e83a3b7f770fb17d6c579" + integrity sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA== + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +consola@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.2.3.tgz#0741857aa88cfa0d6fd53f1cff0375136e98502f" + integrity sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ== + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + integrity sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + +copy-webpack-plugin@^12.0.2: + version "12.0.2" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz#935e57b8e6183c82f95bd937df658a59f6a2da28" + integrity sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA== + dependencies: + fast-glob "^3.3.2" + glob-parent "^6.0.1" + globby "^14.0.0" + normalize-path "^3.0.0" + schema-utils "^4.2.0" + serialize-javascript "^6.0.2" + +core-js-compat@^3.36.1, core-js-compat@^3.37.1: + version "3.37.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.1.tgz#c844310c7852f4bdf49b8d339730b97e17ff09ee" + integrity sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg== + dependencies: + browserslist "^4.23.0" + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cosmiconfig@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +cosmiconfig@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.0.tgz#34c3fc58287b915f3ae905ab6dc3de258b55ad9d" + integrity sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg== + dependencies: + env-paths "^2.2.1" + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + +create-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" + integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-config "^29.7.0" + jest-util "^29.7.0" + prompts "^2.0.1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypto-random-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-4.0.0.tgz#5a3cc53d7dd86183df5da0312816ceeeb5bb1fc2" + integrity sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA== + dependencies: + type-fest "^1.0.1" + +css-color-keywords@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" + integrity sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg== + +css-functions-list@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.2.tgz#9a54c6dd8416ed25c1079cd88234e927526c1922" + integrity sha512-c+N0v6wbKVxTu5gOBBFkr9BEdBWaqqjQeiJ8QvSRIJOf+UxlJh930m8e6/WNeODIK0mYLFkoONrnj16i2EcvfQ== + +css-loader@^6.7.1: + version "6.11.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba" + integrity sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g== + dependencies: + icss-utils "^5.1.0" + postcss "^8.4.33" + postcss-modules-extract-imports "^3.1.0" + postcss-modules-local-by-default "^4.0.5" + postcss-modules-scope "^3.2.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.2.0" + semver "^7.5.4" + +css-loader@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-7.1.2.tgz#64671541c6efe06b0e22e750503106bdd86880f8" + integrity sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA== + dependencies: + icss-utils "^5.1.0" + postcss "^8.4.33" + postcss-modules-extract-imports "^3.1.0" + postcss-modules-local-by-default "^4.0.5" + postcss-modules-scope "^3.2.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.2.0" + semver "^7.5.4" + +css-select@^4.1.3: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== + dependencies: + boolbase "^1.0.0" + css-what "^6.0.1" + domhandler "^4.3.1" + domutils "^2.8.0" + nth-check "^2.0.1" + +css-to-react-native@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-3.2.0.tgz#cdd8099f71024e149e4f6fe17a7d46ecd55f1e32" + integrity sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ== + dependencies: + camelize "^1.0.0" + css-color-keywords "^1.0.0" + postcss-value-parser "^4.0.2" + +css-tree@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" + integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== + dependencies: + mdn-data "2.0.30" + source-map-js "^1.0.1" + +css-what@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssom@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" + integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== + +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-2.3.0.tgz#ff665a0ddbdc31864b09647f34163443d90b0852" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== + dependencies: + cssom "~0.3.6" + +csstype@3.1.3, csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +damerau-levenshtein@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" + integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== + +data-urls@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143" + integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ== + dependencies: + abab "^2.0.6" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.3.6: + version "4.3.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" + integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== + dependencies: + ms "2.1.2" + +decimal.js@^10.4.2: + version "10.4.3" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" + integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== + +dedent@^1.0.0: + version "1.5.3" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" + integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== + +deep-eql@^4.1.3: + version "4.1.4" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.4.tgz#d0d3912865911bb8fac5afb4e3acfa6a28dc72b7" + integrity sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg== + dependencies: + type-detect "^4.0.0" + +deep-equal@^2.0.5: + version "2.2.3" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" + integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.5" + es-get-iterator "^1.1.3" + get-intrinsic "^1.2.2" + is-arguments "^1.1.1" + is-array-buffer "^3.0.2" + is-date-object "^1.0.5" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + isarray "^2.0.5" + object-is "^1.1.5" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + side-channel "^1.0.4" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.13" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +default-browser-id@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.0.tgz#a1d98bf960c15082d8a3fa69e83150ccccc3af26" + integrity sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA== + +default-browser@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.2.1.tgz#7b7ba61204ff3e425b556869ae6d3e9d9f1712cf" + integrity sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg== + dependencies: + bundle-name "^4.1.0" + default-browser-id "^5.0.0" + +default-gateway@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" + integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== + dependencies: + execa "^5.0.0" + +defaults@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== + dependencies: + clone "^1.0.2" + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-lazy-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" + integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== + +define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +defu@^6.1.4: + version "6.1.4" + resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" + integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +dequal@^2.0.2, dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-indent@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" + integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-accessibility-api@^0.5.9: + version "0.5.16" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" + integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== + +dom-accessibility-api@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" + integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== + +dom-converter@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== + dependencies: + utila "~0.4" + +dom-serializer@^1.0.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domexception@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673" + integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== + dependencies: + webidl-conversions "^7.0.0" + +domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +domutils@^2.5.2, domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +dotenv-defaults@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dotenv-defaults/-/dotenv-defaults-2.0.2.tgz#6b3ec2e4319aafb70940abda72d3856770ee77ac" + integrity sha512-iOIzovWfsUHU91L5i8bJce3NYK5JXeAwH50Jh6+ARUdLiiGlYWfGw6UkzsYqaXZH/hjE/eCd/PlfM/qqyK0AMg== + dependencies: + dotenv "^8.2.0" + +dotenv-webpack@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/dotenv-webpack/-/dotenv-webpack-8.1.0.tgz#4d66abc4a30395b46a030ebcd125320232b54873" + integrity sha512-owK1JcsPkIobeqjVrk6h7jPED/W6ZpdFsMPR+5ursB7/SdgDyO+VzAU+szK8C8u3qUhtENyYnj8eyXMR5kkGag== + dependencies: + dotenv-defaults "^2.0.2" + +dotenv@^16.3.1: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + +dotenv@^8.2.0: + version "8.6.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" + integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +ejs@^3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== + dependencies: + jake "^10.8.5" + +electron-to-chromium@^1.4.820: + version "1.4.824" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.824.tgz#5adbaf8f2a3466777d97384d6af27abc120bab8b" + integrity sha512-GTQnZOP1v0wCuoWzKOxL8rurg9T13QRYISkoICGaZzskBf9laC3V8g9BHTpJv+j9vBRcKOulbGXwMzuzNdVrAA== + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +endent@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/endent/-/endent-2.1.0.tgz#5aaba698fb569e5e18e69e1ff7a28ff35373cd88" + integrity sha512-r8VyPX7XL8U01Xgnb1CjZ3XV+z90cXIJ9JPE/R9SEC9vpw2P6CfsRPJmp20DppC5N7ZAMCmjYkJIa744Iyg96w== + dependencies: + dedent "^0.7.0" + fast-json-parse "^1.0.3" + objectorarray "^1.0.5" + +enhanced-resolve@^5.0.0, enhanced-resolve@^5.12.0, enhanced-resolve@^5.17.0, enhanced-resolve@^5.7.0: + version "5.17.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz#d037603789dd9555b89aaec7eb78845c49089bc5" + integrity sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +env-paths@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +envinfo@^7.7.3: + version "7.13.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.13.0.tgz#81fbb81e5da35d74e814941aeab7c325a606fb31" + integrity sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-get-iterator@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + +es-iterator-helpers@^1.0.19: + version "1.0.19" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz#117003d0e5fec237b4b5c08aded722e0c6d50ca8" + integrity sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-set-tostringtag "^2.0.3" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + iterator.prototype "^1.1.2" + safe-array-concat "^1.1.2" + +es-module-lexer@^1.2.1, es-module-lexer@^1.5.0: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== + dependencies: + hasown "^2.0.0" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +esbuild-register@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/esbuild-register/-/esbuild-register-3.5.0.tgz#449613fb29ab94325c722f560f800dd946dc8ea8" + integrity sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A== + dependencies: + debug "^4.3.4" + +"esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0": + version "0.21.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + +escalade@^3.1.1, escalade@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escodegen@^2.0.0, escodegen@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.1.0.tgz#ba93bbb7a43986d29d6041f99f5262da773e2e17" + integrity sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionalDependencies: + source-map "~0.6.1" + +eslint-config-prettier@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" + integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== + +eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + +eslint-import-resolver-typescript@^3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz#7b983680edd3f1c5bce1a5829ae0bc2d57fe9efa" + integrity sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg== + dependencies: + debug "^4.3.4" + enhanced-resolve "^5.12.0" + eslint-module-utils "^2.7.4" + fast-glob "^3.3.1" + get-tsconfig "^4.5.0" + is-core-module "^2.11.0" + is-glob "^4.0.3" + +eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz#52f2404300c3bd33deece9d7372fb337cc1d7c34" + integrity sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q== + dependencies: + debug "^3.2.7" + +eslint-plugin-import@^2.29.1: + version "2.29.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" + integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== + dependencies: + array-includes "^3.1.7" + array.prototype.findlastindex "^1.2.3" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.8.0" + hasown "^2.0.0" + is-core-module "^2.13.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.fromentries "^2.0.7" + object.groupby "^1.0.1" + object.values "^1.1.7" + semver "^6.3.1" + tsconfig-paths "^3.15.0" + +eslint-plugin-jsx-a11y@^6.9.0: + version "6.9.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.9.0.tgz#67ab8ff460d4d3d6a0b4a570e9c1670a0a8245c8" + integrity sha512-nOFOCaJG2pYqORjK19lqPqxMO/JpvdCZdPtNdxY3kvom3jTvkAbOvQvD8wuD0G8BYR0IGAGYDlzqWJOh/ybn2g== + dependencies: + aria-query "~5.1.3" + array-includes "^3.1.8" + array.prototype.flatmap "^1.3.2" + ast-types-flow "^0.0.8" + axe-core "^4.9.1" + axobject-query "~3.1.1" + damerau-levenshtein "^1.0.8" + emoji-regex "^9.2.2" + es-iterator-helpers "^1.0.19" + hasown "^2.0.2" + jsx-ast-utils "^3.3.5" + language-tags "^1.0.9" + minimatch "^3.1.2" + object.fromentries "^2.0.8" + safe-regex-test "^1.0.3" + string.prototype.includes "^2.0.0" + +eslint-plugin-prettier@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz#17cfade9e732cef32b5f5be53bd4e07afd8e67e1" + integrity sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw== + dependencies: + prettier-linter-helpers "^1.0.0" + synckit "^0.8.6" + +eslint-plugin-react-hooks@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" + integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== + +eslint-plugin-react@^7.34.3: + version "7.34.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.3.tgz#9965f27bd1250a787b5d4cfcc765e5a5d58dcb7b" + integrity sha512-aoW4MV891jkUulwDApQbPYTVZmeuSyFrudpbTAQuj5Fv8VL+o6df2xIGpw8B0hPjAaih1/Fb0om9grCdyFYemA== + dependencies: + array-includes "^3.1.8" + array.prototype.findlast "^1.2.5" + array.prototype.flatmap "^1.3.2" + array.prototype.toreversed "^1.1.2" + array.prototype.tosorted "^1.1.4" + doctrine "^2.1.0" + es-iterator-helpers "^1.0.19" + estraverse "^5.3.0" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.8" + object.fromentries "^2.0.8" + object.hasown "^1.1.4" + object.values "^1.2.0" + prop-types "^15.8.1" + resolve "^2.0.0-next.5" + semver "^6.3.1" + string.prototype.matchall "^4.0.11" + +eslint-plugin-storybook@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-storybook/-/eslint-plugin-storybook-0.8.0.tgz#23185ecabdc289cae55248c090f0c1d8fbae6c41" + integrity sha512-CZeVO5EzmPY7qghO2t64oaFM+8FTaD4uzOEjHKp516exyTKo+skKAL9GI3QALS2BXhyALJjNtwbmr1XinGE8bA== + dependencies: + "@storybook/csf" "^0.0.1" + "@typescript-eslint/utils" "^5.62.0" + requireindex "^1.2.0" + ts-dedent "^2.2.0" + +eslint-scope@5.1.1, eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@8.x: + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +execa@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" + integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^8.0.1" + human-signals "^5.0.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^4.1.0" + strip-final-newline "^3.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + dependencies: + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + +express@^4.17.3, express@^4.19.2: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-diff@^1.1.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + +fast-glob@^3.2.9, fast-glob@^3.3.1, fast-glob@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + 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.4" + +fast-json-parse@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fast-json-parse/-/fast-json-parse-1.0.3.tgz#43e5c61ee4efa9265633046b770fb682a7577c4d" + integrity sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw== + +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fast-uri@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134" + integrity sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw== + +fastest-levenshtein@^1.0.12, fastest-levenshtein@^1.0.16: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + +fd-package-json@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fd-package-json/-/fd-package-json-1.2.0.tgz#4f218bb8ff65c21011d1f4f17cb3d0c9e72f8da7" + integrity sha512-45LSPmWf+gC5tdCQMNH4s9Sr00bIkiD9aN7dc5hqkrEw1geRYyDQS1v1oMHAW3ysfxfndqGsrDREHHjNNbKUfA== + dependencies: + walk-up-path "^3.0.1" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +file-entry-cache@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-9.0.0.tgz#4478e7ceaa5191fa9676a2daa7030211c31b1e7e" + integrity sha512-6MgEugi8p2tiUhqO7GnPsmbCCzj0YRCwwaTbpGRyKZesjRSzkqkAE9fPp7V2yMs5hwfgbQLgdvSSkGNg1s5Uvw== + dependencies: + flat-cache "^5.0.0" + +filelist@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + +filesize@^10.0.12: + version "10.1.4" + resolved "https://registry.yarnpkg.com/filesize/-/filesize-10.1.4.tgz#184f256063a201f08b6e6b3cc47d21b60f5b8d89" + integrity sha512-ryBwPIIeErmxgPnm6cbESAzXjuEFubs+yKYLBZvg3CaiNcmkJChoOGcBSrZ6IwkMwPABwPpVXE6IlNdGJJrvEg== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-cache-dir@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" + integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== + dependencies: + commondir "^1.0.1" + make-dir "^2.0.0" + pkg-dir "^3.0.0" + +find-cache-dir@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flat-cache@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-5.0.0.tgz#26c4da7b0f288b408bb2b506b2cb66c240ddf062" + integrity sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ== + dependencies: + flatted "^3.3.1" + keyv "^4.5.4" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.2.9, flatted@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== + +flow-parser@0.*: + version "0.241.0" + resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.241.0.tgz#cc42f2e1ea8e9ef0a9ab5a9c3d9be01fc121d973" + integrity sha512-82yKXpz7iWknWFsognZUf5a6mBQLnVrYoYSU9Nbu7FTOpKlu3v9ehpiI9mYXuaIO3J0ojX1b83M/InXvld9HUw== + +follow-redirects@^1.0.0: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +foreground-child@^3.1.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.2.1.tgz#767004ccf3a5b30df39bed90718bab43fe0a59f7" + integrity sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +fork-ts-checker-webpack-plugin@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz#dae45dfe7298aa5d553e2580096ced79b6179504" + integrity sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg== + dependencies: + "@babel/code-frame" "^7.16.7" + chalk "^4.1.2" + chokidar "^3.5.3" + cosmiconfig "^7.0.1" + deepmerge "^4.2.2" + fs-extra "^10.0.0" + memfs "^3.4.1" + minimatch "^3.0.4" + node-abort-controller "^3.0.1" + schema-utils "^3.1.1" + semver "^7.3.5" + tapable "^2.2.1" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^11.1.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs-monkey@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" + integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@^2.3.2, fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-func-name@^2.0.1, get-func-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-stream@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" + integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== + +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + dependencies: + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + +get-tsconfig@^4.5.0: + version "4.7.5" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.5.tgz#5e012498579e9a6947511ed0cd403272c7acbbaf" + integrity sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw== + dependencies: + resolve-pkg-maps "^1.0.0" + +giget@^1.0.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/giget/-/giget-1.2.3.tgz#ef6845d1140e89adad595f7f3bb60aa31c672cb6" + integrity sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA== + dependencies: + citty "^0.1.6" + consola "^3.2.3" + defu "^6.1.4" + node-fetch-native "^1.6.3" + nypm "^0.3.8" + ohash "^1.1.3" + pathe "^1.1.2" + tar "^6.2.0" + +github-slugger@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-2.0.0.tgz#52cf2f9279a21eb6c59dd385b410f0c0adda8f1a" + integrity sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.1, glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^10.3.7: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^9.3.2: + version "9.3.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.5.tgz#ca2ed8ca452781a3009685607fdf025a899dfe21" + integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== + dependencies: + fs.realpath "^1.0.0" + minimatch "^8.0.2" + minipass "^4.2.4" + path-scurry "^1.6.1" + +global-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + dependencies: + type-fest "^0.20.2" + +globalthis@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +globby@^14.0.0, globby@^14.0.1: + version "14.0.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-14.0.2.tgz#06554a54ccfe9264e5a9ff8eded46aa1e306482f" + integrity sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw== + dependencies: + "@sindresorhus/merge-streams" "^2.1.0" + fast-glob "^3.3.2" + ignore "^5.2.4" + path-type "^5.0.0" + slash "^5.1.0" + unicorn-magic "^0.1.0" + +globjoin@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/globjoin/-/globjoin-0.1.4.tgz#2f4494ac8919e3767c5cbb691e9f463324285d43" + integrity sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg== + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hast-util-heading-rank@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz#2d5c6f2807a7af5c45f74e623498dd6054d2aba8" + integrity sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-is-element@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz#6e31a6532c217e5b533848c7e52c9d9369ca0932" + integrity sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g== + dependencies: + "@types/hast" "^3.0.0" + +hast-util-to-string@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-3.0.0.tgz#2a131948b4b1b26461a2c8ac876e2c88d02946bd" + integrity sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA== + dependencies: + "@types/hast" "^3.0.0" + +he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-encoding-sniffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9" + integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== + dependencies: + whatwg-encoding "^2.0.0" + +html-entities@^2.1.0, html-entities@^2.4.0: + version "2.5.2" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" + integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +html-minifier-terser@^6.0.2: + version "6.1.0" + resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz#bfc818934cc07918f6b3669f5774ecdfd48f32ab" + integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw== + dependencies: + camel-case "^4.1.2" + clean-css "^5.2.2" + commander "^8.3.0" + he "^1.2.0" + param-case "^3.0.4" + relateurl "^0.2.7" + terser "^5.10.0" + +html-tags@^3.1.0, html-tags@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" + integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== + +html-webpack-plugin@^5.5.0, html-webpack-plugin@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0" + integrity sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw== + dependencies: + "@types/html-minifier-terser" "^6.0.0" + html-minifier-terser "^6.0.2" + lodash "^4.17.21" + pretty-error "^4.0.0" + tapable "^2.0.0" + +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + +http-proxy-middleware@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +human-signals@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" + integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== + +hyperdyperid@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b" + integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + +import-fresh@^3.2.1, import-fresh@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +ini@^1.3.5: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +internal-slot@^1.0.4, internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + +interpret@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" + integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== + +is-absolute-url@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-4.0.1.tgz#16e4d487d4fded05cfe0685e53ec86804a5e94dc" + integrity sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A== + +is-arguments@^1.0.4, is-arguments@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-array-buffer@^3.0.2, is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-core-module@^2.11.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.0.tgz#71c72ec5442ace7e76b306e9d48db361f22699ea" + integrity sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA== + dependencies: + hasown "^2.0.2" + +is-core-module@^2.13.0, is-core-module@^2.13.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.14.0.tgz#43b8ef9f46a6a08888db67b1ffd4ec9e3dfd59d1" + integrity sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A== + dependencies: + hasown "^2.0.2" + +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + +is-date-object@^1.0.1, is-date-object@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-finalizationregistry@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" + integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== + dependencies: + call-bind "^1.0.2" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-generator-function@^1.0.10, is-generator-function@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + +is-map@^2.0.2, is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-network-error@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-network-error/-/is-network-error-1.1.0.tgz#d26a760e3770226d11c169052f266a4803d9c997" + integrity sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-object@5.0.0, is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-set@^2.0.2, is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.13, is-typed-array@^1.1.3: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-weakset@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" + integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + +is-wsl@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2" + integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw== + dependencies: + is-inside-container "^1.0.0" + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + +istanbul-lib-instrument@^5.0.4: + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-instrument@^6.0.0: + version "6.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" + integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== + dependencies: + "@babel/core" "^7.23.9" + "@babel/parser" "^7.23.9" + "@istanbuljs/schema" "^0.1.3" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" + +istanbul-lib-report@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^4.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" + integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +iterator.prototype@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" + integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== + dependencies: + define-properties "^1.2.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + reflect.getprototypeof "^1.0.4" + set-function-name "^2.0.1" + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jake@^10.8.5: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" + integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.4" + minimatch "^3.1.2" + +jest-changed-files@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" + integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== + dependencies: + execa "^5.0.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + +jest-circus@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" + integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^1.0.0" + is-generator-fn "^2.0.0" + jest-each "^29.7.0" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + pretty-format "^29.7.0" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-cli@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" + integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== + dependencies: + "@jest/core" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + chalk "^4.0.0" + create-jest "^29.7.0" + exit "^0.1.2" + import-local "^3.0.2" + jest-config "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + yargs "^17.3.1" + +jest-config@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" + integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.7.0" + "@jest/types" "^29.6.3" + babel-jest "^29.7.0" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.7.0" + jest-environment-node "^29.7.0" + jest-get-type "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-runner "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-docblock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" + integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== + dependencies: + detect-newline "^3.0.0" + +jest-each@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" + integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + jest-get-type "^29.6.3" + jest-util "^29.7.0" + pretty-format "^29.7.0" + +jest-environment-jsdom@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz#d206fa3551933c3fd519e5dfdb58a0f5139a837f" + integrity sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/jsdom" "^20.0.0" + "@types/node" "*" + jest-mock "^29.7.0" + jest-util "^29.7.0" + jsdom "^20.0.0" + +jest-environment-node@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" + integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + +jest-haste-map@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" + integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== + dependencies: + "@jest/types" "^29.6.3" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + jest-worker "^29.7.0" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-leak-detector@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" + integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== + dependencies: + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + dependencies: + chalk "^4.0.0" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-util "^29.7.0" + +jest-pnp-resolver@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + +jest-regex-util@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== + +jest-resolve-dependencies@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" + integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== + dependencies: + jest-regex-util "^29.6.3" + jest-snapshot "^29.7.0" + +jest-resolve@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" + integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-pnp-resolver "^1.2.2" + jest-util "^29.7.0" + jest-validate "^29.7.0" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" + +jest-runner@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" + integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== + dependencies: + "@jest/console" "^29.7.0" + "@jest/environment" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.7.0" + jest-environment-node "^29.7.0" + jest-haste-map "^29.7.0" + jest-leak-detector "^29.7.0" + jest-message-util "^29.7.0" + jest-resolve "^29.7.0" + jest-runtime "^29.7.0" + jest-util "^29.7.0" + jest-watcher "^29.7.0" + jest-worker "^29.7.0" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" + integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/globals" "^29.7.0" + "@jest/source-map" "^29.6.3" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-snapshot@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" + integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.7.0" + graceful-fs "^4.2.9" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + natural-compare "^1.4.0" + pretty-format "^29.7.0" + semver "^7.5.3" + +jest-util@^29.0.0, jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" + integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== + dependencies: + "@jest/types" "^29.6.3" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.6.3" + leven "^3.1.0" + pretty-format "^29.7.0" + +jest-watcher@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" + integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== + dependencies: + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.7.0" + string-length "^4.0.1" + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest-worker@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== + dependencies: + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" + integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== + dependencies: + "@jest/core" "^29.7.0" + "@jest/types" "^29.6.3" + import-local "^3.0.2" + jest-cli "^29.7.0" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jscodeshift@^0.15.1: + version "0.15.2" + resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.15.2.tgz#145563860360b4819a558c75c545f39683e5a0be" + integrity sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA== + dependencies: + "@babel/core" "^7.23.0" + "@babel/parser" "^7.23.0" + "@babel/plugin-transform-class-properties" "^7.22.5" + "@babel/plugin-transform-modules-commonjs" "^7.23.0" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.11" + "@babel/plugin-transform-optional-chaining" "^7.23.0" + "@babel/plugin-transform-private-methods" "^7.22.5" + "@babel/preset-flow" "^7.22.15" + "@babel/preset-typescript" "^7.23.0" + "@babel/register" "^7.22.15" + babel-core "^7.0.0-bridge.0" + chalk "^4.1.2" + flow-parser "0.*" + graceful-fs "^4.2.4" + micromatch "^4.0.4" + neo-async "^2.5.0" + node-dir "^0.1.17" + recast "^0.23.3" + temp "^0.8.4" + write-file-atomic "^2.3.0" + +jsdom@^20.0.0: + version "20.0.3" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.3.tgz#886a41ba1d4726f67a8858028c99489fed6ad4db" + integrity sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ== + dependencies: + abab "^2.0.6" + acorn "^8.8.1" + acorn-globals "^7.0.0" + cssom "^0.5.0" + cssstyle "^2.3.0" + data-urls "^3.0.2" + decimal.js "^10.4.2" + domexception "^4.0.0" + escodegen "^2.0.0" + form-data "^4.0.0" + html-encoding-sniffer "^3.0.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.1" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.2" + parse5 "^7.1.1" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^4.1.2" + w3c-xmlserializer "^4.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^2.0.0" + whatwg-mimetype "^3.0.0" + whatwg-url "^11.0.0" + ws "^8.11.0" + xml-name-validator "^4.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + +json5@^2.2.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonfile@^6.0.1, jsonfile@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: + version "3.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" + +keyv@^4.5.3, keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +known-css-properties@^0.34.0: + version "0.34.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.34.0.tgz#ccd7e9f4388302231b3f174a8b1d5b1f7b576cea" + integrity sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ== + +language-subtag-registry@^0.3.20: + version "0.3.23" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" + integrity sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ== + +language-tags@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" + integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== + dependencies: + language-subtag-registry "^0.3.20" + +launch-editor@^2.6.1: + version "2.8.0" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.8.0.tgz#7255d90bdba414448e2138faa770a74f28451305" + integrity sha512-vJranOAJrI/llyWGRQqiDM+adrw+k83fvmmx3+nV47g3+36xM15jE+zyZ6Ffel02+xSvuM0b2GDRosXZkbb6wA== + dependencies: + picocolors "^1.0.0" + shell-quote "^1.8.1" + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== + +lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +loupe@^2.3.6, loupe@^2.3.7: + version "2.3.7" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" + integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== + dependencies: + get-func-name "^2.0.1" + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lz-string@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" + integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== + +magic-string@0.30.8: + version "0.30.8" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.8.tgz#14e8624246d2bedba70d5462aa99ac9681844613" + integrity sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + +magic-string@^0.30.5: + version "0.30.10" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e" + integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + +make-dir@^2.0.0, make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-dir@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" + +make-error@1.x: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +map-or-similar@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/map-or-similar/-/map-or-similar-1.5.0.tgz#6de2653174adfb5d9edc33c69d3e92a1b76faf08" + integrity sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg== + +markdown-to-jsx@^7.4.5: + version "7.4.7" + resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.4.7.tgz#740ee7ec933865ef5cc683a0992797685a75e2ee" + integrity sha512-0+ls1IQZdU6cwM1yu0ZjjiVWYtkbExSyUIFU2ZeDIFuZM1W42Mh4OlJ4nb4apX4H8smxDHRdFaoIVJGwfv5hkg== + +mathml-tag-names@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" + integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== + +mdn-data@2.0.30: + version "2.0.30" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" + integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^3.4.1, memfs@^3.4.12: + version "3.6.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" + integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== + dependencies: + fs-monkey "^1.0.4" + +memfs@^4.6.0: + version "4.9.3" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.9.3.tgz#41a3218065fe3911d9eba836250c8f4e43f816bc" + integrity sha512-bsYSSnirtYTWi1+OPMFb0M048evMKyUYe0EbtuGQgq6BVQM1g1W8/KIUJCCvjgI/El0j6Q4WsmMiBwLUBSw8LA== + dependencies: + "@jsonjoy.com/json-pack" "^1.0.3" + "@jsonjoy.com/util" "^1.1.2" + tree-dump "^1.0.1" + tslib "^2.0.0" + +memoizerific@^1.11.3: + version "1.11.3" + resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a" + integrity sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog== + dependencies: + map-or-similar "^1.5.0" + +meow@^13.2.0: + version "13.2.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-13.2.0.tgz#6b7d63f913f984063b3cc261b6e8800c4cd3474f" + integrity sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.0, micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" + integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + +min-indent@^1.0.0, min-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^8.0.2: + version "8.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229" + integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^4.2.4: + version "4.2.8" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" + integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mlly@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.1.tgz#e0336429bb0731b6a8e887b438cbdae522c8f32f" + integrity sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA== + dependencies: + acorn "^8.11.3" + pathe "^1.1.2" + pkg-types "^1.1.1" + ufo "^1.5.3" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.5.0, neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +node-abort-controller@^3.0.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" + integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ== + +node-dir@^0.1.17: + version "0.1.17" + resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5" + integrity sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg== + dependencies: + minimatch "^3.0.2" + +node-fetch-native@^1.6.3: + version "1.6.4" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.4.tgz#679fc8fd8111266d47d7e72c379f1bed9acff06e" + integrity sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ== + +node-fetch@^2.6.7: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +npm-run-path@^5.1.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f" + integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ== + dependencies: + path-key "^4.0.0" + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +nwsapi@^2.2.2: + version "2.2.12" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.12.tgz#fb6af5c0ec35b27b4581eb3bbad34ec9e5c696f8" + integrity sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w== + +nypm@^0.3.8: + version "0.3.9" + resolved "https://registry.yarnpkg.com/nypm/-/nypm-0.3.9.tgz#ab74c55075737466847611aa33c3c67741c01d8f" + integrity sha512-BI2SdqqTHg2d4wJh8P9A1W+bslg33vOE9IZDY6eR2QC+Pu1iNBVZUqczrd43rJb+fMzHU7ltAYKsEFY/kHMFcw== + dependencies: + citty "^0.1.6" + consola "^3.2.3" + execa "^8.0.1" + pathe "^1.1.2" + pkg-types "^1.1.1" + ufo "^1.5.3" + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.4, object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.entries@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" + integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +object.fromentries@^2.0.7, object.fromentries@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.groupby@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + +object.hasown@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc" + integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg== + dependencies: + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.values@^1.1.6, object.values@^1.1.7, object.values@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" + integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +objectorarray@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/objectorarray/-/objectorarray-1.0.5.tgz#2c05248bbefabd8f43ad13b41085951aac5e68a5" + integrity sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg== + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +ohash@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.1.3.tgz#f12c3c50bfe7271ce3fd1097d42568122ccdcf07" + integrity sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw== + +on-finished@2.4.1, on-finished@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.0, onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + +open@^10.0.3: + version "10.1.0" + resolved "https://registry.yarnpkg.com/open/-/open-10.1.0.tgz#a7795e6e5d519abe4286d9937bb24b51122598e1" + integrity sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw== + dependencies: + default-browser "^5.2.1" + define-lazy-prop "^3.0.0" + is-inside-container "^1.0.0" + is-wsl "^3.1.0" + +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + 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" + +ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2, p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-retry@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-6.2.0.tgz#8d6df01af298750009691ce2f9b3ad2d5968f3bd" + integrity sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA== + dependencies: + "@types/retry" "0.12.2" + is-network-error "^1.0.0" + retry "^0.13.1" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== + +param-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" + integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.0.0, parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + 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" + +parse5@^7.0.0, parse5@^7.1.1: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.11.1, path-scurry@^1.6.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +path-type@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-5.0.0.tgz#14b01ed7aea7ddf9c7c3f46181d4d04f9c785bb8" + integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg== + +pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== + +picocolors@^1.0.0, picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pirates@^4.0.4, pirates@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + +pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pkg-types@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.1.3.tgz#161bb1242b21daf7795036803f28e30222e476e3" + integrity sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA== + dependencies: + confbox "^0.1.7" + mlly "^1.7.1" + pathe "^1.1.2" + +polished@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/polished/-/polished-4.3.1.tgz#5a00ae32715609f83d89f6f31d0f0261c6170548" + integrity sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA== + dependencies: + "@babel/runtime" "^7.17.8" + +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + +postcss-modules-extract-imports@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" + integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== + +postcss-modules-local-by-default@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz#f1b9bd757a8edf4d8556e8d0f4f894260e3df78f" + integrity sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz#a43d28289a169ce2c15c00c4e64c0858e43457d5" + integrity sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-resolve-nested-selector@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.4.tgz#0068767902fb40f0e6cd7b24faee4fa4bc14a5da" + integrity sha512-R6vHqZWgVnTAPq0C+xjyHfEZqfIYboCBVSy24MjxEDm+tIh1BU4O6o7DP7AA7kHzf136d+Qc5duI4tlpHjixDw== + +postcss-safe-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-7.0.0.tgz#6273d4e5149e286db5a45bc6cf6eafcad464014a" + integrity sha512-ovehqRNVCpuFzbXoTb4qLtyzK3xn3t/CUBxOs8LsnQjQrShaB4lKiHoVqY8ANaC0hBMHq5QVWk77rwGklFUDrg== + +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz#49694cb4e7c649299fea510a29fa6577104bcf53" + integrity sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-selector-parser@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz#5be94b277b8955904476a2400260002ce6c56e38" + integrity sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-sorting@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/postcss-sorting/-/postcss-sorting-8.0.2.tgz#6393385ece272baf74bee9820fb1b58098e4eeca" + integrity sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q== + +postcss-styled-syntax@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/postcss-styled-syntax/-/postcss-styled-syntax-0.6.4.tgz#16c07d2c55261a435031b6a9c7f1d5c2daa2246c" + integrity sha512-uWiLn+9rKgIghUYmTHvXMR6MnyPULMe9Gv3bV537Fg4FH6CA6cn21WMjKss2Qb98LUhT847tKfnRGG3FhSOgUQ== + dependencies: + typescript "^5.3.3" + +postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@8.4.38: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" + +postcss@^8.4.32, postcss@^8.4.40: + version "8.4.40" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.40.tgz#eb81f2a4dd7668ed869a6db25999e02e9ad909d8" + integrity sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.1" + source-map-js "^1.2.0" + +postcss@^8.4.33: + version "8.4.39" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.39.tgz#aa3c94998b61d3a9c259efa51db4b392e1bde0e3" + integrity sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.1" + source-map-js "^1.2.0" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@^3.1.1: + version "3.3.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" + integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== + +pretty-error@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" + integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw== + dependencies: + lodash "^4.17.20" + renderkid "^3.0.0" + +pretty-format@^27.0.2: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + +pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +prompts@^2.0.1, prompts@^2.4.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +prop-types@^15.7.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +psl@^1.1.33: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +pure-rand@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" + integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +qs@^6.11.2: + version "6.12.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.3.tgz#e43ce03c8521b9c7fd7f1f13e514e5ca37727754" + integrity sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ== + dependencies: + side-channel "^1.0.6" + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +react-colorful@^5.1.2: + version "5.6.1" + resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b" + integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw== + +react-confetti@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-confetti/-/react-confetti-6.1.0.tgz#03dc4340d955acd10b174dbf301f374a06e29ce6" + integrity sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw== + dependencies: + tween-functions "^1.2.0" + +react-docgen-typescript@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz#4611055e569edc071204aadb20e1c93e1ab1659c" + integrity sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg== + +react-docgen@^7.0.0: + version "7.0.3" + resolved "https://registry.yarnpkg.com/react-docgen/-/react-docgen-7.0.3.tgz#f811b785f07b1f2023cb899b6bcf9d522b21b95d" + integrity sha512-i8aF1nyKInZnANZ4uZrH49qn1paRgBZ7wZiCNBMnenlPzEv0mRl+ShpTVEI6wZNl8sSc79xZkivtgLKQArcanQ== + dependencies: + "@babel/core" "^7.18.9" + "@babel/traverse" "^7.18.9" + "@babel/types" "^7.18.9" + "@types/babel__core" "^7.18.0" + "@types/babel__traverse" "^7.18.0" + "@types/doctrine" "^0.0.9" + "@types/resolve" "^1.20.2" + doctrine "^3.0.0" + resolve "^1.22.1" + strip-indent "^4.0.0" + +"react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", react-dom@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.2" + +react-element-to-jsx-string@^15.0.0: + version "15.0.0" + resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz#1cafd5b6ad41946ffc8755e254da3fc752a01ac6" + integrity sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ== + dependencies: + "@base2/pretty-print-object" "1.0.1" + is-plain-object "5.0.0" + react-is "18.1.0" + +react-ga4@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/react-ga4/-/react-ga4-2.1.0.tgz#56601f59d95c08466ebd6edfbf8dede55c4678f9" + integrity sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ== + +react-icons@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.3.0.tgz#ccad07a30aebd40a89f8cfa7d82e466019203f1c" + integrity sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg== + +react-is@18.1.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" + integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== + +react-is@^16.13.1, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-is@^18.0.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + +react-router-dom@^6.25.1: + version "6.25.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.25.1.tgz#b89f8d63fc8383ea4e89c44bf31c5843e1f7afa0" + integrity sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ== + dependencies: + "@remix-run/router" "1.18.0" + react-router "6.25.1" + +react-router@6.25.1: + version "6.25.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.25.1.tgz#70b4f1af79954cfcfd23f6ddf5c883e8c904203e" + integrity sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw== + dependencies: + "@remix-run/router" "1.18.0" + +"react@^16.8.0 || ^17.0.0 || ^18.0.0", react@^18.3.1: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + +readable-stream@^2.0.1: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6, readable-stream@^3.4.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +recast@^0.23.3, recast@^0.23.5: + version "0.23.9" + resolved "https://registry.yarnpkg.com/recast/-/recast-0.23.9.tgz#587c5d3a77c2cfcb0c18ccce6da4361528c2587b" + integrity sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q== + dependencies: + ast-types "^0.16.1" + esprima "~4.0.0" + source-map "~0.6.1" + tiny-invariant "^1.3.3" + tslib "^2.0.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + +reflect.getprototypeof@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" + integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.1" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + which-builtin-type "^1.1.3" + +regenerate-unicode-properties@^10.1.0: + version "10.1.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" + integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +regenerator-transform@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" + integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== + dependencies: + "@babel/runtime" "^7.8.4" + +regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + dependencies: + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" + +regexpu-core@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" + integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== + dependencies: + "@babel/regjsgen" "^0.8.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +regjsparser@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" + integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== + dependencies: + jsesc "~0.5.0" + +rehype-external-links@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/rehype-external-links/-/rehype-external-links-3.0.0.tgz#2b28b5cda1932f83f045b6f80a3e1b15f168c6f6" + integrity sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw== + dependencies: + "@types/hast" "^3.0.0" + "@ungap/structured-clone" "^1.0.0" + hast-util-is-element "^3.0.0" + is-absolute-url "^4.0.0" + space-separated-tokens "^2.0.0" + unist-util-visit "^5.0.0" + +rehype-slug@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/rehype-slug/-/rehype-slug-6.0.0.tgz#1d21cf7fc8a83ef874d873c15e6adaee6344eaf1" + integrity sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A== + dependencies: + "@types/hast" "^3.0.0" + github-slugger "^2.0.0" + hast-util-heading-rank "^3.0.0" + hast-util-to-string "^3.0.0" + unist-util-visit "^5.0.0" + +relateurl@^0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== + +renderkid@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/renderkid/-/renderkid-3.0.0.tgz#5fd823e4d6951d37358ecc9a58b1f06836b6268a" + integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg== + dependencies: + css-select "^4.1.3" + dom-converter "^0.2.0" + htmlparser2 "^6.1.0" + lodash "^4.17.21" + strip-ansi "^6.0.1" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requireindex@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef" + integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + +resolve.exports@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" + integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== + +resolve@^1.14.2, resolve@^1.20.0, resolve@^1.22.1, resolve@^1.22.4, resolve@^1.22.8: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^2.0.0-next.5: + version "2.0.0-next.5" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rimraf@^5.0.5: + version "5.0.9" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.9.tgz#c3baa1b886eadc2ec7981a06a593c3d01134ffe9" + integrity sha512-3i7b8OcswU6CpU8Ej89quJD4O98id7TtVM5U4Mybh84zQXdrFmDLouWBEEaD/QfO3gDDfH+AGFCGsR7kngzQnA== + dependencies: + glob "^10.3.7" + +rimraf@~2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +run-applescript@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.0.0.tgz#e5a553c2bffd620e169d276c1cd8f1b64778fbeb" + integrity sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== + dependencies: + xmlchars "^2.2.0" + +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" + +schema-utils@^3.1.1, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0, schema-utils@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" + integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + +semver@^5.6.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.3.4, semver@^7.5.4, semver@^7.6.0: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + +semver@^7.3.5, semver@^7.3.7, semver@^7.5.3: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + 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" + +set-function-name@^2.0.1, set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shallowequal@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" + integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + +side-channel@^1.0.4, side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1, signal-exit@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slash@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-5.1.0.tgz#be3adddcdf09ac38eebe8dcdc7b1a57a75b095ce" + integrity sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg== + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +source-map-js@^1.0.1, source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-support@^0.5.16, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stack-utils@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +stop-iteration-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" + integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + dependencies: + internal-slot "^1.0.4" + +storybook@^8.2.4: + version "8.2.4" + resolved "https://registry.yarnpkg.com/storybook/-/storybook-8.2.4.tgz#ad7bd6ad8e531782d33608e0258a82db8d897976" + integrity sha512-ASavW8vIHiWpFY+4M6ngeqK5oL4OkxqdpmQYxvRqH0gA1G1hfq/vmDw4YC4GnqKwyWPQh2kaV5JFurKZVaeaDQ== + dependencies: + "@babel/core" "^7.24.4" + "@babel/types" "^7.24.0" + "@storybook/codemod" "8.2.4" + "@storybook/core" "8.2.4" + "@types/semver" "^7.3.4" + "@yarnpkg/fslib" "2.10.3" + "@yarnpkg/libzip" "2.3.0" + chalk "^4.1.0" + commander "^6.2.1" + cross-spawn "^7.0.3" + detect-indent "^6.1.0" + envinfo "^7.7.3" + execa "^5.0.0" + fd-package-json "^1.2.0" + find-up "^5.0.0" + fs-extra "^11.1.0" + giget "^1.0.0" + globby "^14.0.1" + jscodeshift "^0.15.1" + leven "^3.1.0" + ora "^5.4.1" + prettier "^3.1.1" + prompts "^2.4.0" + semver "^7.3.7" + strip-json-comments "^3.0.1" + tempy "^3.1.0" + tiny-invariant "^1.3.1" + ts-dedent "^2.0.0" + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.includes@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz#8986d57aee66d5460c144620a6d873778ad7289f" + integrity sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.matchall@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" + integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== + 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" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + regexp.prototype.flags "^1.5.2" + set-function-name "^2.0.2" + side-channel "^1.0.6" + +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1, strip-ansi@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +strip-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-4.0.0.tgz#b41379433dd06f5eae805e21d631e07ee670d853" + integrity sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA== + dependencies: + min-indent "^1.0.1" + +strip-json-comments@^3.0.1, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +style-loader@^3.3.1: + version "3.3.4" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-3.3.4.tgz#f30f786c36db03a45cbd55b6a70d930c479090e7" + integrity sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w== + +style-loader@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-4.0.0.tgz#0ea96e468f43c69600011e0589cb05c44f3b17a5" + integrity sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA== + +styled-components@^6.1.12: + version "6.1.12" + resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.12.tgz#0d9d511aacfb9052936146dcc2856559e6fae4df" + integrity sha512-n/O4PzRPhbYI0k1vKKayfti3C/IGcPf+DqcrOB7O/ab9x4u/zjqraneT5N45+sIe87cxrCApXM8Bna7NYxwoTA== + dependencies: + "@emotion/is-prop-valid" "1.2.2" + "@emotion/unitless" "0.8.1" + "@types/stylis" "4.2.5" + css-to-react-native "3.2.0" + csstype "3.1.3" + postcss "8.4.38" + shallowequal "1.1.0" + stylis "4.3.2" + tslib "2.6.2" + +stylelint-config-recommended@^14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/stylelint-config-recommended/-/stylelint-config-recommended-14.0.1.tgz#d25e86409aaf79ee6c6085c2c14b33c7e23c90c6" + integrity sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg== + +stylelint-config-standard@^36.0.1: + version "36.0.1" + resolved "https://registry.yarnpkg.com/stylelint-config-standard/-/stylelint-config-standard-36.0.1.tgz#727cbb2a1ef3e210f5ce8329cde531129f156609" + integrity sha512-8aX8mTzJ6cuO8mmD5yon61CWuIM4UD8Q5aBcWKGSf6kg+EC3uhB+iOywpTK4ca6ZL7B49en8yanOFtUW0qNzyw== + dependencies: + stylelint-config-recommended "^14.0.1" + +stylelint-order@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/stylelint-order/-/stylelint-order-6.0.4.tgz#3e80d876c61a98d2640de181433686f24284748b" + integrity sha512-0UuKo4+s1hgQ/uAxlYU4h0o0HS4NiQDud0NAUNI0aa8FJdmYHA5ZZTFHiV5FpmE3071e9pZx5j0QpVJW5zOCUA== + dependencies: + postcss "^8.4.32" + postcss-sorting "^8.0.2" + +stylelint@^16.8.1: + version "16.8.1" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-16.8.1.tgz#7d4b2d7922771dd0514446a66f04e954f1dfa444" + integrity sha512-O8aDyfdODSDNz/B3gW2HQ+8kv8pfhSu7ZR7xskQ93+vI6FhKKGUJMQ03Ydu+w3OvXXE0/u4hWU4hCPNOyld+OA== + dependencies: + "@csstools/css-parser-algorithms" "^2.7.1" + "@csstools/css-tokenizer" "^2.4.1" + "@csstools/media-query-list-parser" "^2.1.13" + "@csstools/selector-specificity" "^3.1.1" + "@dual-bundle/import-meta-resolve" "^4.1.0" + balanced-match "^2.0.0" + colord "^2.9.3" + cosmiconfig "^9.0.0" + css-functions-list "^3.2.2" + css-tree "^2.3.1" + debug "^4.3.6" + fast-glob "^3.3.2" + fastest-levenshtein "^1.0.16" + file-entry-cache "^9.0.0" + global-modules "^2.0.0" + globby "^11.1.0" + globjoin "^0.1.4" + html-tags "^3.3.1" + ignore "^5.3.1" + imurmurhash "^0.1.4" + is-plain-object "^5.0.0" + known-css-properties "^0.34.0" + mathml-tag-names "^2.1.3" + meow "^13.2.0" + micromatch "^4.0.7" + normalize-path "^3.0.0" + picocolors "^1.0.1" + postcss "^8.4.40" + postcss-resolve-nested-selector "^0.1.4" + postcss-safe-parser "^7.0.0" + postcss-selector-parser "^6.1.1" + postcss-value-parser "^4.2.0" + resolve-from "^5.0.0" + string-width "^4.2.3" + strip-ansi "^7.1.0" + supports-hyperlinks "^3.0.0" + svg-tags "^1.0.0" + table "^6.8.2" + write-file-atomic "^5.0.1" + +stylis@4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.3.2.tgz#8f76b70777dd53eb669c6f58c997bf0a9972e444" + integrity sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0, supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-hyperlinks@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz#c711352a5c89070779b4dad54c05a2f14b15c94b" + integrity sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svg-tags@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" + integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA== + +swc-loader@^0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/swc-loader/-/swc-loader-0.2.6.tgz#bf0cba8eeff34bb19620ead81d1277fefaec6bc8" + integrity sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg== + dependencies: + "@swc/counter" "^0.1.3" + +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +synckit@^0.8.6: + version "0.8.8" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.8.8.tgz#fe7fe446518e3d3d49f5e429f443cf08b6edfcd7" + integrity sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ== + dependencies: + "@pkgr/core" "^0.1.0" + tslib "^2.6.2" + +table@^6.8.2: + version "6.8.2" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.2.tgz#c5504ccf201213fa227248bdc8c5569716ac6c58" + integrity sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA== + dependencies: + ajv "^8.0.1" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + +tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +tar@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +telejson@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/telejson/-/telejson-7.2.0.tgz#3994f6c9a8f8d7f2dba9be2c7c5bbb447e876f32" + integrity sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ== + dependencies: + memoizerific "^1.11.3" + +temp-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-3.0.0.tgz#7f147b42ee41234cc6ba3138cd8e8aa2302acffa" + integrity sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw== + +temp@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.4.tgz#8c97a33a4770072e0a05f919396c7665a7dd59f2" + integrity sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg== + dependencies: + rimraf "~2.6.2" + +tempy@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/tempy/-/tempy-3.1.0.tgz#00958b6df85db8589cb595465e691852aac038e9" + integrity sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g== + dependencies: + is-stream "^3.0.0" + temp-dir "^3.0.0" + type-fest "^2.12.2" + unique-string "^3.0.0" + +terser-webpack-plugin@^5.3.1, terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.20" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.26.0" + +terser@^5.10.0, terser@^5.26.0: + version "5.31.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.2.tgz#b5ca188107b706084dca82f988089fa6102eba11" + integrity sha512-LGyRZVFm/QElZHy/CPr/O4eNZOZIzsrQ92y4v9UJe/pFJjypje2yI3C2FmPtvUEnhadlSbmG2nXtdcjHOjCfxw== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +thingies@^1.20.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/thingies/-/thingies-1.21.0.tgz#e80fbe58fd6fdaaab8fad9b67bd0a5c943c445c1" + integrity sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g== + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +tiny-invariant@^1.3.1, tiny-invariant@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + +tinyspy@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.2.1.tgz#117b2342f1f38a0dbdcc73a50a454883adf861d1" + integrity sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A== + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tough-cookie@^4.1.2: + version "4.1.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36" + integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + +tr46@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9" + integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== + dependencies: + punycode "^2.1.1" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +tree-dump@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.2.tgz#c460d5921caeb197bde71d0e9a7b479848c5b8ac" + integrity sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ== + +ts-api-utils@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== + +ts-dedent@^2.0.0, ts-dedent@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ts-dedent/-/ts-dedent-2.2.0.tgz#39e4bd297cd036292ae2394eb3412be63f563bb5" + integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ== + +ts-jest@^29.2.3: + version "29.2.3" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.2.3.tgz#3d226ac36b8b820151a38f164414f9f6b412131f" + integrity sha512-yCcfVdiBFngVz9/keHin9EnsrQtQtEu3nRykNy9RVp+FiPFFbPJ3Sg6Qg4+TkmH0vMP5qsTKgXSsk80HRwvdgQ== + dependencies: + bs-logger "0.x" + ejs "^3.1.10" + fast-json-stable-stringify "2.x" + jest-util "^29.0.0" + json5 "^2.2.3" + lodash.memoize "4.x" + make-error "1.x" + semver "^7.5.3" + yargs-parser "^21.0.1" + +ts-loader@^9.5.1: + version "9.5.1" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.5.1.tgz#63d5912a86312f1fbe32cef0859fb8b2193d9b89" + integrity sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.0.0" + micromatch "^4.0.0" + semver "^7.3.4" + source-map "^0.7.4" + +tsconfig-paths-webpack-plugin@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz#3c6892c5e7319c146eee1e7302ed9e6f2be4f763" + integrity sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA== + dependencies: + chalk "^4.1.0" + enhanced-resolve "^5.7.0" + tsconfig-paths "^4.1.2" + +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tsconfig-paths@^4.1.2, tsconfig-paths@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + +tslib@^1.13.0, tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +tween-functions@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff" + integrity sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^1.0.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== + +type-fest@^2.12.2, type-fest@^2.19.0, type-fest@~2.19: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + +typescript@^5.3.3: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== + +typescript@^5.5.3: + version "5.5.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa" + integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ== + +ufo@^1.5.3: + version "1.5.4" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754" + integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ== + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" + integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +unicorn-magic@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.1.0.tgz#1bb9a51c823aaf9d73a8bfcd3d1a23dde94b0ce4" + integrity sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ== + +unique-string@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-3.0.0.tgz#84a1c377aff5fd7a8bc6b55d8244b2bd90d75b9a" + integrity sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ== + dependencies: + crypto-random-string "^4.0.0" + +unist-util-is@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.0.tgz#b775956486aff107a9ded971d996c173374be424" + integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-visit-parents@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz#4d5f85755c3b8f0dc69e21eca5d6d82d22162815" + integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-visit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + +universal-user-agent@^7.0.0, universal-user-agent@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-7.0.2.tgz#52e7d0e9b3dc4df06cc33cb2b9fd79041a54827e" + integrity sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q== + +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +unplugin@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.0.1.tgz#83b528b981cdcea1cad422a12cd02e695195ef3f" + integrity sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA== + dependencies: + acorn "^8.8.1" + chokidar "^3.5.3" + webpack-sources "^3.2.3" + webpack-virtual-modules "^0.5.0" + +unplugin@^1.3.1: + version "1.11.0" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.11.0.tgz#09237b4011075e65c8f4d0ae06e221dee12750e3" + integrity sha512-3r7VWZ/webh0SGgJScpWl2/MRCZK5d3ZYFcNaeci/GQ7Teop7zf0Nl2pUuz7G21BwPd9pcUPOC5KmJ2L3WgC5g== + dependencies: + acorn "^8.11.3" + chokidar "^3.6.0" + webpack-sources "^3.2.3" + webpack-virtual-modules "^0.6.1" + +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== + dependencies: + escalade "^3.1.2" + picocolors "^1.0.1" + +uri-js@^4.2.2, uri-js@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +url@^0.11.0: + version "0.11.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.3.tgz#6f495f4b935de40ce4a0a52faee8954244f3d3ad" + integrity sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw== + dependencies: + punycode "^1.4.1" + qs "^6.11.2" + +use-sync-external-store@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +util@^0.12.4: + version "0.12.5" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" + integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + which-typed-array "^1.1.2" + +utila@~0.4: + version "0.4.0" + resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" + integrity sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +uuid@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + +v8-to-istanbul@^9.0.1: + version "9.3.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" + integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^2.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +w3c-xmlserializer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz#aebdc84920d806222936e3cdce408e32488a3073" + integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw== + dependencies: + xml-name-validator "^4.0.0" + +walk-up-path@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/walk-up-path/-/walk-up-path-3.0.1.tgz#c8d78d5375b4966c717eb17ada73dbd41490e886" + integrity sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA== + +walker@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +watchpack@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.1.tgz#29308f2cac150fa8e4c92f90e0ec954a9fed7fff" + integrity sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== + dependencies: + defaults "^1.0.3" + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +webidl-conversions@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a" + integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== + +webpack-cli@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.1.4.tgz#c8e046ba7eaae4911d7e71e2b25b776fcc35759b" + integrity sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^2.1.1" + "@webpack-cli/info" "^2.0.2" + "@webpack-cli/serve" "^2.0.5" + colorette "^2.0.14" + commander "^10.0.1" + cross-spawn "^7.0.3" + envinfo "^7.7.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^3.1.1" + rechoir "^0.8.0" + webpack-merge "^5.7.3" + +webpack-dev-middleware@^6.1.2: + version "6.1.3" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-6.1.3.tgz#79f4103f8c898564c9e96c3a9c2422de50f249bc" + integrity sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw== + dependencies: + colorette "^2.0.10" + memfs "^3.4.12" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-middleware@^7.1.0: + version "7.2.1" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-7.2.1.tgz#2af00538b6e4eda05f5afdd5d711dbebc05958f7" + integrity sha512-hRLz+jPQXo999Nx9fXVdKlg/aehsw1ajA9skAneGmT03xwmyuhvF93p6HUKKbWhXdcERtGTzUCtIQr+2IQegrA== + dependencies: + colorette "^2.0.10" + memfs "^4.6.0" + mime-types "^2.1.31" + on-finished "^2.4.1" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz#cb6ea47ff796b9251ec49a94f24a425e12e3c9b8" + integrity sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA== + dependencies: + "@types/bonjour" "^3.5.13" + "@types/connect-history-api-fallback" "^1.5.4" + "@types/express" "^4.17.21" + "@types/serve-index" "^1.9.4" + "@types/serve-static" "^1.15.5" + "@types/sockjs" "^0.3.36" + "@types/ws" "^8.5.10" + ansi-html-community "^0.0.8" + bonjour-service "^1.2.1" + chokidar "^3.6.0" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + default-gateway "^6.0.3" + express "^4.17.3" + graceful-fs "^4.2.6" + html-entities "^2.4.0" + http-proxy-middleware "^2.0.3" + ipaddr.js "^2.1.0" + launch-editor "^2.6.1" + open "^10.0.3" + p-retry "^6.2.0" + rimraf "^5.0.5" + schema-utils "^4.2.0" + selfsigned "^2.4.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^7.1.0" + ws "^8.16.0" + +webpack-hot-middleware@^2.25.1: + version "2.26.1" + resolved "https://registry.yarnpkg.com/webpack-hot-middleware/-/webpack-hot-middleware-2.26.1.tgz#87214f1e3f9f3acab9271fef9e6ed7b637d719c0" + integrity sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A== + dependencies: + ansi-html-community "0.0.8" + html-entities "^2.1.0" + strip-ansi "^6.0.0" + +webpack-merge@^5.7.3: + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + +webpack-merge@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-6.0.1.tgz#50c776868e080574725abc5869bd6e4ef0a16c6a" + integrity sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.1" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack-virtual-modules@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz#362f14738a56dae107937ab98ea7062e8bdd3b6c" + integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw== + +webpack-virtual-modules@^0.6.0, webpack-virtual-modules@^0.6.1: + version "0.6.2" + resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8" + integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ== + +webpack@5: + version "5.93.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.93.0.tgz#2e89ec7035579bdfba9760d26c63ac5c3462a5e5" + integrity sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" + acorn "^8.7.1" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.0" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + +webpack@^5.92.1: + version "5.92.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.92.1.tgz#eca5c1725b9e189cffbd86e8b6c3c7400efc5788" + integrity sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" + acorn "^8.7.1" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.0" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +whatwg-encoding@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" + integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== + dependencies: + iconv-lite "0.6.3" + +whatwg-mimetype@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7" + integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== + +whatwg-url@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-11.0.0.tgz#0a849eebb5faf2119b901bb76fd795c2848d4018" + integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== + dependencies: + tr46 "^3.0.0" + webidl-conversions "^7.0.0" + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-builtin-type@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.3.tgz#b1b8443707cc58b6e9bf98d32110ff0c2cbd029b" + integrity sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw== + dependencies: + function.prototype.name "^1.1.5" + has-tostringtag "^1.0.0" + is-async-function "^2.0.0" + is-date-object "^1.0.5" + is-finalizationregistry "^1.0.2" + is-generator-function "^1.0.10" + is-regex "^1.1.4" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.9" + +which-collection@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + +which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.2, which-typed-array@^1.1.9: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + +which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0, wildcard@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^2.3.0: + version "2.4.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" + integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +write-file-atomic@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz#68df4717c55c6fa4281a7860b4c2ba0a6d2b11e7" + integrity sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^4.0.1" + +ws@^8.11.0, ws@^8.16.0, ws@^8.2.3: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + +xml-name-validator@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" + integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yargs-parser@^21.0.1, yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.3.1: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zustand@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.4.tgz#63abdd81edfb190bc61e0bbae045cc4d52158a05" + integrity sha512-/BPMyLKJPtFEvVL0E9E9BTUM63MNyhPGlvxk1XjrfWTUlV+BR8jufjsovHzrtR6YNcBEcL7cMHovL1n9xHawEg== + dependencies: + use-sync-external-store "1.2.0"