diff --git a/README.md b/README.md new file mode 100644 index 0000000..389f6a5 --- /dev/null +++ b/README.md @@ -0,0 +1,491 @@ +# BooQuiz - 실시간 대규모 퀴즈 플랫폼 + +[팀 노션](https://www.notion.so/BooQuiz-127f1897cdf5809c8a44d54384683bc6?pvs=21) | [백로그](https://github.com/orgs/boostcampwm-2024/projects/11) | [그라운드 룰](https://github.com/boostcampwm-2024/web08-BooQuiz/wiki/%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9C-%EB%A3%B0) | [기획서](https://www.notion.so/12cf1897cdf5801487a3dc1438627a99?pvs=21) | [figma](https://www.figma.com/design/1CdBFnF3oWXgAzRdgEhRNU/Web08?node-id=0-1&t=bfZtQb8UJrKIcfTK-1) | [개발위키](https://www.notion.so/12cf1897cdf58093bf0afe75f24401d7?pvs=21) + +## 📝 프로젝트 소개 + +다수의 사용자가 로그인 없이 함께 참여할 수 있는 실시간 퀴즈 플랫폼 BooQuiz 입니다. + +## 🚀 프로젝트 목표 + +> 초기 소규모 서비스에서 시작해 점진적 확장과 기술적 완성도를 함께 추구함 + +- **점진적 개발:** 1명 → 10명 → 300명 사용자로 확장하며 매주 완성된 서비스를 배포. +- **변화 대응:** 주기적인 리팩토링, 아키텍처 조정을 통해 유연성 유지. +- **협업 강화:** 페어 프로그래밍을 통한 적극적인 협업. +- **피드백 기반:** 사용자 피드백을 주기적으로 반영. + +## 핵심 기능 + +### 🎯 입장 코드를 통한 간편한 퀴즈 참여 + +![입장코드 입력하고 접속 가능한 거 보여주기](https://github.com/user-attachments/assets/c71ef9a1-c49c-44bb-a7cc-d2ad618ad88a) + +### ⚡ 300명 이상 동시 접속 지원 + +![300명 접속 테스트](https://github.com/user-attachments/assets/751c481b-1129-422d-aa6c-821b320f7ed2) +![300명 접속 채팅 테스트 ](https://github.com/user-attachments/assets/3c0207cf-3886-4769-aae3-d2b05169c44b) + +### 📊 실시간 퀴즈 진행 및 채팅 기능 + +![다수 사용자 채팅 잘 되는 모습](https://github.com/user-attachments/assets/ccbea755-d385-4fa6-a737-b134ed06e7b4) +![방장이 퀴즈 시작 후에 퀴즈 진행 되는 모습](https://github.com/user-attachments/assets/3d032a19-a494-4773-9988-39fa1349ce8b) + +### 📈 퀴즈존 별 최종 결과 확인 + +![결과 페이지에서 채팅 작동하고 소켓이 끊기면 페이지 이탈 알럿창 뜸](https://github.com/user-attachments/assets/78696d4b-4c95-42a5-8733-83a5a3ff1c66) +![결과 페이지에서 메인페이지로 나가기](https://github.com/user-attachments/assets/71f9bcc2-2e49-4236-b4fd-94ff0c16e5f5) + +### 🎮 원하는 퀴즈를 직접 만들기 + +![퀴즈셋 검색후 생성하기](https://github.com/user-attachments/assets/c89f63b1-61fd-4e70-8d9b-e1b993d426ea) +![퀴즈존 직접 생성하기](https://github.com/user-attachments/assets/94349037-49f3-43ab-ba7a-385eb675ccad) + +## 🛠 기술 스택 + +| 영역 | 기술 스택 | +| ------------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **공통** | ![TypeScript](https://img.shields.io/badge/TypeScript-5.6.3-3178C6?style=flat-square&logo=typescript&logoColor=white) ![WebSocket](https://img.shields.io/badge/WebSocket-8.18.0-010101?style=flat-square&logo=socket.io&logoColor=white) ![TSDoc](https://img.shields.io/badge/TSDoc-0.26.11-3178C6?style=flat-square&logo=typescript&logoColor=white) | +| **Frontend** | ![React](https://img.shields.io/badge/React-18.3.1-61DAFB?style=flat-square&logo=react&logoColor=white) ![Vite](https://img.shields.io/badge/Vite-5.4.10-646CFF?style=flat-square&logo=vite&logoColor=white) ![Tailwind CSS](https://img.shields.io/badge/Tailwind%20CSS-3.4-38B2AC?style=flat-square&logo=tailwind-css&logoColor=white) ![shadcn/ui](https://img.shields.io/badge/shadcn%2Fui-latest-000000?style=flat-square) ![Vitest](https://img.shields.io/badge/Vitest-latest-6E9F18?style=flat-square&logo=vitest&logoColor=white) ![Testing Library](https://img.shields.io/badge/Testing%20Library-latest-E33332?style=flat-square&logo=testing-library&logoColor=white) ![Storybook](https://img.shields.io/badge/Storybook-8.4.2-FF4785?style=flat-square&logo=storybook&logoColor=white) | +| **Backend** | ![NestJS](https://img.shields.io/badge/NestJS-10.4.7-E0234E?style=flat-square&logo=nestjs&logoColor=white) ![MySQL](https://img.shields.io/badge/MySQL-2-4479A1?style=flat-square&logo=mysql&logoColor=white) ![SQLite](https://img.shields.io/badge/SQLite-3-003B57?style=flat-square&logo=sqlite&logoColor=white) ![TypeORM](https://img.shields.io/badge/TypeORM-0.3.20-E93524?style=flat-square&logo=typeorm&logoColor=white) ![Swagger](https://img.shields.io/badge/Swagger-8.0.5-85EA2D?style=flat-square&logo=swagger&logoColor=black) ![Jest](https://img.shields.io/badge/Jest-Testing-C21325?style=flat-square&logo=jest&logoColor=white) ![SuperTest](https://img.shields.io/badge/SuperTest-Testing-009688?style=flat-square&logo=testing-library&logoColor=white) | +| **인프라** | ![NCP](https://img.shields.io/badge/Naver%20Cloud%20Platform-latest-03C75A?style=flat-square&logo=naver&logoColor=white) ![Nginx](https://img.shields.io/badge/Nginx-1.24.0-009639?style=flat-square&logo=nginx&logoColor=white) ![GitHub Actions](https://img.shields.io/badge/GitHub%20Actions-3.0-2088FF?style=flat-square&logo=github-actions&logoColor=white) ![Docker](https://img.shields.io/badge/Docker-24.0.7-2496ED?style=flat-square&logo=docker&logoColor=white) ![Linux](https://img.shields.io/badge/Linux-Ubuntu%2022.04-FCC624?style=flat-square&logo=linux&logoColor=black) ![Grafana](https://img.shields.io/badge/Grafana-10.2.3-F46800?style=flat-square&logo=grafana&logoColor=white) ![Prometheus](https://img.shields.io/badge/Prometheus-2.43.0-E6522C?style=flat-square&logo=prometheus&logoColor=white) ![k6](https://img.shields.io/badge/k6-0.55.0-7D64FF?style=flat-square&logo=k6&logoColor=white)| +| **협업 도구** | ![Notion](https://img.shields.io/badge/Notion-2.0.41-000000?style=flat-square&logo=notion&logoColor=white) ![Figma](https://img.shields.io/badge/Figma-latest-F24E1E?style=flat-square&logo=figma&logoColor=white) ![Excalidraw](https://img.shields.io/badge/Excalidraw-latest-6965DB?style=flat-square&logo=excalidraw&logoColor=white) ![Zoom](https://img.shields.io/badge/Zoom-5.17.0-2D8CFF?style=flat-square&logo=zoom&logoColor=white) ![Git](https://img.shields.io/badge/Git-2.42.0-F05032?style=flat-square&logo=git&logoColor=white) ![GitHub](https://img.shields.io/badge/GitHub-latest-181717?style=flat-square&logo=github&logoColor=white) ![GitHub Projects](https://img.shields.io/badge/GitHub%20Projects-latest-181717?style=flat-square&logo=github&logoColor=white) | + +## 🏗 시스템 아키텍처 + +![system-architecture](https://github.com/user-attachments/assets/ba41c87f-fb55-438f-a122-296abf58e355) + +## 📅 개발 타임라인 + +```mermaid +timeline + title 전체 타임라인 + section 0주차 + 프로젝트 기획 단계: 🎯 팀 빌딩 및 아이디어 도출 : 🔄 서비스 기능 구체화 : 🛠 기술 스택 및 개발 프로세스 확정 + section 1주차 + 프로젝트 기반 구축: 🏗 모노레포 환경 구성 : 📝 기술 문서화 : 🔧 개발 컨벤션 정립 + section 2주차 + 기반 기능 구현: 🎨 UI/컴포넌트 라이브러리 구축 : 💻 퀴즈존 모듈 설계 : 🔌 WebSocket 기본 연결 구현 + section 3주차 + 단일 사용자 기능: 🏗 퀴즈 진행 상태관리 구현(FE) : ⚡ 퀴즈 진행/채점 로직 : 🚀 CI/CD 및 Docker 구성 + section 4주차 + 다중 사용자 지원: 📦 퀴즈 진행 상태관리 리팩토링 : 🔌 WS 메시지 큐 구현 : 👥 동시접속 및 권한 관리 + section 5주차 + 시스템 고도화: ⚡ WebSocket 최적화 : 💾 DB 연동 : 📊 실시간 순위 시스템 : 🎨 UI/UX 개선 + +``` + +### 0-1주차: 프로젝트 기획 및 팀 빌딩 (10/22 ~ 10/27) + +```mermaid +timeline + title 0-1주차 (10/21 - 11/02) + section 0주차 - 팀 빌딩 (10/22 ~ 10/27) + 10/22(일) : 팀원 자기소개 및 역량 공유 + : 프로젝트 아이디어 브레인스토밍 + : BooQuiz(도전골든벨) 초기 아이디어 도출 + + section 아이디어 구체화 (10/24) + 10/24(화) : BooQuiz 서비스 세부 기능 논의 + : 실시간 참여형 퀴즈 플랫폼 컨셉 확정 + : 트래픽 테스트를 위한 방 인원 조절 기능 구상 + : 부스트캠프 테마 퀴즈 콘텐츠 기획 + + section 기술 스택 및 규칙 확정 (10/27) + 10/27(금) : 🔄 기술 스택 최종 확정 + : FE - React / BE - NestJS + : 🔧 개발 프로세스 설정 + : Git 브랜치 전략 수립 (main-dev-feat) + : PR 리뷰 프로세스 확립, 테스트 코드 작성 원칙 + : 🔄 프로젝트 그라운드 룰 설정 + section 프로젝트 기반 구축 (10/28 ~ 10/30) + 10/28: 프로젝트 초기화 + : 모노레포 설정 + 10/30: 저녁 팀 멘토링 + section 문서화 및 규칙 (10/31) + 10/31: README 작성 + : 기술 스택 선정 문서화 + section 개발 환경 설정 (11/01 ~ 11/02) + 11/01: 1주차 데모데이 + 11/02: 이슈/PR 템플릿 추가 + : 개발 컨벤션 정립 +``` + +> 짧은 개발 기간 동안, 실시간 통신 더불어 [팀원의 개인 목표](https://github.com/boostcampwm-2024/web08-BooQuiz/wiki/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B0%9C%EC%9D%B8-%EB%AA%A9%ED%91%9C)에 도전해볼 수 있는 프로젝트 기획 +> 팀의 주차별 목표 설정을 통해 안정적인 진행과 팀원 동기 부여에 기여. + +- 팀원들과 첫 만남에서 각자의 역량과 관심사를 공유하며 프로젝트 방향성 논의. +- 브레인스토밍을 통해 실시간 참여형 퀴즈 플랫폼 아이디어 최종 선정. +- 주차별 현실적인 목표를 설정하여 단계적으로 기능을 확장. +- 매주 데모데이를 통해 실제 배포를 진행하며 프로젝트 발전. + +#### 주차 별 배포 목표 설정 + +- [마일스톤](https://www.notion.so/130f1897cdf58077bda6da265bd6e55b?pvs=21) +- [0순위 태스크 목록](https://www.notion.so/0-777fbc00409f4a9683882b2f810245b2?pvs=21) +- [Github Project 백로그](https://github.com/orgs/boostcampwm-2024/projects/11) + +#### 개발 환경 구축 + +- **모노레포 구조 도입** + - pnpm workspace 적용 +- **코드 품질 관리** + - ESLint, Prettier, Git hook +- **협업 프로세스 확립** + - 브랜치 보호 규칙 설정, PR 템플릿 도입 +- **프로젝트 개발 환경 구축** + - [우리팀이 React를 선택한 이유](https://www.notion.so/React-131f1897cdf5801bb3e5fc02f8c1257b?pvs=21) + +### 2주차: 기반 기능 개발 (11/03 - 11/09) + +```mermaid +timeline + title 2주차 (11/03 - 11/09) + section 기반 기능 구현 + 11/03(일) : 🎨 Frontend/기반 : shadcn UI & Tailwind 설정 + : 🔧 DevOps/기반 : Husky & Commitlint 설정, Prettier 설정 + : 💻 Backend/기반 : 퀴즈존 모듈/리포지토리 인터페이스 설계 + section 컴포넌트 개발 + 11/04(월) : 🎨 Frontend/기반 + : Button, Typography, CommonInput 컴포넌트 + : ContentBox, Layout 컴포넌트 구현 + : 💻 Backend/기반 + : 퀴즈존 메모리 저장소 구현 + : 세션/쿠키 설정 + : 저녁 현우님 개인 멘토링 + 11/05(화) : 🎨 Frontend/기반 + : Storybook 설정 및 컴포넌트 스토리 작성 + : 💻 Backend/기반 + : 컨트롤러/서비스 레이어 테스트 코드 + : 👥 저녁 준현, 현민, 선빈 개인 멘토링 + section WebSocket 구현 + 11/06(수) - 11/07(목) : 💻 Backend/기능 + : WebSocket 게이트웨이 구현 + : 기본 연결 및 이벤트 핸들링 구현 + : SuperWSTest 기반 WS 테스트 + : 🎨 Frontend/기능: Alert Dialog, TextCopy 컴포넌트 추가 + : 🔄 2주차 통합 + : 공통 컴포넌트 라이브러리 완성 + : WebSocket 기본 연결 테스트 + 11/08(금) : 📊 2주차 데모데이 + 11/09(토) : 🧘 2주차 모각글 + +``` + +- 개발 환경과 프로세스 안정화, 핵심 기능 구현 기반 마련. +- 페어 프로그래밍 도입으로 지식 공유와 기술적 성장 촉진 향상 촉진. + +#### 핵심 구현 내용 + +1. **프론트엔드** + - 공통 컴포넌트 라이브러리 구축을 통한 재사용 가능한 UI 패턴 정립 + - Storybook을 통한 컴포넌트 문서화 +2. **백엔드** + - TDD 기반 퀴즈존 모듈 개발 + - WebSocket 게이트웨이 구현 + - 단위 테스트 작성 및 검증 + +#### 고민 사항 + +- [우리는 Vite, StoryBook, Tsdoc을 왜 사용할까?](https://www.notion.so/Vite-StoryBook-Tsdoc-139f1897cdf58069ac4cf71699be8336?pvs=21) +- [우리팀이 WS를 선택한 이유](https://www.notion.so/WS-aed2e304596f49a08c9aeadc670a2d05?pvs=21) +- [Nest 의존성 주입](https://www.notion.so/4286fc95e41246ffa6a530d3fe250618?pvs=21) + +### 3주차: 단일 사용자 기능 구현 (11/10 - 11/16) + +```mermaid +timeline + title 3주차 (11/10 - 11/16) + section 퀴즈존 기본 구현 + 11/10-11/11: 🎨 QuizZoneManager 초기 상태관리 구현 + : 퀴즈존 생성 페이지 퍼널 구조 구현 + : 💻 실시간 퀴즈 진행 이벤트 처리 + section UI/UX 구현 + 11/11-11/12: 🎨 Timer 컴포넌트 개선 + : Progress Bar 애니메이션 구현 + : 💻 답안 제출 및 채점 로직 + : 🔧 Winston 로거 및 CI/CD 구축 + section 기능 개선 + 11/13-11/14: 🎨 실시간 점수/랭킹 표시 + : 퀴즈 결과 화면 개선 + : 💻 API 문서화 (Swagger/TSDoc) + : 🔧 staging 환경 구축 + section 통합 및 테스트 + 11/14: 💻 Docker 컨테이너 설정 + : 환경변수 분리 + : 🔄 3주차 통합 + : 실시간 퀴즈 진행 테스트 + : 단일 사용자 퀴즈 풀이 검증 + section 데모 및 회고 + 11/15: 📊 3주차 데모데이 + 11/16: 🧘 3주차 모각글 + +``` + +- 퍼널 패턴을 통한 퀴즈 진행 단계별 상태 관리 시스템 구축 +- CI/CD 파이프라인 구성을 통한 지속적인 배포 환경 마련 + +#### 핵심 구현 내용 + +1. **프론트엔드** + - 퍼널 패턴 도입 + - 실시간 타이머 구현 + - 테스트 코드 환경 구축 +2. **백엔드** + - 상태와 이벤트를 기반으로 한 퀴즈 진행 처리 구현 +3. **DevOps** + - Docker, GItHub Actions를 통한 CI/CD 환경 구축 + +#### 고민 사항 + +- [토스의 퍼널 패턴을 퀴즈 상태관리에 적용할 수 있을까?](https://jacky0831.tistory.com/110) +- [퍼널 패턴으로 퀴즈존 상태관리 하기](https://jacky0831.tistory.com/113) +- [📚 테스트 코드는 왜 작성해야 할까요? 특히 프론트엔드에서?](https://www.notion.so/140f1897cdf580f68690db6027e73019?pvs=21) +- [SSL 인증서 발급 및 HTTPS 적용하기](https://www.notion.so/Github-Action-Nginx-69760997c9a64ef289626725db4b29cc?pvs=21) +- [Github Action으로 Nginx에 정적 파일 배포하기](https://www.notion.so/Github-Action-Nginx-69760997c9a64ef289626725db4b29cc?pvs=21) +- [github page를 이용하여 정적 페이지 배포](https://www.notion.so/a61a7ca1daec4e28b9b17834366f410a?pvs=21) +- [Github Action으로 도커 이미지 Web Server 배포하기](https://www.notion.so/04e804ab730e46c1af24786363c3a6c8?pvs=21) +- [웹소켓 + 리버스 프록시(엔진엑스)](https://www.notion.so/2325bd00d5b04718b295857ff2dbfcef?pvs=21) + +### 4주차: 다중 사용자 지원 (11/17 - 11/23) + +```mermaid +timeline + title 4주차 (11/17 - 11/23) + section 초기 구현 + 11/17-11/20: 🎨 useTimer, useQuizZone 훅 분리 + : useWebSocket 커스텀 훅 구현 + : 💻 10인 동시접속 처리 로직 + : 참여자 상태 관리 개선 + : 👤 저녁 동현님 개인 멘토링 + section 상태 관리 + 11/17-11/21: 🎨 퀴즈존 타입 시스템 정의 + : 상태 머신 기반 이벤트 처리 + : 💻 퀴즈존 리포지토리 리팩토링 + : 방장 기반 권한 시스템 + : 방장-참여자 권한 테스트 + section 테스트 및 개선 + 11/20-11/21: 🎨 useTimer/useQuizZone 테스트 + : 커넥션 비정상 종료 처리 + : 💻 퀴즈 정답 제출 로깅 + : 다중 사용자 연결 관리 개선 + : 👥 저녁 현민님, 선빈님 멘토링(11/20) + : 👥 저녁 현우님, 준현님 멘토링(11/21) + section 시스템 안정화 + 11/20-11/21: 🎨 WebSocket 메시지 큐 구현 + : base64 인코딩/디코딩 처리 + : 💻 동시성 처리 검증 + : 🔧 Docker 배포 환경 구축 + : 🔄 4주차 통합 및 테스트 + : 다중 사용자 퀴즈 진행 테스트 + section 데모 및 회고 + 11/22: 📊 4주차 데모데이 + 11/23: 🧘 4주차 모각글 +``` + +- 단일 사용자 기능을 다중 사용자 환경으로 확장. +- 백엔드 리펙토링을 통한 실시간 상태 동기화와 메시지 처리 안정성 확보 +- 커스텀 훅 재설계로 코드 재사용성 향상. + +#### 핵심 구현 내용 + +1. **프론트엔드** + - 퀴즈존 상태 관리 커스텀 훅 리팩토링 + - 웹소켓 연결 실시간 동기화 개선 + - 방장/ 참여자 권한 시스템 구현 +2. **백엔드** + - 다중 사용자 퀴즈 진행 처리를 위한 기능 및 구조 확장 + - 사용자 재연결 처리 로직 구현 +3. **DevOps** + - 배포 안전성 확보를 위한 Staging 환경 구축 + +#### 고민 사항 + +- [테스트를 위한 스테이징 환경 구축](https://www.notion.so/a4e628e3a2664f10bc5ccd3915e02436?pvs=21) +- [useReudcer를 통한 퀴즈 상태관리 커스텀 훅 리팩토링](https://www.notion.so/useReducer-147f1897cdf5805fa3d2fde57bcee23e?pvs=21) +- [다중 사용자 기능 확장 설계](https://www.notion.so/6b3677b8bc4a4272ba1d47e759e57630?pvs=21) +- [퀴즈존 상태관리 커스텀 훅을 리팩토링 과정을 돌아보며](https://www.notion.so/151f1897cdf580b39b57d5ee305ab501?pvs=21) +- [Claude 씨와 리펙토링에 대해서 정량적 평가 해보기](https://www.notion.so/Claude-151f1897cdf580c098e5d492a799aab2?pvs=21) +- [모놀리식에 Redis가 필요할까?](https://www.notion.so/Redis-2c032b5d95094655b7f9943c1b6d0ba8?pvs=21) +- [API 서버와 socket 서버의 아키텍처적 분리](https://www.notion.so/API-socket-2c6981d5b7b34559a31f58da6d261a5e?pvs=21) +- [웹소켓 서버 스케일아웃](https://www.notion.so/a0ab4fe9e8d84245a83057b878a6afe0?pvs=21) +- [Nest Logger](https://www.notion.so/1e26632868ef4916af8e112d69d4c518?pvs=21) + +### 5주차: 시스템 안정화와 서비스 사용성 개선(11/24 ~ 11/27) + +```mermaid +timeline + title 5주차 (11/24 ~ 11/28) + section 웹소켓 최적화 + 11/24-11/25: 💻 WebSocket 커스터마이징 + : play gateway 리팩토링 + : 엔티티 분리 및 연결 로직 개선 + section 채팅 기능 구현 + 11/25-11/26: 💻 채팅 모듈 추가 + : 채팅 서비스 통합 + : 퀴즈 제출/진행 처리 개선 + section UI/UX 개선 + 11/26-11/27: 🎨 퀴즈존 입장 처리 에러 핸들링 + : 유효성 검사 문구 및 글자 수 제한 추가 + : 결과 페이지 레이아웃 개선 + : 👥 팀 멘토링 + section DB 연동 및 통합 + 11/27-11/28: 💾 채팅 서비스 종료 처리 구현 + : 💻 퀴즈존 컨트롤러 채팅 서비스 통합 + : 🔄 충돌 UI 수정 및 테스트 + section 데모 및 회고 + 11/28: 📊 5주차 데모데이 + 11/28: 🧘 5주차 모각글 +``` + +- 성능 확인을 위한 부하테스트 +- 실시간 채팅과 순위 시스템 추가로 서비스 완성도 향상. + +#### 핵심 구현 내용 + +1. **프론트엔드** + - 퀴즈존 입장 및 진행 과정에서의 에러 핸들링 + - 채팅 UI 구현 및 전체 레이아웃 개선 + - 퀴즈 제출 페이지별 순위 UI 구현 +2. **백엔드** + - WsAdapter 커스터마이징 + - 퀴즈 진행 중 사용자 상호 작용을 위한 기능 추가 + - 문제별 선착순 제출 순위 산정 구현 + - 퀴즈 목록 관리를 위한 RDB 적용 + - 채팅 이벤트 처리 기능 구현 +3. **인프라** + - Production, Staging MySQL 환경 구축 + - 실시간 모니터링을 위한 WebHook 연동 + +#### 고민 사항 + +- [ErrorBoundary와 Suspense를 통해 에러 처리하기](https://www.notion.so/ErrorBoundary-Suspense-151f1897cdf580b9ac63eb4425c60e75?pvs=21) +- [리액트 Memoization을 통한 최적화를 하려면?](https://www.notion.so/Memoization-143f1897cdf580c5a1aac108af1a2658?pvs=21) +- [부하 테스트 시도](https://www.notion.so/c89e88bb9e4b4792bbc9acd310f6bbd4?pvs=21) +- [NCP 서버 모니터링 with Slack](https://www.notion.so/b003b1f7f8c647d18514d0d75035c5ad?pvs=21) +- [WsAdapter에 세션 적용하기](https://www.notion.so/ws-d71bf00734b44c28b91853606d14097c?pvs=21) +- [서버 클라이언트 시간 동기화](https://www.notion.so/257f72a54c6542f0adaaaca679841ab8?pvs=21) + +### 6주차 최종 데모 준비 + +```mermaid +timeline + title 6주차 (11/29 ~ 12/03) + section 마무리 작업 + 11/29-11/30: 🔄 리드미 및 문서 업데이트 + : 아키텍처, 기능 설명 추가 + : 로고 및 UI 개선 + section 사용자 피드백 반영 + 12/01-12/02: 🎨 채팅 결과 페이지 개선 + : 퀴즈존 생성 페이지 유효성 검증 강화 + : 인터페이스 디자인 개선 + : WebSocket 재연결 로직 강화 + section 최종 코드 리팩토링 + 12/02-12/03: 🔧 채팅 서비스 및 메모리 레포지토리 개선 + : 퀴즈존 입장 처리 에러 핸들링 수정 + : UI 충돌 이슈 해결 + section 최종 안정화 + 12/03: 💻 웹소켓 재연결 처리 보완 + : 메시지 큐 시스템 안정화 + : UI 버그 수정 + : 📊 최종 데모 준비 +``` + +#### 핵심 개선 내용 + +1. **프론트엔드** + - 재연결 처리 로직 개선 + - 웹소켓 종료 시나리오 처리 보완 + - 퀴즈존 생성 시 유효성 검증 강화 + - UI/UX 개선 및 채팅,결과 페이지 레이아웃 최적화 + - 에러 메시지 및 안내 문구 개선 +2. **백엔드** + - 채팅 서비스 아키텍처 개선 + - 메모리 레포지토리 CRUD 로직 최적화 + - 퀴즈존 컨트롤러 에러 핸들링 강화 + - 선착순 제출을 고려한 최종 순위 산정 + +## 🚀 프로젝트 시작하기 + +1. 레포지토리 클론 + +```bash +git clone https://github.com/boostcampwm-2024/web08-BooQuiz.git +``` + +2. 패키지 매니저 설치 (pnpm 사용) 및 패키지 다운로드 + +```bash +npm install -g pnpm + +pnpm install +``` + +3. Frontend 환경 변수 추가 + +```bash +echo -e "VITE_API_URL=http://localhost:3000\nVITE_WS_URL=ws://localhost:3000" > /apps/frontend/.env.development +``` + +2. 프로젝트 실행하기(개발 환경) + +```bash +pnpm run start +``` + +## 📚 프로젝트 구조 + +``` +/ +├── frontend/ # 프론트엔드 애플리케이션 +│ ├── src/ +│ │ ├── blocks/ # 페이지별 주요 컴포넌트 +│ │ ├── components/ # 재사용 가능한 컴포넌트 +│ │ ├── hook/ # 커스텀 훅 +│ │ └── pages/ # 페이지 컴포넌트 +│ └── ... +│ +├── backend/ # 백엔드 애플리케이션 +│ ├── src/ +│ │ ├── common/ # 공통 유틸리티 +│ │ ├── core/ # 핵심 기능 +│ │ ├── quiz-zone/ # 퀴즈존 상태 정보 관리 +│ │ ├── quiz/ # 퀴즈 CRUD +│ │ └── play/ # 실시간 퀴즈 관리 +│ └── ... +└── ... +``` + +## 팀 소개 + +| [J004 강준현](https://github.com/JunhyunKang) | [J074 김현우](https://github.com/krokerdile) | [J086 도선빈](https://github.com/typingmistake) | [J175 이동현](https://github.com/codemario318) | [J217 전현민](https://github.com/joyjhm) | +| --------------------------------------------------------- | --------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------- | +| ![](https://avatars.githubusercontent.com/u/72436328?v=4) | ![](https://avatars.githubusercontent.com/u/39644976?v=4) | ![](https://avatars.githubusercontent.com/u/102957984?v=4) | ![](https://avatars.githubusercontent.com/u/130330767?v=4) | ![](https://avatars.githubusercontent.com/u/77275989?v=4) | + +## Git Branch 전략 + +- GitLab flow를 차용하여 브랜치를 관리하였습니다. + +```mermaid +gitGraph + commit + branch develop + checkout develop + commit + branch "feature/#issue" + checkout "feature/#issue" + commit + commit + checkout develop + merge "feature/#issue" + checkout main + merge develop + +``` + +## 🤝 기여하기 + +1. 이슈 생성 또는 기존 이슈 확인 +2. feature/[기능명] 브랜치 생성 +3. 개발 및 테스트 완료 +4. PR 생성 및 리뷰 요청 diff --git a/apps/backend/.dockerignore b/apps/backend/.dockerignore new file mode 100644 index 0000000..79e6ea4 --- /dev/null +++ b/apps/backend/.dockerignore @@ -0,0 +1,2 @@ +node_modules +test \ No newline at end of file diff --git a/apps/backend/.eslintrc.js b/apps/backend/.eslintrc.js new file mode 100644 index 0000000..259de13 --- /dev/null +++ b/apps/backend/.eslintrc.js @@ -0,0 +1,25 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + sourceType: 'module', + }, + plugins: ['@typescript-eslint/eslint-plugin'], + extends: [ + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: ['.eslintrc.js'], + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + }, +}; diff --git a/apps/backend/.gitignore b/apps/backend/.gitignore new file mode 100644 index 0000000..d20f7b6 --- /dev/null +++ b/apps/backend/.gitignore @@ -0,0 +1,61 @@ +# compiled output +/dist +/node_modules +/build + +:booquiz +:memory + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# temp directory +.temp +.tmp + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +:booquiz \ No newline at end of file diff --git a/apps/backend/Dockerfile b/apps/backend/Dockerfile new file mode 100644 index 0000000..5b443d1 --- /dev/null +++ b/apps/backend/Dockerfile @@ -0,0 +1,64 @@ +# 빌드 단계 +FROM node:20-alpine AS builder +WORKDIR /app + +# pnpm 전역 설치 +RUN npm install -g pnpm + +# 의존성 파일들 복사 +COPY pnpm-lock.yaml pnpm-workspace.yaml ./ +COPY apps/backend/package.json ./apps/backend/ +COPY apps/backend/tsconfig*.json ./apps/backend + +# backend 의존성 설치 (락파일 검사 무시) +RUN pnpm install -C apps/backend --no-frozen-lockfile + +# 소스 코드 복사 및 빌드 실행 +COPY apps/backend ./apps/backend +# 수정: 디렉토리를 이동한 후 빌드 실행 +WORKDIR /app/apps/backend +RUN pnpm run build + +# 프로덕션 단계 +FROM node:20-alpine AS production +WORKDIR /app + +# 빌드 도구 설치 및 환경 변수 설정 +RUN apk add --no-cache build-base +ENV PNPM_HOME="/root/.local/share/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + + +# 프로덕션 환경용 pnpm 설치 +RUN npm install -g pnpm +RUN pnpm add -g pm2 + +# 빌더 단계에서 필요한 파일들만 복사 +COPY --from=builder /app/apps/backend/dist ./dist +COPY --from=builder /app/apps/backend/package.json ./ +COPY pnpm-lock.yaml pnpm-workspace.yaml ./ +# 프로덕션 실행에 필요한 의존성만 설치 (devDependencies 제외) +# --no-frozen-lockfile: 락파일 버전 제약을 완화하여 호환되는 최신 버전 허용 +RUN pnpm install --no-frozen-lockfile + +ARG NODE_ENV +ARG SESSION_SECRET +ARG MYSQL_HOST +ARG DB_PORT +ARG DB_USERNAME +ARG DB_PASSWORD +ARG DB_DATABASE +ARG DB_SYNCHRONIZE + +ENV NODE_ENV=$NODE_ENV +ENV SESSION_SECRET=$SESSION_SECRET +ENV MYSQL_HOST=$MYSQL_HOST +ENV DB_PORT=$DB_PORT +ENV DB_USERNAME=$DB_USERNAME +ENV DB_PASSWORD=$DB_PASSWORD +ENV DB_DATABASE=$DB_DATABASE +ENV DB_SYNCHRONIZE=$DB_SYNCHRONIZE + +# 포트 설정 및 앱 실행 +EXPOSE 3000 +CMD ["pm2-runtime", "start", "dist/src/main.js"] \ No newline at end of file diff --git a/apps/backend/README.md b/apps/backend/README.md new file mode 100644 index 0000000..6d1f3cc --- /dev/null +++ b/apps/backend/README.md @@ -0,0 +1,201 @@ +# BooQuiz Backend + +실시간 대규모 참여형 퀴즈 플랫폼 BooQuiz의 백엔드 레포지토리입니다. + +## 🎯 프로젝트 개요 + +BooQuiz 백엔드는 NestJS 기반의 서버로, 실시간 퀴즈 진행을 위한 WebSocket 통신과 퀴즈 관리를 위한 RESTful API를 제공합니다. + +### 주요 기능 + +- WebSocket 기반 실시간 퀴즈 진행 +- 퀴즈/퀴즈셋 CRUD +- 실시간 답안 제출 및 채점 +- 세션 기반 퀴즈존 관리 +- 300명 이상 동시 접속 지원 + +## 🛠 기술 스택 + +### 핵심 기술 + +- **Framework:** NestJS 10.0.0 +- **Language:** TypeScript 5.1.3 +- **Runtime:** Node.js +- **WebSocket:** ws 8.18.0, @nestjs/websockets +- **Database:** + - MySQL 2 (Production) + - SQLite3 (Development) + - TypeORM + +### 주요 라이브러리 + +- **설정 관리:** @nestjs/config +- **API 문서화:** @nestjs/swagger 8.0.5 +- **ORM:** + - TypeORM 0.3.20 + - typeorm-transactional +- **Validation:** + - class-validator + - class-transformer +- **로깅:** + - winston + - nest-winston + +### 개발 도구 + +- **테스트:** + - Jest + - supertest + - superwstest +- **코드 품질:** + - ESLint + - Prettier +- **빌드 도구:** @nestjs/cli + +## 🏗 프로젝트 구조 + +``` +src/ +├── common/ # 공통 유틸리티 및 상수 +├── core/ # 핵심 기능 (WebSocket 어댑터 등) +├── logger/ # 로깅 설정 +├── play/ # 실시간 퀴즈 진행 관련 +│ ├── dto/ # 데이터 전송 객체 +│ ├── entities/ # 엔티티 정의 +│ └── gateway/ # WebSocket 게이트웨이 +├── quiz/ # 퀴즈 관리 +│ ├── dto/ # 데이터 전송 객체 +│ ├── entity/ # 엔티티 정의 +│ └── repository/ # 리포지토리 계층 +└── quiz-zone/ # 퀴즈존 관리 + ├── dto/ # 데이터 전송 객체 + ├── entities/ # 엔티티 정의 + └── repository/ # 리포지토리 계층 +``` + +## 🚀 시작하기 + +1. 프로젝트 클론 + +```bash +git clone https://github.com/boostcampwm-2024/web08-BooQuiz.git +``` + +2. pnpm 설치 (없는 경우) + +```bash +npm install -g pnpm +``` + +3. 의존성 설치 + +```bash +pnpm install +``` + +4. 환경 변수 설정 + +```bash +cp .env.example .env +``` + +5. 개발 서버 실행 + +```bash +pnpm start:dev +``` + +## 📦 스크립트 + +```bash +# 개발 서버 실행 +pnpm start:dev + +# 프로덕션 빌드 +pnpm build + +# 프로덕션 실행 +pnpm start:prod + +# 테스트 실행 +pnpm test # 단위 테스트 +pnpm test:e2e # E2E 테스트 +pnpm test:cov # 테스트 커버리지 + +# 린트 및 포맷팅 +pnpm lint +pnpm format +``` + +## 📡 API 문서 + +### REST API + +- Swagger UI: `http://localhost:3000/api` +- API 문서: `http://localhost:3000/api-json` + +### WebSocket 이벤트 + +#### 클라이언트 → 서버 + +- `join`: 퀴즈존 입장 +- `start`: 퀴즈 시작 +- `submit`: 답안 제출 +- `leave`: 퀴즈존 퇴장 + +#### 서버 → 클라이언트 + +- `nextQuiz`: 다음 문제 정보 +- `result`: 제출 결과 +- `summary`: 최종 결과 +- `finish`: 퀴즈 종료 + +## 🧪 테스트 + +프로젝트는 Jest를 사용하여 단위 테스트와 E2E 테스트를 지원합니다. + +```bash +# 단위 테스트 +pnpm test + +# E2E 테스트 +pnpm test:e2e + +# 특정 파일 테스트 +pnpm test src/quiz/quiz.service.spec.ts +``` + +## 📝 개발 가이드라인 + +### 아키텍처 + +- 계층형 아키텍처 (Controller → Service → Repository) +- 도메인 주도 설계 원칙 준수 +- SOLID 원칙 적용 + +### 코드 스타일 + +- ESLint/Prettier 설정 준수 +- NestJS 코딩 컨벤션 따르기 +- 명확한 타입 정의 + +### WebSocket 통신 + +- 세션 기반 연결 관리 +- 실시간 이벤트 처리 +- 연결 끊김 대응 + +## 🔐 보안 고려사항 + +- 세션 기반 인증 +- WebSocket 연결 검증 +- 입력값 검증 +- SQL Injection 방지 +- Rate Limiting + +## 🤝 기여하기 + +1. 이슈 생성 또는 기존 이슈 확인 +2. feature/[기능명] 브랜치 생성 +3. 개발 및 테스트 완료 +4. PR 생성 및 리뷰 요청 diff --git a/apps/backend/config/database.config.ts b/apps/backend/config/database.config.ts new file mode 100644 index 0000000..8e1ef16 --- /dev/null +++ b/apps/backend/config/database.config.ts @@ -0,0 +1,8 @@ +export default () => ({ + host: process.env.MYSQL_HOST, + db_port: parseInt(process.env.DB_PORT, 10), + db_username: process.env.DB_USERNAME, + db_password: process.env.DB_PASSWORD, + db_database: process.env.DB_DATABASE, + db_synchronize: process.env.DB_SYNCHRONIZE === 'true', +}); diff --git a/apps/backend/config/http.config.ts b/apps/backend/config/http.config.ts new file mode 100644 index 0000000..fbd2bd5 --- /dev/null +++ b/apps/backend/config/http.config.ts @@ -0,0 +1,11 @@ +export enum Environment { + Development = 'DEV', + Staging = 'STG', + Production = 'PROD', +} + +export default () => ({ + env: process.env.NODE_ENV || Environment.Development, + port: parseInt(process.env.PORT) || 3000, + sessionSecret: process.env.SESSION_SECRET || 'development-session-secret', +}); diff --git a/apps/backend/config/typeorm.config.ts b/apps/backend/config/typeorm.config.ts new file mode 100644 index 0000000..3ebfc85 --- /dev/null +++ b/apps/backend/config/typeorm.config.ts @@ -0,0 +1,38 @@ +import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm'; +import { Quiz } from '../src/quiz/entity/quiz.entitiy'; +import { QuizSet } from '../src/quiz/entity/quiz-set.entity'; +import { ConfigService } from '@nestjs/config'; +import { Environment } from './http.config'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class TypeormConfig implements TypeOrmOptionsFactory { + constructor(private readonly configService: ConfigService) {} + + createTypeOrmOptions(): TypeOrmModuleOptions { + const env = this.configService.get('env'); + + if(env === 'DEV') { + + return { + type: 'sqlite', + database: ':booquiz', + entities: [Quiz, QuizSet], + synchronize: true, + logging: ['query'], + } + } + + return { + type: 'mysql', + host: this.configService.get('host'), + port: this.configService.get('db_port'), + username: this.configService.get('db_username'), + password: this.configService.get('db_password'), + database: this.configService.get('db_database'), + entities: [Quiz, QuizSet], + synchronize: this.configService.get('db_synchronize', true), + logging: ['query'], + }; + } +} diff --git a/apps/backend/nest-cli.json b/apps/backend/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/apps/backend/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/apps/backend/package.json b/apps/backend/package.json new file mode 100644 index 0000000..0e75b45 --- /dev/null +++ b/apps/backend/package.json @@ -0,0 +1,94 @@ +{ + "name": "backend", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "pm2-runtime start node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "dependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.3.0", + "@nestjs/core": "^10.0.0", + "@nestjs/mapped-types": "*", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-ws": "^10.4.7", + "@nestjs/swagger": "^8.0.5", + "@nestjs/typeorm": "^10.0.2", + "@nestjs/websockets": "^10.4.7", + "backend": "file:", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "cookie": "^1.0.1", + "cookie-parser": "^1.4.7", + "express-session": "^1.18.1", + "get-port": "^7.1.0", + "mysql2": "^3.11.4", + "nest-winston": "^1.9.7", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "socket.io-client": "^4.8.1", + "sqlite3": "^5.1.7", + "superwstest": "^2.0.4", + "typeorm": "^0.3.20", + "typeorm-transactional": "^0.5.0", + "winston": "^3.17.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@nestjs/cli": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@types/cookie-parser": "^1.4.7", + "@types/express": "^5.0.0", + "@types/express-session": "^1.18.0", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.1", + "@types/supertest": "^6.0.0", + "@types/ws": "^8.5.13", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "eslint": "^9.15.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.5.0", + "prettier": "^3.0.0", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/apps/backend/src/app.controller.spec.ts b/apps/backend/src/app.controller.spec.ts new file mode 100644 index 0000000..ccd8274 --- /dev/null +++ b/apps/backend/src/app.controller.spec.ts @@ -0,0 +1,34 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; + +describe('AppController', () => { + let appController: AppController; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + controllers: [AppController], + providers: [ + AppService, + { + provide: 'winston', + useValue: { + log: jest.fn(), + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }, + }, + ], + }).compile(); + + appController = app.get(AppController); + }); + + describe('root', () => { + it('should return "Hello World!"', () => { + expect(appController.getHello()).toBe('Hello World!'); + }); + }); +}); diff --git a/apps/backend/src/app.controller.ts b/apps/backend/src/app.controller.ts new file mode 100644 index 0000000..cce879e --- /dev/null +++ b/apps/backend/src/app.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from '@nestjs/common'; +import { AppService } from './app.service'; + +@Controller() +export class AppController { + constructor(private readonly appService: AppService) {} + + @Get() + getHello(): string { + return this.appService.getHello(); + } +} diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts new file mode 100644 index 0000000..89a91b9 --- /dev/null +++ b/apps/backend/src/app.module.ts @@ -0,0 +1,55 @@ +import { MiddlewareConsumer, Module, NestModule, ValidationPipe } from '@nestjs/common'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; +import { QuizZoneModule } from './quiz-zone/quiz-zone.module'; +import { PlayModule } from './play/play.module'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import httpConfig from '../config/http.config'; +import { APP_PIPE } from '@nestjs/core'; +import { WinstonModule } from 'nest-winston'; +import { winstonConfig } from './logger/winston.config'; +import { HttpLoggingMiddleware } from './logger/http-logger.middleware'; +import databaseConfig from '../config/database.config'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { TypeormConfig } from '../config/typeorm.config'; +import { DataSource } from 'typeorm'; +import { addTransactionalDataSource } from 'typeorm-transactional'; +import { QuizModule } from './quiz/quiz.module'; +import { ChatModule } from './chat/chat.module'; + +@Module({ + imports: [ + QuizZoneModule, + PlayModule, + ConfigModule.forRoot({ + load: [httpConfig, databaseConfig], + isGlobal: true, + }), + WinstonModule.forRoot(winstonConfig), + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useClass: TypeormConfig, + dataSourceFactory: async (options) => { + const dataSource = new DataSource(options); + await dataSource.initialize(); + return addTransactionalDataSource(dataSource); + }, + }), + QuizModule, + ChatModule, + ], + controllers: [AppController], + providers: [ + AppService, + { + provide: APP_PIPE, + useClass: ValidationPipe, + }, + ], +}) +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(HttpLoggingMiddleware).forRoutes('*'); + } +} diff --git a/apps/backend/src/app.service.ts b/apps/backend/src/app.service.ts new file mode 100644 index 0000000..755c06a --- /dev/null +++ b/apps/backend/src/app.service.ts @@ -0,0 +1,12 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; + +@Injectable() +export class AppService { + constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {} + + getHello(): string { + this.logger.error('HELL'); + return 'Hello World!'; + } +} diff --git a/apps/backend/src/chat/chat.controller.spec.ts b/apps/backend/src/chat/chat.controller.spec.ts new file mode 100644 index 0000000..bf05213 --- /dev/null +++ b/apps/backend/src/chat/chat.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ChatController } from './chat.controller'; + +describe('ChatController', () => { + let controller: ChatController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ChatController], + }).compile(); + + controller = module.get(ChatController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/apps/backend/src/chat/chat.controller.ts b/apps/backend/src/chat/chat.controller.ts new file mode 100644 index 0000000..d71b71a --- /dev/null +++ b/apps/backend/src/chat/chat.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('chat') +export class ChatController {} diff --git a/apps/backend/src/chat/chat.module.ts b/apps/backend/src/chat/chat.module.ts new file mode 100644 index 0000000..6ef4ae2 --- /dev/null +++ b/apps/backend/src/chat/chat.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { ChatController } from './chat.controller'; +import { ChatRepositoryMemory } from './repository/chat.memory.repository'; +import { ChatService } from './chat.service'; + +@Module({ + controllers: [ChatController], + providers: [ + ChatService, + { + provide: 'ChatStorage', + useValue: new Map(), + }, + { + provide: 'ChatRepository', + useClass: ChatRepositoryMemory, + }, + ], + exports: [ChatService], +}) +export class ChatModule {} diff --git a/apps/backend/src/chat/chat.service.spec.ts b/apps/backend/src/chat/chat.service.spec.ts new file mode 100644 index 0000000..b6b70cd --- /dev/null +++ b/apps/backend/src/chat/chat.service.spec.ts @@ -0,0 +1,82 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ChatService } from './chat.service'; +import { NotFoundException } from '@nestjs/common'; +import { ChatMessage } from './entities/chat-message.entity'; + +describe('ChatService', () => { + let service: ChatService; + let chatRepository: { [key: string]: jest.Mock }; + + beforeEach(async () => { + chatRepository = { + set: jest.fn(), + get: jest.fn(), + add: jest.fn(), + has: jest.fn(), + delete: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ChatService, + { + provide: 'ChatRepository', + useValue: chatRepository, + }, + ], + }).compile(); + + service = module.get(ChatService); + }); + + it('정의되어 있어야 합니다', () => { + expect(service).toBeDefined(); + }); + + it('채팅방을 설정할 수 있어야 합니다', async () => { + await service.set('room1'); + expect(chatRepository.set).toHaveBeenCalledWith('room1'); + }); + + it('채팅 메시지를 가져올 수 있어야 합니다', async () => { + const chatMessage: ChatMessage = { clientId: '1', message: 'Hello', nickname: 'nickname1' }; + chatRepository.get.mockResolvedValue([chatMessage]); + chatRepository.has.mockResolvedValue(true); + + const messages = await service.get('room1'); + expect(messages).toEqual([chatMessage]); + }); + + it('채팅 메시지를 추가할 수 있어야 합니다', async () => { + const chatMessage: ChatMessage = { clientId: '1', message: 'Hello', nickname: 'nickname1' }; + chatRepository.has.mockResolvedValue(true); + + await service.add('room1', chatMessage); + expect(chatRepository.add).toHaveBeenCalledWith('room1', chatMessage); + }); + + it('존재하지 않는 채팅방에 메시지를 추가하려고 하면 예외가 발생해야 합니다', async () => { + const chatMessage: ChatMessage = { clientId: '1', message: 'Hello', nickname: 'nickname1' }; + chatRepository.has.mockResolvedValue(false); + + await expect(service.add('room1', chatMessage)).rejects.toThrow(NotFoundException); + }); + + it('채팅 메시지가 존재하는지 확인할 수 있어야 합니다', async () => { + await chatRepository.has.mockResolvedValue(true); + + const hasMessages = await service.has('room1'); + expect(hasMessages).toBe(true); + }); + + it('채팅 메시지를 삭제할 수 있어야 합니다', async () => { + await service.delete('room1'); + expect(chatRepository.delete).toHaveBeenCalledWith('room1'); + }); + + it('존재하지 않는 채팅방에 메시지를 가져오려고 하면 예외가 발생해야 합니다', async () => { + await chatRepository.has.mockResolvedValue(false); + + await expect(service.get('room1')).rejects.toThrow(NotFoundException); + }); +}); diff --git a/apps/backend/src/chat/chat.service.ts b/apps/backend/src/chat/chat.service.ts new file mode 100644 index 0000000..428b32c --- /dev/null +++ b/apps/backend/src/chat/chat.service.ts @@ -0,0 +1,37 @@ +import { Injectable, Inject, NotFoundException } from '@nestjs/common'; +import { ChatRepositoryMemory } from './repository/chat.memory.repository'; +import { ChatMessage } from './entities/chat-message.entity'; + +@Injectable() +export class ChatService { + constructor( + @Inject('ChatRepository') + private readonly chatRepository: ChatRepositoryMemory, + ) {} + + async set(id: string) { + this.chatRepository.set(id); + } + + async get(id: string) { + if (!(await this.chatRepository.has(id))) { + throw new NotFoundException('퀴즈 존에 대한 채팅이 존재하지 않습니다.'); + } + return this.chatRepository.get(id); + } + + async add(id: string, chatMessage: ChatMessage) { + if (!(await this.chatRepository.has(id))) { + throw new NotFoundException('퀴즈 존에 대한 채팅이 존재하지 않습니다.'); + } + return this.chatRepository.add(id, chatMessage); + } + + async has(id: string) { + return await this.chatRepository.has(id); + } + + async delete(id: string) { + return this.chatRepository.delete(id); + } +} diff --git a/apps/backend/src/chat/entities/chat-message.entity.ts b/apps/backend/src/chat/entities/chat-message.entity.ts new file mode 100644 index 0000000..961b07b --- /dev/null +++ b/apps/backend/src/chat/entities/chat-message.entity.ts @@ -0,0 +1,13 @@ +/** + * 사용자가 보낸 채팅 메시지를 나타내는 엔티티 + * + * @property clientId - 클라이언트 ID + * @property nickname - 클라이언트의 닉네임 + * @property message - 채팅 메시지 + */ + +export interface ChatMessage { + clientId: string; + nickname: string; + message: string; +} diff --git a/apps/backend/src/chat/repository/chat.memory.repository.spec.ts b/apps/backend/src/chat/repository/chat.memory.repository.spec.ts new file mode 100644 index 0000000..99309b5 --- /dev/null +++ b/apps/backend/src/chat/repository/chat.memory.repository.spec.ts @@ -0,0 +1,73 @@ +import { ChatRepositoryMemory } from './chat.memory.repository'; +import { ChatMessage } from '../entities/chat-message.entity'; + +// ChatRepositoryMemory 테스트 파일 +describe('ChatRepositoryMemory', () => { + let repository: ChatRepositoryMemory; + let chatStorage: Map; + + beforeEach(() => { + chatStorage = new Map(); + repository = new ChatRepositoryMemory(chatStorage); + }); + + it('채팅 레포지토리가 정의되어 있어야 합니다', () => { + expect(repository).toBeDefined(); + }); + + it('채팅 메시지를 추가할 수 있어야 합니다', async () => { + const chatMessage: ChatMessage = { clientId: '1', message: 'Hello', nickname: 'nickname1' }; + await repository.add('room1', chatMessage); + + const messages = await repository.get('room1'); + expect(messages).toEqual([chatMessage]); + }); + + it('채팅 메시지를 가져올 수 있어야 합니다', async () => { + const chatMessage: ChatMessage = { clientId: '1', message: 'Hello', nickname: 'nickname1' }; + chatStorage.set('room1', [chatMessage]); + + const messages = await repository.get('room1'); + expect(messages).toEqual([chatMessage]); + }); + + it('메시지가 없으면 undefined를 반환해야 합니다', async () => { + const messages = await repository.get('room2'); + expect(messages).toBeNull(); + }); + + it('채팅 메시지를 삭제할 수 있어야 합니다', async () => { + const chatMessage: ChatMessage = { clientId: '1', message: 'Hello', nickname: 'nickname1' }; + chatStorage.set('room1', [chatMessage]); + + await repository.delete('room1'); + const messages = await repository.get('room1'); + expect(messages).toBeNull(); + }); + + it('채팅 메시지가 존재하는지 확인할 수 있어야 합니다', async () => { + const chatMessage: ChatMessage = { clientId: '1', message: 'Hello', nickname: 'nickname1' }; + chatStorage.set('room1', [chatMessage]); + + const hasMessages = await repository.has('room1'); + expect(hasMessages).toBe(true); + + const hasNoMessages = await repository.has('room2'); + expect(hasNoMessages).toBe(false); + }); + + it('채팅 메시지의 수를 제한할 수 있어야 합니다', async () => { + const chatMessages: ChatMessage[] = Array.from({ length: 51 }, (_, i) => ({ + clientId: `${i}`, + message: `Message ${i}`, + nickname: `nickname${i}`, + })); + for (const message of chatMessages) { + await repository.add('room1', message); + } + + const messages = await repository.get('room1'); + expect(messages.length).toBe(50); + expect(messages[0].clientId).toBe('1'); + }); +}); diff --git a/apps/backend/src/chat/repository/chat.memory.repository.ts b/apps/backend/src/chat/repository/chat.memory.repository.ts new file mode 100644 index 0000000..897d33a --- /dev/null +++ b/apps/backend/src/chat/repository/chat.memory.repository.ts @@ -0,0 +1,42 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { ChatMessage } from '../entities/chat-message.entity'; + +@Injectable() +export class ChatRepositoryMemory { + private readonly maxMessageLength: number; + + constructor( + @Inject('ChatStorage') + private readonly data: Map, + ) { + this.maxMessageLength = 50; + } + + async set(id: string) { + this.data.set(id, []); + } + + async get(id: string) { + return this.data.get(id) || null; + } + + async add(id: string, chatMessage: ChatMessage) { + let messages = this.data.get(id); + if (!messages) { + messages = []; + } + if (messages.length >= this.maxMessageLength) { + messages.shift(); + } + messages.push(chatMessage); + this.data.set(id, messages); + } + + async has(id: string) { + return this.data.has(id); + } + + async delete(id: string) { + this.data.delete(id); + } +} diff --git a/apps/backend/src/common/base-entity.ts b/apps/backend/src/common/base-entity.ts new file mode 100644 index 0000000..5badc90 --- /dev/null +++ b/apps/backend/src/common/base-entity.ts @@ -0,0 +1,29 @@ +import { + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, BeforeUpdate, +} from 'typeorm'; + +export abstract class BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @CreateDateColumn({ + type: 'datetime', + default: () => 'CURRENT_TIMESTAMP', + name: 'created_at', + }) + createAt: Date; + + @UpdateDateColumn({ + type: 'datetime', + default: () => 'CURRENT_TIMESTAMP', + name: 'updated_at', + }) + updateAt: Date; + + @BeforeUpdate() + updateTimestamp() { + this.updateAt = new Date(); // SQLite에서 수동으로 갱신 + } +} \ No newline at end of file diff --git a/apps/backend/src/common/constants.ts b/apps/backend/src/common/constants.ts new file mode 100644 index 0000000..5e69e13 --- /dev/null +++ b/apps/backend/src/common/constants.ts @@ -0,0 +1,98 @@ +export enum QUIZ_ZONE_STAGE { + LOBBY = 'LOBBY', + IN_PROGRESS = 'IN_PROGRESS', + RESULT = 'RESULT', +} + +export enum PLAYER_STATE { + WAIT = 'WAIT', + PLAY = 'PLAY', + SUBMIT = 'SUBMIT', +} + +export const CLOSE_CODE = { + NORMAL: 1000, + GOING_AWAY: 1001, + PROTOCOL_ERROR: 1002, + REFUSE: 1003, + NO_STATUS: 1005, + ABNORMAL: 1006, + INCONSISTENT_DATA: 1007, + POLICY_VIOLATION: 1008, +}; + +// 닉네임 접두사 +const prefixes: string[] = [ + '불사조', + '천상의', + '불의', + '붉은', + '어둠의', + '달빛', + '푸른', + '검은', + '황금의', + '은빛', + '새벽의', + '태양의', + '영원의', + '바다의', + '대지의', + '하늘의', + '강철의', + '빛의', + '고대의', + '심연의', + '구름의', + '섬광의', + '운명의', + '폭풍의', + '신비의', + '얼음의', + '화염의', + '별의', + '천둥의', + '미친', +]; + +// 닉네임 접미사 +const suffixes: string[] = [ + '마법사', + '현자', + '검사', + '용사', + '기사', + '전사', + '궁수', + '암살자', + '무사', + '도적', + '영웅', + '제왕', + '성자', + '마왕', + '기병', + '법사', + '검객', + '마술사', + '투사', + '마수', + '검투사', + '마인', + '왕', + '사제', + '괴수', + '현자', + '용자', + '유령', + '악마', + '정령', +]; + +export const getRandomNickName = (): string => { + return `${prefixes[Math.floor(Math.random() * prefixes.length)]}${suffixes[Math.floor(Math.random() * suffixes.length)]}`; +}; + +export enum QUIZ_TYPE { + SHORT_ANSWER = 'SHORT', +} diff --git a/apps/backend/src/core/SessionWsAdapter.ts b/apps/backend/src/core/SessionWsAdapter.ts new file mode 100644 index 0000000..dfc9a97 --- /dev/null +++ b/apps/backend/src/core/SessionWsAdapter.ts @@ -0,0 +1,60 @@ +import { WsAdapter } from '@nestjs/platform-ws'; +import { RequestHandler } from 'express'; +import { NestApplication } from '@nestjs/core'; +import { Session } from 'express-session'; + +export interface WebSocketWithSession extends WebSocket { + session: Session; +} + +export class SessionWsAdapter extends WsAdapter { + constructor( + private readonly app: NestApplication | any, + private readonly sessionMiddleware: RequestHandler, + ) { + super(app); + } + + create( + port: number, + options?: Record & { + namespace?: string; + server?: any; + path?: string; + }, + ): any { + const httpServer = this.app.getHttpServer(); + const wsServer = super.create(port, options); + + httpServer.removeAllListeners('upgrade'); + + httpServer.on('upgrade', (request, socket, head) => { + this.sessionMiddleware(request, {} as any, () => { + try { + const baseUrl = 'ws://' + request.headers.host + '/'; + const pathname = new URL(request.url, baseUrl).pathname; + const wsServersCollection = this.wsServersRegistry.get(port); + + let isRequestDelegated = false; + for (const wsServer of wsServersCollection) { + if (pathname === wsServer.path) { + wsServer.handleUpgrade(request, socket, head, (ws: unknown) => { + ws['session'] = request.session; + wsServer.emit('connection', ws, request); + }); + isRequestDelegated = true; + break; + } + } + if (!isRequestDelegated) { + socket.destroy(); + } + } catch (err: any) { + socket.end('HTTP/1.1 400\r\n' + err.message); + } + }); + }); + + return wsServer; + } +} diff --git a/apps/backend/src/logger/http-logger.middleware.ts b/apps/backend/src/logger/http-logger.middleware.ts new file mode 100644 index 0000000..1cac870 --- /dev/null +++ b/apps/backend/src/logger/http-logger.middleware.ts @@ -0,0 +1,21 @@ +import { Inject, Injectable, Logger, NestMiddleware } from '@nestjs/common'; +import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; + +@Injectable() +export class HttpLoggingMiddleware implements NestMiddleware { + constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {} + + use(req: any, res: any, next: () => void) { + const { method, url } = req; + const startTime = Date.now(); + + res.on('finish', () => { + const duration = Date.now() - startTime; + this.logger.log('info', `${method} ${url} ${res.statusCode} - ${duration}ms`, { + context: 'HTTP', + }); + }); + + next(); + } +} diff --git a/apps/backend/src/logger/winston.config.ts b/apps/backend/src/logger/winston.config.ts new file mode 100644 index 0000000..df68ac9 --- /dev/null +++ b/apps/backend/src/logger/winston.config.ts @@ -0,0 +1,20 @@ +import { WinstonModuleOptions } from 'nest-winston'; +import * as winston from 'winston'; + +export const winstonConfig: WinstonModuleOptions = { + transports: [ + new winston.transports.Console({ + format: winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.colorize(), + winston.format.printf(({ level, message, timestamp, context }) => { + return `[${timestamp}] [${level}] ${context ? `[${context}] ` : ''}${message}`; + }), + ), + }), + new winston.transports.File({ + filename: 'logs/app.log', + format: winston.format.json(), + }), + ], +}; diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts new file mode 100644 index 0000000..2749fc0 --- /dev/null +++ b/apps/backend/src/main.ts @@ -0,0 +1,55 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; +import cookieParser from 'cookie-parser'; +import session from 'express-session'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { ConfigService } from '@nestjs/config'; +import { Environment } from '../config/http.config'; +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { SessionWsAdapter } from './core/SessionWsAdapter'; +import { initializeTransactionalContext } from 'typeorm-transactional'; + +async function bootstrap() { + initializeTransactionalContext(); + + const app = await NestFactory.create(AppModule); + const configService = app.get(ConfigService); + + const env = configService.get('env'); + const port = configService.get('port'); + const sessionSecret = configService.get('sessionSecret'); + + switch (env) { + case Environment.Development: + case Environment.Staging: + setupSwagger(app); + break; + case Environment.Production: + break; + } + + const sessionMiddleware = session({ + secret: sessionSecret, + resave: false, + saveUninitialized: true, + }); + + app.use(cookieParser()); + app.use(sessionMiddleware); + + app.useGlobalPipes(new ValidationPipe({ transform: true })); + app.useWebSocketAdapter(new SessionWsAdapter(app, sessionMiddleware)); + + await app.listen(port); +} + +function setupSwagger(app: INestApplication) { + const config = new DocumentBuilder() + .setTitle('BooQuiz') + .setDescription('BooQuiz API description') + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('/api/swagger', app, document); +} + +bootstrap(); diff --git a/apps/backend/src/play/dto/current-quiz-result.dto.ts b/apps/backend/src/play/dto/current-quiz-result.dto.ts new file mode 100644 index 0000000..9f32f48 --- /dev/null +++ b/apps/backend/src/play/dto/current-quiz-result.dto.ts @@ -0,0 +1,5 @@ +export interface CurrentQuizResultDto { + answer?: string; + totalPlayerCount: number; + correctPlayerCount: number; +} diff --git a/apps/backend/src/play/dto/current-quiz.dto.ts b/apps/backend/src/play/dto/current-quiz.dto.ts new file mode 100644 index 0000000..e438c49 --- /dev/null +++ b/apps/backend/src/play/dto/current-quiz.dto.ts @@ -0,0 +1,20 @@ +import { QUIZ_ZONE_STAGE } from '../../common/constants'; + +/** + * 현재 진행중인 퀴즈에 대한 DTO + * + * @property question - 현재 진행 중인 퀴즈의 질문 + * @property stage - 현재 퀴즈의 진행 상태 + * @property currentIndex - 현재 퀴즈의 인덱스 + * @property playTime - 퀴즈 플레이 시간 + * @property startTime - 퀴즈 시작 시간 + * @property deadlineTime - 퀴즈 마감 시간 + */ +export interface CurrentQuizDto { + readonly question: string; + readonly stage: QUIZ_ZONE_STAGE; + readonly currentIndex: number; + readonly playTime: number; + readonly startTime: number; + readonly deadlineTime: number; +} diff --git a/apps/backend/src/play/dto/quiz-join.dto.ts b/apps/backend/src/play/dto/quiz-join.dto.ts new file mode 100644 index 0000000..cc31929 --- /dev/null +++ b/apps/backend/src/play/dto/quiz-join.dto.ts @@ -0,0 +1,8 @@ +/** + * 퀴즈 참여를 위한 DTO + * + * @property quizZoneId - 참여할 퀴즈 존 ID + */ +export interface QuizJoinDto { + quizZoneId: string; +} \ No newline at end of file diff --git a/apps/backend/src/play/dto/quiz-result-summary.dto.ts b/apps/backend/src/play/dto/quiz-result-summary.dto.ts new file mode 100644 index 0000000..78468c7 --- /dev/null +++ b/apps/backend/src/play/dto/quiz-result-summary.dto.ts @@ -0,0 +1,15 @@ +import { SubmittedQuiz } from '../../quiz-zone/entities/submitted-quiz.entity'; +import { Quiz } from '../../quiz-zone/entities/quiz.entity'; + +/** + * 퀴즈 결과 출력을 위한 DTO + * + * @property score - 퀴즈 결과/점수 + * @property submits - 플레이어가 제출한 퀴즈 목록 + * @property quizzes - 전체 퀴즈 목록 + */ +export interface QuizResultSummaryDto { + score: number; + submits: SubmittedQuiz[]; + quizzes: Quiz[]; +} diff --git a/apps/backend/src/play/dto/quiz-submit.dto.ts b/apps/backend/src/play/dto/quiz-submit.dto.ts new file mode 100644 index 0000000..a53e813 --- /dev/null +++ b/apps/backend/src/play/dto/quiz-submit.dto.ts @@ -0,0 +1,11 @@ +/** + * 클라이언트의 퀴즈 제출을 위한 DTO + * @property index - 퀴즈의 인덱스 + * @property answer - 플레이어가 제출한 답 + * @property submittedAt - 플레이어가 정답을 제출한 시각 + */ +export interface QuizSubmitDto { + index: number; + answer: string; + submittedAt: number; +} diff --git a/apps/backend/src/play/dto/response-player.dto.ts b/apps/backend/src/play/dto/response-player.dto.ts new file mode 100644 index 0000000..72e72eb --- /dev/null +++ b/apps/backend/src/play/dto/response-player.dto.ts @@ -0,0 +1,6 @@ +class ResponsePlayerDto { + constructor( + public readonly id: string, + public readonly nickname: string, + ) {} +} diff --git a/apps/backend/src/play/dto/submit-response.dto.ts b/apps/backend/src/play/dto/submit-response.dto.ts new file mode 100644 index 0000000..16d30e3 --- /dev/null +++ b/apps/backend/src/play/dto/submit-response.dto.ts @@ -0,0 +1,9 @@ +import { ChatMessage } from 'src/chat/entities/chat-message.entity'; +export class SubmitResponseDto { + constructor( + public readonly fastestPlayerIds: string[], + public readonly submittedCount: number, + public readonly totalPlayerCount: number, + public readonly chatMessages: ChatMessage[], + ) {} +} diff --git a/apps/backend/src/play/entities/client-info.entity.ts b/apps/backend/src/play/entities/client-info.entity.ts new file mode 100644 index 0000000..642f408 --- /dev/null +++ b/apps/backend/src/play/entities/client-info.entity.ts @@ -0,0 +1,12 @@ +import { WebSocketWithSession } from '../../core/SessionWsAdapter'; + +/** + * 퀴즈 클라이언트의 정보를 나타냅니다. + * + * @property quizZoneId - 클라이언트가 참여한 퀴즈존 ID + * @property socket - 클라이언트의 WebSocket 연결 + */ +export interface ClientInfo { + quizZoneId: string; + socket: WebSocketWithSession; +} diff --git a/apps/backend/src/play/entities/quiz-summary.entity.ts b/apps/backend/src/play/entities/quiz-summary.entity.ts new file mode 100644 index 0000000..1edc0de --- /dev/null +++ b/apps/backend/src/play/entities/quiz-summary.entity.ts @@ -0,0 +1,6 @@ +import { Rank } from './rank.entity'; + +export interface QuizSummary { + readonly ranks: Rank[]; + readonly endSocketTime?: number; +} \ No newline at end of file diff --git a/apps/backend/src/play/entities/rank.entity.ts b/apps/backend/src/play/entities/rank.entity.ts new file mode 100644 index 0000000..7854840 --- /dev/null +++ b/apps/backend/src/play/entities/rank.entity.ts @@ -0,0 +1,6 @@ +export interface Rank { + readonly id: string; + readonly nickname: string; + readonly score: number; + readonly ranking: number; +} \ No newline at end of file diff --git a/apps/backend/src/play/entities/send-event.entity.ts b/apps/backend/src/play/entities/send-event.entity.ts new file mode 100644 index 0000000..4361049 --- /dev/null +++ b/apps/backend/src/play/entities/send-event.entity.ts @@ -0,0 +1,10 @@ +/** + * 웹소켓 서버가 사용자에게 응답할 메시지 형식을 정의합니다. + * + * @property event - 클라이언트에게 전송할 이벤트 이름 + * @property data - 클라이언트에게 전송할 데이터 + */ +export interface SendEventMessage { + event: string; + data: T; +} \ No newline at end of file diff --git a/apps/backend/src/play/play.gateway.spec.ts b/apps/backend/src/play/play.gateway.spec.ts new file mode 100644 index 0000000..f245319 --- /dev/null +++ b/apps/backend/src/play/play.gateway.spec.ts @@ -0,0 +1,53 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PlayGateway } from './play.gateway'; +import { PlayService } from './play.service'; +import { QuizZoneService } from '../quiz-zone/quiz-zone.service'; +import { ChatService } from '../chat/chat.service'; + +describe('PlayGateway', () => { + let gateway: PlayGateway; + + const mockPlayService = { + join: jest.fn(), + }; + + const mockQuizZoneService = { + findOne: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PlayGateway, + { + provide: PlayService, + useValue: mockPlayService, + }, + { + provide: 'PlayInfoStorage', + useValue: new Map(), + }, + { + provide: 'ClientInfoStorage', + useValue: new Map(), + }, + { + provide: QuizZoneService, + useValue: mockQuizZoneService, + }, + { + provide: ChatService, + useValue: { + /* mock implementation */ + }, + }, + ], + }).compile(); + + gateway = module.get(PlayGateway); + }); + + it('should be defined', () => { + expect(gateway).toBeDefined(); + }); +}); diff --git a/apps/backend/src/play/play.gateway.ts b/apps/backend/src/play/play.gateway.ts new file mode 100644 index 0000000..5cf7a7c --- /dev/null +++ b/apps/backend/src/play/play.gateway.ts @@ -0,0 +1,309 @@ +import { + ConnectedSocket, + MessageBody, + OnGatewayInit, + SubscribeMessage, + WebSocketGateway, + WebSocketServer, +} from '@nestjs/websockets'; +import { PlayService } from './play.service'; +import { Server } from 'ws'; +import { QuizSubmitDto } from './dto/quiz-submit.dto'; +import { QuizJoinDto } from './dto/quiz-join.dto'; +import { BadRequestException, Inject } from '@nestjs/common'; +import { SendEventMessage } from './entities/send-event.entity'; +import { ClientInfo } from './entities/client-info.entity'; +import { WebSocketWithSession } from '../core/SessionWsAdapter'; +import { RuntimeException } from '@nestjs/core/errors/exceptions'; +import { SubmitResponseDto } from './dto/submit-response.dto'; +import { ChatMessage } from 'src/chat/entities/chat-message.entity'; +import { ChatService } from '../chat/chat.service'; +import { CLOSE_CODE } from '../common/constants'; // 경로 수정 + +/** + * 퀴즈 게임에 대한 WebSocket 연결을 관리하는 Gateway입니다. + * 클라이언트가 퀴즈 진행, 제출, 타임아웃 및 결과 요약과 관련된 이벤트를 처리합니다. + */ +@WebSocketGateway({ path: '/play' }) +export class PlayGateway implements OnGatewayInit { + @WebSocketServer() + server: Server; + + constructor( + @Inject('ClientInfoStorage') + private readonly clients: Map, + private readonly playService: PlayService, + private readonly chatService: ChatService, + ) {} + + /** + * WebSocket 서버 초기화 시, 퀴즈 진행 및 요약 이벤트를 처리하는 핸들러를 설정합니다. + * + * @param server - WebSocket 서버 인스턴스 + */ + afterInit(server: Server) { + server.on('nextQuiz', (quizZoneId: string) => this.playNextQuiz(quizZoneId)); + server.on('summary', (quizZoneId: string) => this.summary(quizZoneId)); + } + + private sendToClient(clientId: string, event: string, data?: any) { + const { socket } = this.getClientInfo(clientId); + socket.send(JSON.stringify({ event, data })); + } + + private broadcast(clientIds: string[], event: string, data?: any) { + clientIds.forEach((clientId) => { + this.sendToClient(clientId, event, data); + }); + } + + /** + * 클라이언트의 세션 ID를 이용하여 클라이언트 정보를 조회합니다. + * + * @param clientId - 클라이언트의 세션 ID + */ + private getClientInfo(clientId: string): ClientInfo { + const clientInfo = this.clients.get(clientId); + + if (!clientInfo) { + throw new BadRequestException('사용자의 접속 정보를 찾을 수 없습니다.'); + } + + return clientInfo; + } + + private clearClient(clientId: string, reason?: string) { + const { socket } = this.getClientInfo(clientId); + this.clients.delete(clientId); + socket.close(CLOSE_CODE.NORMAL, reason); + } + + /** + * 클라이언트가 퀴즈 방에 참여했다는 메세지를 방의 다른 참여자들에게 전송합니다. + * + * @param client - 클라이언트 소켓 + * @param quizJoinDto - 퀴즈 참여 정보(퀴즈 존 ID) + */ + @SubscribeMessage('join') + async join( + @ConnectedSocket() client: WebSocketWithSession, + @MessageBody() quizJoinDto: QuizJoinDto, + ): Promise> { + const sessionId = client.session.id; + const { quizZoneId } = quizJoinDto; + + const { currentPlayer, players } = await this.playService.joinQuizZone( + quizZoneId, + sessionId, + ); + + const { id, nickname } = currentPlayer; + const playerIds = players.map((player) => player.id); + const data = players.map(({ id, nickname }) => ({ + id, + nickname, + })); + + if (this.clients.has(sessionId) && this.clients.get(sessionId).quizZoneId === quizZoneId) { + this.clients.set(sessionId, { quizZoneId, socket: client }); + return { + event: 'join', + data, + }; + } + this.clients.set(sessionId, { quizZoneId, socket: client }); + + this.broadcast(playerIds, 'someone_join', { id, nickname }); + + return { + event: 'join', + data, + }; + } + + @SubscribeMessage('changeNickname') + async changeNickname( + @ConnectedSocket() client: WebSocketWithSession, + @MessageBody() changedNickname: string, + ): Promise> { + const clientId = client.session.id; + const { quizZoneId } = this.getClientInfo(clientId); + + const { playerIds } = await this.playService.changeNickname( + quizZoneId, + clientId, + changedNickname, + ); + + this.broadcast(playerIds, 'updateNickname', { clientId, changedNickname }); + + return { + event: 'changeNickname', + data: 'OK', + }; + } + + /** + * 퀴즈 게임을 시작하는 메시지를 클라이언트로 전송합니다. + * + * @param client - WebSocket 클라이언트 + */ + @SubscribeMessage('start') + async start(@ConnectedSocket() client: WebSocketWithSession) { + const clientId = client.session.id; + const { quizZoneId } = this.getClientInfo(clientId); + + const playerIds = await this.playService.startQuizZone(quizZoneId, clientId); + + this.broadcast(playerIds, 'start', 'OK'); + + this.server.emit('nextQuiz', quizZoneId); + } + + /** + * 다음 퀴즈를 시작하고 클라이언트에 전달합니다. + * + * @param quizZoneId - WebSocket 클라이언트 + */ + private async playNextQuiz(quizZoneId: string) { + try { + const { nextQuiz, playerIds, currentQuizResult } = await this.playService.playNextQuiz( + quizZoneId, + () => { + this.broadcast(playerIds, 'quizTimeOut'); + this.server.emit('nextQuiz', quizZoneId); + }, + ); + + this.broadcast(playerIds, 'nextQuiz', { nextQuiz, currentQuizResult }); + } catch (error) { + if (error instanceof RuntimeException) { + await this.finishQuizZone(quizZoneId); + } else { + throw error; + } + } + } + + private async finishQuizZone(quizZoneId: string) { + const playerIds = await this.playService.finishQuizZone(quizZoneId); + + this.broadcast(playerIds, 'finish'); + + this.server.emit('summary', quizZoneId); + } + + /** + * 클라이언트가 퀴즈 답안을 제출한 경우 호출됩니다. + * + * @param client - WebSocket 클라이언트 + * @param quizSubmit - 퀴즈 제출 데이터 + * @returns {Promise>} 제출 완료 메시지 + */ + @SubscribeMessage('submit') + async submit( + @ConnectedSocket() client: WebSocketWithSession, + @MessageBody() quizSubmit: QuizSubmitDto, + ): Promise> { + const clientId = client.session.id; + const { quizZoneId } = this.getClientInfo(clientId); + const chatMessages = await this.chatService.get(quizZoneId); + + const { + isLastSubmit, + fastestPlayerIds, + submittedCount, + totalPlayerCount, + otherSubmittedPlayerIds, + } = await this.playService.submit(quizZoneId, clientId, { + ...quizSubmit, + receivedAt: Date.now(), + }); + + if (isLastSubmit) { + this.server.emit('nextQuiz', quizZoneId); + } + + this.broadcast(otherSubmittedPlayerIds, 'someone_submit', { clientId, submittedCount }); + + return { + event: 'submit', + data: { fastestPlayerIds, submittedCount, totalPlayerCount, chatMessages }, + }; + } + + /** + * 퀴즈 진행이 끝나면 요약 결과를 퀴즈 존의 모든 플레이어에게 전송합니다. + * + * @param quizZoneId - WebSocket 클라이언트 + */ + private async summary(quizZoneId: string) { + const summaries = await this.playService.summaryQuizZone(quizZoneId); + const endSocketTime = summaries[0].endSocketTime; + + summaries.map(async ({ id, score, submits, quizzes, ranks, endSocketTime }) => { + this.sendToClient(id, 'summary', { score, submits, quizzes, ranks, endSocketTime }); + }); + + const clientsIds = summaries.map(({ id }) => id); + + this.clearQuizZone(clientsIds, quizZoneId, endSocketTime - Date.now()); + } + + /** + * 퀴즈 방을 나갔다는 메시지를 클라이언트로 전송합니다. + * + * - 방장이 나가면 퀴즈 존을 삭제하고 모든 플레이어에게 방장이 나갔다고 알립니다. + * - 일반 플레이어가 나가면 퀴즈 존에서 나가고 다른 플레이어에게 나갔다고 알립니다. + * @param clientIds - 퀴즈존에 참여하고 있는 클라이언트 id 리스트 + * @param quizZoneId - 퀴즈가 끝난 퀴즈존 id + * @param time - 소켓 연결 종료 시간 종료 시간 + */ + private clearQuizZone(clientIds: string[], quizZoneId: string, time: number) { + setTimeout(() => { + clientIds.forEach((id) => { + this.clearClient(id, 'finish'); + }); + this.playService.clearQuizZone(quizZoneId); + this.chatService.delete(quizZoneId); + }, time); + } + + /** + * 퀴즈 방을 나갔다는 메시지를 클라이언트로 전송합니다. + * + * - 방장이 나가면 퀴즈 존을 삭제하고 모든 플레이어에게 방장이 나갔다고 알립니다. + * - 일반 플레이어가 나가면 퀴즈 존에서 나가고 다른 플레이어에게 나갔다고 알립니다. + * @param client - WebSocket 클라이언트 + */ + @SubscribeMessage('leave') + async leave(@ConnectedSocket() client: WebSocketWithSession) { + const clientId = client.session.id; + const { quizZoneId } = this.getClientInfo(clientId); + + const { isHost, playerIds } = await this.playService.leaveQuizZone(quizZoneId, clientId); + + if (isHost) { + this.broadcast(playerIds, 'close'); + this.clearQuizZone(playerIds, quizZoneId, 0); + } else { + this.broadcast(playerIds, 'someone_leave', clientId); + this.clearClient(clientId, 'Client leave'); + } + + return { event: 'leave', data: 'OK' }; + } + + @SubscribeMessage('chat') + async chat( + @ConnectedSocket() client: WebSocketWithSession, + @MessageBody() message: ChatMessage, + ) { + const clientId = client.session.id; + const { quizZoneId } = this.getClientInfo(clientId); + const clientIds = await this.playService.chatQuizZone(clientId, quizZoneId); + + this.broadcast(clientIds, 'chat', message); + this.chatService.add(quizZoneId, message); + } +} diff --git a/apps/backend/src/play/play.module.ts b/apps/backend/src/play/play.module.ts new file mode 100644 index 0000000..3ffeab8 --- /dev/null +++ b/apps/backend/src/play/play.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { PlayService } from './play.service'; +import { PlayGateway } from './play.gateway'; +import { QuizZoneModule } from '../quiz-zone/quiz-zone.module'; +import { ChatModule } from 'src/chat/chat.module'; + +@Module({ + imports: [QuizZoneModule, ChatModule], + providers: [ + PlayGateway, + { + provide: 'PlayInfoStorage', + useValue: new Map(), + }, + { + provide: 'ClientInfoStorage', + useValue: new Map(), + }, + PlayService, + ], +}) +export class PlayModule {} diff --git a/apps/backend/src/play/play.service.spec.ts b/apps/backend/src/play/play.service.spec.ts new file mode 100644 index 0000000..6170c3a --- /dev/null +++ b/apps/backend/src/play/play.service.spec.ts @@ -0,0 +1,691 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { BadRequestException, NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { PlayService } from './play.service'; +import { QuizZoneService } from '../quiz-zone/quiz-zone.service'; +import { QuizZone } from '../quiz-zone/entities/quiz-zone.entity'; +import { SubmittedQuiz } from '../quiz-zone/entities/submitted-quiz.entity'; +import { PLAYER_STATE, QUIZ_TYPE, QUIZ_ZONE_STAGE } from '../common/constants'; +import { RuntimeException } from '@nestjs/core/errors/exceptions'; + +describe('PlayService', () => { + let service: PlayService; + let quizZoneService: jest.Mocked; + let playsStorage: Map; + + const mockPlayer = { + id: 'player-1', + nickname: 'player1', + state: PLAYER_STATE.WAIT, + score: 0, + submits: [], + }; + + const mockQuizZone: QuizZone = { + hostId: 'host-1', + stage: QUIZ_ZONE_STAGE.LOBBY, + intervalTime: 5000, + maxPlayers: 10, + currentQuizIndex: -1, + title: '퀴즈 존', + description: '퀴즈 존입니다', + currentQuizStartTime: Date.now(), + currentQuizDeadlineTime: Date.now() + 10000, + quizzes: [ + { + question: '1번 문제입니다', + answer: '정답1', + playTime: 30000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, + { + question: '2번 문제입니다', + answer: '정답2', + playTime: 30000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, + ], + players: new Map([['player-1', mockPlayer]]), + }; + + beforeEach(async () => { + playsStorage = new Map(); + const mockQuizZoneService = { + findOne: jest.fn(), + clearQuizZone: jest.fn(), + updateQuizZone: jest.fn(), + }; + + const mockLogger = { + log: jest.fn(), + error: jest.fn(), + }; + + const mockChatService = { + get: jest.fn(), + add: jest.fn(), + has: jest.fn(), + delete: jest.fn(), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PlayService, + { provide: QuizZoneService, useValue: mockQuizZoneService }, + { provide: 'winston', useValue: mockLogger }, + { provide: 'PlayInfoStorage', useValue: playsStorage }, + { provide: 'ChatService', useValue: mockChatService }, // 추가된 부분 + ], + }).compile(); + + service = module.get(PlayService); + quizZoneService = module.get(QuizZoneService); + }); + + describe('joinQuizZone', () => { + it('참여자가 퀴즈존에 정상적으로 참여할 수 있어야 합니다', async () => { + const mockQuizZoneWithPlayers = { + ...mockQuizZone, + players: new Map([ + ['player-1', mockPlayer], + ['player-2', { ...mockPlayer, id: 'player-2', nickname: 'player2' }], + ]), + }; + quizZoneService.findOne.mockResolvedValue(mockQuizZoneWithPlayers); + + const result = await service.joinQuizZone('test-zone', 'player-1'); + + expect(result).toEqual({ + currentPlayer: mockPlayer, + players: [ + { ...mockPlayer, id: 'player-1', nickname: 'player1' }, + { ...mockPlayer, id: 'player-2', nickname: 'player2' }, + ], + }); + }); + + it('퀴즈존에 참여하지 않은 사용자는 NotFoundException이 발생해야 합니다', async () => { + quizZoneService.findOne.mockResolvedValue(mockQuizZone); + + await expect(service.joinQuizZone('test-zone', 'unknown-player')).rejects.toThrow( + new NotFoundException('참여하지 않은 사용자입니다.'), + ); + }); + }); + + describe('startQuizZone', () => { + it('방장이 퀴즈를 정상적으로 시작할 수 있어야 합니다', async () => { + const mockQuizZoneWithMultiplePlayers = { + ...mockQuizZone, + players: new Map([ + ['player-1', mockPlayer], + ['player-2', { ...mockPlayer, id: 'player-2', nickname: 'player2' }], + ]), + }; + quizZoneService.findOne.mockResolvedValue(mockQuizZoneWithMultiplePlayers); + + const result = await service.startQuizZone('test-zone', 'host-1'); + + expect(result).toEqual(['player-1', 'player-2']); + expect(quizZoneService.updateQuizZone).toHaveBeenCalledWith( + 'test-zone', + expect.objectContaining({ + stage: QUIZ_ZONE_STAGE.IN_PROGRESS, + hostId: 'host-1', + }), + ); + }); + + it('방장이 아닌 사용자가 시작하면 UnauthorizedException이 발생해야 합니다', async () => { + quizZoneService.findOne.mockResolvedValue(mockQuizZone); + + await expect(service.startQuizZone('test-zone', 'player-1')).rejects.toThrow( + new UnauthorizedException('방장만 퀴즈를 시작할 수 있습니다.'), + ); + }); + + it('이미 시작된 퀴즈존은 BadRequestException이 발생해야 합니다', async () => { + const inProgressQuizZone = { + ...mockQuizZone, + stage: QUIZ_ZONE_STAGE.IN_PROGRESS, + }; + quizZoneService.findOne.mockResolvedValue(inProgressQuizZone); + + await expect(service.startQuizZone('test-zone', 'host-1')).rejects.toThrow( + new BadRequestException('이미 시작된 퀴즈존입니다.'), + ); + }); + }); + + describe('playNextQuiz', () => { + const timeoutHandle = jest.fn(); + const now = Date.now(); + + it('첫 번째 퀴즈가 정상적으로 설정되어야 합니다', async () => { + const startTime = now + mockQuizZone.intervalTime; + const mockInProgressQuizZone = { + ...mockQuizZone, + stage: QUIZ_ZONE_STAGE.IN_PROGRESS, + currentQuizIndex: -1, + currentQuizStartTime: startTime, + currentQuizDeadlineTime: startTime + mockQuizZone.quizzes[0].playTime, + }; + quizZoneService.findOne.mockResolvedValue(mockInProgressQuizZone); + + const result = await service.playNextQuiz('test-zone', timeoutHandle); + + expect(result).toEqual({ + nextQuiz: { + stage: QUIZ_ZONE_STAGE.IN_PROGRESS, + question: '1번 문제입니다', + currentIndex: 0, + playTime: 30000, + startTime: expect.any(Number), + deadlineTime: expect.any(Number), + }, + playerIds: ['player-1'], + currentQuizResult: { + answer: undefined, + totalPlayerCount: 0, + correctPlayerCount: 0, + }, + }); + + expect(quizZoneService.updateQuizZone).toHaveBeenCalledWith( + 'test-zone', + expect.objectContaining({ + currentQuizIndex: 0, + players: expect.any(Map), + }), + ); + }); + + it('현재 진행중인 퀴즈의 결과를 포함해야 합니다', async () => { + const mockPlayerWithSubmit = { + ...mockPlayer, + state: PLAYER_STATE.SUBMIT, + submits: [ + { + index: 0, + answer: '정답1', + submittedAt: now - 1000, + }, + ], + }; + + const mockQuizZoneWithSubmit = { + ...mockQuizZone, + stage: QUIZ_ZONE_STAGE.IN_PROGRESS, + currentQuizIndex: 0, + players: new Map([['player-1', mockPlayerWithSubmit]]), + }; + quizZoneService.findOne.mockResolvedValue(mockQuizZoneWithSubmit); + + const result = await service.playNextQuiz('test-zone', timeoutHandle); + + expect(result.currentQuizResult).toEqual({ + answer: '정답1', + totalPlayerCount: 1, + correctPlayerCount: 1, + }); + }); + + it('모든 퀴즈가 종료되면 RuntimeException이 발생해야 합니다', async () => { + quizZoneService.findOne.mockResolvedValue({ + ...mockQuizZone, + currentQuizIndex: mockQuizZone.quizzes.length - 1, + }); + + await expect(service.playNextQuiz('test-zone', timeoutHandle)).rejects.toThrow( + new RuntimeException('모든 퀴즈를 출제하였습니다.'), + ); + }); + }); + + describe('submit', () => { + const now = Date.now(); + const submitQuiz: SubmittedQuiz = { + index: 0, + answer: '정답1', + submittedAt: now, + }; + + it('정답을 정상적으로 제출할 수 있어야 합니다', async () => { + const mockPlayerInPlay = { + ...mockPlayer, + state: PLAYER_STATE.PLAY, + }; + + const inProgressQuizZone = { + ...mockQuizZone, + stage: QUIZ_ZONE_STAGE.IN_PROGRESS, + currentQuizIndex: 0, + currentQuizDeadlineTime: now + 5000, + players: new Map([['player-1', mockPlayerInPlay]]), + }; + quizZoneService.findOne.mockResolvedValue(inProgressQuizZone); + + const result = await service.submit('test-zone', 'player-1', submitQuiz); + + expect(result).toEqual({ + isLastSubmit: true, + fastestPlayerIds: ['player-1'], + submittedCount: 1, + totalPlayerCount: 1, + otherSubmittedPlayerIds: [], + }); + }); + + it('여러 플레이어가 있을 때 정답 제출이 정상적으로 처리되어야 합니다', async () => { + const mockPlayerInPlay = { + ...mockPlayer, + state: PLAYER_STATE.PLAY, + }; + const mockPlayer2InSubmit = { + ...mockPlayer, + id: 'player-2', + nickname: 'player2', + state: PLAYER_STATE.SUBMIT, + submits: [{ index: 0, answer: '정답1', submittedAt: now - 1000 }], + }; + + const inProgressQuizZone = { + ...mockQuizZone, + stage: QUIZ_ZONE_STAGE.IN_PROGRESS, + currentQuizIndex: 0, + currentQuizDeadlineTime: now + 5000, + players: new Map([ + ['player-1', mockPlayerInPlay], + ['player-2', mockPlayer2InSubmit], + ]), + }; + quizZoneService.findOne.mockResolvedValue(inProgressQuizZone); + + const result = await service.submit('test-zone', 'player-1', submitQuiz); + + expect(result).toEqual({ + isLastSubmit: true, + fastestPlayerIds: ['player-2', 'player-1'], + submittedCount: 2, + totalPlayerCount: 2, + otherSubmittedPlayerIds: ['player-2'], + }); + }); + + it('게임이 진행 중이 아닐 때는 제출할 수 없어야 합니다', async () => { + quizZoneService.findOne.mockResolvedValue(mockQuizZone); + + await expect(service.submit('test-zone', 'player-1', submitQuiz)).rejects.toThrow( + new BadRequestException('퀴즈를 제출할 수 없습니다.'), + ); + }); + + it('이미 제출한 플레이어는 다시 제출할 수 없어야 합니다', async () => { + const submittedPlayer = { + ...mockPlayer, + state: PLAYER_STATE.SUBMIT, + submits: [{ index: 0, answer: '정답1', submittedAt: now - 1000 }], + }; + + const submittedQuizZone = { + ...mockQuizZone, + stage: QUIZ_ZONE_STAGE.IN_PROGRESS, + currentQuizIndex: 0, + players: new Map([['player-1', submittedPlayer]]), + }; + + quizZoneService.findOne.mockResolvedValue(submittedQuizZone); + + await expect(service.submit('test-zone', 'player-1', submitQuiz)).rejects.toThrow( + new BadRequestException('정답을 제출할 수 없습니다.'), + ); + }); + }); + + describe('leaveQuizZone', () => { + it('일반 참여자가 퀴즈존을 나갈 수 있어야 합니다', async () => { + const mockQuizZoneWithPlayers = { + ...mockQuizZone, + players: new Map([ + ['player-1', mockPlayer], + ['player-2', { ...mockPlayer, id: 'player-2', nickname: 'player2' }], + ]), + }; + quizZoneService.findOne.mockResolvedValue(mockQuizZoneWithPlayers); + + const result = await service.leaveQuizZone('test-zone', 'player-1'); + + expect(result).toEqual({ + isHost: false, + playerIds: ['player-2'], + }); + }); + + it('방장이 나가면 퀴즈존이 삭제되어야 합니다', async () => { + const mockQuizZoneWithPlayers = { + ...mockQuizZone, + players: new Map([ + ['player-1', mockPlayer], + ['player-2', { ...mockPlayer, id: 'player-2', nickname: 'player2' }], + ]), + }; + quizZoneService.findOne.mockResolvedValue(mockQuizZoneWithPlayers); + + const result = await service.leaveQuizZone('test-zone', 'host-1'); + + expect(result).toEqual({ + isHost: true, + playerIds: ['player-1', 'player-2'], + }); + expect(quizZoneService.clearQuizZone).toHaveBeenCalledWith('test-zone'); + }); + it('게임 진행 중에는 나갈 수 없어야 합니다', async () => { + const inProgressQuizZone = { + ...mockQuizZone, + stage: QUIZ_ZONE_STAGE.IN_PROGRESS, + players: new Map([['player-1', { ...mockPlayer, state: PLAYER_STATE.PLAY }]]), + }; + quizZoneService.findOne.mockResolvedValue(inProgressQuizZone); + + await expect(service.leaveQuizZone('test-zone', 'player-1')).rejects.toThrow( + new BadRequestException('게임이 진행중입니다.'), + ); + }); + }); + + describe('summaryQuizZone', () => { + const now = Date.now(); + + it('퀴즈존의 결과를 정상적으로 반환해야 합니다', async () => { + const mockSubmits = [ + { index: 0, answer: '정답1', submittedAt: now - 2000 }, + { index: 1, answer: '정답2', submittedAt: now - 1000 }, + ]; + + const mockPlayerWithResults = { + ...mockPlayer, + state: PLAYER_STATE.SUBMIT, + score: 2, + submits: mockSubmits, + nickname: 'player1', + }; + + const quizZoneWithResults = { + ...mockQuizZone, + stage: QUIZ_ZONE_STAGE.RESULT, + currentQuizIndex: 1, + players: new Map([ + ['player-1', mockPlayerWithResults], + [ + 'player-2', + { + ...mockPlayer, + id: 'player-2', + nickname: 'player2', + score: 1, + submits: [ + { index: 0, answer: '정답1', submittedAt: now - 1500 }, + { index: 1, answer: '오답', submittedAt: now - 500 }, + ], + }, + ], + ]), + }; + + quizZoneService.findOne.mockResolvedValue(quizZoneWithResults); + + const result = await service.summaryQuizZone('test-zone'); + + expect(result).toEqual([ + { + id: 'player-1', + score: 2, + submits: mockSubmits, + quizzes: mockQuizZone.quizzes, + ranks: [ + { id: 'player-1', nickname: 'player1', score: 2, ranking: 1 }, + { id: 'player-2', nickname: 'player2', score: 1, ranking: 2 }, + ], + endSocketTime: expect.any(Number), + }, + { + id: 'player-2', + score: 1, + submits: expect.arrayContaining([ + expect.objectContaining({ + index: 0, + answer: '정답1', + }), + expect.objectContaining({ + index: 1, + answer: '오답', + }), + ]), + quizzes: mockQuizZone.quizzes, + ranks: [ + { id: 'player-1', nickname: 'player1', score: 2, ranking: 1 }, + { id: 'player-2', nickname: 'player2', score: 1, ranking: 2 }, + ], + endSocketTime: expect.any(Number), + }, + ]); + + }); + + it('동점자가 있는 경우 동일한 순위가 부여되어야 합니다', async () => { + const quizZoneWithTiedScores = { + ...mockQuizZone, + stage: QUIZ_ZONE_STAGE.RESULT, + players: new Map([ + [ + 'player-1', + { + ...mockPlayer, + id: 'player-1', + nickname: 'player1', + score: 2, + submits: [], + }, + ], + [ + 'player-2', + { + ...mockPlayer, + id: 'player-2', + nickname: 'player2', + score: 2, + submits: [], + }, + ], + [ + 'player-3', + { + ...mockPlayer, + id: 'player-3', + nickname: 'player3', + score: 1, + submits: [], + }, + ], + ]), + }; + + quizZoneService.findOne.mockResolvedValue(quizZoneWithTiedScores); + + const result = await service.summaryQuizZone('test-zone'); + + const expectedRanks = [ + { id: 'player-1', nickname: 'player1', score: 2, ranking: 1 }, + { id: 'player-2', nickname: 'player2', score: 2, ranking: 1 }, + { id: 'player-3', nickname: 'player3', score: 1, ranking: 3 }, + ]; + + expect(result[0].ranks).toEqual(expectedRanks); + expect(result[1].ranks).toEqual(expectedRanks); + expect(result[2].ranks).toEqual(expectedRanks); + }); + + it('플레이어가 없는 경우 빈 배열을 반환해야 합니다', async () => { + const emptyQuizZone = { + ...mockQuizZone, + stage: QUIZ_ZONE_STAGE.RESULT, + players: new Map(), + }; + quizZoneService.findOne.mockResolvedValue(emptyQuizZone); + + const result = await service.summaryQuizZone('test-zone'); + + expect(result).toEqual([]); + }); + + it('모든 플레이어의 퀴즈 제출 기록이 포함되어야 합니다', async () => { + const mockPlayer1Submits = [ + { index: 0, answer: '정답1', submittedAt: now - 2000 }, + { index: 1, answer: undefined, submittedAt: now - 1000 }, // 시간 초과로 미제출 + ]; + + const mockPlayer2Submits = [ + { index: 0, answer: '오답', submittedAt: now - 1500 }, + { index: 1, answer: '정답2', submittedAt: now - 500 }, + ]; + + const quizZoneWithMixedResults = { + ...mockQuizZone, + stage: QUIZ_ZONE_STAGE.RESULT, + currentQuizIndex: 1, + players: new Map([ + [ + 'player-1', + { + ...mockPlayer, + id: 'player-1', + nickname: 'player1', + score: 1, + submits: mockPlayer1Submits, + }, + ], + [ + 'player-2', + { + ...mockPlayer, + id: 'player-2', + nickname: 'player2', + score: 1, + submits: mockPlayer2Submits, + }, + ], + ]), + }; + + quizZoneService.findOne.mockResolvedValue(quizZoneWithMixedResults); + + const result = await service.summaryQuizZone('test-zone'); + + const expectedRanks = [ + { id: 'player-1', nickname: 'player1', score: 1, ranking: 1 }, + { id: 'player-2', nickname: 'player2', score: 1, ranking: 1 }, + ]; + + expect(result).toEqual([ + { + id: 'player-1', + score: 1, + submits: mockPlayer1Submits, + quizzes: mockQuizZone.quizzes, + ranks: expectedRanks, + endSocketTime: expect.any(Number), + }, + { + id: 'player-2', + score: 1, + submits: mockPlayer2Submits, + quizzes: mockQuizZone.quizzes, + ranks: expectedRanks, + endSocketTime: expect.any(Number), + }, + ]); + }); + }); + + describe('chatQuizZone', () => { + it('플레이어가 플레이 상태일 때는 채팅을 제출할 수 없어야 합니다.', async () => { + const inPlayPlayer = { + ...mockPlayer, + state: PLAYER_STATE.PLAY, + }; + + const inPlayQuizZone = { + ...mockQuizZone, + players: new Map([['player-1', inPlayPlayer]]), + }; + + quizZoneService.findOne.mockResolvedValue(inPlayQuizZone); + + await expect(service.chatQuizZone('player-1', 'test-zone')).rejects.toThrow( + new BadRequestException('채팅을 제출한 플레이어 상태가 PLAY입니다.'), + ); + }); + + it('플레이어가 플레이 상태가 아닐 때는 채팅을 제출할 수 있어야 합니다.', async () => { + const inWaitPlayer = { + ...mockPlayer, + state: PLAYER_STATE.WAIT, + }; + + const inWaitQuizZone = { + ...mockQuizZone, + players: new Map([['player-1', inWaitPlayer]]), + }; + + quizZoneService.findOne.mockResolvedValue(inWaitQuizZone); + + const result = await service.chatQuizZone('player-1', 'test-zone'); + + expect(result).toEqual(['player-1']); + }); + }); + + describe('changeNickname', () => { + it('Lobby에 있는 wait상태의 참여자는 닉네임을 변경할 수 있다.', async () => { + const players = new Map([ + ['player-1', mockPlayer], + ['player-2', { ...mockPlayer, id: 'player-2', nickname: 'player2' }], + ]); + const mockQuizZoneWithPlayers = { + ...mockQuizZone, + players: players, + }; + quizZoneService.findOne.mockResolvedValue(mockQuizZoneWithPlayers); + + const result = await service.changeNickname('test-zone', 'player-2', 'new-nickname'); + expect(players.get('player-2').nickname).toEqual('new-nickname'); + }); + + it('퀴즈존이 Lobby, 사용자는 Wait 상태에서만 닉네임을 변경할 수 있다.', async () => { + const mockQuizZoneWithPlayers = { + ...mockQuizZone, + stage: QUIZ_ZONE_STAGE.IN_PROGRESS, + players: new Map([ + ['player-1', mockPlayer], + [ + 'player-2', + { + ...mockPlayer, + id: 'player-2', + nickname: 'player2', + state: PLAYER_STATE.WAIT, + }, + ], + ]), + }; + quizZoneService.findOne.mockResolvedValue(mockQuizZoneWithPlayers); + + await expect( + service.changeNickname('test-zone', 'player-2', 'new-nickname'), + ).rejects.toThrow(new BadRequestException('현재 닉네임을 변경할 수 없습니다.')); + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); +}); diff --git a/apps/backend/src/play/play.service.ts b/apps/backend/src/play/play.service.ts new file mode 100644 index 0000000..fd033c3 --- /dev/null +++ b/apps/backend/src/play/play.service.ts @@ -0,0 +1,503 @@ +import { + BadRequestException, + Inject, + Injectable, + Logger, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; +import { QuizZoneService } from '../quiz-zone/quiz-zone.service'; +import { SubmittedQuiz } from '../quiz-zone/entities/submitted-quiz.entity'; +import { QuizZone } from '../quiz-zone/entities/quiz-zone.entity'; +import { CurrentQuizDto } from './dto/current-quiz.dto'; +import { PLAYER_STATE, QUIZ_ZONE_STAGE } from '../common/constants'; +import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; +import { RuntimeException } from '@nestjs/core/errors/exceptions'; +import { clearTimeout } from 'node:timers'; +import { Player } from '../quiz-zone/entities/player.entity'; +import { CurrentQuizResultDto } from './dto/current-quiz-result.dto'; + +@Injectable() +export class PlayService { + constructor( + @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, + private readonly quizZoneService: QuizZoneService, + @Inject('PlayInfoStorage') private readonly plays: Map, + ) {} + + async joinQuizZone(quizZoneId: string, sessionId: string) { + const { players } = await this.quizZoneService.findOne(quizZoneId); + + if (!players.has(sessionId)) { + throw new NotFoundException('참여하지 않은 사용자입니다.'); + } + + return { + currentPlayer: players.get(sessionId), + players: [...players.values()], + }; + } + + async startQuizZone(quizZoneId: string, clientId: string) { + const quizZone = await this.quizZoneService.findOne(quizZoneId); + const { hostId, stage, players } = quizZone; + + if (hostId !== clientId) { + throw new UnauthorizedException('방장만 퀴즈를 시작할 수 있습니다.'); + } + + if (stage !== QUIZ_ZONE_STAGE.LOBBY) { + throw new BadRequestException('이미 시작된 퀴즈존입니다.'); + } + + await this.quizZoneService.updateQuizZone(quizZoneId, { + ...quizZone, + stage: QUIZ_ZONE_STAGE.IN_PROGRESS, + }); + + return [...players.values()].map((player) => player.id); + } + + /** + * 다음 퀴즈를 준비하고 타이밍과 퀴즈 데이터를 반환합니다. + * @param quizZoneId - 퀴즈 존 ID + * @param timeoutHandle - 타임아웃이 발생하면 실행되어야할 콜백 함수 + * @returns 퀴즈 존에 설정된 인터벌 시간과 다음 퀴즈 데이터를 포함하는 객체 + * @throws {RuntimeException} 더 이상 진행할 퀴즈가 없을 경우 예외가 발생합니다. + * @throws {NotFoundException} 퀴즈존 정보가 없을 경우 예외가 발생합니다.. + */ + async playNextQuiz(quizZoneId: string, timeoutHandle: Function) { + const quizZone = await this.quizZoneService.findOne(quizZoneId); + const { players, intervalTime } = quizZone; + + const currentQuizResult = this.getCurrentQuizResult(quizZone); + + const nextQuiz = await this.nextQuiz(quizZoneId); + + await this.quizZoneService.updateQuizZone(quizZoneId, { + ...quizZone, + players: new Map( + [...players].map(([id, player]) => [id, { ...player, state: PLAYER_STATE.WAIT }]), + ), + }); + + setTimeout(() => { + this.quizZoneService.updateQuizZone(quizZoneId, { + ...quizZone, + players: new Map( + [...players].map(([id, player]) => [ + id, + { ...player, state: PLAYER_STATE.PLAY }, + ]), + ), + }); + }, intervalTime); + + this.setQuizZoneHandle( + quizZoneId, + () => { + this.quizTimeOut(quizZoneId); + timeoutHandle(); + }, + intervalTime + nextQuiz.playTime, + ); + + return { + nextQuiz, + playerIds: [...players.values()].map((player) => player.id), + currentQuizResult, + }; + } + + private getCurrentQuizResult(quizZone: QuizZone): CurrentQuizResultDto { + const { players, currentQuizIndex } = quizZone; + const currentQuizResult = {} as CurrentQuizResultDto; + + if (currentQuizIndex === -1) { + currentQuizResult['answer'] = undefined; + currentQuizResult['totalPlayerCount'] = 0; + currentQuizResult['correctPlayerCount'] = 0; + } else if (currentQuizIndex >= 0) { + const answer: string = quizZone.quizzes.at(currentQuizIndex).answer.replace(/\s/g, ''); + currentQuizResult['answer'] = answer; + currentQuizResult['totalPlayerCount'] = players.size; + currentQuizResult['correctPlayerCount'] = [...players.values()].filter( + (player) => player.submits[currentQuizIndex]?.answer.replace(/\s/g, '') === answer, + ).length; + } + return currentQuizResult; + } + /** + * 퀴즈 존에서 다음 퀴즈를 불러오고 퀴즈 타이밍을 업데이트합니다. + * @param quizZoneId - 퀴즈 존 ID + * @returns 출제할 퀴즈에 대한 정보를 포함하는 객체를 반환 + * @throws {NotFoundException} 모든 퀴즈가 이미 진행되었을 경우 예외가 발생합니다. + */ + private async nextQuiz(quizZoneId: string): Promise { + const quizZone = await this.quizZoneService.findOne(quizZoneId); + quizZone.currentQuizIndex++; + + const { quizzes, currentQuizIndex, intervalTime } = quizZone; + + if (currentQuizIndex >= quizzes.length) { + throw new RuntimeException('모든 퀴즈를 출제하였습니다.'); + } + + const nextQuiz = quizzes.at(currentQuizIndex); + + quizZone.currentQuizStartTime = Date.now() + intervalTime; + quizZone.currentQuizDeadlineTime = quizZone.currentQuizStartTime + nextQuiz.playTime; + + return { + stage: quizZone.stage, + question: nextQuiz.question, + currentIndex: quizZone.currentQuizIndex, + playTime: nextQuiz.playTime, + startTime: quizZone.currentQuizStartTime, + deadlineTime: quizZone.currentQuizDeadlineTime, + }; + } + + async finishQuizZone(quizZoneId: string) { + const quizZone = await this.quizZoneService.findOne(quizZoneId); + quizZone.stage = QUIZ_ZONE_STAGE.RESULT; + return [...quizZone.players.values()].map((player) => player.id); + } + + /** + * 퀴즈 시간이 초과될 경우 퀴즈에 대해 미제출 답변으로 제출합니다. + * @param quizZoneId - 퀴즈 존 ID. + */ + private async quizTimeOut(quizZoneId: string) { + const quizZone = await this.quizZoneService.findOne(quizZoneId); + const { players } = quizZone; + + players.forEach((player) => { + if (player.state === PLAYER_STATE.PLAY) { + this.submitQuiz(quizZone, player.id); + } + }); + + this.clearQuizZoneHandle(quizZoneId); + + return { + playerIds: [...players.values()].map(({ id }) => id), + }; + } + + /** + * 특정 퀴즈 존에서 현재 퀴즈에 대한 답변을 제출합니다. + * @param quizZoneId - 퀴즈 존 ID + * @param clientId - 플레이어 ID + * @param submittedQuiz - 제출된 퀴즈의 답변과 메타데이터 + * @throws {BadRequestException} 답변을 제출할 수 없는 경우 예외가 발생합니다. + */ + async submit(quizZoneId: string, clientId: string, submittedQuiz: SubmittedQuiz) { + const quizZone = await this.quizZoneService.findOne(quizZoneId); + const { stage, players } = quizZone; + + if (stage !== QUIZ_ZONE_STAGE.IN_PROGRESS) { + throw new BadRequestException('퀴즈를 제출할 수 없습니다.'); + } + + const submittedCount = [...players.values()].filter( + (player) => player.state === PLAYER_STATE.SUBMIT, + ).length; + + submittedQuiz.submitRank = submittedCount + 1; + + this.submitQuiz(quizZone, clientId, submittedQuiz); + + const submittedPlayers = [...players.values()].filter( + (player) => player.state === PLAYER_STATE.SUBMIT, + ); + + const fastestPlayerIds = this.getFastestPlayerIds( + submittedPlayers, + quizZone.currentQuizIndex, + ); + + const isLastSubmit = [...players.values()].every( + ({ state }) => state === PLAYER_STATE.SUBMIT, + ); + + isLastSubmit && this.clearQuizZoneHandle(quizZoneId); + + return { + isLastSubmit, + fastestPlayerIds, + submittedCount: submittedPlayers.length, + totalPlayerCount: players.size, + otherSubmittedPlayerIds: submittedPlayers + .filter((player) => player.id !== clientId) + .map(({ id }) => id), + }; + } + + private getFastestPlayerIds(submittedPlayers: Player[], currentQuizIndex: number, count = 3) { + return submittedPlayers + .sort( + (a, b) => + a.submits[currentQuizIndex].submittedAt - + b.submits[currentQuizIndex].submittedAt, + ) + .slice(0, count) + .map(({ id }) => id); + } + + /** + * 제출된 퀴즈 답변을 처리합니다. + * @param quizZone - 퀴즈 존 객체 + * @param clientId - 플레이어 ID + * @param submitQuiz - (선택적) 제출된 퀴즈 데이터(사용하지 않을 경우 미제출 답변) + * @throws {BadRequestException} 플레이어가 답변을 제출할 수 없는 상태일 경우 예외가 발생합니다. + */ + private submitQuiz(quizZone: QuizZone, clientId: string, submitQuiz?: SubmittedQuiz) { + const { players, currentQuizIndex, quizzes, currentQuizDeadlineTime } = quizZone; + const quiz = quizzes.at(currentQuizIndex); + const player = players.get(clientId); + + if (player.state !== PLAYER_STATE.PLAY) { + throw new BadRequestException('정답을 제출할 수 없습니다.'); + } + + const now = Date.now(); + + const submittedQuiz = { + index: currentQuizIndex, + answer: '', + submittedAt: now, + receivedAt: now, + submitRank: players.size, + ...submitQuiz, + }; + + player.submits.push(submittedQuiz); + + if ( + quiz.answer.replace(/\s/g, '') === submittedQuiz.answer.replace(/\s/g, '') && + submittedQuiz.submittedAt <= currentQuizDeadlineTime + ) { + player.score++; + } + + player.state = PLAYER_STATE.SUBMIT; + } + + /** + * 퀴즈 존에서 사용자의 퀴즈 진행 요약 결과를 제공합니다. + * @param quizZoneId - 퀴즈 존 ID + * @param socketConnectTime - 퀴즈 결과 시간 socket 연결 시간 + * @returns 퀴즈 결과 요약 DTO를 포함한 Promise + */ + async summaryQuizZone(quizZoneId: string, socketConnectTime: number = 30 * 1000) { + const quizZone = await this.quizZoneService.findOne(quizZoneId); + const { players, quizzes } = quizZone; + + this.clearQuizZoneHandle(quizZoneId); + + const ranks = this.getRanking( + players, + quizzes.map((quiz) => quiz.answer), + ); + + const now = Date.now(); + const endSocketTime = now + socketConnectTime; + + + const summaries = [...players.values()].map(({ id, score, submits }) => ({ + id, + score, + submits, + quizzes, + ranks, + endSocketTime + })); + + quizZone.summaries = {ranks, endSocketTime}; + return summaries; + } + + public clearQuizZone(quizZoneId: string) { + this.quizZoneService.clearQuizZone(quizZoneId); + } + + private calculateQuizRanks( + players: Map, + quizAnswers: string[], + ): Map { + const quizRanks = new Map(); + quizAnswers.forEach((answer, quizIndex) => { + const sortedCorrectPlayerIds = [...players.values()] + .filter((player) => player.submits[quizIndex]?.answer === answer) + .sort((a, b) => a.submits[quizIndex].submitRank - b.submits[quizIndex].submitRank) + .map((player) => player.id); + + quizRanks.set(quizIndex, sortedCorrectPlayerIds); + }); + + return quizRanks; + } + private getPlayersCorrectRankCount( + players: Map, + quizRanks: Map, + ) { + return new Map( + [...players.keys()].map((playerId) => { + const rankCounts = new Map(); + + quizRanks.forEach((correctPlayers) => { + const rank = correctPlayers.indexOf(playerId) + 1; + if (rank > 0) { + rankCounts.set(rank, (rankCounts.get(rank) || 0) + 1); + } + }); + + return [playerId, rankCounts]; + }), + ); + } + + private compareRankCounts( + playerARankCount: Map, + playerBRankCount: Map, + ): number { + const maxRank = Math.max(...[...playerARankCount.keys()], ...[...playerBRankCount.keys()]); + + for (let rank = 1; rank <= maxRank; rank++) { + const aCount = playerARankCount.get(rank) || 0; + const bCount = playerBRankCount.get(rank) || 0; + if (aCount !== bCount) { + return bCount - aCount; + } + } + return 0; + } + + private compareRanks( + currentPlayer: Player, + prevPlayer: Player, + currentRankCount: Map, + prevRankCount: Map, + ): boolean { + if (currentPlayer.score !== prevPlayer.score) { + return true; + } + return this.compareRankCounts(currentRankCount, prevRankCount) !== 0; + } + + private getRanking(players: Map, quizAnswers: string[]) { + const quizRanks = this.calculateQuizRanks(players, quizAnswers); + + const playersCorrectRankCount = this.getPlayersCorrectRankCount(players, quizRanks); + + const sortedPlayers = [...players.values()].sort((playerA, playerB) => { + if (playerB.score !== playerA.score) { + return playerB.score - playerA.score; + } + + return this.compareRankCounts( + playersCorrectRankCount.get(playerA.id), + playersCorrectRankCount.get(playerB.id), + ); + }); + let currentRank = 1; + let sameRankCount = 0; + + return sortedPlayers.map((player, index) => { + if (index > 0) { + const prevPlayer = sortedPlayers[index - 1]; + if ( + this.compareRanks( + player, + prevPlayer, + playersCorrectRankCount.get(player.id), + playersCorrectRankCount.get(prevPlayer.id), + ) + ) { + currentRank += sameRankCount + 1; + sameRankCount = 0; + } else { + sameRankCount++; + } + } + + return { + id: player.id, + nickname: player.nickname, + score: player.score, + ranking: currentRank, + }; + }); + } + + async leaveQuizZone(quizZoneId: string, clientId: string) { + const quizZone = await this.quizZoneService.findOne(quizZoneId); + const { stage, hostId, players } = quizZone; + + const isHost = hostId === clientId; + + if (stage !== QUIZ_ZONE_STAGE.LOBBY) { + throw new BadRequestException('게임이 진행중입니다.'); + } + + if (isHost) { + await this.quizZoneService.clearQuizZone(quizZoneId); + } else { + players.delete(clientId); + } + + return { + isHost, + playerIds: [...players.values()].map((player) => player.id), + }; + } + + async chatQuizZone(clientId: string, quizZoneId: string) { + const quizZone = await this.quizZoneService.findOne(quizZoneId); + const { players } = quizZone; + + if (players.get(clientId).state === PLAYER_STATE.PLAY) { + throw new BadRequestException('채팅을 제출한 플레이어 상태가 PLAY입니다.'); + } + + return [...players.values()] + .filter((player) => player.state !== PLAYER_STATE.PLAY) + .map((player) => player.id); + } + + private setQuizZoneHandle(quizZoneId: string, handle: Function, time: number) { + this.plays.set( + quizZoneId, + setTimeout(() => handle(), time), + ); + } + + private clearQuizZoneHandle(quizZoneId: string) { + const submitHandle = this.plays.get(quizZoneId); + + clearTimeout(submitHandle); + this.plays.set(quizZoneId, undefined); + } + + async changeNickname(quizZoneId: string, clientId: string, changedNickname: string) { + const quizZone = await this.quizZoneService.findOne(quizZoneId); + const { players } = quizZone; + + if (!players.has(clientId)) { + throw new NotFoundException('사용자 정보를 찾을 수 없습니다.'); + } + + const player = players.get(clientId); + + if (player.state !== PLAYER_STATE.WAIT || quizZone.stage !== QUIZ_ZONE_STAGE.LOBBY) { + throw new BadRequestException('현재 닉네임을 변경할 수 없습니다.'); + } + + player.nickname = changedNickname; + + return { + playerIds: [...players.values()].map((player) => player.id), + }; + } +} diff --git a/apps/backend/src/quiz-zone/dto/check-existing-quiz-zone.dto.ts b/apps/backend/src/quiz-zone/dto/check-existing-quiz-zone.dto.ts new file mode 100644 index 0000000..ed13a29 --- /dev/null +++ b/apps/backend/src/quiz-zone/dto/check-existing-quiz-zone.dto.ts @@ -0,0 +1,7 @@ +class CheckExistingQuizZoneDto { + constructor( + public readonly isDuplicateConnection: boolean, + public readonly newQuizZoneId: string, + public readonly existingQuizZoneId?: string, + ) {} +} diff --git a/apps/backend/src/quiz-zone/dto/create-quiz-zone.dto.ts b/apps/backend/src/quiz-zone/dto/create-quiz-zone.dto.ts new file mode 100644 index 0000000..bb25797 --- /dev/null +++ b/apps/backend/src/quiz-zone/dto/create-quiz-zone.dto.ts @@ -0,0 +1,32 @@ +import { IsInt, IsNotEmpty, IsString, Length, Matches, Max, Min } from 'class-validator'; + +/** + * 퀴즈존을 생성할 때 사용하는 DTO 클래스 + * + * 퀴즈존 ID는 다음 규칙을 따릅니다: + * - 5-10글자 길이 + * - 숫자와 알파벳 조합 + * - 중복 불가 (중복 체크 로직 추가 예정) + */ +export class CreateQuizZoneDto { + @IsString({ message: '핀번호가 없습니다.' }) + @Length(5, 10, { message: '핀번호는 5글자 이상 10글자 이하로 입력해주세요.' }) + @Matches(RegExp('^[a-zA-Z0-9]*$'), { message: '숫자와 알파벳 조합만 가능합니다.' }) + readonly quizZoneId: string; + + @IsString({ message: '제목이 없습니다.' }) + @Length(1, 100, { message: '제목은 1글자 이상 100글자 이하로 입력해주세요.' }) + readonly title: string; + + @IsString({ message: '설명이 없습니다.' }) + @Length(0, 300, { message: '설명은 300글자 이하로 입력해주세요.' }) + readonly description: string; + + @IsInt({ message: '최대 플레이어 수가 없습니다.' }) + @Min(1, { message: '최소 1명 이상이어야 합니다.' }) + @Max(300, { message: '최대 300명까지 가능합니다.' }) + readonly limitPlayerCount: number; + + @IsNotEmpty({ message: '퀴즈존을 선택해주세요.' }) + quizSetId: number; +} diff --git a/apps/backend/src/quiz-zone/dto/find-quiz-zone.dto.ts b/apps/backend/src/quiz-zone/dto/find-quiz-zone.dto.ts new file mode 100644 index 0000000..37e3b3d --- /dev/null +++ b/apps/backend/src/quiz-zone/dto/find-quiz-zone.dto.ts @@ -0,0 +1,53 @@ +import { PLAYER_STATE, QUIZ_ZONE_STAGE } from '../../common/constants'; +import { CurrentQuizDto } from '../../play/dto/current-quiz.dto'; +import { SubmittedQuiz } from '../entities/submitted-quiz.entity'; +import { ChatMessage } from 'src/chat/entities/chat-message.entity'; +import { Rank } from '../../play/entities/rank.entity'; +import { Quiz } from '../entities/quiz.entity'; + +/** + * 퀴즈 게임에 참여하는 플레이어 엔티티 + * + * @property id: 플레이어 세션 ID + * @property nickname: 플레이어의 닉네임 + * @property score: 플레이어의 점수 + * @property submits: 플레이어가 제출한 퀴즈 목록 + * @property state: 플레이어의 현재 상태 + */ +export interface Player { + id: string; + nickname: string; + score?: number; + submits?: SubmittedQuiz[]; + state: PLAYER_STATE; +} + +/** + * 퀴즈 존을 찾기 위한 DTO + * + * @property currentPlayer - 현재 플레이어 + * @property title - 퀴즈 존의 제목 + * @property description - 퀴즈 존의 설명 + * @property quizCount - 퀴즈 존의 퀴즈 개수 + * @property stage - 퀴즈 존의 진행 상태 + * @property hostId - 퀴즈 존의 호스트 ID + * @property currentQuiz - 현재 출제 중인 퀴즈 + * @property maxPlayers - 퀴즈 존의 최대 플레이어 수 + */ +export interface FindQuizZoneDto { + readonly currentPlayer: Player; + readonly title: string; + readonly description: string; + readonly quizCount: number; + readonly stage: QUIZ_ZONE_STAGE; + readonly hostId: string; + readonly currentQuiz?: CurrentQuizDto; + readonly maxPlayers?: number; + readonly chatMessages?: ChatMessage[]; + + readonly ranks?: Rank[]; + readonly endSocketTime?: number; + readonly score?: number; + readonly quizzes?: Quiz[]; + readonly submits?: SubmittedQuiz[]; +} diff --git a/apps/backend/src/quiz-zone/entities/player.entity.ts b/apps/backend/src/quiz-zone/entities/player.entity.ts new file mode 100644 index 0000000..e3d5a03 --- /dev/null +++ b/apps/backend/src/quiz-zone/entities/player.entity.ts @@ -0,0 +1,18 @@ +import { SubmittedQuiz } from './submitted-quiz.entity'; +import { PLAYER_STATE } from '../../common/constants'; + +/** + * 퀴즈 게임에 참여하는 플레이어 엔티티 + * + * @property id: 플레이어 세션 ID + * @property score: 플레이어의 점수 + * @property submits: 플레이어가 제출한 퀴즈 목록 + * @property state: 플레이어의 현재 상태 + */ +export interface Player { + id: string; + nickname: string; + score: number; + submits: SubmittedQuiz[]; + state: PLAYER_STATE; +} diff --git a/apps/backend/src/quiz-zone/entities/quiz-zone.entity.ts b/apps/backend/src/quiz-zone/entities/quiz-zone.entity.ts new file mode 100644 index 0000000..4e6bbbf --- /dev/null +++ b/apps/backend/src/quiz-zone/entities/quiz-zone.entity.ts @@ -0,0 +1,33 @@ +import { Quiz } from './quiz.entity'; +import { Player } from './player.entity'; +import { QUIZ_ZONE_STAGE } from '../../common/constants'; +import { QuizSummary } from '../../play/entities/quiz-summary.entity'; +/** + * 퀴즈 게임을 진행하는 공간을 나타내는 퀴즈존 인터페이스 + * + * @property players 플레이어 목록 + * @property hostId 퀴즈 존을 생성한 관리자 ID + * @property maxPlayers 퀴즈 존의 최대 플레이어 수 + * @property quizzes 퀴즈 목록 + * @property stage 퀴즈 존의 현재 상태 + * @property title 퀴즈 세트의 제목 + * @property description 퀴즈 세트의 설명 + * @property currentQuizIndex 현재 출제 중인 퀴즈의 인덱스 + * @property currentQuizStartTime 현재 퀴즈의 출제 시작 시간 + * @property currentQuizDeadlineTime 현재 퀴즈의 제출 마감 시간 + * @property intervalTime 퀴즈 간의 간격 시간 + */ +export interface QuizZone { + players: Map; + hostId: string; + maxPlayers: number; + quizzes: Quiz[]; + stage: QUIZ_ZONE_STAGE; + title: string; + description: string; + currentQuizIndex: number; + currentQuizStartTime: number; + currentQuizDeadlineTime: number; + intervalTime: number; + summaries?: QuizSummary; +} diff --git a/apps/backend/src/quiz-zone/entities/quiz.entity.ts b/apps/backend/src/quiz-zone/entities/quiz.entity.ts new file mode 100644 index 0000000..7da76fb --- /dev/null +++ b/apps/backend/src/quiz-zone/entities/quiz.entity.ts @@ -0,0 +1,15 @@ +import { QUIZ_TYPE } from '../../common/constants'; + +/** + * 퀴즈 엔티티 + * + * @property question: 퀴즈의 질문 + * @property answer: 퀴즈의 정답 + * @property playTime: 퀴즈의 플레이 시간 + */ +export interface Quiz { + question: string; + answer: string; + playTime: number; + quizType: QUIZ_TYPE; +} diff --git a/apps/backend/src/quiz-zone/entities/submitted-quiz.entity.ts b/apps/backend/src/quiz-zone/entities/submitted-quiz.entity.ts new file mode 100644 index 0000000..1b81796 --- /dev/null +++ b/apps/backend/src/quiz-zone/entities/submitted-quiz.entity.ts @@ -0,0 +1,15 @@ +/** + * 플레이어가 제출한 퀴즈 엔티티 + * + * @property index: 퀴즈의 인덱스 + * @property answer: 플레이어가 제출한 답 + * @property submittedAt: 플레이어가 제출한 시각 + * @property receivedAt: 플레이어가 제출한 시각 + */ +export interface SubmittedQuiz { + index: number; + answer?: string; + submittedAt?: number; + receivedAt?: number; + submitRank?: number; +} diff --git a/apps/backend/src/quiz-zone/quiz-zone.controller.spec.ts b/apps/backend/src/quiz-zone/quiz-zone.controller.spec.ts new file mode 100644 index 0000000..0f307e3 --- /dev/null +++ b/apps/backend/src/quiz-zone/quiz-zone.controller.spec.ts @@ -0,0 +1,112 @@ +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { QuizZoneController } from './quiz-zone.controller'; +import { QuizZoneService } from './quiz-zone.service'; +import { CreateQuizZoneDto } from './dto/create-quiz-zone.dto'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ChatService } from '../chat/chat.service'; + +describe('QuizZoneController', () => { + let controller: QuizZoneController; + let service: QuizZoneService; + + const mockQuizZoneService = { + create: jest.fn(), + getQuizZoneInfo: jest.fn(), + }; + + const mockChatService = { + set: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [QuizZoneController], + providers: [ + { + provide: QuizZoneService, + useValue: mockQuizZoneService, + }, + { + provide: ChatService, + useValue: mockChatService, + }, + ], + }).compile(); + + controller = module.get(QuizZoneController); + service = module.get(QuizZoneService); + }); + + describe('create', () => { + const createQuizZoneDto: CreateQuizZoneDto = { + quizZoneId: 'test123', + title: 'Test Quiz', + description: 'Test Description', + limitPlayerCount: 8, + quizSetId: 1, + }; + + it('세션 정보가 없으면 BadRequestException을 던진다', async () => { + await expect(controller.create(createQuizZoneDto, {})).rejects.toThrow( + BadRequestException, + ); + }); + + it('퀴즈존을 성공적으로 생성한다', async () => { + const session = { id: 'sessionId' }; + const { quizZoneId } = createQuizZoneDto; + + await controller.create(createQuizZoneDto, session); + + expect(service.create).toHaveBeenCalledWith(createQuizZoneDto, session.id); + expect(mockChatService.set).toHaveBeenCalledWith(quizZoneId); + }); + + it('퀴즈존의 세션 아이디가 중복되면 예외가 발생한다.', async () => { + const session = { id: 'sessionId' }; + mockQuizZoneService.create.mockRejectedValue(new BadRequestException()); + + await expect(controller.create(createQuizZoneDto, session)).rejects.toThrow( + BadRequestException, + ); + }); + }); + + describe('findQuizZoneInfo', () => { + const quizZoneId = 'test123'; + const mockQuizZoneInfo = { + title: 'Test Quiz', + description: 'Test Description', + quizCount: 5, + stage: 'LOBBY', + playerCount: 1, + maxPlayers: 8, + serverTime: 0, + }; + + Date.now = jest.fn().mockReturnValue(0); + + it('퀴즈존 정보를 성공적으로 조회한다', async () => { + const session = { id: 'sessionId' }; + mockQuizZoneService.getQuizZoneInfo.mockResolvedValue(mockQuizZoneInfo); + + const result = await controller.findQuizZoneInfo(session, quizZoneId); + + expect(service.getQuizZoneInfo).toHaveBeenCalledWith(session.id, quizZoneId, undefined); + expect(result).toEqual(mockQuizZoneInfo); + }); + + it('퀴즈존 정보가 없으면 NotFoundException 던진다', async () => { + const session = { id: 'sessionId' }; + mockQuizZoneService.getQuizZoneInfo.mockRejectedValue(new NotFoundException()); + + await expect(controller.findQuizZoneInfo(session, quizZoneId)).rejects.toThrow( + NotFoundException, + ); + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); +}); diff --git a/apps/backend/src/quiz-zone/quiz-zone.controller.ts b/apps/backend/src/quiz-zone/quiz-zone.controller.ts new file mode 100644 index 0000000..ab8d6da --- /dev/null +++ b/apps/backend/src/quiz-zone/quiz-zone.controller.ts @@ -0,0 +1,83 @@ +import { + BadRequestException, + Body, + Controller, + Get, + HttpCode, + Param, + Post, + Session, +} from '@nestjs/common'; +import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { QuizZoneService } from './quiz-zone.service'; +import { ChatService } from '../chat/chat.service'; +import { CreateQuizZoneDto } from './dto/create-quiz-zone.dto'; + +@ApiTags('Quiz Zone') +@Controller('quiz-zone') +export class QuizZoneController { + constructor( + private readonly quizZoneService: QuizZoneService, + private readonly chatService: ChatService, + ) {} + + @Post() + @HttpCode(201) + @ApiOperation({ summary: '새로운 퀴즈존 생성' }) + @ApiResponse({ status: 201, description: '퀴즈존이 성공적으로 생성되었습니다.' }) + @ApiResponse({ status: 400, description: '세션 정보가 없습니다.' }) + async create( + @Body() createQuizZoneDto: CreateQuizZoneDto, + @Session() session: Record, + ): Promise { + if (!session || !session.id) { + throw new BadRequestException('세션 정보가 없습니다.'); + } + const hostId = session.id; + await this.quizZoneService.create(createQuizZoneDto, hostId); + await this.chatService.set(createQuizZoneDto.quizZoneId); + } + + @Get('check/:quizZoneId') + @HttpCode(200) + @ApiOperation({ summary: '사용자 참여중인 퀴즈존 정보 확인' }) + @ApiParam({ name: 'id', description: '퀴즈존의 ID' }) + @ApiResponse({ + status: 200, + description: '기존 참여 정보가 성공적으로 반환되었습니다.', + }) + @ApiResponse({ status: 400, description: '세션 정보가 없습니다.' }) + async checkExistingQuizZoneParticipation( + @Session() session: Record, + @Param('quizZoneId') quizZoneId: string, + ) { + const sessionQuizZoneId = session.quizZoneId; + return sessionQuizZoneId === undefined || sessionQuizZoneId === quizZoneId; + } + + @Get(':quizZoneId') + @HttpCode(200) + @ApiOperation({ summary: '사용자에 대한 퀴즈존 정보 반환' }) + @ApiParam({ name: 'id', description: '퀴즈존의 ID' }) + @ApiResponse({ + status: 200, + description: '대기실 정보가 성공적으로 반환되었습니다.', + }) + @ApiResponse({ status: 400, description: '세션 정보가 없습니다.' }) + async findQuizZoneInfo( + @Session() session: Record, + @Param('quizZoneId') quizZoneId: string, + ) { + const serverTime = Date.now(); + const quizZoneInfo = await this.quizZoneService.getQuizZoneInfo( + session.id, + quizZoneId, + session.quizZoneId, + ); + session['quizZoneId'] = quizZoneId; + return { + ...quizZoneInfo, + serverTime, + }; + } +} diff --git a/apps/backend/src/quiz-zone/quiz-zone.module.ts b/apps/backend/src/quiz-zone/quiz-zone.module.ts new file mode 100644 index 0000000..6394170 --- /dev/null +++ b/apps/backend/src/quiz-zone/quiz-zone.module.ts @@ -0,0 +1,25 @@ +import { Module } from '@nestjs/common'; +import { QuizZoneService } from './quiz-zone.service'; +import { QuizZoneController } from './quiz-zone.controller'; +import { QuizZoneRepositoryMemory } from './repository/quiz-zone.memory.repository'; +import { QuizService } from '../quiz/quiz.service'; +import { QuizModule } from '../quiz/quiz.module'; +import { ChatModule } from 'src/chat/chat.module'; + +@Module({ + controllers: [QuizZoneController], + imports: [QuizModule, ChatModule], + providers: [ + QuizZoneService, + { + provide: 'QuizZoneRepository', + useClass: QuizZoneRepositoryMemory, + }, + { + provide: 'QuizZoneStorage', + useValue: new Map(), + }, + ], + exports: [QuizZoneService], +}) +export class QuizZoneModule {} diff --git a/apps/backend/src/quiz-zone/quiz-zone.service.spec.ts b/apps/backend/src/quiz-zone/quiz-zone.service.spec.ts new file mode 100644 index 0000000..b6a8e13 --- /dev/null +++ b/apps/backend/src/quiz-zone/quiz-zone.service.spec.ts @@ -0,0 +1,789 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { QuizZoneService } from './quiz-zone.service'; +import { BadRequestException, ConflictException, NotFoundException } from '@nestjs/common'; +import { QuizZone } from './entities/quiz-zone.entity'; +import { IQuizZoneRepository } from './repository/quiz-zone.repository.interface'; +import { Quiz } from './entities/quiz.entity'; +import { PLAYER_STATE, QUIZ_TYPE, QUIZ_ZONE_STAGE } from '../common/constants'; +import { QuizService } from '../quiz/quiz.service'; +import { ChatService } from '../chat/chat.service'; + +const nickNames: string[] = [ + '전설의고양이', + '피카츄꼬리', + '킹왕짱짱맨', + '용맹한기사단', + '무적의검사', + '그림자암살자', + '마법의마스터', + '불꽃의전사', + '어둠의기사', + '번개의제왕', +]; + +const playTime = 30_000; + +const quizzes: Quiz[] = [ + { + question: '포도가 자기소개하면?', + answer: '포도당', + playTime, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, + { + question: '고양이를 싫어하는 동물은?', + answer: '미어캣', + playTime, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, + { + question: '게를 냉동실에 넣으면?', + answer: '게으름', + playTime, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, + { + question: '오리를 생으로 먹으면?', + answer: '회오리', + playTime, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, + { + question: '네 사람이 동시에 오줌을 누면?', + answer: '포뇨', + playTime, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, + { + question: '지브리가 뭘로 돈 벌게요?', + answer: '토토로', + playTime, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, +]; + +describe('QuizZoneService', () => { + let service: QuizZoneService; + let repository: IQuizZoneRepository; + let quizService: QuizService; + let chatService: ChatService; + const mockQuizZoneRepository = { + set: jest.fn(), + get: jest.fn(), + delete: jest.fn(), + has: jest.fn(), + }; + + const mockQuizService = { + createQuizzes: jest.fn(), + getQuizzes: jest.fn(), + updateQuiz: jest.fn(), + deleteQuiz: jest.fn(), + findQuizSet: jest.fn(), + findQuiz: jest.fn(), + }; + + const mockChatService = { + get: jest.fn(), + add: jest.fn(), + has: jest.fn(), + delete: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + QuizZoneService, + { + provide: 'QuizZoneRepository', + useValue: mockQuizZoneRepository, + }, + { + provide: QuizService, + useValue: mockQuizService, + }, + { + provide: ChatService, + useValue: mockChatService, + }, + ], + }).compile(); + + service = module.get(QuizZoneService); + repository = module.get('QuizZoneRepository'); + quizService = module.get(QuizService); + chatService = module.get(ChatService); // chatService 추가 + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('create', () => { + it('새로운 퀴즈존을 생성한다', async () => { + // given + const createQuizZoneDto = { + quizZoneId: 'test123', + title: '테스트 퀴즈', + description: '테스트용 퀴즈입니다', + limitPlayerCount: 10, + quizSetId: 1, + maxPlayers: 10, + }; + const adminId = 'adminId'; + const mockQuizzes = [ + { + question: '문제1', + answer: '답1', + playTime: 30000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, + { + question: '문제2', + answer: '답2', + playTime: 30000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, + ]; + + mockQuizZoneRepository.has.mockResolvedValue(false); + mockQuizService.getQuizzes.mockResolvedValue(mockQuizzes); + + // when + await service.create(createQuizZoneDto, adminId); + + // then + expect(repository.set).toHaveBeenCalledWith( + createQuizZoneDto.quizZoneId, + expect.objectContaining({ + hostId: adminId, + title: createQuizZoneDto.title, + description: createQuizZoneDto.description, + maxPlayers: 10, + stage: QUIZ_ZONE_STAGE.LOBBY, + currentQuizIndex: -1, + currentQuizStartTime: 0, + currentQuizDeadlineTime: 0, + intervalTime: 5000, + players: expect.any(Map), + quizzes: mockQuizzes.map((quiz) => ({ + ...quiz, + question: Buffer.from(quiz.question).toString('base64'), + playTime: quiz.playTime * 1000, + })), + }), + ); + }); + + it('이미 존재하는 퀴즈존 ID로 생성 시 ConflictException을 던진다', async () => { + // given + const createQuizZoneDto = { + quizZoneId: 'test123', + title: '테스트 퀴즈', + description: '테스트용 퀴즈입니다', + limitPlayerCount: 100, + quizSetId: 1, + }; + const adminId = 'adminId'; + + mockQuizZoneRepository.has.mockResolvedValue(true); + + // when & then + await expect(service.create(createQuizZoneDto, adminId)).rejects.toThrow( + ConflictException, + ); + }); + }); + + describe('findOne', () => { + const quizZoneId = 'testQuizZone'; + + it('퀴즈존을 ID로 조회한다', async () => { + // given + const mockQuizZone: QuizZone = { + players: new Map(), + hostId: 'adminId', + title: '테스트 퀴즈', + description: '테스트 퀴즈입니다', + maxPlayers: 10, + quizzes: quizzes.map((quiz) => ({ + ...quiz, + question: Buffer.from(quiz.question).toString('base64'), + })), + stage: QUIZ_ZONE_STAGE.LOBBY, + currentQuizIndex: -1, + currentQuizStartTime: 0, + currentQuizDeadlineTime: 0, + intervalTime: 5000, + }; + + mockQuizZoneRepository.get.mockResolvedValue(mockQuizZone); + + // when + const result = await (service as any).findOne(quizZoneId); + + // then + expect(repository.get).toHaveBeenCalledWith(quizZoneId); + expect(result).toEqual(mockQuizZone); + }); + + it('존재하지 않는 퀴즈존 조회 시 NotFoundException을 던진다', async () => { + // given + mockQuizZoneRepository.get.mockResolvedValue(null); + + // when & then + await expect((service as any).findOne(quizZoneId)).rejects.toThrow(NotFoundException); + expect(repository.get).toHaveBeenCalledWith(quizZoneId); + }); + }); + + describe('getQuizZoneInfo', () => { + const quizZoneId = 'testQuizZone'; + const clientId = 'clientId'; + + it('이미 접속해있는 퀴즈존이 있을때 다른 퀴즈존에 동시에 접속하면 기존 퀴즈존을 나가고 새로운 곳에 추가한다.', async () => { + // given + const newQuizZoneId = 'newQuizZone'; + const existingQuizZoneId = 'existingQuizZone'; + + // 기존 퀴즈존 설정 + const existingQuizZone: QuizZone = { + players: new Map([ + [ + clientId, + { + id: clientId, + nickname: nickNames[0], + state: PLAYER_STATE.WAIT, + score: 0, + submits: [], + }, + ], + [ + 'otherPlayer', + { + id: 'otherPlayer', + nickname: nickNames[1], + state: PLAYER_STATE.WAIT, + score: 0, + submits: [], + }, + ], + ]), + hostId: 'adminId', + maxPlayers: 10, + title: '기존 퀴즈', + description: '기존 퀴즈입니다', + quizzes: quizzes, + stage: QUIZ_ZONE_STAGE.LOBBY, + currentQuizIndex: -1, + currentQuizStartTime: 0, + currentQuizDeadlineTime: 0, + intervalTime: 5000, + }; + + // 새로운 퀴즈존 설정 + const newQuizZone = { + ...existingQuizZone, + players: new Map(), + title: '새로운 퀴즈', + description: '새로운 퀴즈입니다', + }; + + // Mock 설정 + mockQuizZoneRepository.has.mockResolvedValue(true); + mockQuizZoneRepository.get.mockImplementation((id) => { + if (id === existingQuizZoneId) return existingQuizZone; + if (id === newQuizZoneId) return newQuizZone; + }); + + // when + await service.getQuizZoneInfo(clientId, newQuizZoneId, existingQuizZoneId); + + // then + // 1. 기존 퀴즈존에서 해당 플레이어만 제거되었는지 확인 + expect(existingQuizZone.players.has(clientId)).toBeFalsy(); + expect(existingQuizZone.players.has('otherPlayer')).toBeTruthy(); + + // 2. 새로운 퀴즈존에 플레이어가 추가되었는지 확인 + const newPlayer = newQuizZone.players.get(clientId); + expect(newPlayer).toEqual({ + id: clientId, + nickname: expect.any(String), + state: PLAYER_STATE.WAIT, + score: 0, + submits: [], + }); + }); + + it('이전 퀴즈존이 존재하지 않으면 새로운 퀴즈존에만 참여한다', async () => { + // given + const clientId = 'player1'; + const newQuizZoneId = 'newQuizZone'; + const nonExistingQuizZoneId = 'nonExisting'; + + const newQuizZone: QuizZone = { + players: new Map(), + hostId: 'adminId', + title: '새로운 퀴즈', + description: '새로운 퀴즈입니다', + quizzes: quizzes, + maxPlayers: 10, + stage: QUIZ_ZONE_STAGE.LOBBY, + currentQuizIndex: -1, + currentQuizStartTime: 0, + currentQuizDeadlineTime: 0, + intervalTime: 5000, + }; + + mockQuizZoneRepository.has.mockResolvedValue(false); + mockQuizZoneRepository.get.mockResolvedValue(newQuizZone); + + // when + await service.getQuizZoneInfo(clientId, newQuizZoneId, nonExistingQuizZoneId); + + // then + const newPlayer = newQuizZone.players.get(clientId); + expect(newPlayer).toEqual({ + id: clientId, + nickname: expect.any(String), + state: PLAYER_STATE.WAIT, + score: 0, + submits: [], + }); + }); + + it('LOBBY 단계에서 새로운 플레이어가 접속하면 플레이어를 추가하고 정보를 반환한다', async () => { + // given + const mockQuizZone: QuizZone = { + players: new Map([ + [ + 'player1', + { + id: 'player1', + nickname: nickNames[0], + state: PLAYER_STATE.WAIT, + score: 0, + submits: [], + }, + ], + ]), + hostId: 'adminId', + maxPlayers: 10, + title: '테스트 퀴즈', + description: '테스트 퀴즈입니다', + quizzes: quizzes, + stage: QUIZ_ZONE_STAGE.LOBBY, + currentQuizIndex: -1, + currentQuizStartTime: 0, + currentQuizDeadlineTime: 0, + intervalTime: 5000, + }; + + mockQuizZoneRepository.get.mockResolvedValue(mockQuizZone); + + // when + const result = await service.getQuizZoneInfo(clientId, quizZoneId); + + // then + const addedPlayer = mockQuizZone.players.get(clientId); + const nickname = addedPlayer.nickname; + expect(addedPlayer).toEqual({ + id: clientId, + nickname: nickname, + score: 0, + submits: [], + state: PLAYER_STATE.WAIT, + }); + + expect(result).toEqual({ + currentPlayer: { + id: clientId, + nickname: nickname, + state: PLAYER_STATE.WAIT, + }, + title: '테스트 퀴즈', + maxPlayers: 10, + description: '테스트 퀴즈입니다', + quizCount: quizzes.length, + stage: QUIZ_ZONE_STAGE.LOBBY, + hostId: 'adminId', + }); + }); + + it('LOBBY 단계에서 이미 참가한 플레이어가 접속하면 기존 정보를 반환한다', async () => { + // given + const existingPlayer = { + id: clientId, + nickname: '닉네임', + state: PLAYER_STATE.WAIT, + score: 0, + submits: [], + }; + const mockQuizZone: QuizZone = { + players: new Map([[clientId, existingPlayer]]), + hostId: 'adminId', + maxPlayers: 10, + title: '테스트 퀴즈', + description: '테스트 퀴즈입니다', + quizzes: quizzes, + stage: QUIZ_ZONE_STAGE.LOBBY, + currentQuizIndex: -1, + currentQuizStartTime: 0, + currentQuizDeadlineTime: 0, + intervalTime: 5000, + }; + + mockQuizZoneRepository.get.mockResolvedValue(mockQuizZone); + + // when + const result = await service.getQuizZoneInfo(clientId, quizZoneId); + + // then + expect(mockQuizZone.players.size).toBe(1); + expect(result.currentPlayer).toEqual({ + id: clientId, + nickname: '닉네임', + state: PLAYER_STATE.WAIT, + }); + }); + + it('LOBBY 단계에서 정원이 초과된 경우 BadRequestException을 던진다', async () => { + // given + const mockQuizZone: QuizZone = { + players: new Map([ + [ + 'player1', + { + id: 'player1', + nickname: '닉네임1', + state: PLAYER_STATE.WAIT, + score: 0, + submits: [], + }, + ], + [ + 'player2', + { + id: 'player2', + nickname: '닉네임2', + state: PLAYER_STATE.WAIT, + score: 0, + submits: [], + }, + ], + ]), + hostId: 'adminId', + maxPlayers: 2, + title: '테스트 퀴즈', + description: '테스트 퀴즈입니다', + quizzes: quizzes, + stage: QUIZ_ZONE_STAGE.LOBBY, + currentQuizIndex: -1, + currentQuizStartTime: 0, + currentQuizDeadlineTime: 0, + intervalTime: 5000, + }; + + mockQuizZoneRepository.get.mockResolvedValue(mockQuizZone); + + // when & then + await expect(service.getQuizZoneInfo(clientId, quizZoneId)).rejects.toThrow( + BadRequestException, + ); + }); + + it('IN_PROGRESS 단계일 때 진행 정보를 반환한다', async () => { + // given + const mockQuizZone: QuizZone = { + players: new Map([ + [ + clientId, + { + id: clientId, + nickname: '닉네임', + state: PLAYER_STATE.WAIT, + score: 0, + submits: [], + }, + ], + ]), + hostId: 'adminId', + maxPlayers: 10, + title: '테스트 퀴즈', + description: '테스트 퀴즈입니다', + quizzes: quizzes, + stage: QUIZ_ZONE_STAGE.IN_PROGRESS, + currentQuizIndex: 1, + currentQuizStartTime: Date.now(), + currentQuizDeadlineTime: Date.now() + playTime, + intervalTime: 5000, + }; + + mockQuizZoneRepository.get.mockResolvedValue(mockQuizZone); + + // when + const result = await service.getQuizZoneInfo(clientId, quizZoneId); + + // then + expect(result).toEqual({ + currentPlayer: { id: clientId, nickname: '닉네임', state: PLAYER_STATE.WAIT }, + title: '테스트 퀴즈', + description: '테스트 퀴즈입니다', + quizCount: quizzes.length, + stage: QUIZ_ZONE_STAGE.IN_PROGRESS, + hostId: 'adminId', + maxPlayers: 10, + currentQuiz: { + currentIndex: 1, + startTime: mockQuizZone.currentQuizStartTime, + deadlineTime: mockQuizZone.currentQuizDeadlineTime, + playTime: quizzes[1].playTime, + question: quizzes[1].question, + stage: QUIZ_ZONE_STAGE.IN_PROGRESS, + }, + }); + }); + + it('RESULT 단계일 때 결과 정보를 반환한다', async () => { + // given + const mockQuizZone: QuizZone = { + players: new Map([ + [ + clientId, + { + id: clientId, + nickname: '닉네임', + state: PLAYER_STATE.WAIT, + score: 100, + submits: [], + }, + ], + ]), + hostId: 'adminId', + maxPlayers: 10, + title: '테스트 퀴즈', + description: '테스트 퀴즈입니다', + quizzes: quizzes, + stage: QUIZ_ZONE_STAGE.RESULT, + currentQuizIndex: -1, + currentQuizStartTime: Date.now(), + currentQuizDeadlineTime: Date.now() + playTime, + intervalTime: 5000, + summaries: { + ranks: [ + {id: "player1", nickname: "미친투사", score: 0, ranking: 1}, + {id: "player2", nickname: "미친투사", score: 0, ranking: 1} + ], + endSocketTime: Date.now(), + } + }; + + mockQuizZoneRepository.get.mockResolvedValue(mockQuizZone); + + // when + const result = await service.getQuizZoneInfo(clientId, quizZoneId); + + // then + expect(result).toMatchObject({ + currentPlayer: { + id: clientId, + nickname: '닉네임', + state: PLAYER_STATE.WAIT, + score: 100, + submits: [], + }, + title: '테스트 퀴즈', + description: '테스트 퀴즈입니다', + quizCount: quizzes.length, + stage: QUIZ_ZONE_STAGE.RESULT, + hostId: 'adminId', + ranks: [ + {id: "player1", nickname: "미친투사", score: 0, ranking: 1}, + {id: "player2", nickname: "미친투사", score: 0, ranking: 1} + ], + endSocketTime: expect.any(Number), + }); + }); + + it('존재하지 않는 퀴즈존을 조회하면 NotFoundException을 던진다', async () => { + // given + mockQuizZoneRepository.get.mockResolvedValue(null); + + // when & then + await expect(service.getQuizZoneInfo(clientId, quizZoneId)).rejects.toThrow( + NotFoundException, + ); + }); + + it('참가하지 않은 플레이어가 IN_PROGRESS 단계의 퀴즈존을 조회하면 BadRequestException을 던진다', async () => { + // given + const mockQuizZone: QuizZone = { + players: new Map(), + hostId: 'adminId', + maxPlayers: 10, + title: '테스트 퀴즈', + description: '테스트 퀴즈입니다', + quizzes: quizzes, + stage: QUIZ_ZONE_STAGE.IN_PROGRESS, + currentQuizIndex: -1, + currentQuizStartTime: 0, + currentQuizDeadlineTime: 0, + intervalTime: 5000, + }; + + mockQuizZoneRepository.get.mockResolvedValue(mockQuizZone); + + // when & then + await expect(service.getQuizZoneInfo(clientId, quizZoneId)).rejects.toThrow( + BadRequestException, + ); + }); + }); + + describe('clearQuizZone', () => { + it('퀴즈존을 삭제한다', async () => { + const quizZoneId = 'testQuizZone'; + + mockQuizZoneRepository.has.mockResolvedValue(true); + + await service.clearQuizZone(quizZoneId); + + expect(repository.delete).toHaveBeenCalledWith(quizZoneId); + }); + + it('존재하지 않는 퀴즈존을 삭제 시 예외를 던진다', async () => { + const quizZoneId = 'testQuizZone'; + + mockQuizZoneRepository.has.mockResolvedValue(false); + + await expect(service.clearQuizZone(quizZoneId)).rejects.toThrow(BadRequestException); + }); + }); + describe('findOthersInfo', () => { + it('다른 플레이어들의 닉네임 목록을 반환한다', async () => { + const quizZoneId = 'testQuizZone'; + const clientId = 'clientId'; + const mockQuizZone: QuizZone = { + players: new Map([ + [ + 'clientId', + { + id: 'clientId', + nickname: 'nick1', + state: PLAYER_STATE.WAIT, + score: 0, + submits: [], + }, + ], + [ + 'player2', + { + id: 'player2', + nickname: 'nick2', + state: PLAYER_STATE.WAIT, + score: 0, + submits: [], + }, + ], + [ + 'player3', + { + id: 'player3', + nickname: 'nick3', + state: PLAYER_STATE.WAIT, + score: 0, + submits: [], + }, + ], + ]), + hostId: 'adminId', + maxPlayers: 10, + title: '테스트 퀴즈', + description: '테스트 퀴즈입니다', + quizzes: quizzes, + stage: QUIZ_ZONE_STAGE.LOBBY, + currentQuizIndex: -1, + currentQuizStartTime: 0, + currentQuizDeadlineTime: 0, + intervalTime: 5000, + }; + + mockQuizZoneRepository.get.mockResolvedValue(mockQuizZone); + + const result = await service.findOthersInfo(quizZoneId, clientId); + + expect(result).toEqual([ + { nickname: 'nick2', id: 'player2' }, + { nickname: 'nick3', id: 'player3' }, + ]); + }); + + it('다른 플레이어가 없으면 빈 배열을 반환한다', async () => { + const quizZoneId = 'testQuizZone'; + const clientId = 'clientId'; + const mockQuizZone: QuizZone = { + players: new Map([ + [ + clientId, + { + id: clientId, + nickname: 'nick1', + state: PLAYER_STATE.WAIT, + score: 0, + submits: [], + }, + ], + ]), + hostId: 'adminId', + maxPlayers: 10, + title: '테스트 퀴즈', + description: '테스트 퀴즈입니다', + quizzes: quizzes, + stage: QUIZ_ZONE_STAGE.LOBBY, + currentQuizIndex: -1, + currentQuizStartTime: 0, + currentQuizDeadlineTime: 0, + intervalTime: 5000, + }; + + mockQuizZoneRepository.get.mockResolvedValue(mockQuizZone); + + const result = await service.findOthersInfo(quizZoneId, clientId); + + expect(result).toEqual([]); + }); + }); + + describe('getQuizZoneStage', () => { + it('퀴즈존의 현재 단계를 반환한다', async () => { + const quizZoneId = 'testQuizZone'; + const mockQuizZone: QuizZone = { + players: new Map(), + hostId: 'adminId', + maxPlayers: 10, + title: '테스트 퀴즈', + description: '테스트 퀴즈입니다', + quizzes: quizzes, + stage: QUIZ_ZONE_STAGE.LOBBY, + currentQuizIndex: -1, + currentQuizStartTime: 0, + currentQuizDeadlineTime: 0, + intervalTime: 5000, + }; + + mockQuizZoneRepository.get.mockResolvedValue(mockQuizZone); + + const result = await service.getQuizZoneStage(quizZoneId); + + expect(result).toEqual(QUIZ_ZONE_STAGE.LOBBY); + }); + + it('존재하지 않는 퀴즈존을 조회하면 NotFoundException을 던진다', async () => { + const quizZoneId = 'testQuizZone'; + mockQuizZoneRepository.get.mockResolvedValue(null); + + await expect(service.getQuizZoneStage(quizZoneId)).rejects.toThrow(NotFoundException); + }); + }); +}); diff --git a/apps/backend/src/quiz-zone/quiz-zone.service.ts b/apps/backend/src/quiz-zone/quiz-zone.service.ts new file mode 100644 index 0000000..d2aeda3 --- /dev/null +++ b/apps/backend/src/quiz-zone/quiz-zone.service.ts @@ -0,0 +1,273 @@ +import { + BadRequestException, + ConflictException, + Inject, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { Quiz } from './entities/quiz.entity'; +import { Player } from './entities/player.entity'; +import { QuizZone } from './entities/quiz-zone.entity'; +import { IQuizZoneRepository } from './repository/quiz-zone.repository.interface'; +import { getRandomNickName, PLAYER_STATE, QUIZ_ZONE_STAGE } from '../common/constants'; +import { FindQuizZoneDto } from './dto/find-quiz-zone.dto'; +import { CreateQuizZoneDto } from './dto/create-quiz-zone.dto'; +import { QuizService } from '../quiz/quiz.service'; +import { ChatService } from '../chat/chat.service'; + +const INTERVAL_TIME = 5000; + +@Injectable() +export class QuizZoneService { + constructor( + @Inject('QuizZoneRepository') + private readonly repository: IQuizZoneRepository, + @Inject(QuizService) + private readonly quizService: QuizService, + @Inject(ChatService) + private readonly chatService: ChatService, + ) {} + + /** + * 새로운 퀴즈 존을 생성합니다. + * + * @param createQuizZoneDto - 등록될 퀴즈존DTO + * @param hostId + * @returns 퀴즈 존을 생성하고 저장하는 비동기 작업 + * @throws(ConflictException) 이미 저장된 ID인 경우 예외 발생 + */ + async create(createQuizZoneDto: CreateQuizZoneDto, hostId: string): Promise { + const { quizZoneId, title, description, limitPlayerCount, quizSetId } = createQuizZoneDto; + const hasQuizZone = await this.repository.has(quizZoneId); + + if (hasQuizZone) { + throw new ConflictException('이미 존재하는 퀴즈존입니다.'); + } + + const player: Player = { + id: hostId, + nickname: getRandomNickName(), + score: 0, + submits: [], + state: PLAYER_STATE.WAIT, + }; + + const quizzes: Quiz[] = (await this.quizService.getQuizzes(quizSetId)).map((quiz) => ({ + question: Buffer.from(quiz.question).toString('base64'), + answer: quiz.answer, + playTime: quiz.playTime * 1000, + quizType: quiz.quizType, + })); + + const quizZone: QuizZone = { + players: new Map([[hostId, player]]), + title, + description, + hostId: hostId, + maxPlayers: limitPlayerCount, + quizzes, + stage: QUIZ_ZONE_STAGE.LOBBY, + currentQuizIndex: -1, + currentQuizStartTime: 0, + currentQuizDeadlineTime: 0, + intervalTime: INTERVAL_TIME, + }; + + await this.repository.set(quizZoneId, quizZone); + } + + async getQuizZoneInfo(clientId: string, quizZoneId: string, sessionQuizZoneId?: string) { + if (sessionQuizZoneId !== undefined && sessionQuizZoneId !== quizZoneId) { + if (await this.repository.has(sessionQuizZoneId)) { + await this.leave(sessionQuizZoneId, clientId); + } + } + + const quizZoneStage = await this.getQuizZoneStage(quizZoneId); + + if (quizZoneStage === QUIZ_ZONE_STAGE.LOBBY) { + await this.setPlayerInfo(clientId, quizZoneId); + return this.getLobbyInfo(clientId, quizZoneId); + } + + if (quizZoneStage === QUIZ_ZONE_STAGE.IN_PROGRESS) { + await this.checkValidPlayer(clientId, quizZoneId); + return this.getProgressInfo(clientId, quizZoneId); + } + if (quizZoneStage === QUIZ_ZONE_STAGE.RESULT) { + await this.checkValidPlayer(clientId, quizZoneId); + return this.getResultInfo(clientId, quizZoneId); + } + } + + /** + * 퀴즈 존을 ID로 조회합니다. + * + * @param quizZoneId - 조회할 퀴즈 존의 ID + * @returns 퀴즈 존 객체 + * @throws {NotFoundException} 퀴즈 존을 찾을 수 없는 경우 + */ + async findOne(quizZoneId: string): Promise { + const quizZone = await this.repository.get(quizZoneId); + + if (!quizZone) { + throw new NotFoundException('퀴즈존 정보를 확인할 수 없습니다.'); + } + + return quizZone; + } + + private async getLobbyInfo(clinetId: string, quizZoneId: string): Promise { + const { players, title, description, quizzes, stage, hostId, maxPlayers } = + await this.findOne(quizZoneId); + const { id, nickname, state } = players.get(clinetId); + const chatMessages = await this.chatService.get(quizZoneId); + + return { + currentPlayer: { id, nickname, state }, + title: title, + description: description, + quizCount: quizzes.length, + maxPlayers: maxPlayers, + stage: stage, + hostId: hostId, + chatMessages: chatMessages, + }; + } + + private async getProgressInfo(clientId: string, quizZoneId: string): Promise { + const { + players, + stage, + maxPlayers, + currentQuizIndex, + currentQuizStartTime, + currentQuizDeadlineTime, + hostId, + title, + description, + quizzes, + } = await this.findOne(quizZoneId); + const { id, nickname, state } = players.get(clientId); + const chatMessages = await this.chatService.get(quizZoneId); + + return { + currentPlayer: { id, nickname, state }, + title, + description, + quizCount: quizzes.length, + stage, + maxPlayers: maxPlayers, + hostId: hostId, + currentQuiz: { + currentIndex: currentQuizIndex, + startTime: currentQuizStartTime, + deadlineTime: currentQuizDeadlineTime, + playTime: quizzes[currentQuizIndex].playTime, + question: quizzes[currentQuizIndex].question, + stage: stage, + }, + chatMessages: chatMessages, + }; + } + + private async getResultInfo(clientId: string, quizZoneId: string): Promise { + const { players, stage, title, description, hostId, quizzes, summaries } = + await this.findOne(quizZoneId); + const { id, nickname, state, submits, score } = players.get(clientId); + const chatMessages = await this.chatService.get(quizZoneId); + + return { + currentPlayer: { id, nickname, state, score, submits }, + title, + description, + quizCount: quizzes.length, + stage: stage, + hostId, + chatMessages, + ranks: summaries.ranks, + endSocketTime: summaries.endSocketTime, + quizzes, + score, + submits + }; + } + + private async setPlayerInfo(clientId: string, quizZoneId: string) { + const { players, maxPlayers } = await this.findOne(quizZoneId); + const playerCount = players.size; + + // 이미 참가한 플레이어인 경우 그냥 리턴 + if (players.has(clientId)) { + return; + } + + // 정원 초과인 경우 예외 발생 + if (playerCount >= maxPlayers) { + throw new BadRequestException('퀴즈존 정원이 초과되었습니다.'); + } + + players.set(clientId, { + id: clientId, + nickname: getRandomNickName(), + score: 0, + submits: [], + state: PLAYER_STATE.WAIT, + }); + } + + private async checkValidPlayer(clientId: string, quizZoneId: string) { + const { players } = await this.findOne(quizZoneId); + + if (!players.has(clientId)) { + throw new BadRequestException('참가하지 않은 플레이어입니다.'); + } + } + + /** + * 퀴즈 존을 삭제합니다. + * + * @param quizZoneId - 삭제할 퀴즈 존의 ID + * @returns 퀴즈 존 삭제 작업 + */ + async clearQuizZone(quizZoneId: string): Promise { + const hasQuizZone = await this.repository.has(quizZoneId); + + if (!hasQuizZone) { + throw new BadRequestException('존재하지 않는 퀴즈존입니다.'); + } + + await this.repository.delete(quizZoneId); + } + + /** + * + * @param quizZoneId - 대상 퀴즈 존 ID + * @param clientId - 제외시킬 클라이언트 ID + * @returns + */ + async findOthersInfo(quizZoneId: string, clientId: string) { + const { players } = await this.findOne(quizZoneId); + + return [...players.values()] + .filter((player) => player.id !== clientId) + .map(({ id, nickname }) => ({ nickname, id })); + } + + async getQuizZoneStage(quizZoneId: string) { + const { stage } = await this.findOne(quizZoneId); + + return stage; + } + + private async leave(quizZoneId: string, clientId: any) { + const quizZone = await this.findOne(quizZoneId); + quizZone.players.delete(clientId); + } + + async updateQuizZone(quizZoneId: string, quizZone: QuizZone) { + await this.repository.set(quizZoneId, { + ...quizZone, + }); + } +} diff --git a/apps/backend/src/quiz-zone/repository/quiz-zone.memory.repository.spec.ts b/apps/backend/src/quiz-zone/repository/quiz-zone.memory.repository.spec.ts new file mode 100644 index 0000000..1d74bc9 --- /dev/null +++ b/apps/backend/src/quiz-zone/repository/quiz-zone.memory.repository.spec.ts @@ -0,0 +1,49 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { QuizZoneRepositoryMemory } from './quiz-zone.memory.repository'; +import { QuizZone } from '../entities/quiz-zone.entity'; +import { QUIZ_ZONE_STAGE } from '../../common/constants'; + +describe('QuizZoneRepositoryMemory', () => { + let storage: Map; + let repository: QuizZoneRepositoryMemory; + + beforeEach(async () => { + storage = new Map(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + QuizZoneRepositoryMemory, + { + provide: 'QuizZoneStorage', + useValue: storage, + }, + ], + }).compile(); + + repository = module.get(QuizZoneRepositoryMemory); + }); + + describe('set', () => { + it('세션 id와 퀴즈존 정보를 통해 새로운 퀴즈존 정보를 저장한다.', async () => { + const quizZoneId = 'some-id'; + + const quizZone: QuizZone = { + players: new Map(), + hostId: 'adminId', + maxPlayers: 4, + quizzes: [], + stage: QUIZ_ZONE_STAGE.LOBBY, + title: 'title', + description: 'description', + currentQuizIndex: 0, + currentQuizStartTime: 0, + currentQuizDeadlineTime: 0, + intervalTime: 30_000, + }; + + await repository.set(quizZoneId, quizZone); + + expect(storage.get(quizZoneId)).toEqual(quizZone); // 생성된 객체와 동일한지 확인 + }); + }); +}); diff --git a/apps/backend/src/quiz-zone/repository/quiz-zone.memory.repository.ts b/apps/backend/src/quiz-zone/repository/quiz-zone.memory.repository.ts new file mode 100644 index 0000000..61edd73 --- /dev/null +++ b/apps/backend/src/quiz-zone/repository/quiz-zone.memory.repository.ts @@ -0,0 +1,27 @@ +import { IQuizZoneRepository } from './quiz-zone.repository.interface'; +import { Inject, Injectable } from '@nestjs/common'; +import { QuizZone } from '../entities/quiz-zone.entity'; + +@Injectable() +export class QuizZoneRepositoryMemory implements IQuizZoneRepository { + constructor( + @Inject('QuizZoneStorage') + private readonly data: Map, + ) {} + + async get(id: string) { + return this.data.get(id) ?? null; + } + + async set(id: string, quizZone: QuizZone) { + this.data.set(id, quizZone); + } + + async has(id: string) { + return this.data.has(id); + } + + async delete(id: string) { + this.data.delete(id); + } +} diff --git a/apps/backend/src/quiz-zone/repository/quiz-zone.repository.interface.ts b/apps/backend/src/quiz-zone/repository/quiz-zone.repository.interface.ts new file mode 100644 index 0000000..4e7c044 --- /dev/null +++ b/apps/backend/src/quiz-zone/repository/quiz-zone.repository.interface.ts @@ -0,0 +1,41 @@ +import { QuizZone } from '../entities/quiz-zone.entity'; + +/** + * 퀴즈 존 저장소를 위한 인터페이스입니다. + * + * 이 인터페이스는 퀴즈 존 데이터를 저장, 조회, 삭제하는 메서드를 정의합니다. + */ +export interface IQuizZoneRepository { + /** + * 주어진 키에 해당하는 퀴즈 존을 반환합니다. + * + * @param key - 조회할 퀴즈 존의 키 + * @returns 퀴즈 존 객체 + */ + get(key: string): Promise; + + /** + * 주어진 키에 퀴즈 존을 저장합니다. + * + * @param key - 퀴즈 존을 저장할 키 + * @param value - 저장할 퀴즈 존 객체 + * @returns 저장 작업 완료 + */ + set(key: string, value: QuizZone): Promise; + + /** + * 주어진 키에 해당하는 퀴즈 존을 삭제합니다. + * + * @param key - 삭제할 퀴즈 존의 키 + * @returns 삭제 작업 완료 + */ + delete(key: string): Promise; + + /** + * 주어진 키에 해당하는 퀴즈존이 존재하는지 확인합니다. + * + * @param key - 존재 여부를 확인할 퀴즈존의 키 + * @returns 존재 여부 + */ + has(key: string): Promise; +} diff --git a/apps/backend/src/quiz/dto/create-quiz-set-request.dto.ts b/apps/backend/src/quiz/dto/create-quiz-set-request.dto.ts new file mode 100644 index 0000000..209e7c3 --- /dev/null +++ b/apps/backend/src/quiz/dto/create-quiz-set-request.dto.ts @@ -0,0 +1,61 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { QUIZ_TYPE } from '../../common/constants'; +import { Quiz } from '../entity/quiz.entitiy'; +import { QuizSet } from '../entity/quiz-set.entity'; +import { + ArrayMaxSize, + ArrayMinSize, + IsBoolean, + IsEnum, + IsNumber, + IsString, + Length, + Max, + Min, + ValidateNested, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +export class QuizDetailsDto { + @ApiProperty({ description: '퀴즈 질문' }) + @IsString({ message: '퀴즈 질문은 문자열이어야 합니다.' }) + @Length(1, 200, { message: '퀴즈의 질문은 1~200자 이어야 합니다.' }) + readonly question: string; + + @ApiProperty({ description: '퀴즈 정답' }) + @IsString({ message: '퀴즈 정답은 문자열이어야 합니다.' }) + @Length(1, 30, { message: '퀴즈의 대답은 1~30자 이어야 합니다.' }) + readonly answer: string; + + @ApiProperty({ description: '퀴즈 시간' }) + @Min(3, { message: '퀴즈 시간은 3초 이상이어야 합니다.' }) + @Max(60 * 10, { message: '퀴즈 시간은 10분 이하여야 합니다.' }) + @IsNumber({}, { message: '퀴즈 시간 값이 숫자가 아닙니다.' }) + readonly playTime: number; + + @ApiProperty({ description: '퀴즈 타입' }) + @IsEnum(QUIZ_TYPE, { message: '정해진 퀴즈 타입이 아닙니다.' }) + readonly quizType: QUIZ_TYPE; + + toEntity(quizSet: QuizSet) { + return new Quiz(this.question, this.answer, this.playTime, this.quizType, quizSet); + } +} + +export class CreateQuizSetRequestDto { + @ApiProperty() + @IsString({ message: '퀴즈셋의 이름은 문자열이어야 합니다.' }) + @Length(1, 30, { message: '퀴즈셋의 이름은 1~30자 이어야 합니다.' }) + readonly quizSetName: string; + + @ApiProperty({ description: '퀴즈셋이 기본으로 보일지 결정하는 flag입니다' }) + @IsBoolean({ message: '값이 boolean이어야 합니다.' }) + readonly recommended?: boolean = false; + + @ApiProperty({ type: [QuizDetailsDto], description: '퀴즈 세부 정보' }) + @ValidateNested({ each: true }) + @ArrayMinSize(1, { message: '퀴즈가 한개 이상 포함되어야합니다.' }) + @ArrayMaxSize(10, { message: '퀴즈는 최대 10개만 생성할 수 있습니다.' }) + @Type(() => QuizDetailsDto) + readonly quizDetails: QuizDetailsDto[]; +} diff --git a/apps/backend/src/quiz/dto/create-quiz-set-response.dto.ts b/apps/backend/src/quiz/dto/create-quiz-set-response.dto.ts new file mode 100644 index 0000000..d4eba95 --- /dev/null +++ b/apps/backend/src/quiz/dto/create-quiz-set-response.dto.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateQuizSetResponseDto { + @ApiProperty({ description: '새로 생성된 퀴즈셋 id' }) + readonly id: number; +} diff --git a/apps/backend/src/quiz/dto/find-quizzes-response.dto.ts b/apps/backend/src/quiz/dto/find-quizzes-response.dto.ts new file mode 100644 index 0000000..bcd449c --- /dev/null +++ b/apps/backend/src/quiz/dto/find-quizzes-response.dto.ts @@ -0,0 +1,15 @@ +import { QUIZ_TYPE } from '../../common/constants'; +import { ApiProperty } from '@nestjs/swagger'; + +export class FindQuizzesResponseDto { + @ApiProperty({ description: '해당 퀴즈 질문' }) + readonly question: string; + @ApiProperty({ description: '해당 퀴즈 정답' }) + readonly answer: string; + @ApiProperty({ description: '해당 퀴즈 시간' }) + readonly playTime: number; + @ApiProperty({ description: '해당 퀴즈 타입' }) + readonly quizType: QUIZ_TYPE; + @ApiProperty({ description: '해당 퀴즈 id' }) + readonly id: number; +} diff --git a/apps/backend/src/quiz/dto/search-quiz-set-request.dto.ts b/apps/backend/src/quiz/dto/search-quiz-set-request.dto.ts new file mode 100644 index 0000000..5776bca --- /dev/null +++ b/apps/backend/src/quiz/dto/search-quiz-set-request.dto.ts @@ -0,0 +1,14 @@ +import { Type } from 'class-transformer'; +import { IsNumber, IsString, Length } from 'class-validator'; + +export class SearchQuizSetRequestDTO { + @IsString({ message: '퀴즈셋의 이름은 문자열이어야 합니다.' }) + @Length(0, 30, { message: '퀴즈셋의 이름은 1~30자 이어야 합니다.' }) + readonly name: string = ''; + @Type(() => Number) + @IsNumber({}, { message: 'page 값이 숫자가 아닙니다.' }) + readonly page?: number = 1; + @Type(() => Number) + @IsNumber({}, { message: '페이지 크기 값이 숫자가 아닙니다.' }) + readonly size?: number = 10; +} diff --git a/apps/backend/src/quiz/dto/search-quiz-set-response.dto.ts b/apps/backend/src/quiz/dto/search-quiz-set-response.dto.ts new file mode 100644 index 0000000..7f91102 --- /dev/null +++ b/apps/backend/src/quiz/dto/search-quiz-set-response.dto.ts @@ -0,0 +1,20 @@ +import { QuizSet } from '../entity/quiz-set.entity'; + +export class SearchQuizSetResponseDTO { + + readonly quizSetDetails: QuizSetDetails[]; + readonly total: number; + readonly currentPage: number; +} + +export class QuizSetDetails { + readonly id: number; + readonly name: string; + + static from(quizSet : QuizSet) : QuizSetDetails { + return { + id: quizSet.id, + name: quizSet.name, + }; + } +} \ No newline at end of file diff --git a/apps/backend/src/quiz/dto/update-quiz-request.dto.ts b/apps/backend/src/quiz/dto/update-quiz-request.dto.ts new file mode 100644 index 0000000..4f1fc79 --- /dev/null +++ b/apps/backend/src/quiz/dto/update-quiz-request.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { QUIZ_TYPE } from '../../common/constants'; +import { Quiz } from '../entity/quiz.entitiy'; +import { IsEnum, IsNumber, IsString, Length, Max, Min } from 'class-validator'; + +export class UpdateQuizRequestDto { + @ApiProperty({ description: '업데이트 하는 퀴즈 질문' }) + @IsString({message: '퀴즈 질문은 문자열이어야 합니다.'}) + @Length(1, 200) + readonly question: string; + @ApiProperty({ description: '업데이트 하는 퀴즈 정답' }) + @IsString({message: '퀴즈 정답은 문자열이어야 합니다.'}) + @Length(1, 30, {message: '퀴즈의 대답은 1~30자 이어야 합니다.'}) + readonly answer: string; + @ApiProperty({ description: '업데이트 하는 퀴즈 시간' }) + @Min(3, {message: '퀴즈 시간은 3초 이상이어야 합니다.'}) + @Max(60 * 10, {message: '퀴즈 시간은 10분 이하여야 합니다.'}) + @IsNumber({}, {message: '퀴즈 시간 값이 숫자가 아닙니다.'}) + readonly playTime: number; + @ApiProperty({ description: '업데이트 하는 퀴즈 타입' }) + @IsEnum(QUIZ_TYPE, {message: '정해진 퀴즈 타입이 아닙니다.'}) + readonly quizType: QUIZ_TYPE; +} diff --git a/apps/backend/src/quiz/entity/quiz-set.entity.ts b/apps/backend/src/quiz/entity/quiz-set.entity.ts new file mode 100644 index 0000000..e25d9f6 --- /dev/null +++ b/apps/backend/src/quiz/entity/quiz-set.entity.ts @@ -0,0 +1,24 @@ +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; +import { Quiz } from './quiz.entitiy'; +import { BaseEntity } from '../../common/base-entity'; + +@Entity('quiz_set') +export class QuizSet extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ type: 'varchar', length: 50 }) + name: string; + + @Column({ type: 'boolean', default: false }) + recommended: boolean; + + @OneToMany((type) => Quiz, (quiz) => quiz.quizSet) + quizzes?: Quiz[]; + + constructor(name: string, recommended: boolean = false) { + super(); + this.name = name; + this.recommended = recommended; + } +} diff --git a/apps/backend/src/quiz/entity/quiz.entitiy.ts b/apps/backend/src/quiz/entity/quiz.entitiy.ts new file mode 100644 index 0000000..4303ad3 --- /dev/null +++ b/apps/backend/src/quiz/entity/quiz.entitiy.ts @@ -0,0 +1,48 @@ +import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { QuizSet } from './quiz-set.entity'; +import { QUIZ_TYPE } from '../../common/constants'; +import { BaseEntity } from '../../common/base-entity'; + +/** + * 퀴즈 엔티티 + * + * @property question: 퀴즈의 질문 + * @property answer: 퀴즈의 정답 + * @property playTime: 퀴즈의 플레이 시간 + */ +@Entity() +export class Quiz extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ type: 'varchar', length: 255 }) + question: string; + + @Column({ type: 'varchar', length: 50 }) + answer: string; + + @Column({ type: 'int', name: 'play_time' }) + playTime: number; + + @Column({ name: 'quiz_type', type: 'varchar', length: 20 }) + quizType: QUIZ_TYPE; + + @ManyToOne((type) => QuizSet, (quizSet) => quizSet.quizzes) + @JoinColumn({ name: 'quiz_set_id', referencedColumnName: 'id' }) + quizSet: QuizSet; + + constructor( + question: string, + answer: string, + playTime: number, + quizType: QUIZ_TYPE, + quizSet: QuizSet, + ) { + super(); + this.question = question; + this.answer = answer; + this.playTime = playTime; + this.quizType = quizType; + this.quizSet = quizSet; + } +} diff --git a/apps/backend/src/quiz/quiz-set.controller.spec.ts b/apps/backend/src/quiz/quiz-set.controller.spec.ts new file mode 100644 index 0000000..02b8124 --- /dev/null +++ b/apps/backend/src/quiz/quiz-set.controller.spec.ts @@ -0,0 +1,93 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { QuizController } from './quiz.controller'; +import { QuizService } from './quiz.service'; +import { CreateQuizSetRequestDto } from './dto/create-quiz-set-request.dto'; +import { NotFoundException } from '@nestjs/common'; +import { QuizSetController } from './quiz-set.controller'; +import { QUIZ_TYPE } from '../common/constants'; + +describe('QuizController', () => { + let quizSetController: QuizSetController; + let quizService: QuizService; + + const mockQuizService = { + createQuizSet: jest.fn(), + createQuizzes: jest.fn(), + getQuizzes: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [QuizSetController], + providers: [ + { + provide: QuizService, + useValue: mockQuizService, + }, + ], + }).compile(); + + quizSetController = module.get(QuizSetController); + quizService = module.get(QuizService); + }); + + + describe('createQuiz', () => { + it('새로운 퀴즈를 생성한다.', async () => { + // given + const dto = { + quizSetName: "퀴즈셋 이름", + quizDetails: + [ + { + question: '지브리는 뭘로 돈 벌게요?', + answer: '토토로', + playTime: 30000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, + ] + } as CreateQuizSetRequestDto; + + // when + await quizSetController.createQuizSet(dto); + + // then + expect(quizService.createQuizzes).toHaveBeenCalledWith(dto); + }); + }); + + describe('findQuizzes', () => { + it('퀴즈셋의 퀴즈들을 성공적으로 반환한다.', async () => { + // given + const quizSetId = 1; + const quizzes = [ + { + id: 1, + question: '퀴즈 질문', + answer: '퀴즈 정답', + playTime: 1000, + quizType: 'SHORT_ANSWER', + }, + ]; + mockQuizService.getQuizzes.mockResolvedValue(quizzes); + + // when + const result = await quizSetController.findQuizzes(quizSetId); + + // then + expect(quizService.getQuizzes).toHaveBeenCalledWith(quizSetId); + expect(result).toEqual(quizzes); + }); + + it('존재하지 않는 퀴즈셋 ID인 경우 예외를 던진다.', async () => { + // given + const quizSetId = 2; + mockQuizService.getQuizzes.mockRejectedValue( + new NotFoundException(`해당 퀴즈셋을 찾을 수 없습니다.`), + ); + + // when & then + await expect(quizSetController.findQuizzes(quizSetId)).rejects.toThrow(NotFoundException); + }); + }); +}); diff --git a/apps/backend/src/quiz/quiz-set.controller.ts b/apps/backend/src/quiz/quiz-set.controller.ts new file mode 100644 index 0000000..9e435e4 --- /dev/null +++ b/apps/backend/src/quiz/quiz-set.controller.ts @@ -0,0 +1,64 @@ +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Query } from '@nestjs/common'; +import { QuizService } from './quiz.service'; +import { SearchQuizSetResponseDTO } from './dto/search-quiz-set-response.dto'; +import { SearchQuizSetRequestDTO } from './dto/search-quiz-set-request.dto'; +import { CreateQuizSetRequestDto } from './dto/create-quiz-set-request.dto'; +import { FindQuizzesResponseDto } from './dto/find-quizzes-response.dto'; + +@ApiTags('Quiz') +@Controller('quiz-set') +export class QuizSetController { + constructor(private quizService: QuizService) {} + + @Get() + @HttpCode(HttpStatus.OK) + @ApiOperation({summary: '퀴즈셋 검색'}) + @ApiResponse({ + status: HttpStatus.OK, + description: '퀴즈셋의 검색을 성공적으로 반환했습니다', + type: SearchQuizSetResponseDTO + }) + async searchQuizSet( + @Query() searchQuery: SearchQuizSetRequestDTO, + ): Promise { + return this.quizService.searchQuizSet(searchQuery); + } + + @Post() + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: '새로운 퀴즈셋, 퀴즈 생성' }) + @ApiResponse({ status: HttpStatus.CREATED, description: '퀴즈셋이 성공적으로 생성되었습니다.' }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: '요청 데이터가 유효하지 않습니다' }) + async createQuizSet( + @Body() createSetQuizDto: CreateQuizSetRequestDto + ) { + return this.quizService.createQuizzes(createSetQuizDto); + } + + @Get(':quizSetId') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '해당 퀴즈셋의 퀴즈들 조회' }) + @ApiResponse({ + status: HttpStatus.OK, + description: '해당 퀴즈셋의 퀴즈들을 성공적으로 반환했습니다.', + type: FindQuizzesResponseDto, + isArray: true, + }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: '해당 퀴즈셋의 id가 없습니다.' }) + async findQuizzes(@Param('quizSetId') quizSetId: number): Promise { + return this.quizService.getQuizzes(quizSetId); + } + + @Delete(':quizSetId') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '퀴즈셋 삭제' }) + @ApiResponse({ + status: HttpStatus.OK, + description: '해당 퀴즈셋의 퀴즈들을 성공적으로 삭제했습니다.', + }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: '해당 퀴즈셋의 id가 없습니다.' }) + async deleteQuizSet(@Param('quizSetId') quizSetId: number): Promise { + return this.quizService.deleteQuizSet(quizSetId); + } +} \ No newline at end of file diff --git a/apps/backend/src/quiz/quiz.controller.spec.ts b/apps/backend/src/quiz/quiz.controller.spec.ts new file mode 100644 index 0000000..42699fa --- /dev/null +++ b/apps/backend/src/quiz/quiz.controller.spec.ts @@ -0,0 +1,67 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { QuizController } from './quiz.controller'; +import { QuizService } from './quiz.service'; +import { UpdateQuizRequestDto } from './dto/update-quiz-request.dto'; +import { QUIZ_TYPE } from '../common/constants'; + +describe('QuizController', () => { + let quizController: QuizController; + let quizService: QuizService; + + const mockQuizService = { + createQuizSet: jest.fn(), + createQuizzes: jest.fn(), + getQuizzes: jest.fn(), + deleteQuiz: jest.fn(), + updateQuiz: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [QuizController], + providers: [ + { + provide: QuizService, + useValue: mockQuizService, + }, + ], + }).compile(); + + quizController = module.get(QuizController); + quizService = module.get(QuizService); + }); + + it('updateQuiz', async () => { + //given + const quizId = 1; + const updateQuizRequestDto: UpdateQuizRequestDto = { + question: '업데이트 퀴즈 질문', + answer: '업데이트 퀴즈 정답', + playTime: 10, + quizType: QUIZ_TYPE.SHORT_ANSWER + } + + mockQuizService.updateQuiz.mockResolvedValue(undefined); + + //when + await quizController.updateQuiz(quizId, updateQuizRequestDto); + + //then + expect(mockQuizService.updateQuiz).toHaveBeenCalled(); + expect(mockQuizService.updateQuiz).toHaveBeenCalledWith(quizId, updateQuizRequestDto); + }) + + it('deleteQuiz', async () => { + //given + const quizId = 1; + mockQuizService.deleteQuiz.mockResolvedValue(undefined); + + //when + await quizController.deleteQuiz(quizId); + + //then + expect(mockQuizService.deleteQuiz).toHaveBeenCalled(); + expect(mockQuizService.deleteQuiz).toHaveBeenCalledWith(quizId); + }) + +}); diff --git a/apps/backend/src/quiz/quiz.controller.ts b/apps/backend/src/quiz/quiz.controller.ts new file mode 100644 index 0000000..5176094 --- /dev/null +++ b/apps/backend/src/quiz/quiz.controller.ts @@ -0,0 +1,44 @@ +import { + Body, + Controller, + Delete, + HttpCode, + HttpStatus, + Param, + Patch, +} from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { QuizService } from './quiz.service'; +import { UpdateQuizRequestDto } from './dto/update-quiz-request.dto'; + +@ApiTags('Quiz') +@Controller('quiz') +export class QuizController { + constructor(private readonly quizService: QuizService) {} + + @Patch(':quizId') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '퀴즈 수정' }) + @ApiResponse({ + status: HttpStatus.OK, + description: '퀴즈의 정보를 성공적으로 수정하였습니다.', + }) + async updateQuiz( + @Param('quizId') quizId: number, + @Body() updateQuizRequestDto: UpdateQuizRequestDto, + ) { + return this.quizService.updateQuiz(quizId, updateQuizRequestDto); + } + + @Delete(':quizId') + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: '퀴즈 삭제' }) + @ApiResponse({ + status: HttpStatus.OK, + description: '퀴즈의 정보를 성공적으로 삭제하였습니다.', + }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, description: '해당 퀴즈셋의 id가 없습니다.' }) + async deleteQuiz(@Param('quizId') quizId: number): Promise { + return this.quizService.deleteQuiz(quizId); + } +} diff --git a/apps/backend/src/quiz/quiz.module.ts b/apps/backend/src/quiz/quiz.module.ts new file mode 100644 index 0000000..df9f608 --- /dev/null +++ b/apps/backend/src/quiz/quiz.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Quiz } from './entity/quiz.entitiy'; +import { QuizSet } from './entity/quiz-set.entity'; +import { QuizController } from './quiz.controller'; +import { QuizService } from './quiz.service'; +import { QuizRepository } from './repository/quiz.repository'; +import { QuizSetRepository } from './repository/quiz-set.repository'; +import { QuizSetController } from './quiz-set.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Quiz, QuizSet])], + controllers: [QuizController, QuizSetController], + providers: [QuizService, QuizRepository, QuizSetRepository], + exports: [QuizService], +}) +export class QuizModule {} diff --git a/apps/backend/src/quiz/quiz.service.spec.ts b/apps/backend/src/quiz/quiz.service.spec.ts new file mode 100644 index 0000000..433af56 --- /dev/null +++ b/apps/backend/src/quiz/quiz.service.spec.ts @@ -0,0 +1,352 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { QuizService } from './quiz.service'; +import { QuizRepository } from './repository/quiz.repository'; +import { QuizSetRepository } from './repository/quiz-set.repository'; +import { QuizSet } from './entity/quiz-set.entity'; +import { QUIZ_TYPE } from '../common/constants'; +import { CreateQuizSetRequestDto } from './dto/create-quiz-set-request.dto'; +import { BadRequestException } from '@nestjs/common'; +import { UpdateQuizRequestDto } from './dto/update-quiz-request.dto'; +import { SearchQuizSetRequestDTO } from './dto/search-quiz-set-request.dto'; +import { DataSource } from 'typeorm'; +import { addTransactionalDataSource, initializeTransactionalContext } from 'typeorm-transactional'; + +describe('QuizService', () => { + let service: QuizService; + let quizRepository: QuizRepository; + let quizSetRepository: QuizSetRepository; + + beforeAll(async () => { + initializeTransactionalContext(); + + // 테스트용 데이터소스 설정 + const dataSource = new DataSource({ + type: 'sqlite', + database: ':memory:', + dropSchema: true, + entities: [], // 필요한 엔티티 추가 + synchronize: true, + logging: false, + }); + await dataSource.initialize(); + addTransactionalDataSource(dataSource); + }); + + const mockQuizRepository = { + save: jest.fn(), + findBy: jest.fn(), + findOneBy: jest.fn(), + delete: jest.fn(), + }; + + const mockQuizSetRepository = { + save: jest.fn(), + findOneBy: jest.fn(), + searchByName: jest.fn(), + countByName: jest.fn(), + findByRecommend: jest.fn(), + countByRecommend: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + QuizService, + { + provide: QuizRepository, + useValue: mockQuizRepository, + }, + { + provide: QuizSetRepository, + useValue: mockQuizSetRepository, + }, + ], + }).compile(); + + service = module.get(QuizService); + quizRepository = module.get(QuizRepository); + quizSetRepository = module.get(QuizSetRepository); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('searchQuizSet', () => { + it('퀴즈셋 정상적으로 검색', async () => { + //given + const dto = { + name: '퀴즈셋 검색', + page: 1, + size: 10, + } as SearchQuizSetRequestDTO; + const quizSets = [ + { id: 1, name: '퀴즈셋 검색1' }, + { id: 2, name: '퀴즈셋 검색2' }, + ] as QuizSet[]; + const count = quizSets.length; + + mockQuizSetRepository.searchByName.mockResolvedValue(quizSets); + mockQuizSetRepository.countByName.mockResolvedValue(count); + + //when + const response = await service.searchQuizSet(dto); + + //then + expect(response).toEqual({ + quizSetDetails: response.quizSetDetails, + total: count, + currentPage: 1, + }); + }); + + it('퀴즈셋 recommend 반환', async () => { + //given + const dto = { + page: 1, + size: 10, + } as SearchQuizSetRequestDTO; + const quizSets = [ + { id: 1, name: '퀴즈셋 검색1', recommended: true }, + { id: 2, name: '퀴즈셋 검색2', recommended: true }, + { id: 2, name: '퀴즈셋 검색2', recommended: false }, + ] as QuizSet[]; + const count = quizSets.length; + + mockQuizSetRepository.findByRecommend.mockResolvedValue(quizSets.slice(0,2)); + mockQuizSetRepository.countByRecommend.mockResolvedValue(2); + + //when + const response = await service.searchQuizSet(dto); + + console.log(response); + //then + expect(response).toEqual({ + quizSetDetails: response.quizSetDetails, + total: 2, + currentPage: 1, + }); + }); + }); + + describe('createQuiz', () => { + it('새로운 퀴즈를 하나 생성한다', async () => { + //given + const quizSetId = 1; + const dto = { + quizSetName: '퀴즈셋 이름', + quizDetails: [ + { + question: '지브리는 뭘로 돈 벌게요?', + answer: '토토로', + playTime: 30000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, + ], + } as CreateQuizSetRequestDto; + const quiz = { + ...dto[0], + id: 1, + quizSet: { + id: quizSetId, + name: '퀴즈셋 이름', + }, + }; + + mockQuizSetRepository.save.mockResolvedValue({ quizSetId }); + dto.quizDetails[0].toEntity = jest.fn().mockReturnValue(quiz); + mockQuizRepository.save.mockResolvedValue(quiz); + + //when + const result = await service.createQuizzes(dto); + + //then + expect(dto.quizDetails[0].toEntity).toHaveBeenCalledTimes(1); + + expect(mockQuizRepository.save).toHaveBeenCalledTimes(1); + expect(mockQuizRepository.save).toHaveBeenCalledWith([quiz]); + }); + + it('새로운 퀴즈를 여러 개를 생성한다', async () => { + //given + const quizSetId = 1; + const dto = { + quizSetName: '퀴즈셋 이름', + quizDetails: [ + { + question: '지브리는 뭘로 돈 벌게요?', + answer: '토토로', + playTime: 30000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, + ], + } as CreateQuizSetRequestDto; + + const quiz1 = { + ...dto[0], + id: 1, + quizSet: { + id: quizSetId, + name: '퀴즈셋 이름', + }, + }; + const quiz2 = { + ...dto[1], + id: 2, + quizSet: { + id: quizSetId, + name: '퀴즈셋 이름', + }, + }; + + mockQuizSetRepository.save.mockResolvedValue({ quizSetId }); + dto.quizDetails[0].toEntity = jest.fn().mockReturnValue(quiz1); + mockQuizRepository.save.mockResolvedValue([quiz1]); + + //when + const result = await service.createQuizzes(dto); + + //then + expect(dto.quizDetails[0].toEntity).toHaveBeenCalledTimes(1); + + expect(mockQuizRepository.save).toHaveBeenCalledTimes(1); + expect(mockQuizRepository.save).toHaveBeenCalledWith([quiz1]); + }); + }); + + describe('getQuizzes', () => { + it('퀴즈셋의 퀴즈들을 정상적으로 반환한다. ', async () => { + //given + const quizList = [ + { + id: 1, + question: '네명에서 오줌을 싸면?', + answer: '포뇨', + playTime: 30000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, + { + id: 2, + question: '지브리는 뭘로 돈 벌게요?', + answer: '토토로', + playTime: 30000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, + ]; + const quizSetId = 1; + const quizSet = { id: quizSetId, name: '퀴즈셋 이름' } as QuizSet; + + mockQuizSetRepository.findOneBy.mockResolvedValue(quizSet); + mockQuizRepository.findBy.mockResolvedValue(quizList); + + //when + const result = await service.getQuizzes(quizSetId); + + // then + expect(mockQuizSetRepository.findOneBy).toHaveBeenCalledTimes(1); + expect(mockQuizRepository.findBy).toHaveBeenCalledTimes(1); + expect(result).toEqual(quizList); + }); + + it('존재하지 않는 QuizSetId인 경우 예외를 던진다.', async () => { + // given + const quizSetId = 2; + + // QuizSetId가 존재하지 않을 때 null 반환 설정 + mockQuizSetRepository.findOneBy.mockResolvedValue(null); + + // when & then + await expect(service.getQuizzes(quizSetId)).rejects.toThrow(BadRequestException); + + // Mock 함수 호출 검증 + expect(mockQuizSetRepository.findOneBy).toHaveBeenCalledWith({ id: quizSetId }); + expect(mockQuizRepository.findBy).not.toHaveBeenCalled(); + }); + }); + + describe('updateQuiz', () => { + it('퀴즈가 정상적으로 업데이트 된다', async () => { + //given + const quizId = 1; + const quiz = { + id: quizId, + question: '네명에서 오줌을 싸면?', + answer: '포뇨', + playTime: 30000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }; + const dto = { + question: '테스트 질문', + answer: '테스트 정답', + playTime: 1000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + } as UpdateQuizRequestDto; + + mockQuizRepository.findOneBy.mockResolvedValue(quiz); + mockQuizRepository.save.mockResolvedValue(undefined); + + //when + await service.updateQuiz(quizId, dto); + + //then + expect(mockQuizRepository.findOneBy).toHaveBeenCalledTimes(1); + expect(mockQuizRepository.save).toHaveBeenCalledWith({ ...quiz, ...dto }); + }); + + it('존재하지 않는 quiz 업데이트 하려고 하면 오류가 발생한다', async () => { + //given + const quizId = 100; + const quiz = { + id: quizId, + question: '네명에서 오줌을 싸면?', + answer: '포뇨', + playTime: 30000, + quizType: 'SHORT_ANSWER', + }; + const dto = { + question: '테스트 질문', + answer: '테스트 정답', + playTime: 1000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + } as UpdateQuizRequestDto; + + mockQuizRepository.findOneBy.mockResolvedValue(null); + + //when + //then + await expect(service.updateQuiz(quizId, dto)).rejects.toThrow(BadRequestException); + }); + }); + + describe('deleteQuiz', () => { + it('퀴즈를 정상적으로 삭제한다', async () => { + //given + const quizId = 1; + const quiz = { + id: quizId, + question: '네명에서 오줌을 싸면?', + answer: '포뇨', + playTime: 30000, + quizType: 'SHORT_ANSWER', + }; + mockQuizRepository.delete.mockResolvedValue(undefined); + mockQuizRepository.findOneBy.mockResolvedValue(quiz); + + //when + await service.deleteQuiz(quizId); + + //then + expect(mockQuizRepository.delete).toHaveBeenCalledTimes(1); + }); + + it('존재하지 않는 퀴즈를 삭제한다', async () => { + //given + const quizId = 100; + mockQuizRepository.findOneBy.mockResolvedValue(null); + + //when + //then + await expect(service.deleteQuiz(quizId)).rejects.toThrow(BadRequestException); + }); + }); +}); diff --git a/apps/backend/src/quiz/quiz.service.ts b/apps/backend/src/quiz/quiz.service.ts new file mode 100644 index 0000000..a04abb4 --- /dev/null +++ b/apps/backend/src/quiz/quiz.service.ts @@ -0,0 +1,92 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { CreateQuizSetRequestDto } from './dto/create-quiz-set-request.dto'; +import { QuizRepository } from './repository/quiz.repository'; +import { QuizSetRepository } from './repository/quiz-set.repository'; +import { UpdateQuizRequestDto } from './dto/update-quiz-request.dto'; +import { QuizSetDetails } from './dto/search-quiz-set-response.dto'; +import { SearchQuizSetRequestDTO } from './dto/search-quiz-set-request.dto'; +import { FindQuizzesResponseDto } from './dto/find-quizzes-response.dto'; +import { Transactional } from 'typeorm-transactional'; + +@Injectable() +export class QuizService { + constructor( + private quizRepository: QuizRepository, + private quizSetRepository: QuizSetRepository, + ) {} + + @Transactional() + async createQuizzes(createQuizDto: CreateQuizSetRequestDto) { + const quizSet = await this.quizSetRepository.save({ + name: createQuizDto.quizSetName, + recommended: createQuizDto.recommended, + }); + + const quizzes = createQuizDto.quizDetails.map((dto) => { + return dto.toEntity(quizSet); + }); + + await this.quizRepository.save(quizzes); + + return quizSet.id; + } + + async getQuizzes(quizSetId: number): Promise { + const quizSet = await this.findQuizSet(quizSetId); + return await this.quizRepository.findBy({ quizSet: { id: quizSetId } }); + } + + async updateQuiz(quizId: number, updateQuizRequestDto: UpdateQuizRequestDto) { + const quiz = await this.findQuiz(quizId); + + const updatedQuiz = { + ...quiz, + ...updateQuizRequestDto, + }; + + await this.quizRepository.save(updatedQuiz); + } + + async deleteQuiz(quizId: number) { + await this.findQuiz(quizId); + + await this.quizRepository.delete({ id: quizId }); + } + + async deleteQuizSet(quizSetId: number) { + const quizSet = await this.findQuizSet(quizSetId); + + await this.quizSetRepository.delete({ id: quizSetId }); + } + + async findQuizSet(quizSetId: number) { + const quizSet = await this.quizSetRepository.findOneBy({ id: quizSetId }); + if (!quizSet) { + throw new BadRequestException(`해당 퀴즈셋을 찾을 수 없습니다.`); + } + + return quizSet; + } + + async findQuiz(quizId: number) { + const quiz = await this.quizRepository.findOneBy({ id: quizId }); + if (!quiz) { + throw new BadRequestException(`퀴즈를 찾을 수 없습니다.`); + } + + return quiz; + } + + async searchQuizSet(searchQuery: SearchQuizSetRequestDTO) { + const { name, page, size } = searchQuery; + + const [quizSets, count] = await Promise.all([ + this.quizSetRepository.searchByName(name, page, size), + this.quizSetRepository.countByName(name), + ]); + + const quizSetDetails = quizSets.map(QuizSetDetails.from); + return { quizSetDetails, total: count, currentPage: page }; + } + +} diff --git a/apps/backend/src/quiz/repository/quiz-set.repository.ts b/apps/backend/src/quiz/repository/quiz-set.repository.ts new file mode 100644 index 0000000..4275e55 --- /dev/null +++ b/apps/backend/src/quiz/repository/quiz-set.repository.ts @@ -0,0 +1,23 @@ +import { DataSource, ILike, Repository } from 'typeorm'; +import { QuizSet } from '../entity/quiz-set.entity'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class QuizSetRepository extends Repository { + constructor(dataSource: DataSource) { + super(QuizSet, dataSource.manager); + } + + searchByName(name: string, page: number, pageSize: number) { + return this.find({ + where: {name: ILike(`${name}%`)}, + order: {recommended: 'DESC', createAt: 'desc' }, + skip: (page - 1) * pageSize, + take: pageSize, + }); + } + + countByName(name: string) { + return this.count({ where: { name: ILike(`${name}%`) } }); + } +} diff --git a/apps/backend/src/quiz/repository/quiz.repository.ts b/apps/backend/src/quiz/repository/quiz.repository.ts new file mode 100644 index 0000000..cca7a10 --- /dev/null +++ b/apps/backend/src/quiz/repository/quiz.repository.ts @@ -0,0 +1,10 @@ +import { Quiz } from '../entity/quiz.entitiy'; +import { Injectable } from '@nestjs/common'; +import { DataSource, Repository } from 'typeorm'; + +@Injectable() +export class QuizRepository extends Repository { + constructor(dataSource: DataSource) { + super(Quiz, dataSource.manager); + } +} diff --git a/apps/backend/test/app.e2e-spec.ts b/apps/backend/test/app.e2e-spec.ts new file mode 100644 index 0000000..954e93a --- /dev/null +++ b/apps/backend/test/app.e2e-spec.ts @@ -0,0 +1,27 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import { AppModule } from './../src/app.module'; + +describe('AppController (e2e)', () => { + let app: INestApplication; + + beforeEach(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + }); + afterAll(async () => { + await app.close(); + }); + + it('/ (GET)', () => { + return request(app.getHttpServer()) + .get('/') + .expect(200) + .expect('Hello World!'); + }); +}); diff --git a/apps/backend/test/jest-e2e.json b/apps/backend/test/jest-e2e.json new file mode 100644 index 0000000..e9d912f --- /dev/null +++ b/apps/backend/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/backend/test/play.e2e-spec.ts b/apps/backend/test/play.e2e-spec.ts new file mode 100644 index 0000000..2019944 --- /dev/null +++ b/apps/backend/test/play.e2e-spec.ts @@ -0,0 +1,69 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import { WsAdapter } from '@nestjs/platform-ws'; +import { PlayModule } from '../src/play/play.module'; +import cookieParser from 'cookie-parser'; +import session from 'express-session'; +import { Server } from 'http'; +import wsrequest from 'superwstest'; + +describe('PlayGateway (e2e)', () => { + let app: INestApplication; + let server: Server; + let agent: any; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [PlayModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.use(cookieParser()); + app.use( + session({ + secret: 'my-secret', + resave: true, + saveUninitialized: true, + }), + ); + app.useWebSocketAdapter(new WsAdapter(app)); + + await app.init(); + + server = app.getHttpServer(); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach((done) => { + server.listen(0, 'localhost', done); + }); + + afterEach((done) => { + server.close(done); + }); + + describe('WebSocket Connection Test', () => { + it('should connect successfully', async () => { + // await wsrequest(server).post('/quiz-zone').expect(201); + await wsrequest(server).ws('/play').set('Cookie', 'sid=12345'); + }); + + it('should receive pong', async () => { + await wsrequest(server) + .ws('/play') + .set('Cookie', 'sid=12345') + .expectJson({}) + // .expectText('connected') + .sendJson({ + event: 'createPlay', + }) + .expectJson({ + event: 'pong', + }) + .close(); + }); + }); +}); diff --git a/apps/backend/test/quiz-zone.e2e-spec.ts b/apps/backend/test/quiz-zone.e2e-spec.ts new file mode 100644 index 0000000..184441e --- /dev/null +++ b/apps/backend/test/quiz-zone.e2e-spec.ts @@ -0,0 +1,43 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; +import cookieParser from 'cookie-parser'; +import session from 'express-session'; +import TestAgent from 'supertest/lib/agent'; + +describe('QuizZoneController (e2e)', () => { + let app: INestApplication; + let agent: TestAgent; // agent 추가 + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.use(cookieParser()); + app.use( + session({ + secret: 'my-secret', + resave: true, + saveUninitialized: true, + }), + ); + + await app.init(); + + agent = request.agent(app.getHttpServer()); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('Error cases', () => { + it('POST /quiz-zone 중복 되면 에러', async () => { + await agent.post('/quiz-zone').expect(201); // 첫 번째 요청 + await agent.post('/quiz-zone').expect(409); // 두 번째 요청 (중복 에러) + }); + }); +}); diff --git a/apps/backend/test/quiz.e2e-spec.ts b/apps/backend/test/quiz.e2e-spec.ts new file mode 100644 index 0000000..78afe63 --- /dev/null +++ b/apps/backend/test/quiz.e2e-spec.ts @@ -0,0 +1,192 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { INestApplication } from '@nestjs/common'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; +import TestAgent from 'supertest/lib/agent'; +import { DataSource } from 'typeorm'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { QuizModule } from '../src/quiz/quiz.module'; +import { Quiz } from '../src/quiz/entity/quiz.entitiy'; +import { QuizSet } from '../src/quiz/entity/quiz-set.entity'; +import { initializeTransactionalContext } from 'typeorm-transactional'; +import { QUIZ_TYPE } from '../src/common/constants'; +import { CreateQuizSetRequestDto } from '../src/quiz/dto/create-quiz-set-request.dto'; +import { UpdateQuizRequestDto } from '../src/quiz/dto/update-quiz-request.dto'; + +describe('QuizController (e2e)', () => { + let app: INestApplication; + let agent: TestAgent; + let dataSource: DataSource; + let quiz: Quiz; + let quizSet: QuizSet; + + beforeAll(async () => { + // 트랜잭션 컨텍스트 초기화 + initializeTransactionalContext(); + + // NestJS 애플리케이션 생성 + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [ + AppModule, + TypeOrmModule.forRoot({ + type: 'sqlite', + database: ':memory', + entities: [Quiz, QuizSet], + synchronize: true, + }), + QuizModule, + ], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + + dataSource = app.get(DataSource); + agent = request.agent(app.getHttpServer()); + }); + + beforeEach(async () => { + await dataSource.query('PRAGMA foreign_keys = OFF'); // 외래 키 비활성화 + const entities = dataSource.entityMetadatas; + for (const entity of entities) { + const repository = dataSource.getRepository(entity.name); + await repository.query(`DELETE FROM ${entity.tableName}`); + } + await dataSource.query('PRAGMA foreign_keys = ON'); // 외래 키 활성화 + + // 초기 데이터 삽입 + const quizSetRepository = dataSource.getRepository(QuizSet); + const quizRepository = dataSource.getRepository(Quiz); + + // QuizSet 데이터 삽입 + quizSet = await quizSetRepository.save({ + id: 1, + name: '테스트 퀴즈셋', + }); + + // Quiz 데이터 삽입 + quiz = await quizRepository.save({ + id: 1, + question: '테스트 퀴즈 질문', + answer: '테스트 정답', + playTime: 5000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + quizSet: { id: 1 }, + }); + }); + + afterAll(async () => { + if (dataSource && dataSource.isInitialized) { + await dataSource.destroy(); + } + await app.close(); + }); + + describe('createQuizzes', () => { + it('새로운 퀴즈 추가 요청 성공', async () => { + const quizSetId = 1; + + const quizData = { + quizSetName: '퀴즈셋 이름', + quizDetails: [ + { + question: '지브리는 뭘로 돈 벌게요?', + answer: '토토로', + playTime: 30000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, + ], + } as CreateQuizSetRequestDto; + + const response = await agent.post(`/quiz/${quizSetId}`).send(quizData).expect(201); + }); + + it('퀴즈 생성 요청 - 존재하지 않는 퀴즈셋 id로 저장 요청 -> 400 반환', async () => { + const quizSetId = 3; + + const quizData = { + quizSetName: '퀴즈셋 이름', + quizDetails: [ + { + question: '지브리는 뭘로 돈 벌게요?', + answer: '토토로', + playTime: 30000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + }, + ], + } as CreateQuizSetRequestDto; + + await agent.post(`/quiz/${quizSetId}`).send(quizData).expect(400); + }); + }); + + describe('getQuizzes', () => { + it('정상적으로 퀴즈 리스트 반환', async () => { + const quizSetId = 1; + + const response = await agent.get(`/quiz/${quizSetId}`).expect(200); + + expect(response.body).toEqual( + expect.arrayContaining([ + { + id: expect.any(Number), + question: expect.any(String), + answer: expect.any(String), + playTime: expect.any(Number), + quizType: expect.any(String), + }, + ]), + ); + }); + + it('퀴즈 리스트 반환 요청 - 존재하지 않는 QuizSetId로 요청 시 400 반환', async () => { + const nonExistentQuizSetId = 10; + const response = await agent.get(`/quiz/${nonExistentQuizSetId}`).expect(400); + + expect(response.body).toEqual({ + statusCode: 400, + message: '해당 퀴즈셋을 찾을 수 없습니다.', + error: 'Bad Request', + }); + }); + }); + describe('updateQuiz', () => { + it('퀴즈 정상적으로 수정', async () => { + const quizId = quiz.id; + const dto = { + question: '질문 수정 테스트', + answer: '대답 수정 테스트', + playTime: 5000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + } as UpdateQuizRequestDto; + + const response = await agent.patch(`/quiz/${quizId}`).send(dto).expect(200); + }); + + it('존재하지 않는 퀴즈 수정 요청 -> 400 에러', async () => { + const quizId = 100; + const dto = { + question: '질문 수정 테스트', + answer: '대답 수정 테스트', + playTime: 5000, + quizType: QUIZ_TYPE.SHORT_ANSWER, + } as UpdateQuizRequestDto; + + const response = await agent.patch(`/quiz/${quizId}`).send(dto).expect(200); + }); + }); + + describe('deleteQuiz', () => { + it('퀴즈 정상적으로 삭제', async () => { + const quizId = quiz.id; + + const response = await agent.delete(`/quiz/${quizId}`).expect(200); + }); + + it('존재하지 않는 퀴즈 삭제 요청 -> 400 에러', async () => { + const quizId = 100; + + const response = await agent.delete(`/quiz/${quizId}`).expect(400); + }); + }); +}); diff --git a/apps/backend/tsconfig.build.json b/apps/backend/tsconfig.build.json new file mode 100644 index 0000000..64f86c6 --- /dev/null +++ b/apps/backend/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] +} diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json new file mode 100644 index 0000000..7667eb9 --- /dev/null +++ b/apps/backend/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "esModuleInterop": true + } +} diff --git a/apps/frontend/.gitignore b/apps/frontend/.gitignore new file mode 100644 index 0000000..f940a99 --- /dev/null +++ b/apps/frontend/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*storybook.log diff --git a/apps/frontend/.storybook/main.ts b/apps/frontend/.storybook/main.ts new file mode 100644 index 0000000..8be8463 --- /dev/null +++ b/apps/frontend/.storybook/main.ts @@ -0,0 +1,29 @@ +import type { StorybookConfig } from '@storybook/react-vite'; +import { join, dirname } from 'path'; + +function getAbsolutePath(value: string): string { + return dirname(require.resolve(join(value, 'package.json'))); +} + +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: [ + getAbsolutePath('@storybook/addon-onboarding'), + getAbsolutePath('@storybook/addon-essentials'), + getAbsolutePath('@chromatic-com/storybook'), + getAbsolutePath('@storybook/addon-interactions'), + // Tailwind 애드온 추가 + { + name: '@storybook/addon-styling', + options: { + postCss: true, + }, + }, + ], + framework: { + name: getAbsolutePath('@storybook/react-vite'), + options: {}, + }, +}; + +export default config; diff --git a/apps/frontend/.storybook/preview.ts b/apps/frontend/.storybook/preview.ts new file mode 100644 index 0000000..50f236b --- /dev/null +++ b/apps/frontend/.storybook/preview.ts @@ -0,0 +1,15 @@ +import type { Preview } from '@storybook/react'; +import '@/index.css'; + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, +}; + +export default preview; diff --git a/apps/frontend/README.md b/apps/frontend/README.md new file mode 100644 index 0000000..95b734b --- /dev/null +++ b/apps/frontend/README.md @@ -0,0 +1,330 @@ +# BooQuiz Frontend + +실시간 대규모 참여형 퀴즈 플랫폼 BooQuiz의 프론트엔드 레포지토리입니다. + +## 🎯 프로젝트 개요 + +BooQuiz는 300명 이상의 사용자가 실시간으로 참여할 수 있는 퀴즈 플랫폼입니다. 도전 골든벨 형식의 퀴즈를 웹 기반으로 즐길 수 있습니다. + +### 주요 기능 + +- 실시간 대규모 참여형 퀴즈 진행 +- QR코드/PIN 번호를 통한 간편 입장 +- 실시간 답안 제출 및 채점 +- 실시간 순위 산정 및 표시 +- 다양한 문제 유형 지원 (객관식, 단답형) + +## 🛠 기술 스택 + +### 핵심 기술 + +- **Framework:** React 18.3.1 +- **Language:** TypeScript 5.6.2 +- **Build Tool:** Vite 5.4.10 +- **Routing:** React Router DOM 6.27.0 +- **상태 관리:** React Hooks + Context API +- **WebSocket:** ws 8.18.0 + +### UI 컴포넌트 + +- **CSS Framework:** Tailwind CSS 3.4.14 +- **UI Components:** + - Radix UI (Alert Dialog, Avatar, Progress, Tooltip) + - shadcn/ui +- **Icons:** Lucide React 0.454.0 +- **Utility Libraries:** + - class-variance-authority + - clsx + - tailwind-merge + - tailwindcss-animate + +### 개발 도구 + +- **테스트:** + - Vitest + - Testing Library (React, Jest DOM, Hooks) + - JSDOM +- **문서화:** Storybook 8.4.2 +- **코드 품질:** + - ESLint with TypeScript support + - TypeScript ESLint + - React Hooks/Refresh plugins +- **타입 지원:** TypeScript ~5.6.2 + +## 🏗 프로젝트 구조 + +``` +src/ +├── assets/ # 정적 리소스 +│ └── images/ # 이미지 파일 +├── blocks/ # 페이지별 주요 컴포넌트 블록 +│ ├── CreateQuizZone/ # 퀴즈존 생성 관련 컴포넌트 +│ ├── QuizZone/ # 퀴즈존 진행 관련 컴포넌트 +│ └── Skeleton/ # 로딩 스켈레톤 컴포넌트 +├── components/ # 재사용 가능한 컴포넌트 +│ ├── boundary/ # 에러 바운더리 컴포넌트 +│ ├── common/ # 공통 UI 컴포넌트 +│ └── ui/ # shadcn/ui 기반 컴포넌트 +├── constants/ # 상수 정의 +├── hook/ # 커스텀 훅 +│ ├── quizZone/ # 퀴즈존 관련 훅 +│ └── common/ # 공통 훅 +├── lib/ # 유틸리티 라이브러리 +├── pages/ # 페이지 컴포넌트 +├── router/ # 라우팅 설정 +├── test/ # 테스트 설정 +├── types/ # TypeScript 타입 정의 +└── utils/ # 유틸리티 함수 +``` + +### 주요 디렉토리 설명 + +#### 📂 blocks + +페이지별 주요 기능 블록을 구성하는 컴포넌트들이 위치합니다. 재사용성보다는 특정 페이지나 기능에 종속적인 컴포넌트들이 포함됩니다. + +- `CreateQuizZone/`: 퀴즈존 생성 관련 컴포넌트 +- `QuizZone/`: 실제 퀴즈 진행 관련 컴포넌트 + +#### 📂 components + +재사용 가능한 UI 컴포넌트들이 위치합니다. + +- `boundary/`: 에러 처리를 위한 바운더리 컴포넌트 +- `common/`: 버튼, 입력창 등 공통 UI 컴포넌트 +- `ui/`: shadcn/ui 기반으로 커스터마이징된 컴포넌트 + +#### 📂 hook + +애플리케이션에서 사용되는 커스텀 훅들이 위치합니다. + +- `useQuizZone`: 퀴즈존 상태 관리 +- `useTimer`: 타이머 기능 +- `useWebSocket`: WebSocket 연결 관리 +- `useValidInput`: 입력값 유효성 검사 + +#### 📂 pages + +애플리케이션의 각 페이지를 구성하는 컴포넌트들이 위치합니다. + +- `MainPage`: 메인 페이지 +- `QuizZonePage`: 퀴즈존 페이지 +- `CreateQuizZonePage`: 퀴즈존 생성 페이지 + +#### 📂 utils + +공통적으로 사용되는 유틸리티 함수들이 위치합니다. + +- `atob`: base64 디코딩 +- `requests`: API 요청 관련 유틸리티 +- `validators`: 유효성 검사 유틸리티 + +## ⚡️ 주요 커스텀 훅 + +### useQuizZone + +퀴즈존의 전체 상태와 생명주기를 관리하는 핵심 훅입니다. + +```typescript +const { + quizZoneState, // 퀴즈존 현재 상태 + initQuizZoneData, // 초기 데이터 설정 + submitQuiz, // 답안 제출 + startQuiz, // 퀴즈 시작 + playQuiz, // 플레이 모드 전환 + exitQuiz, // 퀴즈존 나가기 +} = useQuizZone(); +``` + +### useTimer + +실시간 타이머 기능을 제공하는 훅입니다. + +```typescript +const { time, start } = useTimer({ + initialTime: 60, + onComplete: () => console.log('타이머 완료!'), +}); +``` + +### useWebSocket + +WebSocket 연결 및 메시지 처리를 관리하는 훅입니다. + +```typescript +const { sendMessage, closeConnection } = useWebSocket(wsUrl, messageHandler); +``` + +## 🚀 시작하기 + +1. 프로젝트 클론 + +```bash +git clone https://github.com/boostcampwm-2024/web08-BooQuiz.git +``` + +2. pnpm 설치 (없는 경우) + +```bash +npm install -g pnpm +``` + +3. 의존성 설치 + +```bash +pnpm install +``` + +4. 환경 변수 설정 + +```bash +cp .env.example .env +``` + +필요한 환경 변수: + +- `VITE_WS_URL`: WebSocket 서버 URL + +5. 개발 서버 실행 + +```bash +pnpm dev +``` + +## 📦 스크립트 + +```bash +# 개발 서버 실행 +pnpm dev + +# 프로덕션 빌드 +pnpm build + +# 프로덕션 미리보기 +pnpm preview + +# 테스트 실행 +pnpm test # 감시 모드 +pnpm test:run # 단일 실행 + +# Storybook 실행 +pnpm storybook + +# Storybook 빌드 +pnpm build-storybook + +# 린트 검사 +pnpm lint +``` + +## 🔍 주요 페이지 흐름 + +1. **메인 페이지** + + - PIN 번호 입력 + - QR 코드 스캔 + - 퀴즈존 생성 + +2. **퀴즈존 대기실** + + - 참가자 목록 확인 + - 퀴즈 정보 확인 + - 시작 대기 + +3. **퀴즈 진행** + + - 문제 출제 + - 답안 제출 + - 실시간 피드백 + +4. **결과 확인** + - 최종 점수 + - 순위 확인 + - 문제별 결과 + +## 🧪 테스트 + +프로젝트는 Vitest와 Testing Library를 사용하여 테스트를 작성합니다. + +```bash +# 테스트 감시 모드 실행 +pnpm test + +# 전체 테스트 단일 실행 +pnpm test:run + +# 특정 파일 테스트 +pnpm test src/components/MyComponent.test.tsx +``` + +## 📚 Storybook + +컴포넌트 문서화와 개발을 위해 Storybook을 사용합니다. + +```bash +# Storybook 개발 서버 실행 (기본 포트: 6006) +pnpm storybook + +# Storybook 정적 빌드 +pnpm build-storybook +``` + +## 📝 패키지 관리 + +이 프로젝트는 패키지 관리자로 pnpm을 사용합니다. pnpm은 디스크 공간을 효율적으로 사용하고, 설치 속도가 빠르며, 패키지 버전을 더 엄격하게 관리합니다. + +### pnpm 주요 명령어 + +```bash +# 의존성 설치 +pnpm install + +# 개발 의존성 추가 +pnpm add -D [package-name] + +# 일반 의존성 추가 +pnpm add [package-name] + +# 의존성 제거 +pnpm remove [package-name] + +# 의존성 업데이트 +pnpm update + +# 캐시 정리 +pnpm store prune +``` + +## 📝 개발 가이드라인 + +### 컴포넌트 작성 + +- Presentational/Container 패턴 준수 +- Props에 대한 명확한 타입 정의 +- 재사용 가능한 컴포넌트는 components 디렉토리에 배치 +- 페이지별 컴포넌트는 blocks 디렉토리에 배치 + +### 상태 관리 + +- 지역 상태는 useState 활용 +- 복잡한 상태 로직은 useReducer 사용 + +### 실시간 통신 + +- WebSocket 연결은 useWebSocket 훅 사용 +- 연결 끊김에 대한 자동 재연결 처리 +- 메시지 큐를 통한 순차적 처리 + +## 🔐 보안 고려사항 + +- WebSocket 메시지 검증 +- 사용자 입력 데이터 검증 +- API 요청 시 인증 토큰 관리 +- 환경 변수를 통한 설정 관리 + +## 🤝 기여하기 + +1. 이슈 생성 또는 기존 이슈 확인 +2. feature/[기능명] 브랜치 생성 +3. 개발 및 테스트 완료 +4. PR 생성 및 리뷰 요청 diff --git a/apps/frontend/components.json b/apps/frontend/components.json new file mode 100644 index 0000000..0b03196 --- /dev/null +++ b/apps/frontend/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} \ No newline at end of file diff --git a/apps/frontend/eslint.config.js b/apps/frontend/eslint.config.js new file mode 100644 index 0000000..092408a --- /dev/null +++ b/apps/frontend/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/apps/frontend/index.html b/apps/frontend/index.html new file mode 100644 index 0000000..9bf6476 --- /dev/null +++ b/apps/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + + BooQuiz + + + +
+ + + + \ No newline at end of file diff --git a/apps/frontend/package.json b/apps/frontend/package.json new file mode 100644 index 0000000..5973c27 --- /dev/null +++ b/apps/frontend/package.json @@ -0,0 +1,67 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "start": "vite", + "lint": "eslint .", + "preview": "vite preview", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", + "test": "vitest", + "test:run": "vitest run" + }, + "dependencies": { + "@radix-ui/react-alert-dialog": "^1.1.2", + "@radix-ui/react-avatar": "^1.1.1", + "@radix-ui/react-icons": "^1.3.1", + "@radix-ui/react-progress": "^1.1.0", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.4", + "@storybook/preview-api": "^8.4.2", + "@types/react-router-dom": "^5.3.3", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "lucide-react": "^0.454.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^6.27.0", + "tailwind-merge": "^2.5.4", + "tailwindcss-animate": "^1.0.7", + "ws": "^8.18.0" + }, + "devDependencies": { + "@chromatic-com/storybook": "^3.2.2", + "@eslint/js": "^9.13.0", + "@storybook/addon-essentials": "^8.4.2", + "@storybook/addon-interactions": "^8.4.2", + "@storybook/addon-onboarding": "^8.4.2", + "@storybook/addon-styling": "^2.0.0", + "@storybook/blocks": "^8.4.2", + "@storybook/react": "^8.4.2", + "@storybook/react-vite": "^8.4.2", + "@storybook/test": "^8.4.2", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.0.1", + "@testing-library/react-hooks": "^8.0.1", + "@types/node": "^22.9.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.3", + "autoprefixer": "^10.4.20", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.14", + "globals": "^15.11.0", + "jsdom": "^25.0.1", + "postcss": "^8.4.47", + "storybook": "^8.4.2", + "tailwindcss": "^3.4.14", + "typescript": "~5.6.2", + "typescript-eslint": "^8.11.0", + "vite": "^5.4.10", + "vitest": "^2.1.5" + } +} diff --git a/apps/frontend/postcss.config.js b/apps/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/apps/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/apps/frontend/public/BooQuizFavicon.png b/apps/frontend/public/BooQuizFavicon.png new file mode 100644 index 0000000..0faba3f Binary files /dev/null and b/apps/frontend/public/BooQuizFavicon.png differ diff --git a/apps/frontend/public/vite.svg b/apps/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/apps/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/frontend/src/App.css b/apps/frontend/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/apps/frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/apps/frontend/src/App.tsx b/apps/frontend/src/App.tsx new file mode 100644 index 0000000..51e47d6 --- /dev/null +++ b/apps/frontend/src/App.tsx @@ -0,0 +1,7 @@ +import Router from './router/router'; + +function App() { + return ; +} + +export default App; diff --git a/apps/frontend/src/assets/react.svg b/apps/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/apps/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/frontend/src/blocks/CreateQuizZone/CandidateQuizzes.tsx b/apps/frontend/src/blocks/CreateQuizZone/CandidateQuizzes.tsx new file mode 100644 index 0000000..e9fd476 --- /dev/null +++ b/apps/frontend/src/blocks/CreateQuizZone/CandidateQuizzes.tsx @@ -0,0 +1,62 @@ +import Typography from '@/components/common/Typogrpahy'; +import { Quiz } from '@/types/quizZone.types.ts'; +import { Trash2 } from 'lucide-react'; + +interface CandidateQuizProps { + quizzes: Quiz[]; + removeQuiz: (quiz: Quiz) => void; +} + +const CandidateQuizzes = ({ quizzes, removeQuiz }: CandidateQuizProps) => { + // 퀴즈 타입을 한글로 변환하는 함수 + const getQuizTypeText = (type: string) => { + return type === 'SHORT' ? '단답형' : type; + }; + + return ( +
    + {quizzes.map((quiz, i) => ( +
  • + {/* 상단 정보 영역 */} +
    +
    + {/* 퀴즈 타입 뱃지 */} + + {getQuizTypeText(quiz.quizType ?? '')} + + {/* 시간 뱃지 */} + + {quiz.playTime}초 + +
    + {/* 삭제 버튼 */} + +
    + + {/* 문제 내용 */} +
    + + +
    + + {/* 정답 */} +
    + + +
    +
  • + ))} +
+ ); +}; + +export default CandidateQuizzes; diff --git a/apps/frontend/src/blocks/CreateQuizZone/CreateQuiz.tsx b/apps/frontend/src/blocks/CreateQuizZone/CreateQuiz.tsx new file mode 100644 index 0000000..1d7d783 --- /dev/null +++ b/apps/frontend/src/blocks/CreateQuizZone/CreateQuiz.tsx @@ -0,0 +1,115 @@ +import { ProblemType, Quiz } from '@/types/quizZone.types.ts'; +import useValidState from '@/hook/useValidInput.ts'; +import { validAnswer, validQuestion, validTime } from '@/utils/validators.ts'; +import ContentBox from '@/components/common/ContentBox'; +import Typography from '@/components/common/Typogrpahy'; +import Input from '@/components/common/Input'; +import CommonButton from '@/components/common/CommonButton'; + +interface CreateQuizProps { + handleCreateQuiz: (quiz: Quiz) => void; +} + +const CreateQuiz = ({ handleCreateQuiz }: CreateQuizProps) => { + const [question, questionInvalidMessage, setQuestion, isInvalidQuestion] = + useValidState('', validQuestion); + const [answer, answerInvalidMessage, setAnswer, isInvalidAnswer] = useValidState( + '', + validAnswer, + ); + const [playTime, timeInvalidMessage, setPlayTime, isInvalidPlayTime] = useValidState( + 30, + validTime, + ); + const [quizType, _, __, isInvalidQuizType] = useValidState('SHORT', () => {}); + + const isInvalid = () => + isInvalidQuestion || isInvalidAnswer || isInvalidPlayTime || isInvalidQuizType; + + const handleComplete = () => { + if (isInvalid()) { + // 알럿 추가 + console.log('입력 조건을 확인해주세요.'); + return; + } + + handleCreateQuiz({ + question, + answer, + quizType, + playTime, + }); + }; + + return ( + +
+ {/*
+ + setType(e.target.value as ProblemType)} + /> + {isInvalidType && ( + + )} +
*/} +
+ setPlayTime(parseInt(e.target.value))} + isBorder={true} + /> + {isInvalidPlayTime && ( + + )} +
+
+
+ setQuestion(e.target.value)} + isBorder={true} + error={isInvalidQuestion && questionInvalidMessage} + isShowCount={true} + max={200} + /> + {/* {isInvalidQuestion && ( + + )} */} +
+
+ setAnswer(e.target.value)} + isBorder={true} + error={isInvalidAnswer && answerInvalidMessage} + isShowCount={true} + max={50} + /> + {/* {isInvalidAnswer && ( + + )} */} +
+ handleComplete()} disabled={isInvalid()}> + 추가하기 + +
+ ); +}; + +export default CreateQuiz; diff --git a/apps/frontend/src/blocks/CreateQuizZone/CreateQuizSet.tsx b/apps/frontend/src/blocks/CreateQuizZone/CreateQuizSet.tsx new file mode 100644 index 0000000..4cab76c --- /dev/null +++ b/apps/frontend/src/blocks/CreateQuizZone/CreateQuizSet.tsx @@ -0,0 +1,106 @@ +import { useState } from 'react'; +import { Quiz } from '@/types/quizZone.types.ts'; +import CandidateQuizzes from '@/blocks/CreateQuizZone/CandidateQuizzes.tsx'; +import CreateQuiz from '@/blocks/CreateQuizZone/CreateQuiz.tsx'; +import useValidState from '@/hook/useValidInput.ts'; +import { requestCreateQuizSet } from '@/utils/requests.ts'; +import { validQuizSetName, validQuizzes } from '@/utils/validators.ts'; +import { QUIZ_LIMIT_COUNT } from '@/constants/quiz-set.constants.ts'; +import Input from '@/components/common/Input'; +import ContentBox from '@/components/common/ContentBox'; +import CommonButton from '@/components/common/CommonButton'; + +interface CreateQuizZoneQuizSetProps { + handlePrevStepButton?: () => void; + updateQuizSet?: (quizSetId: string, quizSetName: string) => void; +} + +const CreateQuizSet = ({ handlePrevStepButton, updateQuizSet }: CreateQuizZoneQuizSetProps) => { + const [name, validNameMessage, setName, isInvalidName] = useValidState( + '', + validQuizSetName, + ); + const [quizzes, validQuizzesMessage, setQuizzes, isInvalidQuizzes] = useValidState( + [], + validQuizzes, + ); + + const [isLoading, setIsLoading] = useState(false); + + const isInvalid = () => isInvalidName || isInvalidQuizzes; + + const createQuizSet = async () => { + try { + if (isLoading || isInvalid()) { + return; + } + + setIsLoading(true); + + const quizSetId = await requestCreateQuizSet({ + quizSetName: name, + quizzes: quizzes, + }); + + updateQuizSet?.(quizSetId, name); + handlePrevStepButton?.(); + } catch (error) { + // 얼럿 추가 + console.error('퀴즈 생성 처리중 오류가 발생했습니다.'); + } finally { + setIsLoading(false); + } + }; + + const addQuiz = (quiz: Quiz) => { + if (quizzes.length >= QUIZ_LIMIT_COUNT) { + // 얼럿 추가 + console.log('퀴즈 제한 숫자를 초과하였습니다.'); + } + + setQuizzes([...quizzes, quiz]); + }; + + const removeQuiz = (quiz: Quiz) => { + setQuizzes([...quizzes].filter((q) => q !== quiz)); + }; + + return ( +
+ + setName(e.target.value)} + name="quiz-zone-id" + isBorder={true} + error={validNameMessage} + isShowCount={true} + max={100} + /> + {/* {validNameMessage && } */} + + {validQuizzesMessage && {validQuizzesMessage}} + + + createQuizSet()} + disabled={isInvalid()} + > + 퀴즈셋 만들기 + + {handlePrevStepButton && ( + handlePrevStepButton()} + > + 돌아가기 + + )} + +
+ ); +}; + +export default CreateQuizSet; diff --git a/apps/frontend/src/blocks/CreateQuizZone/CreateQuizZoneBasic.tsx b/apps/frontend/src/blocks/CreateQuizZone/CreateQuizZoneBasic.tsx new file mode 100644 index 0000000..ed11e7c --- /dev/null +++ b/apps/frontend/src/blocks/CreateQuizZone/CreateQuizZoneBasic.tsx @@ -0,0 +1,186 @@ +import ContentBox from '@/components/common/ContentBox.tsx'; +import { ChangeEvent, useMemo } from 'react'; +import SearchQuizSet from '@/blocks/CreateQuizZone/SearchQuizSet.tsx'; +import { useNavigate } from 'react-router-dom'; +import { requestCreateQuizZone } from '@/utils/requests.ts'; +import { + CreateQuizZone, + CreateQuizZoneReducerActions, + CreateQuizZoneStage, + ResponseQuizSet, +} from '@/types/create-quiz-zone.types.ts'; +import Typography from '@/components/common/Typogrpahy'; +import Input from '@/components/common/Input'; +import CommonButton from '@/components/common/CommonButton'; +import { + validateQuizZoneSetCode, + validateQuizZoneSetDescription, + validateQuizZoneSetLimit, + validateQuizZoneSetName, +} from '@/utils/validators'; +import { Button } from '@/components/ui/button'; +import { Plus } from 'lucide-react'; + +interface CreateQuizZoneBasicProps { + quizZone: CreateQuizZone; + updateQuizZoneBasic: (value: string, type: CreateQuizZoneReducerActions) => void; + moveStage: (stage: CreateQuizZoneStage) => void; +} + +const CreateQuizZoneBasic = ({ + quizZone, + updateQuizZoneBasic, + moveStage, +}: CreateQuizZoneBasicProps) => { + const { quizZoneId, title, description, limitPlayerCount, quizSetName } = quizZone; + const navigate = useNavigate(); + + const handleChangeQuizZoneBasic = ( + event: ChangeEvent, + type: CreateQuizZoneReducerActions, + ) => { + const value = event.target.value ?? ''; + updateQuizZoneBasic(value, type); + }; + + const selectQuizSet = (quizSet: ResponseQuizSet) => { + updateQuizZoneBasic(quizSet.id, 'QUIZ_SET_ID'); + updateQuizZoneBasic(quizSet.name, 'QUIZ_SET_NAME'); + }; + + const createQuizZone = async () => { + try { + await requestCreateQuizZone(quizZone); + navigate(`/${quizZoneId}`); + } catch (error) { + console.error(error); + } + }; + + // 유효성 검사 결과를 메모이제이션 + const validationError = useMemo(() => { + return ( + validateQuizZoneSetCode(quizZoneId) || + validateQuizZoneSetName(title) || + validateQuizZoneSetDescription(description) || + validateQuizZoneSetLimit(limitPlayerCount) + ); + }, [title, description, quizZoneId, limitPlayerCount]); + + return ( +
+ + +
+ handleChangeQuizZoneBasic(e, 'QUIZ_ZONE_ID')} + isBorder={true} + className="rounded-md w-full" + placeholder="숫자와 영문자 5자 이상 10자 이하" + error={ + validationError == '5자 이상 입력해주세요.' || + validationError == '10자 이하로 입력해주세요.' || + validationError == '숫자와 알파벳 조합만 가능합니다.' + ? validationError + : '' + } + isShowCount={true} + max={10} + /> + handleChangeQuizZoneBasic(e, 'LIMIT')} + isBorder={true} + className="w-20" + isShowCount={true} + min={1} + max={300} + /> +
+ handleChangeQuizZoneBasic(e, 'TITLE')} + isBorder={true} + placeholder="퀴즈존 제목을 입력하세요" + error={ + validationError == '100자 이하로 입력해주세요.' || + validationError == '제목을 입력해주세요.' + ? validationError + : '' + } + isShowCount={true} + max={100} + /> + + handleChangeQuizZoneBasic(e, 'DESC')} + isBorder={true} + placeholder="퀴즈존 설명을 입력하세요" + error={validationError == '300자 이하로 입력해주세요.' ? validationError : ''} + isShowCount={true} + max={300} + /> +
+ +
+ + +
+
+
+ + {/* 선택된 퀴즈셋 */} + {}} + isBorder={true} + placeholder="선택된 퀴즈셋이 없습니다." + disabled={true} + className="text-lg" + /> +
+
+ {/* 퀴즈셋을 선택하시거나 만들어주세요 */} + +
+ createQuizZone()} + disabled={!!validationError} + > + 퀴즈존 만들기 + +
+ ); +}; + +export default CreateQuizZoneBasic; diff --git a/apps/frontend/src/blocks/CreateQuizZone/SearchQuizSet.tsx b/apps/frontend/src/blocks/CreateQuizZone/SearchQuizSet.tsx new file mode 100644 index 0000000..655f317 --- /dev/null +++ b/apps/frontend/src/blocks/CreateQuizZone/SearchQuizSet.tsx @@ -0,0 +1,89 @@ +import { ChangeEvent, useEffect, useState } from 'react'; +import SearchQuizSetResults from '@/blocks/CreateQuizZone/SearchQuizSetResults.tsx'; +import { requestSearchQuizSets } from '@/utils/requests.ts'; +import { ResponseQuizSet } from '@/types/create-quiz-zone.types.ts'; +import Input from '@/components/common/Input'; +import { Search } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +interface SearchQuizSetProps { + selectQuizSet: (quizSet: ResponseQuizSet) => void; +} + +const SearchQuizSet = ({ selectQuizSet }: SearchQuizSetProps) => { + const [searchKeyword, setSearchKeyword] = useState(''); + const [currentPage] = useState(1); + const [pageSize] = useState(10); + + const [resultCount, setResultCount] = useState(0); + const [quizSets, setQuizSets] = useState([]); + + const [isLoading, setIsLoading] = useState(true); + + const updateSearchQuizSet = async () => { + try { + setIsLoading(true); + + const { quizSets, totalQuizSetCount } = await requestSearchQuizSets({ + name: searchKeyword, + page: currentPage.toString(), + size: pageSize.toString(), + }); + + setQuizSets(quizSets); + setResultCount(totalQuizSetCount); + } catch (error) { + // 얼럿 추가 + console.log(error); + console.log('퀴즈셋 검색 처리중 오류가 발생하였습니다.'); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + updateSearchQuizSet(); + }, []); + + function handleChangeSearchKeyword(event: ChangeEvent) { + const value = event.target.value; + setSearchKeyword(value); + } + + return ( +
+
+ + + +
+ {isLoading ? ( +
Loading...
+ ) : ( + + )} +
+ ); +}; + +export default SearchQuizSet; diff --git a/apps/frontend/src/blocks/CreateQuizZone/SearchQuizSetResults.tsx b/apps/frontend/src/blocks/CreateQuizZone/SearchQuizSetResults.tsx new file mode 100644 index 0000000..1da8043 --- /dev/null +++ b/apps/frontend/src/blocks/CreateQuizZone/SearchQuizSetResults.tsx @@ -0,0 +1,49 @@ +import { ResponseQuizSet } from '@/types/create-quiz-zone.types.ts'; +import { Button } from '@/components/ui/button'; +import { ChevronRight } from 'lucide-react'; + +interface SearchQuizSetResultsProp { + results: ResponseQuizSet[]; + selectQuizSet: (quizSet: ResponseQuizSet) => void; + total: number; +} + +const SearchQuizSetResults = ({ results, selectQuizSet, total }: SearchQuizSetResultsProp) => { + return ( +
+
+ 검색 결과 {total}건 +
+ + {results.length <= 0 ? ( +
+ 검색 결과가 없습니다. 새로운 퀴즈셋을 만들어보세요. +
+ ) : ( +
+

퀴즈셋을 선택해주세요.

+
    + {results.map((result) => ( +
  • + {result.name} + +
  • + ))} +
+
+ )} +
+ ); +}; + +export default SearchQuizSetResults; diff --git a/apps/frontend/src/blocks/QuizZone/QuizCompleted.tsx b/apps/frontend/src/blocks/QuizZone/QuizCompleted.tsx new file mode 100644 index 0000000..9888c91 --- /dev/null +++ b/apps/frontend/src/blocks/QuizZone/QuizCompleted.tsx @@ -0,0 +1,82 @@ +import ContentBox from '@/components/common/ContentBox'; +import Typography from '@/components/common/Typogrpahy'; +import { useTimer } from '@/hook/useTimer'; +import { useEffect } from 'react'; +import { CurrentQuizResult } from '@/types/quizZone.types.ts'; +import { Player } from '@/types/quizZone.types'; +import PodiumPlayers from '@/components/common/PodiumPlayers'; + +interface QuizCompletedProps { + currentPlayer: Player; + isLastQuiz: boolean; + deadlineTime: number; + currentQuizResult?: CurrentQuizResult; +} + +const QuizCompleted = ({ + currentPlayer, + isLastQuiz, + deadlineTime, + currentQuizResult, +}: QuizCompletedProps) => { + const currentTime = new Date().getTime(); + const remainingPrepTime = Math.max(0, deadlineTime - currentTime) / 1000; + + const { fastestPlayers, submittedCount } = currentQuizResult ?? {}; + const { start, time } = useTimer({ + initialTime: remainingPrepTime, + onComplete: () => {}, + }); + + useEffect(() => { + start(); + }, []); + + return ( +
+ + + + {!isLastQuiz && time !== null && ( +
+ + +
+ )} +
+ {currentQuizResult && ( + + + + + )} +
+ ); +}; + +export default QuizCompleted; diff --git a/apps/frontend/src/blocks/QuizZone/QuizInProgress.tsx b/apps/frontend/src/blocks/QuizZone/QuizInProgress.tsx new file mode 100644 index 0000000..0c327c4 --- /dev/null +++ b/apps/frontend/src/blocks/QuizZone/QuizInProgress.tsx @@ -0,0 +1,76 @@ +import CommonButton from '@/components/common/CommonButton'; +import ContentBox from '@/components/common/ContentBox'; +import Input from '@/components/common/Input'; +import ProgressBar from '@/components/common/ProgressBar'; +import Typography from '@/components/common/Typogrpahy'; +import { useTimer } from '@/hook/useTimer'; +import { CurrentQuiz } from '@/types/quizZone.types'; +import { useEffect, useState } from 'react'; + +interface QuizInProgressProps { + playTime: number | null; + currentQuiz: CurrentQuiz; + submitAnswer: (e: any) => void; +} + +const QuizInProgress = ({ currentQuiz, submitAnswer }: QuizInProgressProps) => { + const [answer, setAnswer] = useState(''); + + const MAX_TEXT_LENGTH = 100; + const MIN_TEXT_LENGTH = 1; + + const now = new Date().getTime(); + const { playTime, deadlineTime } = currentQuiz; + + const { start, time } = useTimer({ + initialTime: (deadlineTime - now) / 1000, + onComplete: () => {}, + }); + + useEffect(() => { + start(); + }, []); + + const handleSubmitAnswer = () => { + if (answer.length >= MIN_TEXT_LENGTH && answer.length <= MAX_TEXT_LENGTH) { + submitAnswer(answer); + } + }; + + return ( +
+ {}} /> + + + + setAnswer(e.target.value)} + name="quizAnswer" + value={answer} + placeholder={'정답을 입력해주세요'} + onKeyDown={(e) => { + if (e.key === 'Enter') { + handleSubmitAnswer(); + } + }} + height="h-14" + isBorder={true} + /> + + { + handleSubmitAnswer(); + }} + /> + +
+ ); +}; + +export default QuizInProgress; diff --git a/apps/frontend/src/blocks/QuizZone/QuizWaiting.tsx b/apps/frontend/src/blocks/QuizZone/QuizWaiting.tsx new file mode 100644 index 0000000..a4a1893 --- /dev/null +++ b/apps/frontend/src/blocks/QuizZone/QuizWaiting.tsx @@ -0,0 +1,72 @@ +import ContentBox from '@/components/common/ContentBox'; +import Typography from '@/components/common/Typogrpahy'; +import { useTimer } from '@/hook/useTimer'; +import { useEffect } from 'react'; + +interface QuizWaitingProps { + startTime: number; + playQuiz: () => void; + currentQuizSummary?: { + answer?: string; + correctPlayerCount?: number; + totalPlayerCount?: number; + }; +} + +const QuizWaiting = ({ playQuiz, startTime, currentQuizSummary }: QuizWaitingProps) => { + const currentTime = new Date().getTime(); + const remainingPrepTime = Math.max(0, startTime - currentTime) / 1000; + const { answer, correctPlayerCount, totalPlayerCount } = currentQuizSummary ?? {}; + + const isVisibleSummary = + answer !== undefined && + totalPlayerCount !== undefined && + totalPlayerCount > 0 && + correctPlayerCount !== undefined; + + const { start, time } = useTimer({ + initialTime: remainingPrepTime, + onComplete: () => { + playQuiz(); + }, + }); + + useEffect(() => { + start(); + }, []); + + return ( +
+ + + + + {time !== null && ( +
+ + +
+ )} +
+ {isVisibleSummary && ( + + + + + + )} +
+ ); +}; + +export default QuizWaiting; diff --git a/apps/frontend/src/blocks/QuizZone/QuizZoneInProgress.tsx b/apps/frontend/src/blocks/QuizZone/QuizZoneInProgress.tsx new file mode 100644 index 0000000..6a4ba68 --- /dev/null +++ b/apps/frontend/src/blocks/QuizZone/QuizZoneInProgress.tsx @@ -0,0 +1,48 @@ +import { QuizZone } from '@/types/quizZone.types'; +import QuizWaiting from './QuizWaiting'; +import QuizInProgress from './QuizInProgress'; +import QuizCompleted from './QuizCompleted'; + +interface QuizZoneInProgressProps { + quizZoneState: QuizZone; + submitAnswer: (answer: string) => void; + playQuiz: () => void; +} + +const QuizZoneInProgress = ({ quizZoneState, submitAnswer, playQuiz }: QuizZoneInProgressProps) => { + const { currentPlayer, currentQuiz, currentQuizResult } = quizZoneState; + const { state } = currentPlayer; + const { playTime, startTime } = currentQuiz ?? {}; + + switch (state) { + case 'WAIT': + return ( + + ); + case 'PLAY': + return ( + + ); + case 'SUBMIT': + return ( + + ); + default: + return null; + } +}; + +export default QuizZoneInProgress; diff --git a/apps/frontend/src/blocks/QuizZone/QuizZoneLoading.tsx b/apps/frontend/src/blocks/QuizZone/QuizZoneLoading.tsx new file mode 100644 index 0000000..be6e923 --- /dev/null +++ b/apps/frontend/src/blocks/QuizZone/QuizZoneLoading.tsx @@ -0,0 +1,22 @@ +import ContentBox from '@/components/common/ContentBox'; +import Typography from '@/components/common/Typogrpahy'; + +interface QuizZoneLoadingProps { + title?: string; + description?: string; +} + +const QuizZoneLoading = ({ + title = '결과를 정산하고 있습니다...', + description = '잠시만 기다려주세요', +}: QuizZoneLoadingProps) => { + return ( +
+ + + + +
+ ); +}; +export default QuizZoneLoading; diff --git a/apps/frontend/src/blocks/QuizZone/QuizZoneLobby.tsx b/apps/frontend/src/blocks/QuizZone/QuizZoneLobby.tsx new file mode 100644 index 0000000..a42fd3c --- /dev/null +++ b/apps/frontend/src/blocks/QuizZone/QuizZoneLobby.tsx @@ -0,0 +1,156 @@ +import CommonButton from '@/components/common/CommonButton'; +import ContentBox from '@/components/common/ContentBox'; +import CustomAlert from '@/components/common/CustomAlert'; +import TextCopy from '@/components/common/TextCopy'; +import Typography from '@/components/common/Typogrpahy'; +import { useNavigate } from 'react-router-dom'; +import { ChatMessage, QuizZone } from '@/types/quizZone.types'; +import PlayersGrid from '@/components/common/PlayersGrid'; +import TooltipWrapper from '@/components/common/TooltipWrapper'; +import { cn } from '@/lib/utils'; + +interface QuizZoneLobbyProps { + quizZoneState: QuizZone; + quizZoneId: string; + maxPlayers: number; + sendChat: (chatMessage: ChatMessage) => void; + startQuiz: () => void; + exitQuiz: () => void; +} + +const QuizZoneLobby = ({ + quizZoneState, + quizZoneId, + maxPlayers, + startQuiz, + exitQuiz, +}: QuizZoneLobbyProps) => { + const navigate = useNavigate(); + const handleLeave = () => { + exitQuiz(); + navigate('/'); + }; + + const { currentPlayer } = quizZoneState; + const isHost = quizZoneState.hostId === currentPlayer.id; + + // 공통으로 사용되는 스타일 정의 + const contentBoxStyle = 'min-h-0'; + const flexColumnGap = 'flex flex-col gap-1'; + + const renderInfoItem = ( + label: string, + value: string | number, + valueSize: 'xl' | '2xl' = '2xl', + ) => ( +
+ + +
+ ); + + return ( +
+ + {/* 모바일에서는 세로로, md 이상에서는 가로로 배치 */} +
+ {/* 퀴즈 정보 섹션 - 모바일에서는 전체 너비, md 이상에서는 30% */} + +
+ + +
+ +
+
+ + + + +
+ {renderInfoItem('퀴즈 개수', `${quizZoneState.quizCount ?? '?'}문제`)} + {renderInfoItem( + '현재 참여자', + `${quizZoneState.players?.length ?? '?'} / ${maxPlayers ?? '?'} 명`, + )} +
+ +
+ {isHost ? ( + + ) : ( + {}} + /> + )} + {}} + /> +
+
+ + {/* 참가자 목록 섹션 - 모바일에서는 전체 너비, md 이상에서는 70% */} + +
+
+ + +
+
+ +
+
+
+
+
+
+ ); +}; + +export default QuizZoneLobby; diff --git a/apps/frontend/src/blocks/QuizZone/QuizZoneResult.tsx b/apps/frontend/src/blocks/QuizZone/QuizZoneResult.tsx new file mode 100644 index 0000000..bfa13e4 --- /dev/null +++ b/apps/frontend/src/blocks/QuizZone/QuizZoneResult.tsx @@ -0,0 +1,210 @@ +import ContentBox from '@/components/common/ContentBox'; +import CustomAlert from '@/components/common/CustomAlert'; +import CustomAlertDialog from '@/components/common/CustomAlertDialog'; +import Typography from '@/components/common/Typogrpahy'; +import { useTimer } from '@/hook/useTimer'; +import { QuizZone } from '@/types/quizZone.types'; +import atob from '@/utils/atob'; +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +interface QuizZoneResultProps { + quizZoneState: QuizZone; +} + +interface QuizResult { + question: string; + correctAnswer: string; + userAnswer: string; + isCorrect: boolean; + submitRank: number | null; + timeLimit: number; +} + +const QuizZoneResult = ({ quizZoneState }: QuizZoneResultProps) => { + const navigate = useNavigate(); + const { endSocketTime } = quizZoneState; + const [isHomeAlertOpen, setIsHomeAlertOpen] = useState(false); + const currentTime = new Date().getTime(); + const remainingTime = Math.max(0, ((endSocketTime ?? 0) - currentTime) / 1000); + + const { start, time } = useTimer({ + initialTime: remainingTime, + onComplete: () => { + setIsHomeAlertOpen(true); + }, + }); + useEffect(() => { + start(); + }, []); + + const handleCloseAlert = () => { + setIsHomeAlertOpen(false); + }; + + const quizResults: QuizResult[] = (quizZoneState.quizzes ?? []).map((quiz, index) => { + const submission = quizZoneState.submits?.[index]; + const isCorrect = + submission?.answer?.replace(/\s/g, '') === quiz?.answer?.replace(/\s/g, ''); + return { + question: quiz?.question ? atob(quiz.question) : '문제 정보 없음', + correctAnswer: quiz?.answer ?? '정답 정보 없음', + userAnswer: submission?.answer ?? '미제출', + isCorrect: !!isCorrect, + submitRank: submission?.submitRank ?? null, + timeLimit: (quiz?.playTime ?? 30000) / 1000, + }; + }); + + const stats = { + totalCorrect: quizResults.filter((r) => r.isCorrect).length, + currentPlayerRank: + quizZoneState.ranks?.find((r) => r.id === quizZoneState.currentPlayer.id)?.ranking ?? 0, + totalQuestions: quizResults.length, + }; + + return ( +
+ {/* 상단 요약 정보 */} + +
+ +
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+ + {quizResults.length > 0 && ( + + +
+ {quizResults.map((result, index) => ( + +
+
+ + +
+ +
+
+ + +
+
+ + +
+ {result.submitRank !== null && ( +
+ + +
+ )} +
+
+
+ ))} +
+
+ )} + + navigate('/')} + onCancel={() => {}} + className="w-4/5 md:w-[40rem]" + /> + + navigate('/')} + onCancel={handleCloseAlert} + /> +
+ ); +}; + +export default QuizZoneResult; diff --git a/apps/frontend/src/components/boundary/AsyncBoundary.tsx b/apps/frontend/src/components/boundary/AsyncBoundary.tsx new file mode 100644 index 0000000..ad108f8 --- /dev/null +++ b/apps/frontend/src/components/boundary/AsyncBoundary.tsx @@ -0,0 +1,27 @@ +import { Suspense } from 'react'; +import { ErrorBoundary } from './ErrorBoundary'; + +export interface AsyncBoundaryProps { + children: React.ReactNode; + pending: React.ReactNode; + rejected?: (args: { error: unknown; retry: () => void }) => React.ReactNode; + handleError?: (error: unknown) => void; + onReset?: () => void; +} + +/** + * AsyncBoundary는 비동기 작업의 로딩 상태와 에러를 처리하는 컴포넌트입니다. + */ +export function AsyncBoundary(props: AsyncBoundaryProps) { + const { children, pending, rejected, handleError, onReset } = props; + + return ( + rejected({ error, retry: reset }))} + handleError={handleError} + onReset={onReset} + > + {children} + + ); +} diff --git a/apps/frontend/src/components/boundary/ErrorBoundary.tsx b/apps/frontend/src/components/boundary/ErrorBoundary.tsx new file mode 100644 index 0000000..a328523 --- /dev/null +++ b/apps/frontend/src/components/boundary/ErrorBoundary.tsx @@ -0,0 +1,70 @@ +import { Component } from 'react'; +import CustomAlertDialog from '@/components/common/CustomAlertDialog'; +import { QuizZoneErrorType, quizZoneErrorMessages } from '@/types/error.types'; +import { getQuizZoneErrorType } from '@/utils/errorUtils'; + +export interface ErrorBoundaryProps { + children: React.ReactNode; + fallback?: (args: { error: unknown; reset: () => void }) => React.ReactNode; + handleError?: (error: unknown) => void; + onReset?: () => void; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: unknown; +} + +/** + * ErrorBoundary는 자식 컴포넌트 트리에서 JavaScript 에러를 감지하고 처리하는 React 컴포넌트입니다. + */ +export class ErrorBoundary extends Component { + state: ErrorBoundaryState = { + hasError: false, + error: null, + }; + + static getDerivedStateFromError(error: unknown): ErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: unknown) { + this.props.handleError?.(error); + } + + private resetError = () => { + this.setState({ + hasError: false, + error: null, + }); + this.props.onReset?.(); + }; + + render() { + const { hasError, error } = this.state; + const { children, fallback } = this.props; + + if (hasError) { + if (fallback) { + return fallback({ error, reset: this.resetError }); + } + + const errorType = getQuizZoneErrorType(error); + const errorMessage = + quizZoneErrorMessages[errorType as Exclude]; + + return ( + this.resetError()} + onConfirm={this.resetError} + title={errorMessage.title} + description={errorMessage.description} + confirmText="확인" + /> + ); + } + + return children; + } +} diff --git a/apps/frontend/src/components/common/ChatBox.tsx b/apps/frontend/src/components/common/ChatBox.tsx new file mode 100644 index 0000000..f6ea5ef --- /dev/null +++ b/apps/frontend/src/components/common/ChatBox.tsx @@ -0,0 +1,139 @@ +import { useState, useEffect, useRef } from 'react'; +import { Button } from '@/components/ui/button'; +import Input from '@/components/common/Input'; +import { MessageCircle, Send } from 'lucide-react'; + +export interface ChatMessage { + clientId: string; + nickname: string; + message: string; +} + +interface ChatProps { + clientId: string; + nickname: string; + sendHandler: (chatMessage: ChatMessage) => void; + chatMessages: ChatMessage[]; + className?: string; + disabled?: boolean; +} + +const ChatBox = ({ + clientId, + nickname, + sendHandler, + chatMessages, + className, + disabled = false, +}: ChatProps) => { + const [message, setMessage] = useState(''); + const messageContainerRef = useRef(null); + + // 새 메시지가 추가될 때마다 채팅 영역 스크롤을 맨 아래로 이동 + const scrollToBottom = () => { + if (messageContainerRef.current) { + const { scrollHeight, clientHeight } = messageContainerRef.current; + messageContainerRef.current.scrollTop = scrollHeight - clientHeight; + } + }; + + useEffect(() => { + scrollToBottom(); + }, [chatMessages]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (!message.trim()) return; + + const chatMessage: ChatMessage = { + clientId, + nickname, + message: message.trim(), + }; + + sendHandler(chatMessage); + setMessage(''); + }; + + return ( +
+
+

+ + 실시간 채팅 +

+
+ + {/* ref를 메시지 컨테이너에 직접 적용 */} +
+
+ {chatMessages.map((chat, index) => ( +
+
+ + {chat.nickname} + {chat.clientId === clientId && ( + + (나) + + )} + + +
+ {chat.message} +
+
+
+ ))} +
+
+ +
+ setMessage(e.target.value)} + placeholder="메시지를 입력하세요" + name="chatInput" + disabled={disabled} + className="flex-1 bg-white shadow-sm border-gray-200 focus-visible:ring-blue-400 h-full p-1" + /> + +
+
+ ); +}; + +export default ChatBox; diff --git a/apps/frontend/src/components/common/CommonButton.stories.tsx b/apps/frontend/src/components/common/CommonButton.stories.tsx new file mode 100644 index 0000000..d462fce --- /dev/null +++ b/apps/frontend/src/components/common/CommonButton.stories.tsx @@ -0,0 +1,83 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import CommonButton from './CommonButton'; +import type { CommonButtonProps } from './CommonButton'; + +const meta = { + title: 'Components/Common/CommonButton', + component: CommonButton, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + text: { + control: 'text', + description: '버튼에 표시될 텍스트', + }, + isFilled: { + control: 'boolean', + description: '버튼의 스타일을 결정하는 플래그', + }, + clickEvent: { + action: 'clicked', + description: '버튼 클릭 시 실행될 이벤트 핸들러', + }, + width: { + control: 'text', + description: '버튼의 너비', + }, + height: { + control: 'text', + description: '버튼의 높이', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// 기본 버튼 +export const Default: Story = { + args: { + text: '기본 버튼', + isFilled: false, + clickEvent: () => { + alert('버튼이 클릭되었습니다!'); + }, + }, +}; + +// 활성화된 버튼 +export const Fulfilled: Story = { + args: { + text: '활성화 버튼', + isFilled: true, + clickEvent: () => { + alert('버튼이 클릭되었습니다!'); + }, + }, +}; + +// 커스텀 크기 버튼 +export const CustomSize: Story = { + args: { + text: '커스텀 크기 버튼', + isFilled: true, + width: '300px', + height: '40px', + clickEvent: () => { + alert('버튼이 클릭되었습니다!'); + }, + }, +}; + +// 버튼 클릭 이벤트 +export const ClickEvent: Story = { + args: { + text: '클릭해보세요', + isFilled: true, + clickEvent: () => { + alert('버튼이 클릭되었습니다!'); + }, + }, +}; diff --git a/apps/frontend/src/components/common/CommonButton.tsx b/apps/frontend/src/components/common/CommonButton.tsx new file mode 100644 index 0000000..256a1a0 --- /dev/null +++ b/apps/frontend/src/components/common/CommonButton.tsx @@ -0,0 +1,85 @@ +import { ButtonHTMLAttributes, forwardRef } from 'react'; + +export interface CommonButtonProps extends ButtonHTMLAttributes { + text?: string; + isFilled?: boolean; // isFulfill -> isFilled로 변경 + clickEvent: () => void; + width?: string; + height?: string; +} + +const CommonButton = forwardRef( + ( + { + text, + isFilled = false, // isFulfill -> isFilled로 변경 + clickEvent, + disabled = false, + className = '', + type = 'button', + ...props + }, + ref, + ) => { + // 기본 스타일 클래스들 + const baseStyles = [ + 'rounded-lg', + 'border-2', + 'px-4', + 'py-2', + 'font-medium', + 'transition-all', + 'duration-200', + 'focus:outline-none', + 'focus:ring-2', + 'focus:ring-blue-600', + 'focus:ring-offset-2', + ]; + + // 조건부 스타일 클래스들 + const conditionalStyles = isFilled + ? [ + 'border-blue-600', + 'bg-blue-600', + 'text-white', + 'hover:bg-blue-700', + 'hover:border-blue-700', + disabled && 'opacity-50 hover:bg-blue-600 hover:border-blue-600', + ] + : [ + 'border-blue-600', + 'bg-white', + 'text-blue-600', + 'hover:bg-blue-50', + disabled && 'opacity-50 hover:bg-white', + ]; + + // 사용자 정의 클래스와 기본 클래스 결합 + const combinedClassName = [ + ...baseStyles, + ...conditionalStyles, + disabled && 'cursor-not-allowed', + className, + ] + .filter(Boolean) + .join(' '); + + return ( + + ); + }, +); + +CommonButton.displayName = 'CommonButton'; + +export default CommonButton; diff --git a/apps/frontend/src/components/common/ContentBox.stories.tsx b/apps/frontend/src/components/common/ContentBox.stories.tsx new file mode 100644 index 0000000..a32d449 --- /dev/null +++ b/apps/frontend/src/components/common/ContentBox.stories.tsx @@ -0,0 +1,42 @@ +import { Meta, StoryObj } from '@storybook/react'; +import ContentBox from './ContentBox'; + +const meta: Meta = { + title: 'Components/ContentBox', + component: ContentBox, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + children:

이것은 ContentBox 안에 있는 기본 내용입니다.

, + }, +}; + +export const WithMultipleChildren: Story = { + args: { + children: ( + <> +

ContentBox 제목

+

여러 자식 요소를 포함한 ContentBox 예시입니다.

+ + + ), + }, +}; + +export const WithLongContent: Story = { + args: { + children: ( +

+ 이것은 긴 내용을 가진 ContentBox 예시입니다. ContentBox는 내용의 길이에 따라 + 자동으로 크기가 조절됩니다. 이를 통해 다양한 길이의 콘텐츠를 유연하게 표시할 수 + 있습니다. +

+ ), + }, +}; diff --git a/apps/frontend/src/components/common/ContentBox.tsx b/apps/frontend/src/components/common/ContentBox.tsx new file mode 100644 index 0000000..1240565 --- /dev/null +++ b/apps/frontend/src/components/common/ContentBox.tsx @@ -0,0 +1,33 @@ +/** + * @description + * 자식 요소들을 스타일이 적용된 div로 감싸는 컴포넌트입니다. + * + * @example + * ```tsx + * + *

이것은 ContentBox 안에 있는 내용입니다.

+ *
+ * ``` + * + * @param {ContentBoxProps} props - ContentBox 컴포넌트의 props입니다. + * @param {ReactNode} props.children - ContentBox 안에 감싸질 내용입니다. + * + * @returns {JSX.Element} 자식 요소들을 포함하는 스타일이 적용된 div를 반환합니다. + */ +import { ReactNode } from 'react'; + +interface ContentBoxProps { + children: ReactNode; + className?: string; +} + +const ContentBox = ({ children, className }: ContentBoxProps) => { + return ( +
+ {children} +
+ ); +}; +export default ContentBox; diff --git a/apps/frontend/src/components/common/CustomAlert.stories.tsx b/apps/frontend/src/components/common/CustomAlert.stories.tsx new file mode 100644 index 0000000..a10c1c3 --- /dev/null +++ b/apps/frontend/src/components/common/CustomAlert.stories.tsx @@ -0,0 +1,191 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import CustomAlert from './CustomAlert'; +import type { CustomAlertProps } from './CustomAlert'; + +import { + Info, + AlertCircle, + LogOut, + Trash2, + Download, +} from 'lucide-react'; + +/** + * CustomAlert 컴포넌트는 사용자 정의 알림 대화 상자를 생성합니다. + * 이 컴포넌트는 트리거 버튼을 클릭하면 나타나는 알림 대화 상자를 포함합니다. + */ +const meta = { + title: 'Components/CustomAlert', + component: CustomAlert, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + trigger: { + control: { + type: 'object', + }, + defaultValue: { + text: 'Trigger', + variant: 'default', + size: 'default', + disabled: false, + icon: null, + }, + }, + alert: { + control: { + type: 'object', + }, + defaultValue: { + title: 'Title', + description: 'Description', + type: 'info', + cancelText: 'Cancel', + confirmText: 'Confirm', + }, + }, + onConfirm: { + action: 'confirm', + }, + onCancel: {}, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// 기본 알럿 +export const Default: Story = { + args: { + trigger: { + text: '알럿 열기', + variant: 'default', + }, + alert: { + title: '기본 알럿', + description: '기본적인 알럿 메시지입니다.', + type: 'info', + }, + onConfirm: () => console.log('확인 clicked'), + }, +}; + +// 정보 알럿 +export const InfoType: Story = { + args: { + trigger: { + text: '공지사항', + variant: 'outline', + icon: , + }, + alert: { + type: 'info', + title: '업데이트 안내', + description: '새로운 기능이 추가되었습니다. 지금 확인해보세요!', + confirmText: '확인하기', + }, + onConfirm: () => console.log('info confirmed'), + }, +}; + +// 성공 알럿 +export const Success: Story = { + args: { + trigger: { + text: '다운로드', + variant: 'default', + icon: , + }, + alert: { + type: 'success', + title: '다운로드 완료', + description: '파일이 성공적으로 다운로드되었습니다.', + confirmText: '확인', + }, + onConfirm: () => console.log('success confirmed'), + }, +}; + +// 경고 알럿 +export const Warning: Story = { + args: { + trigger: { + text: '나가기', + variant: 'outline', + icon: , + }, + alert: { + type: 'warning', + title: '변경사항이 있습니다', + description: '저장하지 않고 나가시겠습니까?', + confirmText: '나가기', + cancelText: '취소', + }, + onConfirm: () => console.log('warning confirmed'), + onCancel: () => console.log('warning cancelled'), + }, +}; + +// 에러 알럿 +export const Error: Story = { + args: { + trigger: { + text: '삭제', + variant: 'destructive', + icon: , + }, + alert: { + type: 'error', + title: '삭제 확인', + description: '이 작업은 되돌릴 수 없습니다. 정말 삭제하시겠습니까?', + confirmText: '삭제', + cancelText: '취소', + }, + onConfirm: () => console.log('error confirmed'), + onCancel: () => console.log('error cancelled'), + }, +}; + +// 비활성화된 트리거 +export const DisabledTrigger: Story = { + args: { + trigger: { + text: '실행 불가', + variant: 'default', + disabled: true, + icon: , + }, + alert: { + type: 'info', + title: '실행할 수 없음', + description: '현재 이 작업을 실행할 수 없습니다.', + }, + onConfirm: () => console.log('disabled confirmed'), + }, +}; + +// 긴 내용의 알럿 +export const LongContent: Story = { + args: { + trigger: { + text: '상세 정보', + variant: 'outline', + icon: , + }, + alert: { + type: 'info', + title: '이용약관 변경 안내', + description: `이용약관이 변경되었습니다. 주요 변경사항은 다음과 같습니다: + + 1. 개인정보 보호정책 강화 + 2. 서비스 이용규칙 개선 + 3. 결제 정책 변경 + + 자세한 내용은 공지사항을 참고해주세요.`, + confirmText: '확인했습니다', + }, + onConfirm: () => console.log('long content confirmed'), + }, +}; diff --git a/apps/frontend/src/components/common/CustomAlert.tsx b/apps/frontend/src/components/common/CustomAlert.tsx new file mode 100644 index 0000000..90a01a5 --- /dev/null +++ b/apps/frontend/src/components/common/CustomAlert.tsx @@ -0,0 +1,96 @@ +import { AlertDialog, AlertDialogTrigger } from '@/components/ui/alert-dialog'; +import { Button } from '@/components/ui/button'; +import CustomAlertDialogContent from '@/components/common/CustomAlertDialogContent.tsx'; + +export interface CustomAlertProps { + // 알럿 트리거 버튼 설정 + trigger: { + text: string; + variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'; + size?: 'default' | 'sm' | 'lg' | 'icon'; + disabled?: boolean; + icon?: React.ReactNode; + }; + // 알럿 내용 설정 + alert: { + title: string; + description?: string; + type?: 'info' | 'success' | 'warning' | 'error'; + cancelText?: string; + confirmText?: string; + }; + // 콜백 함수 + onConfirm: () => void; + onCancel?: () => void; + className?: string; +} + +/** + * @description + * CustomAlert 컴포넌트는 사용자 정의 알림 대화 상자를 생성합니다. + * 이 컴포넌트는 트리거 버튼을 클릭하면 나타나는 알림 대화 상자를 포함합니다. + * + * @example + * ```tsx + * console.log('Confirmed')} + * onCancel={() => console.log('Cancelled')} + * /> + * ``` + * + * @param {Object} props - CustomAlert 컴포넌트의 속성 + * @param {Object} props.trigger - 알럿 트리거 버튼 설정 + * @param {string} props.trigger.text - 트리거 버튼의 텍스트 + * @param {'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'} [props.trigger.variant] - 트리거 버튼의 스타일 변형 + * @param {'default' | 'sm' | 'lg' | 'icon'} [props.trigger.size] - 트리거 버튼의 크기 + * @param {boolean} [props.trigger.disabled] - 트리거 버튼의 비활성화 여부 + * @param {React.ReactNode} [props.trigger.icon] - 트리거 버튼에 표시할 아이콘 + * @param {Object} props.alert - 알럿 내용 설정 + * @param {string} props.alert.title - 알럿의 제목 + * @param {string} [props.alert.description] - 알럿의 설명 + * @param {'info' | 'success' | 'warning' | 'error'} [props.alert.type] - 알럿의 유형 + * @param {string} [props.alert.cancelText] - 취소 버튼의 텍스트 + * @param {string} [props.alert.confirmText] - 확인 버튼의 텍스트 + * @param {Function} props.onConfirm - 확인 버튼 클릭 시 호출되는 콜백 함수 + * @param {Function} [props.onCancel] - 취소 버튼 클릭 시 호출되는 콜백 함수 + * + * @return {JSX.Element} 사용자 정의 알림 대화 상자 컴포넌트 + */ +const CustomAlert = ({ trigger, alert, onConfirm, onCancel, className }: CustomAlertProps) => { + const handleCancel = () => { + onCancel?.(); + }; + + const handleConfirm = () => { + onConfirm(); + }; + + return ( + + + + + + + ); +}; + +export default CustomAlert; diff --git a/apps/frontend/src/components/common/CustomAlertDialog.tsx b/apps/frontend/src/components/common/CustomAlertDialog.tsx new file mode 100644 index 0000000..cc73a2e --- /dev/null +++ b/apps/frontend/src/components/common/CustomAlertDialog.tsx @@ -0,0 +1,40 @@ +import { AlertDialog } from '../ui/alert-dialog'; +import CustomAlertDialogContent from './CustomAlertDialogContent'; + +interface CustomAlertDialogProps { + showError: boolean; + setShowError: (show: boolean) => void; + title: string; + description?: string; + onConfirm: () => void; + onCancel?: () => void; + confirmText?: string; + cancelText?: string; +} + +const CustomAlertDialog = ({ + showError, + setShowError, + onConfirm, + onCancel = () => {}, + title, + description, + confirmText = '확인', + cancelText = '취소', +}: CustomAlertDialogProps) => { + return ( + + + + ); +}; + +export default CustomAlertDialog; diff --git a/apps/frontend/src/components/common/CustomAlertDialogContent.tsx b/apps/frontend/src/components/common/CustomAlertDialogContent.tsx new file mode 100644 index 0000000..f57aa18 --- /dev/null +++ b/apps/frontend/src/components/common/CustomAlertDialogContent.tsx @@ -0,0 +1,76 @@ +import { AlertCircle, AlertTriangle, CheckCircle2, Info } from 'lucide-react'; +import { + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog.tsx'; + +interface CustomAlertContentDialogProps { + title: string; + description?: string; + type?: 'info' | 'success' | 'warning' | 'error'; + cancelText?: string; + confirmText?: string; + handleConfirm?: () => void; + handleCancel?: () => void; +} + +const getAlertIcon = (type: string) => { + switch (type) { + case 'success': + return ; + case 'warning': + return ; + case 'error': + return ; + default: + return ; + } +}; +const getAlertStyle = (type: string) => { + switch (type) { + case 'success': + return 'border-[#22c55e]/20 '; + case 'warning': + return 'border-[#eab308]/20'; + case 'error': + return 'border-[#ef4444]/20 '; + default: + return 'border-[#2563eb]/20 '; + } +}; + +const CustomAlertDialogContent = ({ + title, + description, + type, + confirmText, + cancelText, + handleConfirm, + handleCancel, +}: CustomAlertContentDialogProps) => { + return ( + + +
+ {getAlertIcon(type ?? 'info')} + {title} +
+ {description && {description}} +
+ + + {cancelText ?? '취소'} + + {confirmText ?? '확인'} + + +
+ ); +}; + +export default CustomAlertDialogContent; diff --git a/apps/frontend/src/components/common/Input.stories.tsx b/apps/frontend/src/components/common/Input.stories.tsx new file mode 100644 index 0000000..a5f0ee1 --- /dev/null +++ b/apps/frontend/src/components/common/Input.stories.tsx @@ -0,0 +1,195 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { useState } from '@storybook/preview-api'; +import Input from './Input'; + +const meta: Meta = { + title: 'Components/Input', + component: Input, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + type: { + control: 'select', + options: ['text', 'password', 'email', 'number'], + description: '입력 필드의 타입', + }, + label: { + control: 'text', + description: '입력 필드의 라벨', + }, + value: { + control: 'text', + description: '입력 필드의 값', + }, + name: { + control: 'text', + description: '입력 필드의 이름', + }, + placeholder: { + control: 'text', + description: '입력 필드의 플레이스홀더', + }, + disabled: { + control: 'boolean', + description: '입력 필드의 비활성화 여부', + }, + error: { + control: 'text', + description: '입력 필드의 에러 메시지', + }, + isUnderline: { + control: 'boolean', + description: '밑줄 표시 여부', + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// 기본 스토리 +export const Default: Story = { + render: () => { + const [value, setValue] = useState(''); + return ( +
+ setValue(e.target.value)} + placeholder="기본 입력 필드입니다" + /> +
+ ); + }, +}; + +// 라벨이 있는 입력 필드 +export const WithLabel: Story = { + render: () => { + const [value, setValue] = useState(''); + return ( +
+ setValue(e.target.value)} + placeholder="이름을 입력하세요" + /> +
+ ); + }, +}; + +// 에러 메시지가 있는 입력 필드 +export const WithError: Story = { + render: () => { + const [value, setValue] = useState(''); + return ( +
+ setValue(e.target.value)} + placeholder="이메일을 입력하세요" + error="올바른 이메일 형식이 아닙니다" + /> +
+ ); + }, +}; + +// 밑줄이 있는 입력 필드 +export const WithUnderline: Story = { + render: () => { + const [value, setValue] = useState(''); + return ( +
+ setValue(e.target.value)} + placeholder="제목을 입력하세요" + isUnderline + /> +
+ ); + }, +}; + +// 비활성화된 입력 필드 +export const Disabled: Story = { + render: () => { + return ( +
+ {}} + disabled + /> +
+ ); + }, +}; + +// 다양한 타입의 입력 필드 +export const DifferentTypes: Story = { + render: () => { + const [values, setValues] = useState({ + text: '', + password: '', + email: '', + number: '', + }); + + const handleChange = + (field: keyof typeof values) => (e: React.ChangeEvent) => { + setValues((prev) => ({ ...prev, [field]: e.target.value })); + }; + + return ( +
+ + + + +
+ ); + }, +}; diff --git a/apps/frontend/src/components/common/Input.tsx b/apps/frontend/src/components/common/Input.tsx new file mode 100644 index 0000000..36e7df9 --- /dev/null +++ b/apps/frontend/src/components/common/Input.tsx @@ -0,0 +1,200 @@ +import { forwardRef, ChangeEvent } from 'react'; +import Typography from './Typogrpahy'; + +interface InputProps { + type?: string; + label?: string; + value: string | number; + onChange: (e: ChangeEvent) => void; + name: string; + placeholder?: string; + disabled?: boolean; + error?: string | false; + isUnderline?: boolean; + onKeyDown?: (e: React.KeyboardEvent) => void; + isAutoFocus?: boolean; + className?: string; + isBorder?: boolean; + step?: number; + min?: number; + max?: number; + isShowCount?: boolean; + height?: string; // 높이 prop 추가 +} + +/** + * `Input` 컴포넌트는 다양한 입력 필드를 렌더링합니다. + * + * @example + * ```tsx + * + * ``` + * + * @param {string} type - 입력 필드의 타입 (기본값: 'text'). + * @param {string} label - 입력 필드의 레이블. + * @param {string | number} value - 입력 필드의 값. + * @param {function} onChange - 값이 변경될 때 호출되는 함수. + * @param {string} name - 입력 필드의 이름. + * @param {string} placeholder - 입력 필드의 플레이스홀더. + * @param {boolean} disabled - 입력 필드를 비활성화할지 여부 (기본값: false). + * @param {boolean} error - 에러 상태인지 여부 (기본값: false). + * @param {boolean} isUnderline - 밑줄을 표시할지 여부 (기본값: false). + * @param {function} onKeyDown - 키가 눌릴 때 호출되는 함수. + * @param {boolean} isAutoFocus - 자동 포커스를 설정할지 여부 (기본값: false). + * @param {string} className - 추가적인 클래스 이름. + * @param {boolean} isBorder - 테두리를 표시할지 여부 (기본값: false). + * @param {number} step - 입력 필드의 step 속성. + * @param {number} min - 입력 필드의 최소값. + * @param {number} max - 입력 필드의 최대값. + * @param {boolean} isShowCount - 입력된 글자 수를 표시할지 여부. + * @param {string} height - 입력 필드의 높이 (기본값: 'h-8'). + * @param {object} rest - 기타 전달할 속성들. + * @returns {JSX.Element} 렌더링된 입력 필드 컴포넌트. + */ +const Input = forwardRef( + ( + { + type = 'text', + label, + value, + onChange, + name, + placeholder, + disabled = false, + error = false, + isUnderline = false, + onKeyDown, + isAutoFocus = false, + className, + isBorder = false, + step, + min, + max, + isShowCount, + height = 'h-8', + ...rest + }, + ref, + ) => { + const getFontSizeClass = () => { + // Tailwind의 height 클래스에 따른 글씨 크기 매핑 + const heightToFontSize: Record = { + 'h-6': 'text-xs', + 'h-8': 'text-sm', + 'h-10': 'text-base', + 'h-12': 'text-lg', + 'h-14': 'text-xl', + 'h-16': 'text-2xl', + }; + + return heightToFontSize[height] || 'text-base'; + }; + + const classes = className ? `input-wrapper ${className}` : 'input-wrapper'; + const fontSizeClass = getFontSizeClass(); + + return ( +
+ {label && ( + + )} + + + {isUnderline &&
} +
+ {error && ( + + {error} + + )} + + {typeof value === 'string' && isShowCount && ( +
+ +
+ )} + {typeof value === 'number' && isShowCount && ( +
+ +
+ )} +
+
+ ); + }, +); + +Input.displayName = 'Input'; + +export default Input; diff --git a/apps/frontend/src/components/common/Logo.tsx b/apps/frontend/src/components/common/Logo.tsx new file mode 100644 index 0000000..801f3f5 --- /dev/null +++ b/apps/frontend/src/components/common/Logo.tsx @@ -0,0 +1,112 @@ +import { SVGProps } from 'react'; + +interface LogoProps extends SVGProps { + color?: string; +} + +const Logo = ({ color = 'currentColor', ...props }: LogoProps) => { + return ( + + + + + + + + + + + + + + + + + ); +}; + +export default Logo; diff --git a/apps/frontend/src/components/common/NavBar.tsx b/apps/frontend/src/components/common/NavBar.tsx new file mode 100644 index 0000000..f2dc53c --- /dev/null +++ b/apps/frontend/src/components/common/NavBar.tsx @@ -0,0 +1,27 @@ +import { Button } from '@/components/ui/button'; +import TooltipWrapper from './TooltipWrapper'; +import { useNavigate } from 'react-router-dom'; +import Logo from './Logo'; + +const Navbar = () => { + const navigate = useNavigate(); + return ( + + ); +}; + +export default Navbar; diff --git a/apps/frontend/src/components/common/ParticipantGrid.tsx b/apps/frontend/src/components/common/ParticipantGrid.tsx new file mode 100644 index 0000000..e69de29 diff --git a/apps/frontend/src/components/common/PlayersGrid.tsx b/apps/frontend/src/components/common/PlayersGrid.tsx new file mode 100644 index 0000000..06efbdf --- /dev/null +++ b/apps/frontend/src/components/common/PlayersGrid.tsx @@ -0,0 +1,91 @@ +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Crown } from 'lucide-react'; +import Typography from '@/components/common/Typogrpahy'; +import { Player } from '@/types/quizZone.types'; + +// 개별 플레이어 카드 컴포넌트 +interface PlayerCardProps { + isCurrentPlayer?: boolean; + player: Player; + isHost: boolean; +} + +const PlayerCard = ({ isCurrentPlayer = false, player, isHost }: PlayerCardProps) => { + const initials = player.nickname + .split(' ') + .map((word) => word[0]) + .join('') + .toUpperCase() + .slice(0, 2); + + return ( +
+
+ + + + {initials} + + + {isHost && ( + + )} +
+
+ {isCurrentPlayer ? ( + + ) : ( + + )} +
+
+ ); +}; + +interface PlayersGridProps { + currentPlayer?: Player; + players: Player[]; + hostId: string; + className?: string; +} + +const PlayersGrid = ({ currentPlayer, players, hostId, className = '' }: PlayersGridProps) => { + if (!players.length) { + return ( +
+ +
+ ); + } + + return ( +
+ {players.map((player) => ( + + ))} +
+ ); +}; + +export default PlayersGrid; diff --git a/apps/frontend/src/components/common/PodiumPlayers.tsx b/apps/frontend/src/components/common/PodiumPlayers.tsx new file mode 100644 index 0000000..bbd7d02 --- /dev/null +++ b/apps/frontend/src/components/common/PodiumPlayers.tsx @@ -0,0 +1,155 @@ +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Crown } from 'lucide-react'; +import Typography from '@/components/common/Typogrpahy'; +import { Player } from '@/types/quizZone.types'; + +// 포디움 위의 개별 플레이어 컴포넌트 +interface PodiumPlayerCardProps { + player: Player; + rank: number; + isHost: boolean; + isCurrentPlayer?: boolean; +} + +const PodiumPlayerCard = ({ + player, + rank, + isHost, + isCurrentPlayer = false, +}: PodiumPlayerCardProps) => { + const initials = player.nickname + .split(' ') + .map((word) => word[0]) + .join('') + .toUpperCase() + .slice(0, 2); + + const medalColors: { [key: number]: string } = { + 1: 'bg-yellow-400', + 2: 'bg-gray-300', + 3: 'bg-amber-600', + }; + + return ( +
+
+
+ {rank} +
+ + + + {initials} + + + {isHost && ( + + )} +
+
+ +
+
+ ); +}; + +interface PodiumPlayersProps { + currentPlayer?: Player; + players: Player[]; + hostId: string; + className?: string; +} + +const PodiumPlayers = ({ currentPlayer, players, hostId, className = '' }: PodiumPlayersProps) => { + // 상위 3명의 플레이어만 선택 + const topThreePlayers = players.slice(0, 3); + + if (!players.length) { + return ( +
+ +
+ ); + } + + // 참가자가 1명일 때는 바로 1등으로 처리 + if (topThreePlayers.length === 1) { + return ( +
+
+ +
+
+
+ ); + } + + // 2등, 1등, 3등 순서로 배치하기 위한 재정렬 + const orderedPlayers = [ + topThreePlayers[1], // 2등 + topThreePlayers[0], // 1등 + topThreePlayers[2], // 3등 + ].filter(Boolean); // undefined 제거 + + return ( +
+ {orderedPlayers.map((player, index) => { + if (!player) return null; + + // 실제 순위 계산 (2등, 1등, 3등 순서로 표시되므로) + const rank = index === 0 ? 2 : index === 1 ? 1 : 3; + + // 포디움 높이 설정 + const podiumHeight = rank === 1 ? 'h-24' : rank === 2 ? 'h-16' : 'h-12'; + + return ( +
+ +
+
+ ); + })} +
+ ); +}; + +export default PodiumPlayers; diff --git a/apps/frontend/src/components/common/ProgressBar.stories.tsx b/apps/frontend/src/components/common/ProgressBar.stories.tsx new file mode 100644 index 0000000..efc4e65 --- /dev/null +++ b/apps/frontend/src/components/common/ProgressBar.stories.tsx @@ -0,0 +1,22 @@ +import { Meta, StoryObj } from '@storybook/react'; +import ProgressBar from './ProgressBar'; + +const meta: Meta = { + title: 'Components/ProgressBar', + component: ProgressBar, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + playTime: 30000, + time: Date.now(), + onTimeEnd: () => { + alert('time end'); + }, + }, +}; diff --git a/apps/frontend/src/components/common/ProgressBar.tsx b/apps/frontend/src/components/common/ProgressBar.tsx new file mode 100644 index 0000000..e3acab8 --- /dev/null +++ b/apps/frontend/src/components/common/ProgressBar.tsx @@ -0,0 +1,43 @@ +import { Progress } from '@/components/ui/progress'; +import Typography from './Typogrpahy'; + +export interface ProgressBarProps { + playTime: number; + time: number; + onTimeEnd?: () => void; +} + +/** + * @description + * ProgressBar 컴포넌트는 주어진 플레이 시간과 현재 시간을 기반으로 진행도를 시각적으로 표시합니다. + * + * @example + * console.log('Time ended!')} + * /> + */ +const ProgressBar = ({ playTime, time, onTimeEnd }: ProgressBarProps) => { + // 진행도 계산 (남은 시간 / 전체 시간 * 100) + const progress = Math.max(0, Math.min(100, (time / playTime) * 100000)); + + // 시간이 다 되었을 때 콜백 실행 + if (time <= 0) { + onTimeEnd?.(); + } + + return ( +
+ + +
+ ); +}; + +export default ProgressBar; diff --git a/apps/frontend/src/components/common/TextCopy.stories.tsx b/apps/frontend/src/components/common/TextCopy.stories.tsx new file mode 100644 index 0000000..f406d89 --- /dev/null +++ b/apps/frontend/src/components/common/TextCopy.stories.tsx @@ -0,0 +1,22 @@ +import { Meta, StoryObj } from '@storybook/react'; +import TextCopy from './TextCopy'; +import type { TextCopyProps } from './TextCopy'; + +const meta: Meta = { + title: 'Components/TextCopy', + component: TextCopy, + tags: ['autodocs'], + args: { + text: '이것은 복사할 텍스트입니다.', + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + text: '이것은 복사할 텍스트입니다.', + }, +}; diff --git a/apps/frontend/src/components/common/TextCopy.tsx b/apps/frontend/src/components/common/TextCopy.tsx new file mode 100644 index 0000000..f63995a --- /dev/null +++ b/apps/frontend/src/components/common/TextCopy.tsx @@ -0,0 +1,39 @@ +import Typography, { TypographyProps } from './Typogrpahy'; +import { Copy } from 'lucide-react'; + +export interface TextCopyProps { + text: string; + size?: TypographyProps['size']; + bold?: TypographyProps['bold']; +} + +/** + * @description + * 주어진 텍스트를 복사할 수 있는 컴포넌트입니다. + * + * @example + * ```tsx + * + * ``` + * + * @param text - 복사할 텍스트입니다. + * @param size - 텍스트의 크기를 지정합니다. 기본값은 '4xl'입니다. + * @returns 주어진 텍스트와 복사 아이콘을 포함하는 컴포넌트를 반환합니다. + */ +const TextCopy = ({ text, size = '4xl', bold = false }: TextCopyProps) => { + const handleCopy = () => { + navigator.clipboard.writeText(text); + }; + + return ( +
+ + +
+ ); +}; + +export default TextCopy; diff --git a/apps/frontend/src/components/common/TimerDisplay.stories.tsx b/apps/frontend/src/components/common/TimerDisplay.stories.tsx new file mode 100644 index 0000000..a7259d0 --- /dev/null +++ b/apps/frontend/src/components/common/TimerDisplay.stories.tsx @@ -0,0 +1,22 @@ +import { Meta, StoryObj } from '@storybook/react'; +import TimerDisplay from './TimerDisplay'; + +const meta: Meta = { + title: 'Components/TimerDisplay', + component: TimerDisplay, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + time: 10, + isFulfill: false, + onTimeEnd: () => { + alert('time end'); + }, + }, +}; diff --git a/apps/frontend/src/components/common/TimerDisplay.tsx b/apps/frontend/src/components/common/TimerDisplay.tsx new file mode 100644 index 0000000..1dfe7d5 --- /dev/null +++ b/apps/frontend/src/components/common/TimerDisplay.tsx @@ -0,0 +1,62 @@ +import { useState, useEffect } from 'react'; +import Typography from './Typogrpahy'; + +interface TimerDisplayProps { + time?: number; + isFulfill: boolean; + width?: string; + height?: string; + onTimeEnd?: () => void; +} + +/** + * @description + * 이 컴포넌트는 주어진 시간에서 시작하여 매 초마다 감소하는 카운트다운 타이머를 표시합니다. + * 타이머가 0에 도달하면 `onTimeEnd` 콜백 함수를 호출합니다. + * 타이머의 배경색과 호버 효과는 `isFulfill` 속성에 따라 사용자 정의할 수 있습니다. + * + * @example + * ```tsx + * console.log('시간 종료!')} /> + * ``` + * + * @param {TimerDisplayProps} props - TimerDisplay 컴포넌트의 속성. + * @param {number} [props.time=3] - 카운트다운 타이머의 초기 시간 값. + * @param {boolean} props.isFulfill - 타이머의 배경색과 호버 효과를 결정합니다. + * @param {string} [props.width] - 타이머 디스플레이의 너비. + * @param {string} [props.height] - 타이머 디스플레이의 높이. + * @param {() => void} props.onTimeEnd - 타이머가 0에 도달했을 때 호출되는 콜백 함수. + * + * @returns {JSX.Element} TimerDisplay 컴포넌트. + */ +const TimerDisplay = ({ time = 3, isFulfill = true, onTimeEnd }: TimerDisplayProps) => { + const backgroundColorClass = isFulfill ? 'bg-gray300' : 'bg-white'; + const textColorClass = 'black'; + const INTERVAL_SECOND = 1000; + const hoverBackgroundColorClass = isFulfill ? 'hover:bg-gray500' : 'hover:bg-[#f5f5f5]'; + + const [timeValue, setTimeValue] = useState(time); + + useEffect(() => { + if (timeValue === 0) { + if (onTimeEnd) { + onTimeEnd(); + } + } + + const interval = setInterval(() => { + setTimeValue(timeValue - 1); + }, INTERVAL_SECOND); + + return () => clearInterval(interval); + }, [timeValue, onTimeEnd]); + return ( +
+ +
+ ); +}; + +export default TimerDisplay; diff --git a/apps/frontend/src/components/common/TooltipWrapper.tsx b/apps/frontend/src/components/common/TooltipWrapper.tsx new file mode 100644 index 0000000..2655b42 --- /dev/null +++ b/apps/frontend/src/components/common/TooltipWrapper.tsx @@ -0,0 +1,40 @@ +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; + +interface TooltipWrapperProps { + content: string; + children: React.ReactNode; + delayDuration?: number; + side?: 'top' | 'right' | 'bottom' | 'left'; + align?: 'start' | 'center' | 'end'; + className?: string; + type?: 'button' | 'submit' | 'reset' | undefined; +} + +const TooltipWrapper = ({ + content, + children, + delayDuration = 200, + side = 'top', + align = 'center', + type = undefined, + className = '', +}: TooltipWrapperProps) => { + return ( + + + +
{children}
+
+ + {content} + +
+
+ ); +}; + +export default TooltipWrapper; diff --git a/apps/frontend/src/components/common/Typography.stories.tsx b/apps/frontend/src/components/common/Typography.stories.tsx new file mode 100644 index 0000000..b7bbb04 --- /dev/null +++ b/apps/frontend/src/components/common/Typography.stories.tsx @@ -0,0 +1,133 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import Typography from './Typogrpahy'; + +/** + * Typography 컴포넌트는 일관된 텍스트 스타일링을 위한 기본 컴포넌트입니다. + * 다양한 크기와 색상 옵션을 제공합니다. + */ +const meta: Meta = { + title: 'Components/Typography', + component: Typography, + tags: ['autodocs'], + parameters: { + layout: 'centered', + }, + argTypes: { + text: { + description: '표시할 텍스트 내용', + control: { type: 'text' }, + }, + size: { + description: '텍스트의 크기', + control: { + type: 'select', + }, + options: ['xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl'], + }, + color: { + description: '텍스트의 색상', + control: { + type: 'select', + }, + options: ['gray', 'red', 'black'], + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +/** + * 기본 Typography 스타일입니다. + */ +export const Default: Story = { + args: { + text: 'Hello World', + size: 'base', + color: 'black', + }, +}; + +/** + * 가장 작은 크기의 텍스트입니다. + */ +export const ExtraSmall: Story = { + args: { + text: 'Extra Small Text', + size: 'xs', + color: 'black', + }, +}; + +/** + * 작은 크기의 텍스트입니다. + */ +export const Small: Story = { + args: { + text: 'Small Text', + size: 'sm', + color: 'black', + }, +}; + +/** + * 큰 크기의 텍스트입니다. + */ +export const Large: Story = { + args: { + text: 'Large Text', + size: 'lg', + color: 'black', + }, +}; + +/** + * 회색 텍스트 스타일입니다. + */ +export const GrayText: Story = { + args: { + text: 'Gray Colored Text', + size: 'base', + color: 'gray', + }, +}; + +/** + * 빨간색 텍스트 스타일입니다. + */ +export const RedText: Story = { + args: { + text: 'Red Colored Text', + size: 'base', + color: 'red', + }, +}; + +/** + * 모든 크기를 한번에 보여주는 예시입니다. + */ +export const AllSizes: Story = { + render: () => ( +
+ {(['xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl'] as const).map( + (size) => ( + + ), + )} +
+ ), +}; + +/** + * 모든 색상을 한번에 보여주는 예시입니다. + */ +export const AllColors: Story = { + render: () => ( +
+ {(['black', 'gray', 'red'] as const).map((color) => ( + + ))} +
+ ), +}; diff --git a/apps/frontend/src/components/common/Typogrpahy.tsx b/apps/frontend/src/components/common/Typogrpahy.tsx new file mode 100644 index 0000000..4c8fdc3 --- /dev/null +++ b/apps/frontend/src/components/common/Typogrpahy.tsx @@ -0,0 +1,51 @@ +export interface TypographyProps { + text: string; + size: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl'; + color: 'gray' | 'red' | 'black' | 'blue'; + bold?: boolean; +} + +/** + * @description + * `Typography` 컴포넌트는 Tailwind CSS를 사용하여 텍스트의 크기와 색상을 조절할 수 있도록 합니다. + * + * @component + * @example + * ```tsx + * + * ``` + * + * @param {TypographyProps} props - 컴포넌트에 전달되는 props + * @param {string} props.text - 표시할 텍스트 + * @param {'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl'} props.size - 텍스트의 크기 + * @param {'gray' | 'red' | 'black'} props.color - 텍스트의 색상 + * + * @returns {JSX.Element} Tailwind CSS 클래스를 적용한 텍스트를 포함하는 `

` 요소 + */ + +const Typography = ({ text, size, color, bold = false }: TypographyProps) => { + const sizeClasses = { + xs: 'text-xs', + sm: 'text-sm', + base: 'text-base', + lg: 'text-lg', + xl: 'text-xl', + '2xl': 'text-2xl', + '3xl': 'text-3xl', + '4xl': 'text-4xl', + '5xl': 'text-5xl', + '6xl': 'text-6xl', + }; + + const colorClasses = { + gray: 'text-gray-400', + red: 'text-red-600', + blue: 'text-blue-600', + black: 'text-black', + }; + + const classes = `break-all ${sizeClasses[size] || sizeClasses.base} ${colorClasses[color] || colorClasses.black} ${bold ? 'font-bold' : ''}`; + return

{text}

; +}; + +export default Typography; diff --git a/apps/frontend/src/components/ui/alert-dialog.tsx b/apps/frontend/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..fa2b442 --- /dev/null +++ b/apps/frontend/src/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/apps/frontend/src/components/ui/avatar.tsx b/apps/frontend/src/components/ui/avatar.tsx new file mode 100644 index 0000000..991f56e --- /dev/null +++ b/apps/frontend/src/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/apps/frontend/src/components/ui/button.tsx b/apps/frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..65d4fcd --- /dev/null +++ b/apps/frontend/src/components/ui/button.tsx @@ -0,0 +1,57 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/apps/frontend/src/components/ui/progress.tsx b/apps/frontend/src/components/ui/progress.tsx new file mode 100644 index 0000000..7f3cd52 --- /dev/null +++ b/apps/frontend/src/components/ui/progress.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import * as ProgressPrimitive from '@radix-ui/react-progress'; + +import { cn } from '@/lib/utils'; + +// 새로운 인터페이스를 정의하여 time prop을 추가합니다. +interface ProgressProps extends React.ComponentPropsWithoutRef { + time?: number; +} + +const Progress = React.forwardRef, ProgressProps>( + ({ className, value, time = 33, ...props }, ref) => ( + + time ? 'bg-[#2563eb]' : 'bg-red-600'} transition-all`} + style={{ transform: `translateX(-${100 - (value || 0)}%)` }} + /> + + ), +); +Progress.displayName = ProgressPrimitive.Root.displayName; + +export { Progress }; diff --git a/apps/frontend/src/components/ui/tooltip.tsx b/apps/frontend/src/components/ui/tooltip.tsx new file mode 100644 index 0000000..218d183 --- /dev/null +++ b/apps/frontend/src/components/ui/tooltip.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/apps/frontend/src/constants/quiz-set.constants.ts b/apps/frontend/src/constants/quiz-set.constants.ts new file mode 100644 index 0000000..3ff3d6e --- /dev/null +++ b/apps/frontend/src/constants/quiz-set.constants.ts @@ -0,0 +1 @@ +export const QUIZ_LIMIT_COUNT = 10; diff --git a/apps/frontend/src/hook/quizZone/useQuizZone.test.ts b/apps/frontend/src/hook/quizZone/useQuizZone.test.ts new file mode 100644 index 0000000..b9b5cde --- /dev/null +++ b/apps/frontend/src/hook/quizZone/useQuizZone.test.ts @@ -0,0 +1,102 @@ +import { renderHook, act } from '@testing-library/react'; +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { QuizZone } from '@/types/quizZone.types'; +import useQuizZone from './useQuizZone'; + +// useWebSocket 모킹 +vi.mock('@/hook/useWebSocket', () => ({ + default: () => ({ + beginConnection: vi.fn(), + sendMessage: vi.fn(), + closeConnection: vi.fn(), + messageHandler: vi.fn(), + }), +})); + +// env 모킹 +vi.mock('@/utils/atob', () => ({ + default: vi.fn((str) => str), +})); + +describe('useQuizZone', () => { + const mockQuizZoneId = 'test-quiz-zone-id'; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('초기 상태가 올바르게 설정되어야 합니다', () => { + const { result } = renderHook(() => useQuizZone(mockQuizZoneId)); + + expect(result.current.quizZoneState).toEqual({ + stage: 'LOBBY', + currentPlayer: { + id: '', + nickname: '', + }, + title: '', + description: '', + hostId: '', + quizCount: 0, + players: [], + score: 0, + submits: [], + quizzes: [], + chatMessages: [], + maxPlayers: 0, + offset: 0, + serverTime: 0, + }); + }); + + it('playQuiz 액션이 상태를 올바르게 업데이트해야 합니다', () => { + const { result } = renderHook(() => useQuizZone(mockQuizZoneId)); + + act(() => { + result.current.playQuiz(); + }); + + expect(result.current.quizZoneState.stage).toBe('IN_PROGRESS'); + expect(result.current.quizZoneState.currentPlayer.state).toBe('PLAY'); + }); + + it('init 액션이 상태를 올바르게 업데이트해야 합니다', () => { + const { result } = renderHook(() => useQuizZone(mockQuizZoneId)); + + const mockQuizZone: Partial = { + stage: 'LOBBY', + currentPlayer: { + id: 'player1', + nickname: 'Player 1', + state: 'WAIT', + }, + title: 'Test Quiz', + description: 'Test Description', + hostId: 'host1', + quizCount: 5, + serverTime: Date.now(), + }; + + act(() => { + result.current.initQuizZoneData(mockQuizZone as QuizZone, Date.now()); + }); + + expect(result.current.quizZoneState.title).toBe('Test Quiz'); + expect(result.current.quizZoneState.currentPlayer.id).toBe('player1'); + expect(result.current.quizZoneState.quizCount).toBe(5); + }); + + describe('state transitions', () => { + it('LOBBY에서 IN_PROGRESS로 상태 전환이 올바르게 되어야 합니다', () => { + const { result } = renderHook(() => useQuizZone(mockQuizZoneId)); + + expect(result.current.quizZoneState.stage).toBe('LOBBY'); + + act(() => { + result.current.playQuiz(); + }); + + expect(result.current.quizZoneState.stage).toBe('IN_PROGRESS'); + }); + }); +}); diff --git a/apps/frontend/src/hook/quizZone/useQuizZone.tsx b/apps/frontend/src/hook/quizZone/useQuizZone.tsx new file mode 100644 index 0000000..770f007 --- /dev/null +++ b/apps/frontend/src/hook/quizZone/useQuizZone.tsx @@ -0,0 +1,325 @@ +import { useReducer } from 'react'; +import useWebSocket from '@/hook/useWebSocket.tsx'; +import { + ChatMessage, + InitQuizZoneResponse, + NextQuizResponse, + Player, + QuizZone, + QuizZoneResultState, + SomeoneSubmitResponse, + SubmitResponse, +} from '@/types/quizZone.types.ts'; +import atob from '@/utils/atob'; + +export type QuizZoneAction = + | { type: 'init'; payload: InitQuizZoneResponse } + | { type: 'join'; payload: Player[] } + | { type: 'someone_join'; payload: Player } + | { type: 'someone_leave'; payload: string } + | { type: 'start'; payload: undefined } + | { type: 'submit'; payload: SubmitResponse } + | { type: 'someone_submit'; payload: SomeoneSubmitResponse } + | { type: 'nextQuiz'; payload: NextQuizResponse } + | { type: 'playQuiz'; payload: undefined } + | { type: 'quizTimeout'; payload: undefined } + | { type: 'finish'; payload: undefined } + | { type: 'summary'; payload: QuizZoneResultState } + | { type: 'chat'; payload: ChatMessage } + | { type: 'leave'; payload: undefined }; + +export type chatAction = { + type: 'chat'; + payload: ChatMessage; +}; + +type Reducer = (state: S, action: A) => S; + +const quizZoneReducer: Reducer = (state, action) => { + const { type, payload } = action; + + switch (type) { + case 'init': + const { quizZone, now } = payload; + const receiveTime = new Date().getTime(); + return { + ...state, + ...quizZone, + currentQuiz: + quizZone.currentQuiz !== undefined + ? { + ...quizZone.currentQuiz, + question: atob(quizZone.currentQuiz?.question ?? ''), + } + : undefined, + offset: quizZone.serverTime - (now + receiveTime) / 2, + players: [], + }; + case 'join': + return { ...state, players: payload }; + case 'someone_join': + const isPlayerExist = state.players?.some((player) => player.id === payload.id); + if (isPlayerExist) { + return state; // 이미 존재하는 플레이어라면 상태 변경 없음 + } + return { ...state, players: [...(state.players ?? []), payload] }; + case 'someone_leave': + return { + ...state, + players: state.players?.filter((player) => player.id !== payload) ?? [], + }; + case 'start': + return { + ...state, + stage: 'LOBBY', + }; + case 'submit': + return { + ...state, + state: 'IN_PROGRESS', + currentPlayer: { + ...state.currentPlayer, + state: 'SUBMIT', + }, + currentQuizResult: { + ...payload, + fastestPlayers: payload.fastestPlayerIds + .map((id) => state.players?.find((p) => p.id === id)) + .filter((p) => !!p), + }, + }; + case 'someone_submit': + const { clientId, submittedCount } = payload; + const player = state.players?.find((p) => p.id === clientId); + const fastestPlayers = state.currentQuizResult?.fastestPlayers ?? []; + + return { + ...state, + currentQuizResult: { + ...state.currentQuizResult!, + fastestPlayers: [...fastestPlayers, player].slice(0, 3).filter((p) => !!p), + submittedCount, + }, + }; + case 'nextQuiz': + const { nextQuiz } = payload; + + return { + ...state, + stage: 'IN_PROGRESS', + currentPlayer: { + ...state.currentPlayer, + state: 'WAIT', + }, + currentQuiz: { + ...state.currentQuiz, + ...nextQuiz, + question: atob(nextQuiz.question), + startTime: nextQuiz.startTime - state.offset, + deadlineTime: nextQuiz.deadlineTime - state.offset, + quizType: 'SHORT', + }, + currentQuizResult: { + ...state.currentQuizResult, + ...payload.currentQuizResult, + }, + }; + case 'playQuiz': + return { + ...state, + stage: 'IN_PROGRESS', + currentPlayer: { + ...state.currentPlayer, + state: 'PLAY', + }, + }; + case 'quizTimeout': + return { + ...state, + state: 'IN_PROGRESS', + currentPlayer: { + ...state.currentPlayer, + state: 'WAIT', + }, + }; + case 'finish': + return { + ...state, + stage: 'RESULT', + isLastQuiz: true, + }; + case 'summary': + return { + ...state, + ...payload, + stage: 'RESULT', + endSocketTime: payload.endSocketTime - state.offset, + }; + case 'chat': + return { + ...state, + chatMessages: [...(state.chatMessages || []), payload], + }; + case 'leave': + return { + isQuizZoneEnd: true, + ...state, + }; + + default: + return state; + } +}; + +export const chatMessagesReducer: Reducer = (chatMessages, action) => { + const { type, payload } = action; + + switch (type) { + case 'chat': + return [...chatMessages, payload]; + default: + return chatMessages; + } +}; + +/** + * @description 다중 사용자 퀴즈 게임 환경에서 퀴즈존 상태와 상호작용을 관리하는 커스텀 훅입니다. + * + * @example + * ```tsx + * const QuizComponent = () => { + * const { + * quizZoneState, + * initQuizZoneData, + * submitQuiz, + * startQuiz, + * playQuiz + * } = useQuizZone(); + * + * // 퀴즈 초기화 + * useEffect(() => { + * initQuizZoneData(initialData); + * }, []); + * + * // 답안 제출 + * const handleSubmit = (answer: string) => { + * submitQuiz(answer); + * }; + * ``` + * + * @returns {Object} 퀴즈존 상태와 제어 함수들을 포함하는 객체 + * @returns {QuizZone} .quizZoneState - 현재 퀴즈존의 상태 + * @returns {Function} .initQuizZoneData - 초기 데이터로 퀴즈존을 초기화하는 함수 + * @returns {Function} .submitQuiz - 현재 퀴즈에 대한 답안을 제출하는 함수 + * @returns {Function} .startQuiz - 퀴즈 세션을 시작하는 함수 + * @returns {Function} .playQuiz - 퀴즈 상태를 플레이 모드로 변경하는 함수 + */ + +const useQuizZone = ( + quizZoneId: string, + handleReconnect?: () => void, + handleClose?: () => void, +) => { + const initialQuizZoneState: QuizZone = { + stage: 'LOBBY', + currentPlayer: { + id: '', + nickname: '', + }, + title: '', + description: '', + hostId: '', + quizCount: 0, + players: [], + score: 0, + submits: [], + quizzes: [], + chatMessages: [], + maxPlayers: 0, + offset: 0, + serverTime: 0, + }; + + const [quizZoneState, dispatch] = useReducer(quizZoneReducer, initialQuizZoneState); + + const messageHandler = (event: MessageEvent) => { + const { event: QuizZoneEvent, data } = JSON.parse(event.data); + + dispatch({ + type: QuizZoneEvent, + payload: data, + }); + }; + + const handleFinish = () => { + dispatch({ type: 'leave', payload: undefined }); + handleClose?.(); + }; + + const wsUrl = `${import.meta.env.VITE_WS_URL}/play`; + const { beginConnection, sendMessage, closeConnection } = useWebSocket({ + wsUrl, + messageHandler, + handleFinish, + handleReconnect, + }); + + //initialize QuizZOne + const initQuizZoneData = async (quizZone: QuizZone, now: number) => { + dispatch({ type: 'init', payload: { quizZone, now } }); + beginConnection(); + joinQuizZone({ quizZoneId }); + }; + + //퀴즈 시작 함수 + const startQuiz = () => { + const message = JSON.stringify({ event: 'start' }); + sendMessage(message); + }; + + //퀴즈존 나가기 함수 + const exitQuiz = () => { + const message = JSON.stringify({ event: 'leave' }); + sendMessage(message); + }; + + // 퀴즈 제출 함수 + const submitQuiz = (answer: string) => { + const message = JSON.stringify({ + event: 'submit', + data: { + answer, + index: quizZoneState.currentQuiz?.currentIndex, + submittedAt: Date.now(), + }, + }); + sendMessage(message); + }; + + const playQuiz = () => { + dispatch({ type: 'playQuiz', payload: undefined }); + }; + + const joinQuizZone = ({ quizZoneId }: any) => { + const message = JSON.stringify({ event: 'join', data: { quizZoneId } }); + sendMessage(message); + }; + + const sendChat = (chatMessage: any) => { + sendMessage(JSON.stringify({ event: 'chat', data: chatMessage })); + }; + + return { + quizZoneState, + initQuizZoneData, + submitQuiz, + startQuiz, + playQuiz, + closeConnection, + exitQuiz, + joinQuizZone, + sendChat, + }; +}; + +export default useQuizZone; diff --git a/apps/frontend/src/hook/useAsyncError.ts b/apps/frontend/src/hook/useAsyncError.ts new file mode 100644 index 0000000..5abd50c --- /dev/null +++ b/apps/frontend/src/hook/useAsyncError.ts @@ -0,0 +1,11 @@ +import { useState, useCallback } from 'react'; + +export const useAsyncError = () => { + const [, setError] = useState(); + + return useCallback((error: unknown) => { + setError(() => { + throw error; + }); + }, []); +}; diff --git a/apps/frontend/src/hook/useTimer.ts b/apps/frontend/src/hook/useTimer.ts new file mode 100644 index 0000000..3b0c418 --- /dev/null +++ b/apps/frontend/src/hook/useTimer.ts @@ -0,0 +1,92 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import TimerWorker from '@/workers/timer.worker?worker'; + +interface TimerConfig { + initialTime: number; + onComplete?: () => void; + autoStart?: boolean; +} + +/** + * Web Worker를 활용한 정확한 카운트다운 타이머 커스텀 훅 + * + * @param {TimerConfig} config - 타이머 설정 객체 + * @returns {object} 타이머 상태와 컨트롤 함수들 + */ +export const useTimer = ({ initialTime, onComplete }: TimerConfig) => { + const [time, setTime] = useState(initialTime); + const [isRunning, setIsRunning] = useState(false); + const workerRef = useRef(null); + + useEffect(() => { + // Worker가 이미 존재하면 종료 + if (workerRef.current) { + workerRef.current.terminate(); + } + + // 새 Worker 생성 + workerRef.current = new TimerWorker(); + + // Worker 메시지 핸들러 + workerRef.current.onmessage = (event) => { + const { type, payload } = event.data; + // console.log('Received from worker:', type, payload); // 디버깅용 + + switch (type) { + case 'TICK': + setTime(payload.time); + break; + case 'COMPLETE': + setTime(0); + setIsRunning(false); + onComplete?.(); + break; + } + }; + + // Clean up + return () => { + workerRef.current?.terminate(); + }; + }, []); + + // 타이머 시작 + const start = useCallback(() => { + if (isRunning || !workerRef.current) return; + + workerRef.current.postMessage({ + type: 'START', + payload: { + duration: initialTime, + serverTime: Date.now(), + }, + }); + + setIsRunning(true); + }, [isRunning, initialTime]); + + // 타이머 정지 + const stop = useCallback(() => { + if (!isRunning || !workerRef.current) return; + + workerRef.current.postMessage({ type: 'STOP' }); + setIsRunning(false); + }, [isRunning]); + + // 타이머 리셋 + const reset = useCallback(() => { + if (!workerRef.current) return; + + workerRef.current.postMessage({ type: 'RESET' }); + setTime(initialTime); + setIsRunning(false); + }, [initialTime]); + + return { + time, + isRunning, + start, + stop, + reset, + }; +}; diff --git a/apps/frontend/src/hook/useValidInput.test.ts b/apps/frontend/src/hook/useValidInput.test.ts new file mode 100644 index 0000000..6b69f63 --- /dev/null +++ b/apps/frontend/src/hook/useValidInput.test.ts @@ -0,0 +1,124 @@ +import { renderHook, act } from '@testing-library/react'; +import { describe, test, expect } from 'vitest'; +import useValidState from './useValidInput'; + +describe('useValidState', () => { + test('초기값이 유효한 경우', () => { + const validator = (value: number) => { + if (value < 0) return '음수는 입력할 수 없습니다.'; + }; + + const { result } = renderHook(() => useValidState(10, validator)); + const [state, errorMessage, _, isInvalid] = result.current; + + expect(state).toBe(10); + expect(errorMessage).toBe(''); + expect(isInvalid).toBe(false); + }); + + test('초기값이 유효하지 않은 경우', () => { + const validator = (value: number) => { + if (value < 0) return '음수는 입력할 수 없습니다.'; + }; + + const { result } = renderHook(() => useValidState(-5, validator)); + const [state, errorMessage, _, isInvalid] = result.current; + + expect(state).toBe(-5); + expect(errorMessage).toBe('음수는 입력할 수 없습니다.'); + expect(isInvalid).toBe(true); + }); + + test('유효한 값으로 상태 업데이트', () => { + const validator = (value: number) => { + if (value < 0) return '음수는 입력할 수 없습니다.'; + }; + + const { result } = renderHook(() => useValidState(0, validator)); + + act(() => { + const [, , setValue] = result.current; + setValue(5); + }); + + const [state, errorMessage, _, isInvalid] = result.current; + expect(state).toBe(5); + expect(errorMessage).toBe(''); + expect(isInvalid).toBe(false); + }); + + test('유효하지 않은 값으로 상태 업데이트', () => { + const validator = (value: number) => { + if (value < 0) return '음수는 입력할 수 없습니다.'; + }; + + const { result } = renderHook(() => useValidState(0, validator)); + + act(() => { + const [, , setValue] = result.current; + setValue(-10); + }); + + const [state, errorMessage, _, isInvalid] = result.current; + expect(state).toBe(-10); + expect(errorMessage).toBe('음수는 입력할 수 없습니다.'); + expect(isInvalid).toBe(true); + }); + + test('다중 조건 검증', () => { + const validator = (value: number) => { + if (value < 0) return '음수는 입력할 수 없습니다.'; + if (value > 100) return '100보다 큰 숫자는 입력할 수 없습니다.'; + }; + + const { result } = renderHook(() => useValidState(0, validator)); + + // 유효한 값 테스트 + act(() => { + const [, , setValue] = result.current; + setValue(50); + }); + + expect(result.current[0]).toBe(50); + expect(result.current[1]).toBe(''); + expect(result.current[3]).toBe(false); + + // 범위를 초과하는 값 테스트 + act(() => { + const [, , setValue] = result.current; + setValue(150); + }); + + expect(result.current[0]).toBe(150); + expect(result.current[1]).toBe('100보다 큰 숫자는 입력할 수 없습니다.'); + expect(result.current[3]).toBe(true); + }); + + test('문자열 유효성 검사', () => { + const validator = (value: string) => { + if (value.length < 3) return '최소 3글자 이상이어야 합니다.'; + }; + + const { result } = renderHook(() => useValidState('', validator)); + + // 유효하지 않은 값 테스트 + act(() => { + const [, , setValue] = result.current; + setValue('ab'); + }); + + expect(result.current[0]).toBe('ab'); + expect(result.current[1]).toBe('최소 3글자 이상이어야 합니다.'); + expect(result.current[3]).toBe(true); + + // 유효한 값 테스트 + act(() => { + const [, , setValue] = result.current; + setValue('abc'); + }); + + expect(result.current[0]).toBe('abc'); + expect(result.current[1]).toBe(''); + expect(result.current[3]).toBe(false); + }); +}); diff --git a/apps/frontend/src/hook/useValidInput.ts b/apps/frontend/src/hook/useValidInput.ts new file mode 100644 index 0000000..fd57374 --- /dev/null +++ b/apps/frontend/src/hook/useValidInput.ts @@ -0,0 +1,30 @@ +import { useState } from 'react'; + +const useValidState = ( + initialState: T, + validator: (state: T) => string | void, +): [T, string, (state: T) => void, boolean] => { + const msg = validator(initialState) ?? ''; + + const [state, setState] = useState(initialState); + const [isInvalid, setIsInvalid] = useState(msg.length !== 0); + const [InvalidMessage, setInvalidMessage] = useState(msg); + + const setValidateValue = (newState: T) => { + const message = validator(newState); + + setState(newState); + + if (!message) { + setInvalidMessage(''); + setIsInvalid(false); + } else { + setInvalidMessage(message); + setIsInvalid(true); + } + }; + + return [state, InvalidMessage, setValidateValue, isInvalid]; +}; + +export default useValidState; diff --git a/apps/frontend/src/hook/useWebSocket.tsx b/apps/frontend/src/hook/useWebSocket.tsx new file mode 100644 index 0000000..f292fdf --- /dev/null +++ b/apps/frontend/src/hook/useWebSocket.tsx @@ -0,0 +1,70 @@ +import { useRef } from 'react'; + +interface WebSocketConfig { + wsUrl: string; + messageHandler: (event: MessageEvent) => void; + handleFinish?: () => void; + handleReconnect?: () => void; +} + +const useWebSocket = ({ + wsUrl, + messageHandler, + handleFinish, + handleReconnect, +}: WebSocketConfig) => { + const ws = useRef(null); + const messageQueue = useRef([]); + + const beginConnection = () => { + if (ws.current !== null) { + return; + } + + ws.current = new WebSocket(wsUrl); + + ws.current.onopen = () => { + while (messageQueue.current.length > 0) { + const message = messageQueue.current.shift()!; + sendMessage(message); + } + }; + + ws.current.onclose = (ev: CloseEvent) => { + const { wasClean, reason } = ev; + + ws.current = null; + + if (reason == 'finish') { + handleFinish?.(); + } + + if (!wasClean) { + handleReconnect?.(); + } + }; + + ws.current.onerror = (error) => { + console.error('WebSocket error:', error); + }; + + ws.current.onmessage = messageHandler; + }; + + const sendMessage = (message: string) => { + if (ws.current?.readyState === WebSocket.OPEN) { + ws.current.send(message); + } else { + console.warn('WebSocket is not connected. Message not sent:', message); + messageQueue.current.push(message); + } + }; + + const closeConnection = () => { + ws.current?.close(); + }; + + return { beginConnection, sendMessage, closeConnection }; +}; + +export default useWebSocket; diff --git a/apps/frontend/src/index.css b/apps/frontend/src/index.css new file mode 100644 index 0000000..89b55f8 --- /dev/null +++ b/apps/frontend/src/index.css @@ -0,0 +1,66 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 10% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem + } + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55% + } +} +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/frontend/src/lib/utils.ts b/apps/frontend/src/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/apps/frontend/src/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx new file mode 100644 index 0000000..523a22c --- /dev/null +++ b/apps/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { createRoot } from 'react-dom/client'; +import './index.css'; +import App from './App.tsx'; +import { BrowserRouter } from 'react-router-dom'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/apps/frontend/src/pages/CreateQuizZonePage.tsx b/apps/frontend/src/pages/CreateQuizZonePage.tsx new file mode 100644 index 0000000..ab38e51 --- /dev/null +++ b/apps/frontend/src/pages/CreateQuizZonePage.tsx @@ -0,0 +1,83 @@ +import CreateQuizZoneBasic from '@/blocks/CreateQuizZone/CreateQuizZoneBasic.tsx'; +import CreateQuizSet from '@/blocks/CreateQuizZone/CreateQuizSet.tsx'; +import { useReducer, useState } from 'react'; +import { + CreateQuizZone, + CreateQuizZoneReducerAction, + CreateQuizZoneReducerActions, + CreateQuizZoneStage, +} from '@/types/create-quiz-zone.types.ts'; +import Typography from '@/components/common/Typogrpahy'; + +const CreateQuizZoneReducer = (state: CreateQuizZone, action: CreateQuizZoneReducerAction) => { + const { type, payload } = action; + + switch (type) { + case 'QUIZ_ZONE_ID': + return { ...state, quizZoneId: payload }; + case 'TITLE': + return { ...state, title: payload }; + case 'DESC': + return { ...state, description: payload }; + case 'LIMIT': + return { ...state, limitPlayerCount: parseInt(payload) }; + case 'QUIZ_SET_ID': + return { ...state, quizSetId: payload }; + case 'QUIZ_SET_NAME': + return { ...state, quizSetName: payload }; + } +}; + +const CreateQuizZonePage = () => { + const [stage, setState] = useState('QUIZ_ZONE'); + const [quizZone, dispatch] = useReducer(CreateQuizZoneReducer, { + quizZoneId: '', + title: '', + description: '', + limitPlayerCount: 10, + quizSetId: '', + quizSetName: '', + }); + + const moveStage = (stage: CreateQuizZoneStage) => { + setState(stage); + }; + + const updateQuizZoneBasic = (payload: string, type: CreateQuizZoneReducerActions) => { + dispatch({ type, payload }); + }; + + const updateQuizSet = (quizSetId: string, quizSetName: string) => { + updateQuizZoneBasic(quizSetId, 'QUIZ_SET_ID'); + updateQuizZoneBasic(quizSetName, 'QUIZ_SET_NAME'); + }; + + const getCreateQuizZoneStageBlock = (stage: CreateQuizZoneStage) => { + switch (stage) { + case 'QUIZ_ZONE': + return ( + + ); + case 'QUIZ_SET': + return ( + moveStage('QUIZ_ZONE')} + updateQuizSet={updateQuizSet} + /> + ); + } + }; + + return ( +
+ + {getCreateQuizZoneStageBlock(stage)} +
+ ); +}; + +export default CreateQuizZonePage; diff --git a/apps/frontend/src/pages/MainPage.tsx b/apps/frontend/src/pages/MainPage.tsx new file mode 100644 index 0000000..7bed27d --- /dev/null +++ b/apps/frontend/src/pages/MainPage.tsx @@ -0,0 +1,167 @@ +import CommonButton from '@/components/common/CommonButton'; +import ContentBox from '@/components/common/ContentBox'; +import Input from '@/components/common/Input'; +import Typography from '@/components/common/Typogrpahy'; +import TooltipWrapper from '@/components/common/TooltipWrapper'; +import { AsyncBoundary } from '@/components/boundary/AsyncBoundary'; +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAsyncError } from '@/hook/useAsyncError'; +import { ValidationError } from '@/types/error.types'; +import Logo from '@/components/common/Logo'; +import CustomAlertDialogContent from '@/components/common/CustomAlertDialogContent.tsx'; +import { AlertDialog } from '@radix-ui/react-alert-dialog'; + +const MainPageContent = () => { + const [input, setInput] = useState(''); + const [alertIsOpen, setAlertIsOpen] = useState(false); + const navigate = useNavigate(); + const throwError = useAsyncError(); + + const validateInput = (input: string) => { + if (!input) { + throw new ValidationError('퀴즈존 코드를 입력해주세요'); + } + if (input.length < 5 || input.length > 10) { + throw new ValidationError('퀴즈존 코드를 입력해주세요'); + } + }; + + const handleCreateQuizZone = async () => { + try { + navigate(`/quiz-zone`); + } catch (error) { + throwError(error); + } + }; + + const handleClickJoin = async () => { + try { + validateInput(input); + + const isEntered = await requestCheckEnteredQuizZone(input); + + if (isEntered) { + moveQuizZoneLobby(input); + } else { + setAlertIsOpen(true); + } + } catch (e) { + throwError(e); + } + }; + + const requestCheckEnteredQuizZone = async (enterCode: string) => { + const response = await fetch(`/api/quiz-zone/check/${enterCode}`); + + if (!response.ok) { + throw throwError(response); + } + + return (await response.json()) as boolean; + }; + + const moveQuizZoneLobby = (enterCode: string) => { + navigate(`/${enterCode}`); + }; + + return ( +
+ + + + + + + + +
+ + setInput(e.target.value)} + name="입장코드" + isAutoFocus={false} + placeholder="입장 코드를 작성해주세요" + height="h-10 sm:h-12" + isBorder={true} + className="mb-4 w-full" + /> + + + + +
+
+ + + +
+ + setAlertIsOpen(false)} + handleConfirm={() => moveQuizZoneLobby(input)} + /> + +
+ ); +}; + +const MainPage = () => { + const navigate = useNavigate(); + + return ( + +
+
+ } + handleError={(error: any) => { + console.error('Main Page Error:', error); + }} + onReset={() => navigate('/')} + > + +
+ ); +}; + +export default MainPage; diff --git a/apps/frontend/src/pages/NotFoundPage.tsx b/apps/frontend/src/pages/NotFoundPage.tsx new file mode 100644 index 0000000..705b0a0 --- /dev/null +++ b/apps/frontend/src/pages/NotFoundPage.tsx @@ -0,0 +1,50 @@ +import CommonButton from '@/components/common/CommonButton'; +import ContentBox from '@/components/common/ContentBox'; +import Typography from '@/components/common/Typogrpahy'; +import TooltipWrapper from '@/components/common/TooltipWrapper'; +import { useNavigate } from 'react-router-dom'; +import Logo from '@/components/common/Logo'; + +const NotFound = () => { + const navigate = useNavigate(); + const handleMoveMainPage = () => { + navigate(`/`); + }; + + return ( +
+ + + + + + + + + + + + + +
+ ); +}; + +export default NotFound; diff --git a/apps/frontend/src/pages/QuizZonePage.tsx b/apps/frontend/src/pages/QuizZonePage.tsx new file mode 100644 index 0000000..4b4b184 --- /dev/null +++ b/apps/frontend/src/pages/QuizZonePage.tsx @@ -0,0 +1,170 @@ +import QuizZoneInProgress from '@/blocks/QuizZone/QuizZoneInProgress'; +import QuizZoneLoading from '@/blocks/QuizZone/QuizZoneLoading'; +import QuizZoneLobby from '@/blocks/QuizZone/QuizZoneLobby'; +import QuizZoneResult from '@/blocks/QuizZone/QuizZoneResult'; +import { AsyncBoundary } from '@/components/boundary/AsyncBoundary'; +import ChatBox from '@/components/common/ChatBox'; +import useQuizZone from '@/hook/quizZone/useQuizZone'; +import { useAsyncError } from '@/hook/useAsyncError'; +import { useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { requestQuizZone } from '@/utils/requests.ts'; +import { AlertDialog } from '@radix-ui/react-alert-dialog'; +import CustomAlertDialogContent from '@/components/common/CustomAlertDialogContent.tsx'; + +const QuizZoneContent = () => { + const [isLoading, setIsLoading] = useState(true); + const [isDisconnection, setIsDisconnection] = useState(false); + const [isClose, setIsClose] = useState(false); + + const navigate = useNavigate(); + const { quizZoneId } = useParams(); + const throwError = useAsyncError(); + + if (quizZoneId === undefined) { + throwError(new Error('접속하려는 퀴즈존의 입장 코드를 확인하세요.')); + return; + } + + const reconnectHandler = () => { + setIsDisconnection(true); + }; + + const closeHandler = () => { + setIsClose(true); + }; + + const { initQuizZoneData, quizZoneState, submitQuiz, startQuiz, playQuiz, exitQuiz, sendChat } = + useQuizZone(quizZoneId, reconnectHandler, closeHandler); + + const initQuizZone = async () => { + try { + setIsLoading(true); + + const quizZone = await requestQuizZone(quizZoneId); + const now = new Date().getTime(); + await initQuizZoneData(quizZone, now); + + setIsLoading(false); + setIsDisconnection(false); + } catch (error) { + throwError(error); + } + }; + + useEffect(() => { + initQuizZone(); + }, []); + + const shouldShowChat = () => { + if (!quizZoneState.currentPlayer?.id || !quizZoneState.stage) { + return false; + } + + const isPlaying = + quizZoneState.currentPlayer.state === 'PLAY' && quizZoneState.stage === 'IN_PROGRESS'; + const isSinglePlayer = quizZoneState.players?.length === 1; + + return !isPlaying && !isSinglePlayer; + }; + + if (isLoading) { + return
로딩 중...
; + } + + const renderQuizZone = () => { + switch (quizZoneState.stage) { + case 'LOBBY': + return ( + + ); + case 'IN_PROGRESS': + return ( + + ); + case 'RESULT': + // endSocketTime이 Null이면 로딩 중 + if (!quizZoneState.endSocketTime) { + return ; + } + return ; + default: + return null; + } + }; + return ( +
+
+ {/* QuizZone 컨텐츠를 위한 컨테이너 */} +
{renderQuizZone()}
+ + {/* 채팅 박스 컨테이너 */} + {shouldShowChat() && ( + + )} +
+ + navigate('/')} + handleConfirm={() => initQuizZone()} + /> + + + setIsClose(false)} + confirmText={'나가기'} + handleConfirm={() => navigate('/')} + /> + +
+ ); +}; + +const QuizZonePage = () => { + const navigate = useNavigate(); + + return ( + +
+
+ } + handleError={(error: any) => { + console.error('QuizZone Error:', error); + }} + onReset={() => navigate('/')} + > + +
+ ); +}; + +export default QuizZonePage; diff --git a/apps/frontend/src/pages/RootLayout.tsx b/apps/frontend/src/pages/RootLayout.tsx new file mode 100644 index 0000000..4309f9a --- /dev/null +++ b/apps/frontend/src/pages/RootLayout.tsx @@ -0,0 +1,17 @@ +import Navbar from '@/components/common/NavBar'; +import { Outlet } from 'react-router-dom'; + +const RootLayout = () => { + return ( +
+ +
+
+ +
+
+
+ ); +}; + +export default RootLayout; diff --git a/apps/frontend/src/router/router.tsx b/apps/frontend/src/router/router.tsx new file mode 100644 index 0000000..81bddd9 --- /dev/null +++ b/apps/frontend/src/router/router.tsx @@ -0,0 +1,21 @@ +import { Route, Routes } from 'react-router-dom'; +import MainPage from '@/pages/MainPage'; +import RootLayout from '../pages/RootLayout'; +import QuizZonePage from '@/pages/QuizZonePage'; +import CreateQuizZonePage from '@/pages/CreateQuizZonePage.tsx'; +import NotFound from '@/pages/NotFoundPage'; + +function Router() { + return ( + + }> + } /> + } /> + } /> + } /> + + + ); +} + +export default Router; diff --git a/apps/frontend/src/test/setup.ts b/apps/frontend/src/test/setup.ts new file mode 100644 index 0000000..c87a65d --- /dev/null +++ b/apps/frontend/src/test/setup.ts @@ -0,0 +1,7 @@ +import '@testing-library/jest-dom'; +import { afterEach } from 'vitest'; +import { cleanup } from '@testing-library/react'; +// 각 테스트 후 cleanup +afterEach(() => { + cleanup(); +}); diff --git a/apps/frontend/src/types/create-quiz-zone.types.ts b/apps/frontend/src/types/create-quiz-zone.types.ts new file mode 100644 index 0000000..7bb5559 --- /dev/null +++ b/apps/frontend/src/types/create-quiz-zone.types.ts @@ -0,0 +1,31 @@ +export type CreateQuizZoneStage = 'QUIZ_ZONE' | 'QUIZ_SET' | 'SUMMARY'; + +export interface CreateQuizZone { + quizZoneId: string; + title: string; + description: string; + limitPlayerCount: number; + quizSetId: string; + quizSetName: string; +} + +export type CreateQuizZoneReducerActions = + | 'QUIZ_ZONE_ID' + | 'TITLE' + | 'DESC' + | 'LIMIT' + | 'QUIZ_SET_ID' + | 'QUIZ_SET_NAME'; + +export type CreateQuizZoneReducerAction = { type: CreateQuizZoneReducerActions; payload: string }; + +export interface ResponseQuizSet { + id: string; + name: string; +} + +export interface ResponseSearchQuizSets { + quizSetDetails: ResponseQuizSet[]; + currentPage: number; + total: number; +} diff --git a/apps/frontend/src/types/error.types.ts b/apps/frontend/src/types/error.types.ts new file mode 100644 index 0000000..b953e54 --- /dev/null +++ b/apps/frontend/src/types/error.types.ts @@ -0,0 +1,55 @@ +export type QuizZoneErrorType = + | 'NOT_FOUND' + | 'ALREADY_STARTED' + | 'ROOM_FULL' + | 'ALREADY_ENDED' + | 'NOT_AUTHORIZED' + | 'SESSION_ERROR' + | 'QUIZ_COMPLETE' + | 'VALIDATION_ERROR' + | null; + +export class ValidationError extends Error { + constructor(message: string) { + super(message); + this.name = 'ValidationError'; + } +} + +export const quizZoneErrorMessages: Record< + Exclude, + { title: string; description: string } +> = { + NOT_FOUND: { + title: '찾을 수 없는 퀴즈존이에요', + description: '퀴즈존이 없어졌거나 다른 곳으로 이동했어요', + }, + ALREADY_STARTED: { + title: '이미 시작된 퀴즈존이에요', + description: '다음 퀴즈존을 기다려주세요', + }, + ROOM_FULL: { + title: '정원이 가득 찼어요', + description: '다른 퀴즈존을 둘러보시는 건 어떨까요?', + }, + ALREADY_ENDED: { + title: '종료된 퀴즈존이에요', + description: '새로운 퀴즈존에 도전해보세요', + }, + NOT_AUTHORIZED: { + title: '참여할 수 없는 퀴즈존이에요', + description: '퀴즈존에 참여하지 않은 사용자예요', + }, + SESSION_ERROR: { + title: '일시적인 오류가 발생했어요', + description: '잠시 후 다시 시도해주세요', + }, + QUIZ_COMPLETE: { + title: '퀴즈가 끝났어요', + description: '모든 문제가 출제되었어요', + }, + VALIDATION_ERROR: { + title: '입력값이 올바르지 않아요', + description: '입력 조건을 확인해주세요', + }, +}; diff --git a/apps/frontend/src/types/quizZone.types.ts b/apps/frontend/src/types/quizZone.types.ts new file mode 100644 index 0000000..c55c2c0 --- /dev/null +++ b/apps/frontend/src/types/quizZone.types.ts @@ -0,0 +1,128 @@ +export type QuizZoneStage = 'LOBBY' | 'IN_PROGRESS' | 'RESULT'; +export type PlayerState = 'WAIT' | 'PLAY' | 'SUBMIT'; +export type ProblemType = 'SHORT'; + +export interface Player { + id: string; + nickname: string; + state?: PlayerState; +} + +export interface QuizZone { + stage: QuizZoneStage; + currentPlayer: Player; + title: string; + description: string; + hostId: string; + quizCount: number; + players?: Player[]; + maxPlayers?: number; + currentQuiz?: CurrentQuiz; + currentQuizResult?: CurrentQuizResult; + score?: number; + quizzes?: Quiz[]; + submits?: SubmittedQuiz[]; + ranks?: Rank[]; + isLastQuiz?: boolean; + chatMessages?: ChatMessage[]; + isQuizZoneEnd?: boolean; + endSocketTime?: number; + serverTime: number; + offset: number; +} + +export interface Rank { + id: string; + nickname: string; + score: number; + ranking: number; +} + +export interface QuizZoneLobbyState { + stage: QuizZoneStage; + title: string; + description: string; + quizCount: number; + hostId: string; + players: Player[]; + currentPlayer: Player; +} + +export interface CurrentQuiz { + question: string; + currentIndex: number; + playTime: number; + startTime: number; + deadlineTime: number; + type?: ProblemType; +} + +export interface QuizZoneProgressState { + currentPlayer: Player; + currentQuiz: CurrentQuiz; +} + +export interface Quiz { + question: string; + answer: string; + playTime: number; + quizType?: ProblemType; +} + +export interface SubmittedQuiz { + index: number; + answer: string; + submittedAt: number; + receivedAt: number; + submitRank?: number; +} + +export interface QuizZoneResultState { + score: number; + submits: SubmittedQuiz[]; + quizzes: Quiz[]; + ranks: Rank[]; + endSocketTime: number; +} + +export interface SubmitResponse { + fastestPlayerIds: string[]; + submittedCount: number; + totalPlayerCount: number; + chatMessages: ChatMessage[]; +} + +export interface SomeoneSubmitResponse { + clientId: string; + submittedCount: number; +} + +export interface CurrentQuizResult { + answer?: string; + correctPlayerCount?: number; + totalPlayerCount: number; + submittedCount: number; + fastestPlayers: Player[]; +} + +export interface NextQuizResponse { + nextQuiz: CurrentQuiz; + currentQuizResult: CurrentQuizResult; +} + +export interface QuizSet { + quizSetId?: string; + quizSetName: string; + quizzes: Quiz[]; +} + +export interface ChatMessage { + clientId: string; + nickname: string; + message: string; +} + +export interface InitQuizZoneResponse { + quizZone: QuizZone; + now: number; +} diff --git a/apps/frontend/src/types/timer.types.ts b/apps/frontend/src/types/timer.types.ts new file mode 100644 index 0000000..63ad825 --- /dev/null +++ b/apps/frontend/src/types/timer.types.ts @@ -0,0 +1,14 @@ +export interface TimerMessage { + type: 'START' | 'STOP' | 'RESET'; + payload?: { + duration: number; + serverTime?: number; + }; +} + +export interface TimerResponse { + type: 'TICK' | 'COMPLETE'; + payload?: { + time: number; + }; +} diff --git a/apps/frontend/src/utils/atob.ts b/apps/frontend/src/utils/atob.ts new file mode 100644 index 0000000..5673ddf --- /dev/null +++ b/apps/frontend/src/utils/atob.ts @@ -0,0 +1,11 @@ +function atob(encodedString: string): string { + try { + // 브라우저 native atob 사용 + return decodeURIComponent(escape(window.atob(encodedString))); + } catch (error) { + console.error('Base64 디코딩 실패:', error); + return encodedString; // 실패 시 원본 문자열 반환 + } +} + +export default atob; diff --git a/apps/frontend/src/utils/errorUtils.ts b/apps/frontend/src/utils/errorUtils.ts new file mode 100644 index 0000000..cff6229 --- /dev/null +++ b/apps/frontend/src/utils/errorUtils.ts @@ -0,0 +1,34 @@ +import { QuizZoneErrorType, ValidationError } from '@/types/error.types'; + +export const getQuizZoneErrorType = (error: unknown): QuizZoneErrorType => { + if (error instanceof ValidationError) { + return 'VALIDATION_ERROR'; + } + if (error instanceof Response) { + switch (error.status) { + case 404: + return 'NOT_FOUND'; + case 409: + return 'ALREADY_STARTED'; + case 400: + return 'NOT_AUTHORIZED'; + default: + return 'SESSION_ERROR'; + } + } + + if (error instanceof Error) { + switch (error.message) { + case '퀴즈존 정원이 초과되었습니다.': + return 'ROOM_FULL'; + case '이미 종료된 퀴즈존입니다.': + return 'ALREADY_ENDED'; + case '퀴즈가 모두 종료되었습니다.': + return 'QUIZ_COMPLETE'; + default: + return 'SESSION_ERROR'; + } + } + + return 'SESSION_ERROR'; +}; diff --git a/apps/frontend/src/utils/requests.ts b/apps/frontend/src/utils/requests.ts new file mode 100644 index 0000000..c944808 --- /dev/null +++ b/apps/frontend/src/utils/requests.ts @@ -0,0 +1,65 @@ +import { QuizSet, QuizZone } from '@/types/quizZone.types.ts'; +import { CreateQuizZone, ResponseSearchQuizSets } from '@/types/create-quiz-zone.types.ts'; + +export const requestCreateQuizZone = async (quizZone: CreateQuizZone) => { + const response = await fetch('api/quiz-zone', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(quizZone), + }); + + if (!response.ok) { + throw new Error('퀴즈존 생성 처리중 오류가 발생하였습니다.'); + } +}; +export const requestCreateQuizSet = async (quizSet: QuizSet) => { + const { quizSetName, quizzes } = quizSet; + + const response = await fetch('api/quiz-set', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + quizSetName, + quizDetails: quizzes, + }), + }); + + if (!response.ok) { + throw Error(); + } + + return (await response.json()) as string; +}; + +export const requestSearchQuizSets = async (params: Record) => { + const searchParams = new URLSearchParams(params); + const url = `api/quiz-set?${searchParams.toString()}`; + + const response = await fetch(url, { + method: 'GET', + }); + + if (!response.ok) { + console.log(response.status); + } + + const { quizSetDetails, total, currentPage } = + (await response.json()) as ResponseSearchQuizSets; + + return { + quizSets: quizSetDetails, + totalQuizSetCount: total, + currentPage: currentPage, + }; +}; +export const requestQuizZone = async (quizZoneId: string) => { + const response = await fetch(`/api/quiz-zone/${quizZoneId}`, { method: 'GET' }); + + if (!response.ok) { + throw response; + } + + return (await response.json()) as QuizZone; +}; diff --git a/apps/frontend/src/utils/validators.ts b/apps/frontend/src/utils/validators.ts new file mode 100644 index 0000000..a4fa943 --- /dev/null +++ b/apps/frontend/src/utils/validators.ts @@ -0,0 +1,49 @@ +import { Quiz } from '@/types/quizZone.types.ts'; +import { QUIZ_LIMIT_COUNT } from '@/constants/quiz-set.constants.ts'; + +export const validQuizSetName = (name: string) => { + if (name.length <= 0) return '퀴즈존 이름 입력을 확인하세요'; + if (name.length > 100) return '퀴즈존 이름 길이를 확인하세요. (최대 100자)'; +}; +export const validQuizzes = (quizzes: Quiz[]) => { + if (quizzes.length === 0) return '퀴즈를 1개 이상 등록해야합니다.'; + if (quizzes.length > QUIZ_LIMIT_COUNT) return '퀴즈는 최대 10개만 등록할 수 있습니다.'; +}; +export const validQuestion = (question: string) => { + if (question.length <= 0) return '문제를 입력해주세요.'; + if (question.length > 200) return '문제 길이를 초과하였습니다. 최대 200자'; +}; +export const validAnswer = (answer: string) => { + if (answer.length <= 0) return '답안을 입력해주세요.'; + if (answer.length > 50) return '답안 길이를 초과하였습니다. 최대 50자'; +}; +export const validTime = (time: number) => { + if (time <= 0) return '제한시간은 0초 보다 커야합니다.'; + if (time > 60) return '제한시간은 60초를 초과할 수 없습니다.'; +}; + +//QuizZone 생성 관련 유효성 검사 + +//퀴즈존 이름 유효성 검사 +export const validateQuizZoneSetName = (name: string) => { + if (name.length <= 0) return '제목을 입력해주세요.'; + if (name.length > 100) return '100자 이하로 입력해주세요.'; +}; + +//퀴즈존 설명 유효성 검사 +export const validateQuizZoneSetDescription = (description: string) => { + if (description.length > 300) return '300자 이하로 입력해주세요.'; +}; + +//퀴즈존 입장 코드 유효성 검사 +export const validateQuizZoneSetCode = (code: string) => { + if (code.length < 5) return '5자 이상 입력해주세요.'; + if (code.length > 10) return '10자 이하로 입력해주세요.'; + if (!/^[a-zA-Z0-9]*$/g.test(code)) return '숫자와 알파벳 조합만 가능합니다.'; +}; + +//입장 인원 제한 유효성 검사 +export const validateQuizZoneSetLimit = (limit: number) => { + if (limit < 1) return '최소 1명 이상 지정해주세요.'; + if (limit > 300) return '최대 인원은 300명입니다.'; +}; diff --git a/apps/frontend/src/vite-env.d.ts b/apps/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..ce0977e --- /dev/null +++ b/apps/frontend/src/vite-env.d.ts @@ -0,0 +1,7 @@ +/// +declare module '*?worker' { + const workerConstructor: { + new (): Worker; + }; + export default workerConstructor; +} diff --git a/apps/frontend/src/workers/timer.worker.ts b/apps/frontend/src/workers/timer.worker.ts new file mode 100644 index 0000000..c1940c4 --- /dev/null +++ b/apps/frontend/src/workers/timer.worker.ts @@ -0,0 +1,85 @@ +let timerId: ReturnType | null = null; +let startTime: number | null = null; +let duration: number | null = null; +let timeOffset: number = 0; +let pausedTimeRemaining: number | null = null; + +self.onmessage = (event: MessageEvent) => { + const { type, payload } = event.data; + + switch (type) { + case 'START': + if (!payload?.duration) return; + + if (pausedTimeRemaining !== null) { + startTimer(pausedTimeRemaining, self.postMessage); + pausedTimeRemaining = null; + } else { + if (payload.serverTime) { + timeOffset = Date.now() - payload.serverTime; + } + startTimer(payload.duration, self.postMessage); + } + break; + + case 'STOP': + if (timerId !== null && startTime !== null && duration !== null) { + const currentTime = Date.now() - timeOffset; + const elapsed = (currentTime - startTime) / 1000; + pausedTimeRemaining = Math.max(0, duration - elapsed); + } + stopTimer(); + break; + + case 'RESET': + resetTimer(); + break; + } +}; + +function startTimer( + newDuration: number, + postMessage: { + (message: any, targetOrigin: string, transfer?: Transferable[]): void; + (message: any, options?: WindowPostMessageOptions): void; + }, +) { + stopTimer(); // 기존 타이머가 있다면 정리 + + duration = newDuration; + startTime = Date.now() - timeOffset; + + timerId = setInterval(() => { + if (!startTime || !duration) return; + + const currentTime = Date.now() - timeOffset; + const elapsed = (currentTime - startTime) / 1000; + const remaining = Math.max(0, duration - elapsed); + const roundedRemaining = Math.round(remaining * 10) / 10; + + if (roundedRemaining <= 0) { + postMessage({ type: 'COMPLETE' }); + stopTimer(); + } else { + postMessage({ + type: 'TICK', + payload: { time: roundedRemaining }, + }); + } + }, 100); +} + +function stopTimer() { + if (timerId !== null) { + clearInterval(timerId); + timerId = null; + } +} + +function resetTimer() { + stopTimer(); + startTime = null; + duration = null; + timeOffset = 0; + pausedTimeRemaining = null; +} diff --git a/apps/frontend/tailwind.config.cjs b/apps/frontend/tailwind.config.cjs new file mode 100644 index 0000000..d56d1da --- /dev/null +++ b/apps/frontend/tailwind.config.cjs @@ -0,0 +1,60 @@ +/** @type {import('tailwindcss').Config} */ + +export default { + darkMode: ["class"], + content: [ + "./src/**/*.{js,jsx,ts,tsx}", + ], + theme: { + extend: { + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + colors: { + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + } + } + } + }, + plugins: [require("tailwindcss-animate")], +} \ No newline at end of file diff --git a/apps/frontend/tsconfig.app.json b/apps/frontend/tsconfig.app.json new file mode 100644 index 0000000..b5494cb --- /dev/null +++ b/apps/frontend/tsconfig.app.json @@ -0,0 +1,34 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + }, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "emitDeclarationOnly": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json new file mode 100644 index 0000000..5bd43f4 --- /dev/null +++ b/apps/frontend/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "types": ["vitest/globals", "@testing-library/jest-dom"], + "lib": ["webworker", "es2015"] + }, + "files": [], + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] +} diff --git a/apps/frontend/tsconfig.node.json b/apps/frontend/tsconfig.node.json new file mode 100644 index 0000000..9d1b70b --- /dev/null +++ b/apps/frontend/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "emitDeclarationOnly": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts new file mode 100644 index 0000000..86c4110 --- /dev/null +++ b/apps/frontend/vite.config.ts @@ -0,0 +1,53 @@ +import path from 'path'; +import react from '@vitejs/plugin-react'; +import { defineConfig, loadEnv } from 'vite'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd()); + + return { + plugins: [react()], + worker: { + format: 'es', + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + proxy: { + '/api': { + target: env.VITE_API_URL, + changeOrigin: true, + secure: false, + rewrite: (path) => path.replace(/^\/api/, ''), // /api prefix 제거 + }, + }, + }, + build: { + clean: true, // 빌드 전에 outDir을 청소합니다 + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes('timer.worker')) { + return 'worker'; + } + }, + // 캐시 무효화를 위한 더 안전한 방법 + entryFileNames: `assets/[name].[hash].js`, + chunkFileNames: `assets/[name].[hash].js`, + assetFileNames: `assets/[name].[hash].[ext]`, + }, + }, + // 캐시 설정 + manifest: true, // manifest 파일 생성 + sourcemap: true, + }, + test: { + environment: 'jsdom', + globals: true, + setupFiles: './src/test/setup.ts', + }, + }; +}); diff --git a/commitlint.config.cjs b/commitlint.config.cjs new file mode 100644 index 0000000..9a02168 --- /dev/null +++ b/commitlint.config.cjs @@ -0,0 +1,35 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-case': [2, 'always', 'lower-case'], + 'type-enum': [ + 2, + 'always', + [ + 'feat', + 'fix', + 'design', + '!breaking change', + '!hotfix', + 'style', + 'refactor', + 'comment', + 'docs', + 'test', + 'chore', + 'rename', + 'remove', + ], + ], + 'subject-empty': [2, 'never'], + 'subject-case': [0, 'never'], // 대소문자 제한 해제 + 'subject-full-stop': [2, 'never', '.'], + 'body-leading-blank': [2, 'always'], + 'footer-leading-blank': [2, 'always'], + }, + parserPreset: { + parserOpts: { + issuePrefixes: ['EPIC: #', 'Story: #', 'Task: #'], + }, + }, +}; diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..1bf9641 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,21 @@ +**web08-booquiz v1.0.0** • [**Docs**](modules.md) + +*** + +# web08-BooQuiz + +

+ +

+ +[팀 노션](https://www.notion.so/127f1897cdf5809c8a44d54384683bc6?pvs=21) | [백로그](https://github.com/orgs/boostcampwm-2024/projects/11) | [그라운드 룰](https://github.com/boostcampwm-2024/web08-BooQuiz/wiki/%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9C-%EB%A3%B0) + +## 프로젝트 소개 + +300명 이상의 부스트 캠퍼를 감당 할 수 있는 실시간 퀴즈 플랫폼 + +## 팀 소개 + +| [J004 강준현](https://github.com/JunhyunKang) | [J074 김현우](https://github.com/krokerdile) | [J086 도선빈](https://github.com/typingmistake) | [J175 이동현](https://github.com/codemario318) | [J217 전현민](https://github.com/joyjhm) | +| --- | --- | --- | --- | --- | +|![](https://avatars.githubusercontent.com/u/72436328?v=4)|![](https://avatars.githubusercontent.com/u/39644976?v=4)|![](https://avatars.githubusercontent.com/u/102957984?v=4)|![](https://avatars.githubusercontent.com/u/130330767?v=4)|![](https://avatars.githubusercontent.com/u/77275989?v=4)| diff --git a/docs/backend/src/app.controller/README.md b/docs/backend/src/app.controller/README.md new file mode 100644 index 0000000..af7838c --- /dev/null +++ b/docs/backend/src/app.controller/README.md @@ -0,0 +1,13 @@ +[**web08-booquiz v1.0.0**](../../../README.md) • **Docs** + +*** + +[web08-booquiz v1.0.0](../../../modules.md) / backend/src/app.controller + +# backend/src/app.controller + +## Index + +### Classes + +- [AppController](classes/AppController.md) diff --git a/docs/backend/src/app.controller/classes/AppController.md b/docs/backend/src/app.controller/classes/AppController.md new file mode 100644 index 0000000..40b7053 --- /dev/null +++ b/docs/backend/src/app.controller/classes/AppController.md @@ -0,0 +1,39 @@ +[**web08-booquiz v1.0.0**](../../../../README.md) • **Docs** + +*** + +[web08-booquiz v1.0.0](../../../../modules.md) / [backend/src/app.controller](../README.md) / AppController + +# Class: AppController + +## Constructors + +### new AppController() + +> **new AppController**(`appService`): [`AppController`](AppController.md) + +#### Parameters + +• **appService**: [`AppService`](../../app.service/classes/AppService.md) + +#### Returns + +[`AppController`](AppController.md) + +#### Defined in + +app.controller.ts:6 + +## Methods + +### getHello() + +> **getHello**(): `string` + +#### Returns + +`string` + +#### Defined in + +app.controller.ts:9 diff --git a/docs/backend/src/app.module/README.md b/docs/backend/src/app.module/README.md new file mode 100644 index 0000000..aa3c9c9 --- /dev/null +++ b/docs/backend/src/app.module/README.md @@ -0,0 +1,13 @@ +[**web08-booquiz v1.0.0**](../../../README.md) • **Docs** + +*** + +[web08-booquiz v1.0.0](../../../modules.md) / backend/src/app.module + +# backend/src/app.module + +## Index + +### Classes + +- [AppModule](classes/AppModule.md) diff --git a/docs/backend/src/app.module/classes/AppModule.md b/docs/backend/src/app.module/classes/AppModule.md new file mode 100644 index 0000000..bc74e24 --- /dev/null +++ b/docs/backend/src/app.module/classes/AppModule.md @@ -0,0 +1,17 @@ +[**web08-booquiz v1.0.0**](../../../../README.md) • **Docs** + +*** + +[web08-booquiz v1.0.0](../../../../modules.md) / [backend/src/app.module](../README.md) / AppModule + +# Class: AppModule + +## Constructors + +### new AppModule() + +> **new AppModule**(): [`AppModule`](AppModule.md) + +#### Returns + +[`AppModule`](AppModule.md) diff --git a/docs/backend/src/app.service/README.md b/docs/backend/src/app.service/README.md new file mode 100644 index 0000000..356d7a5 --- /dev/null +++ b/docs/backend/src/app.service/README.md @@ -0,0 +1,13 @@ +[**web08-booquiz v1.0.0**](../../../README.md) • **Docs** + +*** + +[web08-booquiz v1.0.0](../../../modules.md) / backend/src/app.service + +# backend/src/app.service + +## Index + +### Classes + +- [AppService](classes/AppService.md) diff --git a/docs/backend/src/app.service/classes/AppService.md b/docs/backend/src/app.service/classes/AppService.md new file mode 100644 index 0000000..7bf04a7 --- /dev/null +++ b/docs/backend/src/app.service/classes/AppService.md @@ -0,0 +1,31 @@ +[**web08-booquiz v1.0.0**](../../../../README.md) • **Docs** + +*** + +[web08-booquiz v1.0.0](../../../../modules.md) / [backend/src/app.service](../README.md) / AppService + +# Class: AppService + +## Constructors + +### new AppService() + +> **new AppService**(): [`AppService`](AppService.md) + +#### Returns + +[`AppService`](AppService.md) + +## Methods + +### getHello() + +> **getHello**(): `string` + +#### Returns + +`string` + +#### Defined in + +app.service.ts:5 diff --git a/docs/backend/src/main/README.md b/docs/backend/src/main/README.md new file mode 100644 index 0000000..830fe9b --- /dev/null +++ b/docs/backend/src/main/README.md @@ -0,0 +1,7 @@ +[**web08-booquiz v1.0.0**](../../../README.md) • **Docs** + +*** + +[web08-booquiz v1.0.0](../../../modules.md) / backend/src/main + +# backend/src/main diff --git a/docs/modules.md b/docs/modules.md new file mode 100644 index 0000000..de7d0a5 --- /dev/null +++ b/docs/modules.md @@ -0,0 +1,12 @@ +[**web08-booquiz v1.0.0**](README.md) • **Docs** + +*** + +# web08-booquiz v1.0.0 + +## Modules + +- [backend/src/app.controller](backend/src/app.controller/README.md) +- [backend/src/app.module](backend/src/app.module/README.md) +- [backend/src/app.service](backend/src/app.service/README.md) +- [backend/src/main](backend/src/main/README.md) diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs new file mode 100644 index 0000000..b236a4a --- /dev/null +++ b/ecosystem.config.cjs @@ -0,0 +1,10 @@ +module.exports = { + apps: [ + { + name: "my-app", + script: "./dist/main.js", // 애플리케이션 시작 파일 + instances: "max", // 모든 CPU 코어를 사용할 경우 'max' + exec_mode: "cluster", // 클러스터 모드로 실행 + }, + ], +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..bf1a3c5 --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "dependencies": { + "@nestjs/mapped-types": "*" + }, + "name": "web08-booquiz", + "private": true, + "version": "1.0.0", + "workspaces": [ + "apps/*", + "packages/*" + ], + "description": "300명 이상의 부스트캠퍼를 감당할 수 있는 퀴즈 플랫폼", + "main": "index.js", + "type": "module", + "scripts": { + "build": "pnpm run build -w apps/backend && pnpm run build -w apps/frontend", + "dev": "pnpm run dev -w apps/backend & pnpm run dev -w apps/frontend", + "docs:backend": "typedoc --tsconfig apps/backend/tsconfig.json --entryPointStrategy expand --out docs/backend", + "docs:frontend": "typedoc --tsconfig apps/frontend/tsconfig.json --entryPointStrategy expand --out docs/frontend", + "docs:shared": "typedoc --tsconfig packages/shared/tsconfig.json --entryPointStrategy expand --out docs/shared", + "docs": "pnpm run docs:backend && pnpm run docs:frontend", + "prepare": "husky", + "preinstall": "npx only-allow pnpm", + "start": "pnpm --stream -r start" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/boostcampwm-2024/web08-BooQuiz.git" + }, + "keywords": [ + "real-time", + "quiz", + "game", + "websocket" + ], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/boostcampwm-2024/web08-BooQuiz/issues" + }, + "homepage": "https://github.com/boostcampwm-2024/web08-BooQuiz#readme", + "devDependencies": { + "@commitlint/cli": "^19.5.0", + "@commitlint/config-conventional": "^19.5.0", + "husky": "^9.1.6", + "prettier": "^3.3.3", + "typedoc": "^0.26.11", + "typescript": "^5.x.x" + } +} diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..4347320 --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,11 @@ +{ + "name": "@shared/utils", + "version": "1.0.0", + "main": "dist/index.js", + "scripts": { + "build": "tsc" + }, + "devDependencies": { + "typescript": "^5.x.x" + } +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..513b49d --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..0fae427 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,13239 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@nestjs/mapped-types': + specifier: '*' + version: 2.0.5(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + devDependencies: + '@commitlint/cli': + specifier: ^19.5.0 + version: 19.5.0(@types/node@22.9.0)(typescript@5.6.3) + '@commitlint/config-conventional': + specifier: ^19.5.0 + version: 19.5.0 + husky: + specifier: ^9.1.6 + version: 9.1.6 + prettier: + specifier: ^3.3.3 + version: 3.3.3 + typedoc: + specifier: ^0.26.11 + version: 0.26.11(typescript@5.6.3) + typescript: + specifier: ^5.x.x + version: 5.6.3 + + apps/backend: + dependencies: + '@nestjs/common': + specifier: ^10.0.0 + version: 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/config': + specifier: ^3.3.0 + version: 3.3.0(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1) + '@nestjs/core': + specifier: ^10.0.0 + version: 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(@nestjs/websockets@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/mapped-types': + specifier: '*' + version: 2.0.5(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + '@nestjs/platform-express': + specifier: ^10.0.0 + version: 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7) + '@nestjs/platform-ws': + specifier: ^10.4.7 + version: 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.7)(rxjs@7.8.1) + '@nestjs/swagger': + specifier: ^8.0.5 + version: 8.0.5(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + '@nestjs/typeorm': + specifier: ^10.0.2 + version: 10.0.2(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(mysql2@3.11.4)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))) + '@nestjs/websockets': + specifier: ^10.4.7 + version: 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-socket.io@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + backend: + specifier: 'file:' + version: file:apps/backend(@nestjs/platform-socket.io@10.4.7)(encoding@0.1.13)(supertest@7.0.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.1 + version: 0.14.1 + cookie: + specifier: ^1.0.1 + version: 1.0.1 + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 + express-session: + specifier: ^1.18.1 + version: 1.18.1 + get-port: + specifier: ^7.1.0 + version: 7.1.0 + mysql2: + specifier: ^3.11.4 + version: 3.11.4 + nest-winston: + specifier: ^1.9.7 + version: 1.9.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(winston@3.17.0) + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + socket.io-client: + specifier: ^4.8.1 + version: 4.8.1 + sqlite3: + specifier: ^5.1.7 + version: 5.1.7 + superwstest: + specifier: ^2.0.4 + version: 2.0.4(supertest@7.0.0) + typeorm: + specifier: ^0.3.20 + version: 0.3.20(mysql2@3.11.4)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + typeorm-transactional: + specifier: ^0.5.0 + version: 0.5.0(reflect-metadata@0.2.2)(typeorm@0.3.20(mysql2@3.11.4)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))) + winston: + specifier: ^3.17.0 + version: 3.17.0 + ws: + specifier: ^8.18.0 + version: 8.18.0 + devDependencies: + '@nestjs/cli': + specifier: ^10.0.0 + version: 10.4.7 + '@nestjs/schematics': + specifier: ^10.0.0 + version: 10.2.3(chokidar@3.6.0)(typescript@5.6.3) + '@nestjs/testing': + specifier: ^10.0.0 + version: 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-express@10.4.7) + '@types/cookie-parser': + specifier: ^1.4.7 + version: 1.4.7 + '@types/express': + specifier: ^5.0.0 + version: 5.0.0 + '@types/express-session': + specifier: ^1.18.0 + version: 1.18.0 + '@types/jest': + specifier: ^29.5.2 + version: 29.5.14 + '@types/node': + specifier: ^20.3.1 + version: 20.17.6 + '@types/supertest': + specifier: ^6.0.0 + version: 6.0.2 + '@types/ws': + specifier: ^8.5.13 + version: 8.5.13 + '@typescript-eslint/eslint-plugin': + specifier: ^8.0.0 + version: 8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) + '@typescript-eslint/parser': + specifier: ^8.0.0 + version: 8.13.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) + eslint: + specifier: ^9.15.0 + version: 9.15.0(jiti@1.21.6) + eslint-config-prettier: + specifier: ^9.0.0 + version: 9.1.0(eslint@9.15.0(jiti@1.21.6)) + eslint-plugin-prettier: + specifier: ^5.0.0 + version: 5.2.1(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.15.0(jiti@1.21.6)))(eslint@9.15.0(jiti@1.21.6))(prettier@3.3.3) + jest: + specifier: ^29.5.0 + version: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + prettier: + specifier: ^3.0.0 + version: 3.3.3 + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + supertest: + specifier: ^7.0.0 + version: 7.0.0 + ts-jest: + specifier: ^29.1.0 + version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)))(typescript@5.6.3) + ts-loader: + specifier: ^9.4.3 + version: 9.5.1(typescript@5.6.3)(webpack@5.96.1) + ts-node: + specifier: ^10.9.1 + version: 10.9.2(@types/node@20.17.6)(typescript@5.6.3) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 + typescript: + specifier: ^5.1.3 + version: 5.6.3 + + apps/frontend: + dependencies: + '@radix-ui/react-alert-dialog': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-avatar': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-icons': + specifier: ^1.3.1 + version: 1.3.1(react@18.3.1) + '@radix-ui/react-progress': + specifier: ^1.1.0 + version: 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: ^1.1.0 + version: 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: ^1.1.4 + version: 1.1.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/preview-api': + specifier: ^8.4.2 + version: 8.4.2(storybook@8.4.2(prettier@3.3.3)) + '@types/react-router-dom': + specifier: ^5.3.3 + version: 5.3.3 + class-variance-authority: + specifier: ^0.7.0 + version: 0.7.0 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lucide-react: + specifier: ^0.454.0 + version: 0.454.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-router-dom: + specifier: ^6.27.0 + version: 6.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwind-merge: + specifier: ^2.5.4 + version: 2.5.4 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3))) + ws: + specifier: ^8.18.0 + version: 8.18.0 + devDependencies: + '@chromatic-com/storybook': + specifier: ^3.2.2 + version: 3.2.2(react@18.3.1)(storybook@8.4.2(prettier@3.3.3)) + '@eslint/js': + specifier: ^9.13.0 + version: 9.14.0 + '@storybook/addon-essentials': + specifier: ^8.4.2 + version: 8.4.2(@types/react@18.3.12)(storybook@8.4.2(prettier@3.3.3))(webpack-sources@3.2.3) + '@storybook/addon-interactions': + specifier: ^8.4.2 + version: 8.4.2(storybook@8.4.2(prettier@3.3.3)) + '@storybook/addon-onboarding': + specifier: ^8.4.2 + version: 8.4.2(react@18.3.1)(storybook@8.4.2(prettier@3.3.3)) + '@storybook/addon-styling': + specifier: ^2.0.0 + version: 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3)(webpack@5.96.1(esbuild@0.21.5)) + '@storybook/blocks': + specifier: ^8.4.2 + version: 8.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.2(prettier@3.3.3)) + '@storybook/react': + specifier: ^8.4.2 + version: 8.4.2(@storybook/test@8.4.2(storybook@8.4.2(prettier@3.3.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.2(prettier@3.3.3))(typescript@5.6.3) + '@storybook/react-vite': + specifier: ^8.4.2 + version: 8.4.2(@storybook/test@8.4.2(storybook@8.4.2(prettier@3.3.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.4)(storybook@8.4.2(prettier@3.3.3))(typescript@5.6.3)(vite@5.4.10(@types/node@22.9.0)(terser@5.36.0))(webpack-sources@3.2.3) + '@storybook/test': + specifier: ^8.4.2 + version: 8.4.2(storybook@8.4.2(prettier@3.3.3)) + '@testing-library/jest-dom': + specifier: ^6.6.3 + version: 6.6.3 + '@testing-library/react': + specifier: ^16.0.1 + version: 16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/react-hooks': + specifier: ^8.0.1 + version: 8.0.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/node': + specifier: ^22.9.0 + version: 22.9.0 + '@types/react': + specifier: ^18.3.12 + version: 18.3.12 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.1 + '@vitejs/plugin-react': + specifier: ^4.3.3 + version: 4.3.3(vite@5.4.10(@types/node@22.9.0)(terser@5.36.0)) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.20(postcss@8.4.47) + eslint-plugin-react-hooks: + specifier: ^5.0.0 + version: 5.0.0(eslint@9.15.0(jiti@1.21.6)) + eslint-plugin-react-refresh: + specifier: ^0.4.14 + version: 0.4.14(eslint@9.15.0(jiti@1.21.6)) + globals: + specifier: ^15.11.0 + version: 15.12.0 + jsdom: + specifier: ^25.0.1 + version: 25.0.1 + postcss: + specifier: ^8.4.47 + version: 8.4.47 + storybook: + specifier: ^8.4.2 + version: 8.4.2(prettier@3.3.3) + tailwindcss: + specifier: ^3.4.14 + version: 3.4.14(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3)) + typescript: + specifier: ~5.6.2 + version: 5.6.3 + typescript-eslint: + specifier: ^8.11.0 + version: 8.13.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) + vite: + specifier: ^5.4.10 + version: 5.4.10(@types/node@22.9.0)(terser@5.36.0) + vitest: + specifier: ^2.1.5 + version: 2.1.5(@types/node@22.9.0)(jsdom@25.0.1)(terser@5.36.0) + + packages/shared: + devDependencies: + typescript: + specifier: ^5.x.x + version: 5.6.3 + +packages: + + '@adobe/css-tools@4.4.0': + resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==} + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@angular-devkit/core@17.3.11': + resolution: {integrity: sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==} + engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^3.5.2 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/schematics-cli@17.3.11': + resolution: {integrity: sha512-kcOMqp+PHAKkqRad7Zd7PbpqJ0LqLaNZdY1+k66lLWmkEBozgq8v4ASn/puPWf9Bo0HpCiK+EzLf0VHE8Z/y6Q==} + engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + hasBin: true + + '@angular-devkit/schematics@17.3.11': + resolution: {integrity: sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==} + engines: {node: ^18.13.0 || >=20.9.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.26.2': + resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.26.2': + resolution: {integrity: sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.25.9': + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.25.9': + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.2': + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.26.0': + resolution: {integrity: sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.25.9': + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.25.9': + resolution: {integrity: sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.25.9': + resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.25.9': + resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.26.0': + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.25.9': + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.0': + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@chromatic-com/storybook@3.2.2': + resolution: {integrity: sha512-xmXt/GW0hAPbzNTrxYuVo43Adrtjue4DeVrsoIIEeJdGaPNNeNf+DHMlJKOBdlHmCnFUoe9R/0mLM9zUp5bKWw==} + engines: {node: '>=16.0.0', yarn: '>=1.22.18'} + peerDependencies: + storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@commitlint/cli@19.5.0': + resolution: {integrity: sha512-gaGqSliGwB86MDmAAKAtV9SV1SHdmN8pnGq4EJU4+hLisQ7IFfx4jvU4s+pk6tl0+9bv6yT+CaZkufOinkSJIQ==} + engines: {node: '>=v18'} + hasBin: true + + '@commitlint/config-conventional@19.5.0': + resolution: {integrity: sha512-OBhdtJyHNPryZKg0fFpZNOBM1ZDbntMvqMuSmpfyP86XSfwzGw4CaoYRG4RutUPg0BTK07VMRIkNJT6wi2zthg==} + engines: {node: '>=v18'} + + '@commitlint/config-validator@19.5.0': + resolution: {integrity: sha512-CHtj92H5rdhKt17RmgALhfQt95VayrUo2tSqY9g2w+laAXyk7K/Ef6uPm9tn5qSIwSmrLjKaXK9eiNuxmQrDBw==} + engines: {node: '>=v18'} + + '@commitlint/ensure@19.5.0': + resolution: {integrity: sha512-Kv0pYZeMrdg48bHFEU5KKcccRfKmISSm9MvgIgkpI6m+ohFTB55qZlBW6eYqh/XDfRuIO0x4zSmvBjmOwWTwkg==} + engines: {node: '>=v18'} + + '@commitlint/execute-rule@19.5.0': + resolution: {integrity: sha512-aqyGgytXhl2ejlk+/rfgtwpPexYyri4t8/n4ku6rRJoRhGZpLFMqrZ+YaubeGysCP6oz4mMA34YSTaSOKEeNrg==} + engines: {node: '>=v18'} + + '@commitlint/format@19.5.0': + resolution: {integrity: sha512-yNy088miE52stCI3dhG/vvxFo9e4jFkU1Mj3xECfzp/bIS/JUay4491huAlVcffOoMK1cd296q0W92NlER6r3A==} + engines: {node: '>=v18'} + + '@commitlint/is-ignored@19.5.0': + resolution: {integrity: sha512-0XQ7Llsf9iL/ANtwyZ6G0NGp5Y3EQ8eDQSxv/SRcfJ0awlBY4tHFAvwWbw66FVUaWICH7iE5en+FD9TQsokZ5w==} + engines: {node: '>=v18'} + + '@commitlint/lint@19.5.0': + resolution: {integrity: sha512-cAAQwJcRtiBxQWO0eprrAbOurtJz8U6MgYqLz+p9kLElirzSCc0vGMcyCaA1O7AqBuxo11l1XsY3FhOFowLAAg==} + engines: {node: '>=v18'} + + '@commitlint/load@19.5.0': + resolution: {integrity: sha512-INOUhkL/qaKqwcTUvCE8iIUf5XHsEPCLY9looJ/ipzi7jtGhgmtH7OOFiNvwYgH7mA8osUWOUDV8t4E2HAi4xA==} + engines: {node: '>=v18'} + + '@commitlint/message@19.5.0': + resolution: {integrity: sha512-R7AM4YnbxN1Joj1tMfCyBryOC5aNJBdxadTZkuqtWi3Xj0kMdutq16XQwuoGbIzL2Pk62TALV1fZDCv36+JhTQ==} + engines: {node: '>=v18'} + + '@commitlint/parse@19.5.0': + resolution: {integrity: sha512-cZ/IxfAlfWYhAQV0TwcbdR1Oc0/r0Ik1GEessDJ3Lbuma/MRO8FRQX76eurcXtmhJC//rj52ZSZuXUg0oIX0Fw==} + engines: {node: '>=v18'} + + '@commitlint/read@19.5.0': + resolution: {integrity: sha512-TjS3HLPsLsxFPQj6jou8/CZFAmOP2y+6V4PGYt3ihbQKTY1Jnv0QG28WRKl/d1ha6zLODPZqsxLEov52dhR9BQ==} + engines: {node: '>=v18'} + + '@commitlint/resolve-extends@19.5.0': + resolution: {integrity: sha512-CU/GscZhCUsJwcKTJS9Ndh3AKGZTNFIOoQB2n8CmFnizE0VnEuJoum+COW+C1lNABEeqk6ssfc1Kkalm4bDklA==} + engines: {node: '>=v18'} + + '@commitlint/rules@19.5.0': + resolution: {integrity: sha512-hDW5TPyf/h1/EufSHEKSp6Hs+YVsDMHazfJ2azIk9tHPXS6UqSz1dIRs1gpqS3eMXgtkT7JH6TW4IShdqOwhAw==} + engines: {node: '>=v18'} + + '@commitlint/to-lines@19.5.0': + resolution: {integrity: sha512-R772oj3NHPkodOSRZ9bBVNq224DOxQtNef5Pl8l2M8ZnkkzQfeSTr4uxawV2Sd3ui05dUVzvLNnzenDBO1KBeQ==} + engines: {node: '>=v18'} + + '@commitlint/top-level@19.5.0': + resolution: {integrity: sha512-IP1YLmGAk0yWrImPRRc578I3dDUI5A2UBJx9FbSOjxe9sTlzFiwVJ+zeMLgAtHMtGZsC8LUnzmW1qRemkFU4ng==} + engines: {node: '>=v18'} + + '@commitlint/types@19.5.0': + resolution: {integrity: sha512-DSHae2obMSMkAtTBSOulg5X7/z+rGLxcXQIkg3OmWvY6wifojge5uVMydfhUvs7yQj+V7jNmRZ2Xzl8GJyqRgg==} + engines: {node: '>=v18'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@dabh/diagnostics@2.0.3': + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + + '@emotion/use-insertion-effect-with-fallbacks@1.1.0': + resolution: {integrity: sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==} + peerDependencies: + react: '>=16.8.0' + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.19.0': + resolution: {integrity: sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.9.0': + resolution: {integrity: sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.2.0': + resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.14.0': + resolution: {integrity: sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.15.0': + resolution: {integrity: sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.4': + resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.3': + resolution: {integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.6.8': + resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==} + + '@floating-ui/dom@1.6.12': + resolution: {integrity: sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==} + + '@floating-ui/react-dom@2.1.2': + resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.8': + resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==} + + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.1': + resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} + engines: {node: '>=18.18'} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/console@29.7.0': + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/core@29.7.0': + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/environment@29.7.0': + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect-utils@29.7.0': + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/expect@29.7.0': + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/fake-timers@29.7.0': + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/globals@29.7.0': + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/reporters@29.7.0': + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/source-map@29.6.3': + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-result@29.7.0': + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/test-sequencer@29.7.0': + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/transform@29.7.0': + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jest/types@29.6.3': + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@joshwooding/vite-plugin-react-docgen-typescript@0.3.0': + resolution: {integrity: sha512-2D6y7fNvFmsLmRt6UCOFJPvFoPMJGT0Uh1Wg0RaigUp7kdQPs6yYn8Dmx6GZkOH/NW0yMTwRz/p0SRMMRo50vA==} + peerDependencies: + typescript: '>= 4.3.x' + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + + '@ljharb/through@2.3.13': + resolution: {integrity: sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==} + engines: {node: '>= 0.4'} + + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + + '@mdx-js/react@3.1.0': + resolution: {integrity: sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + '@microsoft/tsdoc@0.15.0': + resolution: {integrity: sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==} + + '@nestjs/cli@10.4.7': + resolution: {integrity: sha512-4wJTtBJsbvjLIzXl+Qj6DYHv4J7abotuXyk7bes5erL79y+KBT61LulL56SqilzmNnHOAVbXcSXOn9S2aWUn6A==} + engines: {node: '>= 16.14'} + hasBin: true + peerDependencies: + '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 + '@swc/core': ^1.3.62 + peerDependenciesMeta: + '@swc/cli': + optional: true + '@swc/core': + optional: true + + '@nestjs/common@10.4.7': + resolution: {integrity: sha512-gIOpjD3Mx8gfYGxYm/RHPcJzqdknNNFCyY+AxzBT3gc5Xvvik1Dn5OxaMGw5EbVfhZgJKVP0n83giUOAlZQe7w==} + peerDependencies: + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/config@3.3.0': + resolution: {integrity: sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + rxjs: ^7.1.0 + + '@nestjs/core@10.4.7': + resolution: {integrity: sha512-AIpQzW/vGGqSLkKvll1R7uaSNv99AxZI2EFyVJPNGDgFsfXaohfV1Ukl6f+s75Km+6Fj/7aNl80EqzNWQCS8Ig==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/microservices': ^10.0.0 + '@nestjs/platform-express': ^10.0.0 + '@nestjs/websockets': ^10.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + + '@nestjs/mapped-types@2.0.5': + resolution: {integrity: sha512-bSJv4pd6EY99NX9CjBIyn4TVDoSit82DUZlL4I3bqNfy5Gt+gXTa86i3I/i0iIV9P4hntcGM5GyO+FhZAhxtyg==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/mapped-types@2.0.6': + resolution: {integrity: sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/platform-express@10.4.7': + resolution: {integrity: sha512-q6XDOxZPTZ9cxALcVuKUlRBk+cVEv6dW2S8p2yVre22kpEQxq53/OI8EseDvzObGb6hepZ8+yBY04qoYqSlXNQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + + '@nestjs/platform-socket.io@10.4.7': + resolution: {integrity: sha512-CpmrqswpD/O4SyF/IUzKj14BUf0eTLyDja9svPCRIJX8AdF47mKCMbz5vtU6vpJtxVnq1e1Xd+xcdZ6FIf6HtQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/websockets': ^10.0.0 + rxjs: ^7.1.0 + + '@nestjs/platform-ws@10.4.7': + resolution: {integrity: sha512-RpseMN7deXX6P3fKqBeJKonyiB4YFRsccROfz/CkwoxvJjufGVtvS1sELqwwCjTqJB97sLAkOBVkTw3p7SR3dA==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/websockets': ^10.0.0 + rxjs: ^7.1.0 + + '@nestjs/schematics@10.2.3': + resolution: {integrity: sha512-4e8gxaCk7DhBxVUly2PjYL4xC2ifDFexCqq1/u4TtivLGXotVk0wHdYuPYe1tHTHuR1lsOkRbfOCpkdTnigLVg==} + peerDependencies: + typescript: '>=4.8.2' + + '@nestjs/swagger@8.0.5': + resolution: {integrity: sha512-ZmBdsbQNs3wIN5kCuvAVbz3/ULh3gi814oHTP49uTqAGi1aT0YSatUyncwQOHBOlRT+rwF+TNjoAsZ+twIk/Jw==} + peerDependencies: + '@fastify/static': ^6.0.0 || ^7.0.0 + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/testing@10.4.7': + resolution: {integrity: sha512-aS3sQ0v4g8cyHDzW3xJv1+8MiFAkxUNXmnau588IFFI/nBIo/kevLNHNPr85keYekkJ/lwNDW72h8UGg8BYd9w==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + '@nestjs/microservices': ^10.0.0 + '@nestjs/platform-express': ^10.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + + '@nestjs/typeorm@10.0.2': + resolution: {integrity: sha512-H738bJyydK4SQkRCTeh1aFBxoO1E9xdL/HaLGThwrqN95os5mEyAtK7BLADOS+vldP4jDZ2VQPLj4epWwRqCeQ==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + rxjs: ^7.2.0 + typeorm: ^0.3.0 + + '@nestjs/websockets@10.4.7': + resolution: {integrity: sha512-ajuoptYLYm+l3+KtaA9Ed+cO9yB34PtBE8UObavRT8Euh/f7QfeJiKcrU3+BQSAiTWM3nF2qfuV4CfEkP9uKuw==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + '@nestjs/platform-socket.io': ^10.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/platform-socket.io': + optional: true + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@npmcli/fs@1.1.1': + resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} + + '@npmcli/move-file@1.1.2': + resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} + engines: {node: '>=10'} + deprecated: This functionality has been moved to @npmcli/fs + + '@nuxtjs/opencollective@0.3.2': + resolution: {integrity: sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.1.1': + resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@radix-ui/number@1.0.1': + resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} + + '@radix-ui/primitive@1.0.1': + resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} + + '@radix-ui/primitive@1.1.0': + resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + + '@radix-ui/react-alert-dialog@1.1.2': + resolution: {integrity: sha512-eGSlLzPhKO+TErxkiGcCZGuvbVMnLA1MTnyBksGOeGRGkxHiiJUujsjmNTdWTm4iHVSRaUao9/4Ur671auMghQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.0.3': + resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.0': + resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-avatar@1.1.1': + resolution: {integrity: sha512-eoOtThOmxeoizxpX6RiEsQZ2wj5r4+zoeqAwO0cBaFQGjJwIH3dIX0OCxNrCyrrdxG+vBweMETh3VziQG7c1kw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.0.3': + resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.0': + resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.0.1': + resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-compose-refs@1.1.0': + resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.0.1': + resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.0': + resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.1': + resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.2': + resolution: {integrity: sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.0.1': + resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-direction@1.1.0': + resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.0.4': + resolution: {integrity: sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.1': + resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.0.1': + resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-guards@1.1.1': + resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.0.3': + resolution: {integrity: sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-scope@1.1.0': + resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-icons@1.3.1': + resolution: {integrity: sha512-QvYompk0X+8Yjlo/Fv4McrzxohDdM5GgLHyQcPpcsPvlOSXCGFjdbuyGL5dzRbg0GpknAjQJJZzdiRK7iWVuFQ==} + peerDependencies: + react: ^16.x || ^17.x || ^18.x || ^19.x + + '@radix-ui/react-id@1.0.1': + resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-id@1.1.0': + resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-popper@1.1.2': + resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.0': + resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.0.3': + resolution: {integrity: sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.2': + resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.1': + resolution: {integrity: sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@1.0.3': + resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.0.0': + resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.0': + resolution: {integrity: sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.0': + resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@1.2.2': + resolution: {integrity: sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.0': + resolution: {integrity: sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.0.2': + resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.1.0': + resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-toggle-group@1.1.0': + resolution: {integrity: sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.0': + resolution: {integrity: sha512-gwoxaKZ0oJ4vIgzsfESBuSgJNdc0rv12VhHgcqN0TEJmmZixXG/2XpsLK8kzNWYcnaoRIEEQc0bEi3dIvdUpjw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.0': + resolution: {integrity: sha512-ZUKknxhMTL/4hPh+4DuaTot9aO7UD6Kupj4gqXCsBTayX1pD1L+0C2/2VZKXb4tIifQklZ3pf2hG9T+ns+FclQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.1.4': + resolution: {integrity: sha512-QpObUH/ZlpaO4YgHSaYzrLO2VuO+ZBFFgGzjMUPwtiYnAzzNNDPJeEGRrT7qNOrWm/Jr08M1vlp+vTHtnSQ0Uw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.0.1': + resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.0': + resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.0.1': + resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.1.0': + resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.0.3': + resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.0': + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.0.1': + resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.0': + resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.0.1': + resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.0.1': + resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.0': + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.0.1': + resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.0.3': + resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-visually-hidden@1.1.0': + resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.0.1': + resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} + + '@radix-ui/rect@1.1.0': + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + + '@remix-run/router@1.21.0': + resolution: {integrity: sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==} + engines: {node: '>=14.0.0'} + + '@rollup/pluginutils@5.1.3': + resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.24.4': + resolution: {integrity: sha512-jfUJrFct/hTA0XDM5p/htWKoNNTbDLY0KRwEt6pyOA6k2fmk0WVwl65PdUdJZgzGEHWx+49LilkcSaumQRyNQw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.24.4': + resolution: {integrity: sha512-j4nrEO6nHU1nZUuCfRKoCcvh7PIywQPUCBa2UsootTHvTHIoIu2BzueInGJhhvQO/2FTRdNYpf63xsgEqH9IhA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.24.4': + resolution: {integrity: sha512-GmU/QgGtBTeraKyldC7cDVVvAJEOr3dFLKneez/n7BvX57UdhOqDsVwzU7UOnYA7AAOt+Xb26lk79PldDHgMIQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.24.4': + resolution: {integrity: sha512-N6oDBiZCBKlwYcsEPXGDE4g9RoxZLK6vT98M8111cW7VsVJFpNEqvJeIPfsCzbf0XEakPslh72X0gnlMi4Ddgg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.24.4': + resolution: {integrity: sha512-py5oNShCCjCyjWXCZNrRGRpjWsF0ic8f4ieBNra5buQz0O/U6mMXCpC1LvrHuhJsNPgRt36tSYMidGzZiJF6mw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.24.4': + resolution: {integrity: sha512-L7VVVW9FCnTTp4i7KrmHeDsDvjB4++KOBENYtNYAiYl96jeBThFfhP6HVxL74v4SiZEVDH/1ILscR5U9S4ms4g==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.24.4': + resolution: {integrity: sha512-10ICosOwYChROdQoQo589N5idQIisxjaFE/PAnX2i0Zr84mY0k9zul1ArH0rnJ/fpgiqfu13TFZR5A5YJLOYZA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.24.4': + resolution: {integrity: sha512-ySAfWs69LYC7QhRDZNKqNhz2UKN8LDfbKSMAEtoEI0jitwfAG2iZwVqGACJT+kfYvvz3/JgsLlcBP+WWoKCLcw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.24.4': + resolution: {integrity: sha512-uHYJ0HNOI6pGEeZ/5mgm5arNVTI0nLlmrbdph+pGXpC9tFHFDQmDMOEqkmUObRfosJqpU8RliYoGz06qSdtcjg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.24.4': + resolution: {integrity: sha512-38yiWLemQf7aLHDgTg85fh3hW9stJ0Muk7+s6tIkSUOMmi4Xbv5pH/5Bofnsb6spIwD5FJiR+jg71f0CH5OzoA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.24.4': + resolution: {integrity: sha512-q73XUPnkwt9ZNF2xRS4fvneSuaHw2BXuV5rI4cw0fWYVIWIBeDZX7c7FWhFQPNTnE24172K30I+dViWRVD9TwA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.24.4': + resolution: {integrity: sha512-Aie/TbmQi6UXokJqDZdmTJuZBCU3QBDA8oTKRGtd4ABi/nHgXICulfg1KI6n9/koDsiDbvHAiQO3YAUNa/7BCw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.24.4': + resolution: {integrity: sha512-P8MPErVO/y8ohWSP9JY7lLQ8+YMHfTI4bAdtCi3pC2hTeqFJco2jYspzOzTUB8hwUWIIu1xwOrJE11nP+0JFAQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.24.4': + resolution: {integrity: sha512-K03TljaaoPK5FOyNMZAAEmhlyO49LaE4qCsr0lYHUKyb6QacTNF9pnfPpXnFlFD3TXuFbFbz7tJ51FujUXkXYA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.24.4': + resolution: {integrity: sha512-VJYl4xSl/wqG2D5xTYncVWW+26ICV4wubwN9Gs5NrqhJtayikwCXzPL8GDsLnaLU3WwhQ8W02IinYSFJfyo34Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.24.4': + resolution: {integrity: sha512-ku2GvtPwQfCqoPFIJCqZ8o7bJcj+Y54cZSr43hHca6jLwAiCbZdBUOrqE6y29QFajNAzzpIOwsckaTFmN6/8TA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.24.4': + resolution: {integrity: sha512-V3nCe+eTt/W6UYNr/wGvO1fLpHUrnlirlypZfKCT1fG6hWfqhPgQV/K/mRBXBpxc0eKLIF18pIOFVPh0mqHjlg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.24.4': + resolution: {integrity: sha512-LTw1Dfd0mBIEqUVCxbvTE/LLo+9ZxVC9k99v1v4ahg9Aak6FpqOfNu5kRkeTAn0wphoC4JU7No1/rL+bBCEwhg==} + cpu: [x64] + os: [win32] + + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + + '@shikijs/core@1.22.2': + resolution: {integrity: sha512-bvIQcd8BEeR1yFvOYv6HDiyta2FFVePbzeowf5pPS1avczrPK+cjmaxxh0nx5QzbON7+Sv0sQfQVciO7bN72sg==} + + '@shikijs/engine-javascript@1.22.2': + resolution: {integrity: sha512-iOvql09ql6m+3d1vtvP8fLCVCK7BQD1pJFmHIECsujB0V32BJ0Ab6hxk1ewVSMFA58FI0pR2Had9BKZdyQrxTw==} + + '@shikijs/engine-oniguruma@1.22.2': + resolution: {integrity: sha512-GIZPAGzQOy56mGvWMoZRPggn0dTlBf1gutV5TdceLCZlFNqWmuc7u+CzD0Gd9vQUTgLbrt0KLzz6FNprqYAxlA==} + + '@shikijs/types@1.22.2': + resolution: {integrity: sha512-NCWDa6LGZqTuzjsGfXOBWfjS/fDIbDdmVDug+7ykVe1IKT4c1gakrvlfFYp5NhAXH/lyqLM8wsAPo5wNy73Feg==} + + '@shikijs/vscode-textmate@9.3.0': + resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==} + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@10.3.0': + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@sqltools/formatter@1.2.5': + resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + + '@storybook/addon-actions@8.4.2': + resolution: {integrity: sha512-+hA200XN5aeA4T3jq8IifQq6Y+9FyNQ0Q+blM1L0Tl7WLzBc7B1kHQnKvhSj5pvMSBWc/Q/kY7Ev5t9gdOu13g==} + peerDependencies: + storybook: ^8.4.2 + + '@storybook/addon-backgrounds@8.4.2': + resolution: {integrity: sha512-s4uag5VKuk8q2MSnuNS7Sv+v1/mykzGPXe/zZRW2ammtkdHp8Uy78eQS2G0aiG02chXCX+qQgWMyy5QItDcTFQ==} + peerDependencies: + storybook: ^8.4.2 + + '@storybook/addon-controls@8.4.2': + resolution: {integrity: sha512-raCbHEj1xl4F3wKH6IdfEXNRaxKpY4QGhjSTE8Pte5iJSVhKG86taLqqRr+4dC7H1/LVMPU1XCGV4mkgDGtyxQ==} + peerDependencies: + storybook: ^8.4.2 + + '@storybook/addon-docs@8.4.2': + resolution: {integrity: sha512-jIpykha7hv2Inlrq31ZoYg2QhuCuvcO+Q+uvhT45RDTB+2US/fg3rJINKlw2Djq8RPPOXvty5W0yvE6CrWKhnQ==} + peerDependencies: + storybook: ^8.4.2 + + '@storybook/addon-essentials@8.4.2': + resolution: {integrity: sha512-+/vfPrXM/GWU3Kbrg92PepwAZr7lOeulTTYF4THK0CL3DfUUlkGNpBPLP5PtjCuIkVrTCjXiIEdVWk47d5m2+w==} + peerDependencies: + storybook: ^8.4.2 + + '@storybook/addon-highlight@8.4.2': + resolution: {integrity: sha512-vTtwp7nyJ09SXrsMnH+pukCjHjRMjQXgHZHxvbrv09uoH8ldQMv9B7u+X+9Wcy/jYSKFz/ng7pWo4b4a2oXHkg==} + peerDependencies: + storybook: ^8.4.2 + + '@storybook/addon-interactions@8.4.2': + resolution: {integrity: sha512-+/NTENTApeOcONgFNQ6Olbk0GH3pTDG3w0eh00slCB+2agD1BcVKg8SSlHQV0lQF1cK3vWL/X3jeaxdFLYOjjg==} + peerDependencies: + storybook: ^8.4.2 + + '@storybook/addon-measure@8.4.2': + resolution: {integrity: sha512-z+j6xQwcUBSpgzl1XDU+xU4YYgLraLMljECW7NvRNyJ/PYixvol8R3wtzWbr+CBpxmvbXjEJCPlF+EjF9/mBWQ==} + peerDependencies: + storybook: ^8.4.2 + + '@storybook/addon-onboarding@8.4.2': + resolution: {integrity: sha512-zWzOyRASnIPt2AcaEl1KhI+aOaKDuoIcNB7u1GoABj0YM+V9d6o3lvcsmOAQG5pgwgFyqyOnLwpTfvRSEyzGFA==} + peerDependencies: + storybook: ^8.4.2 + + '@storybook/addon-outline@8.4.2': + resolution: {integrity: sha512-oTMlPEyT4CBqzcQbfemoJzJ6yzeRAmvrAx9ssaBcnQQRsKxo0D2Ri/Jmm6SNcR0yBHxYRkvIH+2phLw8aiflCQ==} + peerDependencies: + storybook: ^8.4.2 + + '@storybook/addon-styling@2.0.0': + resolution: {integrity: sha512-CrWMREpDKQf2CjtBRHTwQ3mH87fgjSXiWUS7kDN3ff4XGW9Sm5hFi/tGWnyjYv+J1RzyFQY4YpXAY0iR1QDW6Q==} + deprecated: 'This package has been split into @storybook/addon-styling-webpack and @storybook/addon-themes. More info: https://github.com/storybookjs/addon-styling' + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@storybook/addon-toolbars@8.4.2': + resolution: {integrity: sha512-DidzW/NQS224niMJIjcJI2ls83emqygUcS9GYNGgdc5Xwro/TPgGYOXP2qnXgYUxXQTHbrxmIbHdEehxC7CcYQ==} + peerDependencies: + storybook: ^8.4.2 + + '@storybook/addon-viewport@8.4.2': + resolution: {integrity: sha512-qVQ2UaxCNsUSFHnAAAizNPIJ/QwfMg7p5bBdpYROTZXJe+bxVp0rFzZmQgHZ3/sn+lzE4ItM4QEfxkfQUWi1ag==} + peerDependencies: + storybook: ^8.4.2 + + '@storybook/api@7.6.17': + resolution: {integrity: sha512-l92PI+5XL4zB/o4IBWFCKQWTXvPg9hR45DCJqlPHrLZStiR6Xj1mbrtOjUlgIOH+nYb/SZFZqO53hhrs7X4Nvg==} + + '@storybook/blocks@8.4.2': + resolution: {integrity: sha512-yAAvmOWaD8gIrepOxCh/RxQqd/1xZIwd/V+gsvAhW/thawN+SpI+zK63gmcqAPLX84hJ3Dh5pegRk0SoHNuDVA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^8.4.2 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@storybook/builder-vite@8.4.2': + resolution: {integrity: sha512-dO5FB5yH1C6tr/kBHn1frvGwp8Pt0D1apgXWkJ5ITWEUfh6WwOqX2fqsWsqaNwE7gP0qn0XgwCIEkI/4Mj55SA==} + peerDependencies: + storybook: ^8.4.2 + vite: ^4.0.0 || ^5.0.0 + + '@storybook/channels@7.6.17': + resolution: {integrity: sha512-GFG40pzaSxk1hUr/J/TMqW5AFDDPUSu+HkeE/oqSWJbOodBOLJzHN6CReJS6y1DjYSZLNFt1jftPWZZInG/XUA==} + + '@storybook/channels@7.6.20': + resolution: {integrity: sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==} + + '@storybook/client-logger@7.6.17': + resolution: {integrity: sha512-6WBYqixAXNAXlSaBWwgljWpAu10tPRBJrcFvx2gPUne58EeMM20Gi/iHYBz2kMCY+JLAgeIH7ZxInqwO8vDwiQ==} + + '@storybook/client-logger@7.6.20': + resolution: {integrity: sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==} + + '@storybook/components@7.6.20': + resolution: {integrity: sha512-0d8u4m558R+W5V+rseF/+e9JnMciADLXTpsILrG+TBhwECk0MctIWW18bkqkujdCm8kDZr5U2iM/5kS1Noy7Ug==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@storybook/components@8.4.2': + resolution: {integrity: sha512-+W59oF7D73LAxLNmCfFrfs98cH9pyNHK9HlJoO5/lKbK4IdWhhOoqUR/AJ3ueksoLuetFat4DxyE8SN1H4Bvrg==} + peerDependencies: + storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + + '@storybook/core-events@7.6.17': + resolution: {integrity: sha512-AriWMCm/k1cxlv10f+jZ1wavThTRpLaN3kY019kHWbYT9XgaSuLU67G7GPr3cGnJ6HuA6uhbzu8qtqVCd6OfXA==} + + '@storybook/core-events@7.6.20': + resolution: {integrity: sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==} + + '@storybook/core@8.4.2': + resolution: {integrity: sha512-hF8GWoUZTjwwuV5j4OLhMHZtZQL/NYcVUBReC2Ba06c8PkFIKqKZwATr1zKd301gQ5Qwcn9WgmZxJTMgdKQtOg==} + peerDependencies: + prettier: ^2 || ^3 + peerDependenciesMeta: + prettier: + optional: true + + '@storybook/csf-plugin@8.4.2': + resolution: {integrity: sha512-1f0t6W5xbC1sSAHHs3uXYPIQs2NXAEtIGqn6X9i3xbbub6hDS8PF8BIm7dOjQ8dZOPp7d9ltR64V5CoLlsOigA==} + peerDependencies: + storybook: ^8.4.2 + + '@storybook/csf@0.1.11': + resolution: {integrity: sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg==} + + '@storybook/global@5.0.0': + resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} + + '@storybook/icons@1.2.12': + resolution: {integrity: sha512-UxgyK5W3/UV4VrI3dl6ajGfHM4aOqMAkFLWe2KibeQudLf6NJpDrDMSHwZj+3iKC4jFU7dkKbbtH2h/al4sW3Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@storybook/instrumenter@8.4.2': + resolution: {integrity: sha512-gPYCZ/0O6gRLI3zmenu2N6QtKzxDZFdT2xf4RWcNUSZyp28RZkRCIgKFMt3fTmvE0yMzAjQyRSkBdrONjQ44HA==} + peerDependencies: + storybook: ^8.4.2 + + '@storybook/manager-api@7.6.17': + resolution: {integrity: sha512-IJIV1Yc6yw1dhCY4tReHCfBnUKDqEBnMyHp3mbXpsaHxnxJZrXO45WjRAZIKlQKhl/Ge1CrnznmHRCmYgqmrWg==} + + '@storybook/manager-api@7.6.20': + resolution: {integrity: sha512-gOB3m8hO3gBs9cBoN57T7jU0wNKDh+hi06gLcyd2awARQlAlywnLnr3s1WH5knih6Aq+OpvGBRVKkGLOkaouCQ==} + + '@storybook/manager-api@8.4.2': + resolution: {integrity: sha512-rhPc4cgQDKDH8NUyRh/ZaJW7QIhR/PO5MNX4xc+vz71sM2nO7ONA/FrgLtCuu4SULdwilEPvGefYvLK0dE+Caw==} + peerDependencies: + storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + + '@storybook/preview-api@7.6.20': + resolution: {integrity: sha512-3ic2m9LDZEPwZk02wIhNc3n3rNvbi7VDKn52hDXfAxnL5EYm7yDICAkaWcVaTfblru2zn0EDJt7ROpthscTW5w==} + + '@storybook/preview-api@8.4.2': + resolution: {integrity: sha512-5X/xvIvDPaWJKUBCo5zVeBbbjkhnwcI2KPkuOgrHVRRhuQ5WqD0RYxVtOOFNyQXme7g0nNl5RFNgvT7qv9qGeg==} + peerDependencies: + storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + + '@storybook/react-dom-shim@8.4.2': + resolution: {integrity: sha512-FZVTM1f34FpGnf6e3MDIKkz05gmn8H9wEccvQAgr8pEFe8VWfrpVWeUrmatSAfgrCMNXYC1avDend8UX6IM8Fg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^8.4.2 + + '@storybook/react-vite@8.4.2': + resolution: {integrity: sha512-OoXaW/V1AqLggMyniRcnuwmqQ1/OtSn38t31lePX4nDDeJhbGT3ZPldRrwvsLb0EaD3N27uoL+QbAOgsYJIhwA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^8.4.2 + vite: ^4.0.0 || ^5.0.0 + + '@storybook/react@8.4.2': + resolution: {integrity: sha512-rO5/aVKBVhIKENcL7G8ud4QKC5OyWBPCkJIvY6XUHIuhErJy9/4pP+sZ85jypVwx5kq+EqCPF8AEOWjIxB/4/Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + '@storybook/test': 8.4.2 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta + storybook: ^8.4.2 + typescript: '>= 4.2.x' + peerDependenciesMeta: + '@storybook/test': + optional: true + typescript: + optional: true + + '@storybook/router@7.6.17': + resolution: {integrity: sha512-GnyC0j6Wi5hT4qRhSyT8NPtJfGmf82uZw97LQRWeyYu5gWEshUdM7aj40XlNiScd5cZDp0owO1idduVF2k2l2A==} + + '@storybook/router@7.6.20': + resolution: {integrity: sha512-mCzsWe6GrH47Xb1++foL98Zdek7uM5GhaSlrI7blWVohGa0qIUYbfJngqR4ZsrXmJeeEvqowobh+jlxg3IJh+w==} + + '@storybook/test@8.4.2': + resolution: {integrity: sha512-MipTdboStv0hsqF2Sw8TZgP0YnxCcDYwxkTOd4hmRzev/7Brtvpi4pqjqh8k98ZCvhrCPAPVIoX5drk+oi3YUA==} + peerDependencies: + storybook: ^8.4.2 + + '@storybook/theming@7.6.17': + resolution: {integrity: sha512-ZbaBt3KAbmBtfjNqgMY7wPMBshhSJlhodyMNQypv+95xLD/R+Az6aBYbpVAOygLaUQaQk4ar7H/Ww6lFIoiFbA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@storybook/theming@7.6.20': + resolution: {integrity: sha512-iT1pXHkSkd35JsCte6Qbanmprx5flkqtSHC6Gi6Umqoxlg9IjiLPmpHbaIXzoC06DSW93hPj5Zbi1lPlTvRC7Q==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + '@storybook/theming@8.4.2': + resolution: {integrity: sha512-9j4fnu5LcV+qSs1rdwf61Bt14lms0T1LOZkHxGNcS1c1oH+cPS+sxECh2lxtni+mvOAHUlBs9pKhVZzRPdWpvg==} + peerDependencies: + storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 + + '@storybook/types@7.6.17': + resolution: {integrity: sha512-GRY0xEJQ0PrL7DY2qCNUdIfUOE0Gsue6N+GBJw9ku1IUDFLJRDOF+4Dx2BvYcVCPI5XPqdWKlEyZdMdKjiQN7Q==} + + '@storybook/types@7.6.20': + resolution: {integrity: sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==} + + '@testing-library/dom@10.4.0': + resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.5.0': + resolution: {integrity: sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/jest-dom@6.6.3': + resolution: {integrity: sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react-hooks@8.0.1': + resolution: {integrity: sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g==} + engines: {node: '>=12'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 + react: ^16.9.0 || ^17.0.0 + react-dom: ^16.9.0 || ^17.0.0 + react-test-renderer: ^16.9.0 || ^17.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-dom: + optional: true + react-test-renderer: + optional: true + + '@testing-library/react@16.0.1': + resolution: {integrity: sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 + '@types/react-dom': ^18.0.0 + react: ^18.0.0 + react-dom: ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/user-event@14.5.2': + resolution: {integrity: sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + + '@tootallnate/once@1.1.2': + resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} + engines: {node: '>= 6'} + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + + '@types/body-parser@1.19.5': + resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + + '@types/cls-hooked@4.3.9': + resolution: {integrity: sha512-CMtHMz6Q/dkfcHarq9nioXH8BDPP+v5xvd+N90lBQ2bdmu06UvnLDqxTKoOJzz4SzIwb/x9i4UXGAAcnUDuIvg==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/conventional-commits-parser@5.0.0': + resolution: {integrity: sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==} + + '@types/cookie-parser@1.4.7': + resolution: {integrity: sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==} + + '@types/cookie@0.4.1': + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + + '@types/cors@2.8.17': + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + + '@types/doctrine@0.0.9': + resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/express-serve-static-core@4.19.6': + resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + + '@types/express-serve-static-core@5.0.1': + resolution: {integrity: sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==} + + '@types/express-session@1.18.0': + resolution: {integrity: sha512-27JdDRgor6PoYlURY+Y5kCakqp5ulC0kmf7y+QwaY+hv9jEFuQOThgkjyA53RP3jmKuBsH5GR6qEfFmvb8mwOA==} + + '@types/express@4.17.21': + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + + '@types/express@5.0.0': + resolution: {integrity: sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==} + + '@types/glob@7.2.0': + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + + '@types/graceful-fs@4.1.9': + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/history@4.7.11': + resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} + + '@types/http-errors@2.0.4': + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@29.5.14': + resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/minimatch@5.1.2': + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + + '@types/node@20.17.6': + resolution: {integrity: sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==} + + '@types/node@22.9.0': + resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==} + + '@types/prop-types@15.7.13': + resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} + + '@types/qs@6.9.17': + resolution: {integrity: sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-dom@18.3.1': + resolution: {integrity: sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==} + + '@types/react-router-dom@5.3.3': + resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} + + '@types/react-router@5.1.20': + resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} + + '@types/react@18.3.12': + resolution: {integrity: sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==} + + '@types/resolve@1.20.6': + resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} + + '@types/send@0.17.4': + resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} + + '@types/serve-static@1.15.7': + resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/superagent@8.1.9': + resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + + '@types/supertest@6.0.2': + resolution: {integrity: sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/uuid@9.0.8': + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + + '@types/validator@13.12.2': + resolution: {integrity: sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==} + + '@types/ws@8.5.13': + resolution: {integrity: sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.33': + resolution: {integrity: sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==} + + '@typescript-eslint/eslint-plugin@8.13.0': + resolution: {integrity: sha512-nQtBLiZYMUPkclSeC3id+x4uVd1SGtHuElTxL++SfP47jR0zfkZBJHc+gL4qPsgTuypz0k8Y2GheaDYn6Gy3rg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@8.13.0': + resolution: {integrity: sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@8.13.0': + resolution: {integrity: sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.13.0': + resolution: {integrity: sha512-Rqnn6xXTR316fP4D2pohZenJnp+NwQ1mo7/JM+J1LWZENSLkJI8ID8QNtlvFeb0HnFSK94D6q0cnMX6SbE5/vA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@8.13.0': + resolution: {integrity: sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.13.0': + resolution: {integrity: sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@8.13.0': + resolution: {integrity: sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + '@typescript-eslint/visitor-keys@8.13.0': + resolution: {integrity: sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + '@vitejs/plugin-react@4.3.3': + resolution: {integrity: sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 + + '@vitest/expect@2.0.5': + resolution: {integrity: sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==} + + '@vitest/expect@2.1.5': + resolution: {integrity: sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q==} + + '@vitest/mocker@2.1.5': + resolution: {integrity: sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.0.5': + resolution: {integrity: sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==} + + '@vitest/pretty-format@2.1.4': + resolution: {integrity: sha512-L95zIAkEuTDbUX1IsjRl+vyBSLh3PwLLgKpghl37aCK9Jvw0iP+wKwIFhfjdUtA2myLgjrG6VU6JCFLv8q/3Ww==} + + '@vitest/pretty-format@2.1.5': + resolution: {integrity: sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==} + + '@vitest/runner@2.1.5': + resolution: {integrity: sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g==} + + '@vitest/snapshot@2.1.5': + resolution: {integrity: sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg==} + + '@vitest/spy@2.0.5': + resolution: {integrity: sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==} + + '@vitest/spy@2.1.5': + resolution: {integrity: sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==} + + '@vitest/utils@2.0.5': + resolution: {integrity: sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==} + + '@vitest/utils@2.1.4': + resolution: {integrity: sha512-MXDnZn0Awl2S86PSNIim5PWXgIAx8CIkzu35mBdSApUip6RFOGXBCf3YFyeEu8n1IHk4bWD46DeYFu9mQlFIRg==} + + '@vitest/utils@2.1.5': + resolution: {integrity: sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==} + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + adjust-sourcemap-loader@4.0.0: + resolution: {integrity: sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==} + engines: {node: '>=8.9'} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.1: + resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} + engines: {node: '>= 14'} + + agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + app-root-path@3.1.0: + resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} + engines: {node: '>= 6.0.0'} + + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + + aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + + are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.4: + resolution: {integrity: sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + + async-hook-jl@1.7.6: + resolution: {integrity: sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==} + engines: {node: ^4.7 || >=6.9 || >=7.3} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + autoprefixer@10.4.20: + resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + + babel-jest@29.7.0: + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + + babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + + babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + babel-preset-current-node-syntax@1.1.0: + resolution: {integrity: sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-preset-jest@29.6.3: + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + + backend@file:apps/backend: + resolution: {directory: apps/backend, type: directory} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + + better-opn@3.0.2: + resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} + engines: {node: '>=12.0.0'} + + big.js@5.2.2: + resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browser-assert@1.2.1: + resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==} + + browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cacache@15.3.0: + resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} + engines: {node: '>= 10'} + + call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001678: + resolution: {integrity: sha512-RR+4U/05gNtps58PEBDZcPWTgEO2MBeoPZ96aQcjmfkBWRIDfN451fW2qyDA9/+HohLLIL5GqiMwA+IB1pWarw==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai@5.1.2: + resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} + engines: {node: '>=12'} + + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chromatic@11.18.0: + resolution: {integrity: sha512-3o9Frn1oIS1hFLsJxVH9yVJ1O7+TCYoyL7OZzUorL/DCYduhXr5LDSBfpUsp7EdCPb64ufkbyFzSRNbt/xy9kg==} + hasBin: true + peerDependencies: + '@chromatic-com/cypress': ^0.*.* || ^1.0.0 + '@chromatic-com/playwright': ^0.*.* || ^1.0.0 + peerDependenciesMeta: + '@chromatic-com/cypress': + optional: true + '@chromatic-com/playwright': + optional: true + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + cjs-module-lexer@1.4.1: + resolution: {integrity: sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==} + + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + class-validator@0.14.1: + resolution: {integrity: sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==} + + class-variance-authority@0.7.0: + resolution: {integrity: sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + cls-hooked@4.2.2: + resolution: {integrity: sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==} + engines: {node: ^4.7 || >=6.9 || >=7.3 || >=8.2.1} + + clsx@2.0.0: + resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} + engines: {node: '>=6'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + + colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + comment-json@4.2.5: + resolution: {integrity: sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==} + engines: {node: '>= 6'} + + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + + consola@2.15.3: + resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + conventional-changelog-angular@7.0.0: + resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} + engines: {node: '>=16'} + + conventional-changelog-conventionalcommits@7.0.2: + resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} + engines: {node: '>=16'} + + conventional-commits-parser@5.0.0: + resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} + engines: {node: '>=16'} + hasBin: true + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookie@1.0.1: + resolution: {integrity: sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==} + engines: {node: '>=18'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cosmiconfig-typescript-loader@5.1.0: + resolution: {integrity: sha512-7PtBB+6FdsOvZyJtlF3hEPpACq7RQX6BVGsgC7/lfVXnKMvNCu/XY3ykreqG5w/rBNdu2z8LCIKoF3kpHHdHlA==} + engines: {node: '>=v16'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=8.2' + typescript: '>=4' + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + create-jest@29.7.0: + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-loader@6.11.0: + resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} + engines: {node: '>= 12.13.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.0.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssstyle@4.1.0: + resolution: {integrity: sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==} + engines: {node: '>=18'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + dargs@8.1.0: + resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==} + engines: {node: '>=12'} + + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + dedent@1.5.3: + resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + dotenv-expand@10.0.0: + resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} + engines: {node: '>=12'} + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-to-chromium@1.5.52: + resolution: {integrity: sha512-xtoijJTZ+qeucLBDNztDOuQBE1ksqjvNjvqFoST3nGC7fSpqJ+X6BdTBaY5BHG+IhWWmpc6b/KfpeuEDupEPOQ==} + + emitter-listener@1.1.2: + resolution: {integrity: sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + emojis-list@3.0.0: + resolution: {integrity: sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==} + engines: {node: '>= 4'} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + engine.io-client@6.6.2: + resolution: {integrity: sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.2: + resolution: {integrity: sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==} + engines: {node: '>=10.2.0'} + + enhanced-resolve@5.17.1: + resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@1.5.4: + resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@9.1.0: + resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.2.1: + resolution: {integrity: sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-react-hooks@5.0.0: + resolution: {integrity: sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.14: + resolution: {integrity: sha512-aXvzCTK7ZBv1e7fahFuR3Z/fyQQSIQ711yPgYRj+Oj64tyTgO4iQIDmYXDBqvSWQ/FA4OSCsXOStlF+noU0/NA==} + peerDependencies: + eslint: '>=7' + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@8.2.0: + resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.15.0: + resolution: {integrity: sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + expect-type@1.1.0: + resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} + engines: {node: '>=12.0.0'} + + expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + express-session@1.18.1: + resolution: {integrity: sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==} + engines: {node: '>= 0.8.0'} + + express@4.21.1: + resolution: {integrity: sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==} + engines: {node: '>= 0.10.0'} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-uri@3.0.3: + resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-system-cache@2.3.0: + resolution: {integrity: sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + filesize@10.1.6: + resolution: {integrity: sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==} + engines: {node: '>= 10.4.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + find-up@7.0.0: + resolution: {integrity: sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==} + engines: {node: '>=18'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + fork-ts-checker-webpack-plugin@9.0.2: + resolution: {integrity: sha512-Uochze2R8peoN1XqlSi/rGUkDQpRogtLFocP9+PGu68zk1BDAKXfdeCdyVZpgTk8V8WFVQXdEz426VKjXLO1Gg==} + engines: {node: '>=12.13.0', yarn: '>=1.0.0'} + peerDependencies: + typescript: '>3.6.0' + webpack: ^5.11.0 + + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + + formidable@3.5.2: + resolution: {integrity: sha512-Jqc1btCy3QzRbJaICGwKcBfGWuLADRerLzDqi2NwSt/UkXLsHJw2TVResiaoBufHVHy9aSgClOHCeJsSsFLTbg==} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.1.1: + resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} + engines: {node: '>=14.14'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs-monkey@1.0.6: + resolution: {integrity: sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-port@7.1.0: + resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==} + engines: {node: '>=16'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + git-raw-commits@4.0.0: + resolution: {integrity: sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==} + engines: {node: '>=16'} + hasBin: true + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-promise@4.2.2: + resolution: {integrity: sha512-xcUzJ8NWN5bktoTIX7eOclO1Npxd/dyVqUJxlLIDasT4C7KZyqlPIwkdJ0Ypiy3p2ZKahTjK4M9uC3sNSfNMzw==} + engines: {node: '>=12'} + peerDependencies: + glob: ^7.1.6 + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@10.4.2: + resolution: {integrity: sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==} + engines: {node: '>=16 || 14 >=14.18'} + hasBin: true + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.12.0: + resolution: {integrity: sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==} + engines: {node: '>=18'} + + gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-own-prop@2.0.0: + resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + + has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-util-to-html@9.0.3: + resolution: {integrity: sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hexoid@2.0.0: + resolution: {integrity: sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw==} + engines: {node: '>=8'} + + highlight.js@10.7.3: + resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@4.0.1: + resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} + engines: {node: '>= 6'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.5: + resolution: {integrity: sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==} + engines: {node: '>= 14'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + husky@9.1.6: + resolution: {integrity: sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==} + engines: {node: '>=18'} + hasBin: true + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + icss-utils@5.1.0: + resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + inquirer@8.2.6: + resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} + engines: {node: '>=12.0.0'} + + inquirer@9.2.15: + resolution: {integrity: sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==} + engines: {node: '>=18'} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + ip-address@9.0.5: + resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-generator-function@1.0.10: + resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-text-path@2.0.0: + resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} + engines: {node: '>=8'} + + is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jake@10.9.2: + resolution: {integrity: sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==} + engines: {node: '>=10'} + hasBin: true + + jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-cli@29.7.0: + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@29.7.0: + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + + jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + jest@29.7.0: + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jiti@1.21.6: + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsbn@1.1.0: + resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} + + jsdoc-type-pratt-parser@4.1.0: + resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} + engines: {node: '>=12.0.0'} + + jsdom@25.0.1: + resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.2.1: + resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + libphonenumber-js@1.11.14: + resolution: {integrity: sha512-sexvAfwcW1Lqws4zFp8heAtAEXbEDnvkYCEGzvOoMgZR7JhXo/IkE9MkkGACgBed5fWqh3ShBGnJBdDnU9N8EQ==} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lilconfig@3.1.2: + resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + loader-runner@4.3.0: + resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} + engines: {node: '>=6.11.5'} + + loader-utils@2.0.4: + resolution: {integrity: sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==} + engines: {node: '>=8.9.0'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + locate-path@7.2.0: + resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + long@5.2.3: + resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.1.2: + resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lru.min@1.1.1: + resolution: {integrity: sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + + lucide-react@0.454.0: + resolution: {integrity: sha512-hw7zMDwykCLnEzgncEEjHeA6+45aeEzRYuKHuyRSOPkhko+J3ySGjGIzu+mmMfDFG1vazHepMaYFYHbTFAZAAQ==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc + + lunr@2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.27.0: + resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} + engines: {node: '>=12'} + + magic-string@0.30.12: + resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} + + magic-string@0.30.8: + resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} + engines: {node: '>=12'} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + make-fetch-happen@9.1.0: + resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} + engines: {node: '>= 10'} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + map-or-similar@1.5.0: + resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} + + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + + memoizerific@1.11.3: + resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==} + + meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromark-util-character@2.1.0: + resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==} + + micromark-util-encode@2.0.0: + resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} + + micromark-util-sanitize-uri@2.0.0: + resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} + + micromark-util-symbol@2.0.0: + resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} + + micromark-util-types@2.0.0: + resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + + minipass-fetch@1.4.1: + resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==} + engines: {node: '>=8'} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mkdirp@2.1.6: + resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} + engines: {node: '>=10'} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multer@1.4.4-lts.1: + resolution: {integrity: sha512-WeSGziVj6+Z2/MwQo3GvqzgR+9Uc+qt8SwHKh3gvNPiISKfsMfG4SvCOFYlxxgkXt7yIV2i1yczehm0EOKIxIg==} + engines: {node: '>= 6.0.0'} + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + mysql2@3.11.4: + resolution: {integrity: sha512-Z2o3tY4Z8EvSRDwknaC40MdZ3+m0sKbpnXrShQLdxPrAvcNli7jLrD2Zd2IzsRMw4eK9Yle500FDmlkIqp+krg==} + engines: {node: '>= 8.0'} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + named-placeholders@1.1.3: + resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} + engines: {node: '>=12.0.0'} + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + nest-winston@1.9.7: + resolution: {integrity: sha512-pTTgImRgv7urojsDvaTlenAjyJNLj7ywamfjzrhWKhLhp80AKLYNwf103dVHeqZWe+nzp/vd9DGRs/UN/YadOQ==} + peerDependencies: + '@nestjs/common': ^5.0.0 || ^6.6.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 + winston: ^3.0.0 + + node-abi@3.71.0: + resolution: {integrity: sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==} + engines: {node: '>=10'} + + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-emoji@1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp@8.4.1: + resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==} + engines: {node: '>= 10.12.0'} + hasBin: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. + + nwsapi@2.2.13: + resolution: {integrity: sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.2: + resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + oniguruma-to-js@0.4.3: + resolution: {integrity: sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-locate@6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-exists@5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@0.1.10: + resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} + + path-to-regexp@3.3.0: + resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.0: + resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.1: + resolution: {integrity: sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==} + engines: {node: '>=12'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + polished@4.3.1: + resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} + engines: {node: '>=10'} + + possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.0.1: + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-loader@7.3.4: + resolution: {integrity: sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==} + engines: {node: '>= 14.15.0'} + peerDependencies: + postcss: ^7.0.0 || ^8.0.1 + webpack: ^5.0.0 + + postcss-modules-extract-imports@3.1.0: + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-local-by-default@4.0.5: + resolution: {integrity: sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-scope@3.2.0: + resolution: {integrity: sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-modules-values@4.0.0: + resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.47: + resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} + engines: {node: ^10 || ^12 || >=14} + + prebuild-install@7.1.2: + resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} + engines: {node: '>=10'} + hasBin: true + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + ramda@0.29.0: + resolution: {integrity: sha512-BBea6L67bYLtdbOqfp8f58fPMqEwx0doL+pAi8TZyp2YWz8R9G8z9x75CZI8W+ftqhFHCpEX2cRnUUXK130iKA==} + + random-bytes@1.0.0: + resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} + engines: {node: '>= 0.8'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-confetti@6.1.0: + resolution: {integrity: sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==} + engines: {node: '>=10.18'} + peerDependencies: + react: ^16.3.0 || ^17.0.1 || ^18.0.0 + + react-docgen-typescript@2.2.2: + resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} + peerDependencies: + typescript: '>= 4.3.x' + + react-docgen@7.1.0: + resolution: {integrity: sha512-APPU8HB2uZnpl6Vt/+0AFoVYgSRtfiP6FLrZgPPTDmqSb2R4qZRbgd0A3VzIFxDt5e+Fozjx79WjLWnF69DK8g==} + engines: {node: '>=16.14.0'} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-error-boundary@3.1.4: + resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==} + engines: {node: '>=10', npm: '>=6'} + peerDependencies: + react: '>=16.13.1' + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.6: + resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.5.5: + resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.6.0: + resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-router-dom@6.28.0: + resolution: {integrity: sha512-kQ7Unsl5YdyOltsPGl31zOjLrDv+m2VcIEcIHqYYD3Lp0UppLjrzcfJqDJwXxFw3TH/yvapbnUvPlAj7Kx5nbg==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.28.0: + resolution: {integrity: sha512-HrYdIFqdrnhDw0PqG/AKjAqEqM7AvxCz0DQ4h2W8k6nqmc5uRBYDag0SBxx9iYz5G8gnuNVLzUe13wl9eAsXXg==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + + react-style-singleton@2.2.1: + resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + recast@0.23.9: + resolution: {integrity: sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==} + engines: {node: '>= 4'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + regex-parser@2.3.0: + resolution: {integrity: sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==} + + regex@4.4.0: + resolution: {integrity: sha512-uCUSuobNVeqUupowbdZub6ggI5/JZkYyJdDogddJr60L764oxC2pMZov1fQ3wM9bdyzUILDG+Sqx6NAKAz9rKQ==} + + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-url-loader@5.0.0: + resolution: {integrity: sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==} + engines: {node: '>=12'} + + resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.24.4: + resolution: {integrity: sha512-vGorVWIsWfX3xbcyAS+I047kFKapHYivmkaT63Smj77XwvLSJos6M1xGqZnBPFQFBRZDOcG1QnYEIxAvTr/HjA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + run-async@3.0.0: + resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sass-loader@13.3.3: + resolution: {integrity: sha512-mt5YN2F1MOZr3d/wBRcZxeFgwgkH44wVc2zohO2YF6JiOMkiXe4BYRZpSu2sO1g71mo/j16txzUhsKZlqjVGzA==} + engines: {node: '>= 14.15.0'} + peerDependencies: + fibers: '>= 3.1.0' + node-sass: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + sass: ^1.3.0 + sass-embedded: '*' + webpack: ^5.0.0 + peerDependenciesMeta: + fibers: + optional: true + node-sass: + optional: true + sass: + optional: true + sass-embedded: + optional: true + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sha.js@2.4.11: + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shiki@1.22.2: + resolution: {integrity: sha512-3IZau0NdGKXhH2bBlUk4w1IHNxPh6A5B2sUpyY+8utLu2j/h1QpFkAaUA1bAMxOWWGtTWcAh531vnS4NJKS/lA==} + + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + + side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socket.io-adapter@2.5.5: + resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + + socket.io-client@4.8.1: + resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.0: + resolution: {integrity: sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==} + engines: {node: '>=10.2.0'} + + socks-proxy-agent@6.2.1: + resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} + engines: {node: '>= 10'} + + socks@2.8.3: + resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + sqlite3@5.1.7: + resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==} + + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + + ssri@8.0.1: + resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} + engines: {node: '>= 8'} + + stack-chain@1.3.7: + resolution: {integrity: sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + + store2@2.14.3: + resolution: {integrity: sha512-4QcZ+yx7nzEFiV4BMLnr/pRa5HYzNITX2ri0Zh6sT9EyQHbBHacC6YigllUPU9X3D0f/22QCgfokpKs52YRrUg==} + + storybook@8.4.2: + resolution: {integrity: sha512-GMCgyAulmLNrkUtDkCpFO4SB77YrpiIxq6e5tzaQdXEuaDu1mdNwOuP3VG7nE2FzxmqDvagSgriM68YW9iFaZA==} + hasBin: true + peerDependencies: + prettier: ^2 || ^3 + peerDependenciesMeta: + prettier: + optional: true + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-indent@4.0.0: + resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} + engines: {node: '>=12'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + style-loader@3.3.4: + resolution: {integrity: sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==} + engines: {node: '>= 12.13.0'} + peerDependencies: + webpack: ^5.0.0 + + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + superagent@9.0.2: + resolution: {integrity: sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==} + engines: {node: '>=14.18.0'} + + supertest@7.0.0: + resolution: {integrity: sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA==} + engines: {node: '>=14.18.0'} + + superwstest@2.0.4: + resolution: {integrity: sha512-7u9H76yvLMdjwdrD0BFdc2JN6m2dcEQ8h7+nERrIFGADDw0HBA+clG1Yx/aQ0B/RqKzrHNkVVkGzvVBeknoCeg==} + peerDependencies: + supertest: '*' + peerDependenciesMeta: + supertest: + optional: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + swagger-ui-dist@5.18.2: + resolution: {integrity: sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==} + + symbol-observable@4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + synchronous-promise@2.0.17: + resolution: {integrity: sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==} + + synckit@0.9.2: + resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} + engines: {node: ^14.18.0 || >=16.0.0} + + tailwind-merge@2.5.4: + resolution: {integrity: sha512-0q8cfZHMu9nuYP/b5Shb7Y7Sh1B7Nnl5GqNr1U+n2p6+mybvRtayrQ+0042Z5byvTA8ihjlP8Odo8/VnHbZu4Q==} + + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + + tailwindcss@3.4.14: + resolution: {integrity: sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==} + engines: {node: '>=14.0.0'} + hasBin: true + + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} + + tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} + + telejson@7.2.0: + resolution: {integrity: sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==} + + terser-webpack-plugin@5.3.10: + resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + + terser@5.36.0: + resolution: {integrity: sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==} + engines: {node: '>=10'} + hasBin: true + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + text-extensions@2.4.0: + resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} + engines: {node: '>=8'} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.1: + resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + + tinypool@1.0.2: + resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.61: + resolution: {integrity: sha512-In7VffkDWUPgwa+c9picLUxvb0RltVwTkSgMNFgvlGSWveCzGBemBqTsgJCL4EDFWZ6WH0fKTsot6yNhzy3ZzQ==} + + tldts@6.1.61: + resolution: {integrity: sha512-rv8LUyez4Ygkopqn+M6OLItAOT9FF3REpPQDkdMx5ix8w4qkuE7Vo2o/vw1nxKQYmJDV8JpAMJQr1b+lTKf0FA==} + hasBin: true + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tough-cookie@5.0.0: + resolution: {integrity: sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==} + engines: {node: '>=16'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@5.0.0: + resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} + engines: {node: '>=18'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + ts-api-utils@1.4.0: + resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-jest@29.2.5: + resolution: {integrity: sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + + ts-loader@9.5.1: + resolution: {integrity: sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==} + engines: {node: '>=12.0.0'} + peerDependencies: + typescript: '*' + webpack: ^5.0.0 + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tsconfig-paths-webpack-plugin@4.1.0: + resolution: {integrity: sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==} + engines: {node: '>=10.13.0'} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tween-functions@1.2.0: + resolution: {integrity: sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typedoc@0.26.11: + resolution: {integrity: sha512-sFEgRRtrcDl2FxVP58Ze++ZK2UQAEvtvvH8rRlig1Ja3o7dDaMHmaBfvJmdGnNEFaLTpQsN8dpvZaTqJSu/Ugw==} + engines: {node: '>= 18'} + hasBin: true + peerDependencies: + typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x + + typeorm-transactional@0.5.0: + resolution: {integrity: sha512-53/CwnXpOIJnWU3oVCNbhHB95FwciKSGbY+m/Hw4e2dBM2c4toiOHwf4pmk83Ne7guznmDgVr/5IUfbp+JTPCg==} + engines: {node: '>=12.0.0'} + peerDependencies: + reflect-metadata: '>= 0.1.12' + typeorm: '>= 0.2.8' + + typeorm@0.3.20: + resolution: {integrity: sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==} + engines: {node: '>=16.13.0'} + hasBin: true + peerDependencies: + '@google-cloud/spanner': ^5.18.0 + '@sap/hana-client': ^2.12.25 + better-sqlite3: ^7.1.2 || ^8.0.0 || ^9.0.0 + hdb-pool: ^0.1.6 + ioredis: ^5.0.4 + mongodb: ^5.8.0 + mssql: ^9.1.1 || ^10.0.1 + mysql2: ^2.2.5 || ^3.0.1 + oracledb: ^6.3.0 + pg: ^8.5.1 + pg-native: ^3.0.0 + pg-query-stream: ^4.0.0 + redis: ^3.1.1 || ^4.0.0 + sql.js: ^1.4.0 + sqlite3: ^5.0.3 + ts-node: ^10.7.0 + typeorm-aurora-data-api-driver: ^2.0.0 + peerDependenciesMeta: + '@google-cloud/spanner': + optional: true + '@sap/hana-client': + optional: true + better-sqlite3: + optional: true + hdb-pool: + optional: true + ioredis: + optional: true + mongodb: + optional: true + mssql: + optional: true + mysql2: + optional: true + oracledb: + optional: true + pg: + optional: true + pg-native: + optional: true + pg-query-stream: + optional: true + redis: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + ts-node: + optional: true + typeorm-aurora-data-api-driver: + optional: true + + typescript-eslint@8.13.0: + resolution: {integrity: sha512-vIMpDRJrQd70au2G8w34mPps0ezFSPMEX4pXkTzUkrNbRX+36ais2ksGWN0esZL+ZMaFJEneOBHzCgSqle7DHw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + uid-safe@2.1.5: + resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} + engines: {node: '>= 0.8'} + + uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + + unique-filename@1.1.1: + resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} + + unique-slug@2.0.2: + resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unplugin@1.15.0: + resolution: {integrity: sha512-jTPIs63W+DUEDW207ztbaoO7cQ4p5aVaB823LSlxpsFEU3Mykwxf3ZGC/wzxFJeZlASZYgVrWeo7LgOrqJZ8RA==} + engines: {node: '>=14.0.0'} + peerDependencies: + webpack-sources: ^3 + peerDependenciesMeta: + webpack-sources: + optional: true + + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.2: + resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + use-resize-observer@9.1.0: + resolution: {integrity: sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==} + peerDependencies: + react: 16.8.0 - 18 + react-dom: 16.8.0 - 18 + + use-sidecar@1.1.2: + resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + validator@13.12.0: + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} + engines: {node: '>= 0.10'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite-node@2.1.5: + resolution: {integrity: sha512-rd0QIgx74q4S1Rd56XIiL2cYEdyWn13cunYBIuqh9mpmQr7gGS0IxXoP8R6OaZtNQQLyXSWbd4rXKYUbhFpK5w==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.10: + resolution: {integrity: sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.5: + resolution: {integrity: sha512-P4ljsdpuzRTPI/kbND2sDZ4VmieerR2c9szEZpjc+98Z9ebvnXmM5+0tHEKqYZumXqlvnmfWsjeFOjXVriDG7A==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.5 + '@vitest/ui': 2.1.5 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + watchpack@2.4.2: + resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} + engines: {node: '>=10.13.0'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + webpack-node-externals@3.0.0: + resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} + engines: {node: '>=6'} + + webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + webpack@5.96.1: + resolution: {integrity: sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.0.0: + resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} + engines: {node: '>=18'} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + winston@3.17.0: + resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==} + engines: {node: '>= 12.0.0'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml@2.6.0: + resolution: {integrity: sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==} + engines: {node: '>= 14'} + hasBin: true + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.1.1: + resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} + engines: {node: '>=12.20'} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@adobe/css-tools@4.4.0': {} + + '@alloc/quick-lru@5.2.0': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@angular-devkit/core@17.3.11(chokidar@3.6.0)': + dependencies: + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + jsonc-parser: 3.2.1 + picomatch: 4.0.1 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 3.6.0 + + '@angular-devkit/schematics-cli@17.3.11(chokidar@3.6.0)': + dependencies: + '@angular-devkit/core': 17.3.11(chokidar@3.6.0) + '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) + ansi-colors: 4.1.3 + inquirer: 9.2.15 + symbol-observable: 4.0.0 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - chokidar + + '@angular-devkit/schematics@17.3.11(chokidar@3.6.0)': + dependencies: + '@angular-devkit/core': 17.3.11(chokidar@3.6.0) + jsonc-parser: 3.2.1 + magic-string: 0.30.8 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.26.2': {} + + '@babel/core@7.26.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + convert-source-map: 2.0.0 + debug: 4.3.7 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.26.2': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.0.2 + + '@babel/helper-compilation-targets@7.25.9': + dependencies: + '@babel/compat-data': 7.26.2 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.25.9': {} + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helpers@7.26.0': + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + + '@babel/parser@7.26.2': + dependencies: + '@babel/types': 7.26.0 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-attributes@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-syntax-typescript@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/runtime@7.26.0': + dependencies: + regenerator-runtime: 0.14.1 + + '@babel/template@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@babel/traverse@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + debug: 4.3.7 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.26.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@bcoe/v8-coverage@0.2.3': {} + + '@chromatic-com/storybook@3.2.2(react@18.3.1)(storybook@8.4.2(prettier@3.3.3))': + dependencies: + chromatic: 11.18.0 + filesize: 10.1.6 + jsonfile: 6.1.0 + react-confetti: 6.1.0(react@18.3.1) + storybook: 8.4.2(prettier@3.3.3) + strip-ansi: 7.1.0 + transitivePeerDependencies: + - '@chromatic-com/cypress' + - '@chromatic-com/playwright' + - react + + '@colors/colors@1.5.0': + optional: true + + '@colors/colors@1.6.0': {} + + '@commitlint/cli@19.5.0(@types/node@22.9.0)(typescript@5.6.3)': + dependencies: + '@commitlint/format': 19.5.0 + '@commitlint/lint': 19.5.0 + '@commitlint/load': 19.5.0(@types/node@22.9.0)(typescript@5.6.3) + '@commitlint/read': 19.5.0 + '@commitlint/types': 19.5.0 + tinyexec: 0.3.1 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/config-conventional@19.5.0': + dependencies: + '@commitlint/types': 19.5.0 + conventional-changelog-conventionalcommits: 7.0.2 + + '@commitlint/config-validator@19.5.0': + dependencies: + '@commitlint/types': 19.5.0 + ajv: 8.17.1 + + '@commitlint/ensure@19.5.0': + dependencies: + '@commitlint/types': 19.5.0 + lodash.camelcase: 4.3.0 + lodash.kebabcase: 4.1.1 + lodash.snakecase: 4.1.1 + lodash.startcase: 4.4.0 + lodash.upperfirst: 4.3.1 + + '@commitlint/execute-rule@19.5.0': {} + + '@commitlint/format@19.5.0': + dependencies: + '@commitlint/types': 19.5.0 + chalk: 5.3.0 + + '@commitlint/is-ignored@19.5.0': + dependencies: + '@commitlint/types': 19.5.0 + semver: 7.6.3 + + '@commitlint/lint@19.5.0': + dependencies: + '@commitlint/is-ignored': 19.5.0 + '@commitlint/parse': 19.5.0 + '@commitlint/rules': 19.5.0 + '@commitlint/types': 19.5.0 + + '@commitlint/load@19.5.0(@types/node@22.9.0)(typescript@5.6.3)': + dependencies: + '@commitlint/config-validator': 19.5.0 + '@commitlint/execute-rule': 19.5.0 + '@commitlint/resolve-extends': 19.5.0 + '@commitlint/types': 19.5.0 + chalk: 5.3.0 + cosmiconfig: 9.0.0(typescript@5.6.3) + cosmiconfig-typescript-loader: 5.1.0(@types/node@22.9.0)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3) + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + lodash.uniq: 4.5.0 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/message@19.5.0': {} + + '@commitlint/parse@19.5.0': + dependencies: + '@commitlint/types': 19.5.0 + conventional-changelog-angular: 7.0.0 + conventional-commits-parser: 5.0.0 + + '@commitlint/read@19.5.0': + dependencies: + '@commitlint/top-level': 19.5.0 + '@commitlint/types': 19.5.0 + git-raw-commits: 4.0.0 + minimist: 1.2.8 + tinyexec: 0.3.1 + + '@commitlint/resolve-extends@19.5.0': + dependencies: + '@commitlint/config-validator': 19.5.0 + '@commitlint/types': 19.5.0 + global-directory: 4.0.1 + import-meta-resolve: 4.1.0 + lodash.mergewith: 4.6.2 + resolve-from: 5.0.0 + + '@commitlint/rules@19.5.0': + dependencies: + '@commitlint/ensure': 19.5.0 + '@commitlint/message': 19.5.0 + '@commitlint/to-lines': 19.5.0 + '@commitlint/types': 19.5.0 + + '@commitlint/to-lines@19.5.0': {} + + '@commitlint/top-level@19.5.0': + dependencies: + find-up: 7.0.0 + + '@commitlint/types@19.5.0': + dependencies: + '@types/conventional-commits-parser': 5.0.0 + chalk: 5.3.0 + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@dabh/diagnostics@2.0.3': + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + + '@emotion/use-insertion-effect-with-fallbacks@1.1.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@eslint-community/eslint-utils@4.4.1(eslint@9.15.0(jiti@1.21.6))': + dependencies: + eslint: 9.15.0(jiti@1.21.6) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.19.0': + dependencies: + '@eslint/object-schema': 2.1.4 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/core@0.9.0': {} + + '@eslint/eslintrc@3.2.0': + dependencies: + ajv: 6.12.6 + debug: 4.3.7 + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.14.0': {} + + '@eslint/js@9.15.0': {} + + '@eslint/object-schema@2.1.4': {} + + '@eslint/plugin-kit@0.2.3': + dependencies: + levn: 0.4.1 + + '@floating-ui/core@1.6.8': + dependencies: + '@floating-ui/utils': 0.2.8 + + '@floating-ui/dom@1.6.12': + dependencies: + '@floating-ui/core': 1.6.8 + '@floating-ui/utils': 0.2.8 + + '@floating-ui/react-dom@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.6.12 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/utils@0.2.8': {} + + '@gar/promisify@1.1.3': + optional: true + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.1': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.3': {} + + '@jest/console@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.17.6 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))': + 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': 20.17.6 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + 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.8 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + '@jest/environment@29.7.0': + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.9.0 + jest-mock: 29.7.0 + + '@jest/expect-utils@29.7.0': + dependencies: + jest-get-type: 29.6.3 + + '@jest/expect@29.7.0': + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@29.7.0': + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 22.9.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + '@jest/globals@29.7.0': + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@29.7.0': + 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.25 + '@types/node': 20.17.6 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jest/source-map@29.6.3': + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + '@jest/test-result@29.7.0': + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + '@jest/test-sequencer@29.7.0': + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + '@jest/transform@29.7.0': + dependencies: + '@babel/core': 7.26.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.8 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + '@jest/types@29.6.3': + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 20.17.6 + '@types/yargs': 17.0.33 + chalk: 4.1.2 + + '@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.6.3)(vite@5.4.10(@types/node@22.9.0)(terser@5.36.0))': + dependencies: + glob: 7.2.3 + glob-promise: 4.2.2(glob@7.2.3) + magic-string: 0.27.0 + react-docgen-typescript: 2.2.2(typescript@5.6.3) + vite: 5.4.10(@types/node@22.9.0)(terser@5.36.0) + optionalDependencies: + typescript: 5.6.3 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.6': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@juggle/resize-observer@3.4.0': {} + + '@ljharb/through@2.3.13': + dependencies: + call-bind: 1.0.7 + + '@lukeed/csprng@1.1.0': {} + + '@mdx-js/react@3.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@types/mdx': 2.0.13 + '@types/react': 18.3.12 + react: 18.3.1 + + '@microsoft/tsdoc@0.15.0': {} + + '@nestjs/cli@10.4.7': + dependencies: + '@angular-devkit/core': 17.3.11(chokidar@3.6.0) + '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) + '@angular-devkit/schematics-cli': 17.3.11(chokidar@3.6.0) + '@nestjs/schematics': 10.2.3(chokidar@3.6.0)(typescript@5.6.3) + chalk: 4.1.2 + chokidar: 3.6.0 + cli-table3: 0.6.5 + commander: 4.1.1 + fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.6.3)(webpack@5.96.1) + glob: 10.4.2 + inquirer: 8.2.6 + node-emoji: 1.11.0 + ora: 5.4.1 + tree-kill: 1.2.2 + tsconfig-paths: 4.2.0 + tsconfig-paths-webpack-plugin: 4.1.0 + typescript: 5.6.3 + webpack: 5.96.1 + webpack-node-externals: 3.0.0 + transitivePeerDependencies: + - esbuild + - uglify-js + - webpack-cli + + '@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)': + dependencies: + iterare: 1.2.1 + reflect-metadata: 0.2.2 + rxjs: 7.8.1 + tslib: 2.7.0 + uid: 2.0.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 + + '@nestjs/config@3.3.0(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1)': + dependencies: + '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + dotenv: 16.4.5 + dotenv-expand: 10.0.0 + lodash: 4.17.21 + rxjs: 7.8.1 + + '@nestjs/core@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(@nestjs/websockets@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)': + dependencies: + '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nuxtjs/opencollective': 0.3.2(encoding@0.1.13) + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 3.3.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.1 + tslib: 2.7.0 + uid: 2.0.2 + optionalDependencies: + '@nestjs/platform-express': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7) + '@nestjs/websockets': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-socket.io@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + transitivePeerDependencies: + - encoding + + '@nestjs/mapped-types@2.0.5(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 + + '@nestjs/mapped-types@2.0.6(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 + + '@nestjs/platform-express@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)': + dependencies: + '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(@nestjs/websockets@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) + body-parser: 1.20.3 + cors: 2.8.5 + express: 4.21.1 + multer: 1.4.4-lts.1 + tslib: 2.7.0 + transitivePeerDependencies: + - supports-color + + '@nestjs/platform-socket.io@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.7)(rxjs@7.8.1)': + dependencies: + '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/websockets': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-socket.io@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + rxjs: 7.8.1 + socket.io: 4.8.0 + tslib: 2.7.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + + '@nestjs/platform-ws@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.7)(rxjs@7.8.1)': + dependencies: + '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/websockets': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-socket.io@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + rxjs: 7.8.1 + tslib: 2.7.0 + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@nestjs/schematics@10.2.3(chokidar@3.6.0)(typescript@5.6.3)': + dependencies: + '@angular-devkit/core': 17.3.11(chokidar@3.6.0) + '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) + comment-json: 4.2.5 + jsonc-parser: 3.3.1 + pluralize: 8.0.0 + typescript: 5.6.3 + transitivePeerDependencies: + - chokidar + + '@nestjs/swagger@8.0.5(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': + dependencies: + '@microsoft/tsdoc': 0.15.0 + '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(@nestjs/websockets@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/mapped-types': 2.0.6(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + js-yaml: 4.1.0 + lodash: 4.17.21 + path-to-regexp: 3.3.0 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.18.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 + + '@nestjs/testing@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-express@10.4.7)': + dependencies: + '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(@nestjs/websockets@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) + tslib: 2.7.0 + optionalDependencies: + '@nestjs/platform-express': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7) + + '@nestjs/typeorm@10.0.2(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(mysql2@3.11.4)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)))': + dependencies: + '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(@nestjs/websockets@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) + reflect-metadata: 0.2.2 + rxjs: 7.8.1 + typeorm: 0.3.20(mysql2@3.11.4)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + uuid: 9.0.1 + + '@nestjs/websockets@10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-socket.io@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)': + dependencies: + '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(@nestjs/websockets@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) + iterare: 1.2.1 + object-hash: 3.0.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.1 + tslib: 2.7.0 + optionalDependencies: + '@nestjs/platform-socket.io': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.7)(rxjs@7.8.1) + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@npmcli/fs@1.1.1': + dependencies: + '@gar/promisify': 1.1.3 + semver: 7.6.3 + optional: true + + '@npmcli/move-file@1.1.2': + dependencies: + mkdirp: 1.0.4 + rimraf: 3.0.2 + optional: true + + '@nuxtjs/opencollective@0.3.2(encoding@0.1.13)': + dependencies: + chalk: 4.1.2 + consola: 2.15.3 + node-fetch: 2.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.1.1': {} + + '@radix-ui/number@1.0.1': + dependencies: + '@babel/runtime': 7.26.0 + + '@radix-ui/primitive@1.0.1': + dependencies: + '@babel/runtime': 7.26.0 + + '@radix-ui/primitive@1.1.0': {} + + '@radix-ui/react-alert-dialog@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-arrow@1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-avatar@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-collection@1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-context@1.0.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-context@1.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-context@1.1.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-dialog@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.3.12)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-direction@1.0.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-direction@1.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-focus-scope@1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-icons@1.3.1(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@radix-ui/react-id@1.0.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-id@1.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-popper@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-rect': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-size': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/rect': 1.0.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-popper@1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-portal@1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-portal@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-presence@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-progress@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-select@1.2.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/number': 1.0.1 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-direction': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-popper': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.0.2(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-previous': 1.0.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.5.5(@types/react@18.3.12)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-separator@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-slot@1.0.2(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-slot@1.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-toggle-group@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-toggle@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-toolbar@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-context': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toggle-group': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-tooltip@1.1.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-popper': 1.2.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-use-previous@1.0.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-use-rect@1.0.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/rect': 1.0.1 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-use-rect@1.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.0 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-use-size@1.0.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-use-size@1.1.0(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + + '@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/react-visually-hidden@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@radix-ui/rect@1.0.1': + dependencies: + '@babel/runtime': 7.26.0 + + '@radix-ui/rect@1.1.0': {} + + '@remix-run/router@1.21.0': {} + + '@rollup/pluginutils@5.1.3(rollup@4.24.4)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.24.4 + + '@rollup/rollup-android-arm-eabi@4.24.4': + optional: true + + '@rollup/rollup-android-arm64@4.24.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.24.4': + optional: true + + '@rollup/rollup-darwin-x64@4.24.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.24.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.24.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.24.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.24.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.24.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.24.4': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.24.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.24.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.24.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.24.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.24.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.24.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.24.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.24.4': + optional: true + + '@scarf/scarf@1.4.0': {} + + '@shikijs/core@1.22.2': + dependencies: + '@shikijs/engine-javascript': 1.22.2 + '@shikijs/engine-oniguruma': 1.22.2 + '@shikijs/types': 1.22.2 + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.3 + + '@shikijs/engine-javascript@1.22.2': + dependencies: + '@shikijs/types': 1.22.2 + '@shikijs/vscode-textmate': 9.3.0 + oniguruma-to-js: 0.4.3 + + '@shikijs/engine-oniguruma@1.22.2': + dependencies: + '@shikijs/types': 1.22.2 + '@shikijs/vscode-textmate': 9.3.0 + + '@shikijs/types@1.22.2': + dependencies: + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@9.3.0': {} + + '@sinclair/typebox@0.27.8': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@10.3.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@socket.io/component-emitter@3.1.2': {} + + '@sqltools/formatter@1.2.5': {} + + '@storybook/addon-actions@8.4.2(storybook@8.4.2(prettier@3.3.3))': + dependencies: + '@storybook/global': 5.0.0 + '@types/uuid': 9.0.8 + dequal: 2.0.3 + polished: 4.3.1 + storybook: 8.4.2(prettier@3.3.3) + uuid: 9.0.1 + + '@storybook/addon-backgrounds@8.4.2(storybook@8.4.2(prettier@3.3.3))': + dependencies: + '@storybook/global': 5.0.0 + memoizerific: 1.11.3 + storybook: 8.4.2(prettier@3.3.3) + ts-dedent: 2.2.0 + + '@storybook/addon-controls@8.4.2(storybook@8.4.2(prettier@3.3.3))': + dependencies: + '@storybook/global': 5.0.0 + dequal: 2.0.3 + storybook: 8.4.2(prettier@3.3.3) + ts-dedent: 2.2.0 + + '@storybook/addon-docs@8.4.2(@types/react@18.3.12)(storybook@8.4.2(prettier@3.3.3))(webpack-sources@3.2.3)': + dependencies: + '@mdx-js/react': 3.1.0(@types/react@18.3.12)(react@18.3.1) + '@storybook/blocks': 8.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.2(prettier@3.3.3)) + '@storybook/csf-plugin': 8.4.2(storybook@8.4.2(prettier@3.3.3))(webpack-sources@3.2.3) + '@storybook/react-dom-shim': 8.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.2(prettier@3.3.3)) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 8.4.2(prettier@3.3.3) + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/react' + - webpack-sources + + '@storybook/addon-essentials@8.4.2(@types/react@18.3.12)(storybook@8.4.2(prettier@3.3.3))(webpack-sources@3.2.3)': + dependencies: + '@storybook/addon-actions': 8.4.2(storybook@8.4.2(prettier@3.3.3)) + '@storybook/addon-backgrounds': 8.4.2(storybook@8.4.2(prettier@3.3.3)) + '@storybook/addon-controls': 8.4.2(storybook@8.4.2(prettier@3.3.3)) + '@storybook/addon-docs': 8.4.2(@types/react@18.3.12)(storybook@8.4.2(prettier@3.3.3))(webpack-sources@3.2.3) + '@storybook/addon-highlight': 8.4.2(storybook@8.4.2(prettier@3.3.3)) + '@storybook/addon-measure': 8.4.2(storybook@8.4.2(prettier@3.3.3)) + '@storybook/addon-outline': 8.4.2(storybook@8.4.2(prettier@3.3.3)) + '@storybook/addon-toolbars': 8.4.2(storybook@8.4.2(prettier@3.3.3)) + '@storybook/addon-viewport': 8.4.2(storybook@8.4.2(prettier@3.3.3)) + storybook: 8.4.2(prettier@3.3.3) + ts-dedent: 2.2.0 + transitivePeerDependencies: + - '@types/react' + - webpack-sources + + '@storybook/addon-highlight@8.4.2(storybook@8.4.2(prettier@3.3.3))': + dependencies: + '@storybook/global': 5.0.0 + storybook: 8.4.2(prettier@3.3.3) + + '@storybook/addon-interactions@8.4.2(storybook@8.4.2(prettier@3.3.3))': + dependencies: + '@storybook/global': 5.0.0 + '@storybook/instrumenter': 8.4.2(storybook@8.4.2(prettier@3.3.3)) + '@storybook/test': 8.4.2(storybook@8.4.2(prettier@3.3.3)) + polished: 4.3.1 + storybook: 8.4.2(prettier@3.3.3) + ts-dedent: 2.2.0 + + '@storybook/addon-measure@8.4.2(storybook@8.4.2(prettier@3.3.3))': + dependencies: + '@storybook/global': 5.0.0 + storybook: 8.4.2(prettier@3.3.3) + tiny-invariant: 1.3.3 + + '@storybook/addon-onboarding@8.4.2(react@18.3.1)(storybook@8.4.2(prettier@3.3.3))': + dependencies: + react-confetti: 6.1.0(react@18.3.1) + storybook: 8.4.2(prettier@3.3.3) + transitivePeerDependencies: + - react + + '@storybook/addon-outline@8.4.2(storybook@8.4.2(prettier@3.3.3))': + dependencies: + '@storybook/global': 5.0.0 + storybook: 8.4.2(prettier@3.3.3) + ts-dedent: 2.2.0 + + '@storybook/addon-styling@2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(postcss@8.4.47)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.6.3)(webpack@5.96.1(esbuild@0.21.5))': + dependencies: + '@storybook/api': 7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/components': 7.6.20(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/core-events': 7.6.20 + '@storybook/manager-api': 7.6.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/preview-api': 7.6.20 + '@storybook/theming': 7.6.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/types': 7.6.20 + css-loader: 6.11.0(webpack@5.96.1(esbuild@0.21.5)) + postcss-loader: 7.3.4(postcss@8.4.47)(typescript@5.6.3)(webpack@5.96.1(esbuild@0.21.5)) + resolve-url-loader: 5.0.0 + sass-loader: 13.3.3(webpack@5.96.1(esbuild@0.21.5)) + style-loader: 3.3.4(webpack@5.96.1(esbuild@0.21.5)) + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@rspack/core' + - '@types/react' + - '@types/react-dom' + - fibers + - node-sass + - postcss + - sass + - sass-embedded + - typescript + - webpack + + '@storybook/addon-toolbars@8.4.2(storybook@8.4.2(prettier@3.3.3))': + dependencies: + storybook: 8.4.2(prettier@3.3.3) + + '@storybook/addon-viewport@8.4.2(storybook@8.4.2(prettier@3.3.3))': + dependencies: + memoizerific: 1.11.3 + storybook: 8.4.2(prettier@3.3.3) + + '@storybook/api@7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@storybook/client-logger': 7.6.17 + '@storybook/manager-api': 7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - react + - react-dom + + '@storybook/blocks@8.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.2(prettier@3.3.3))': + dependencies: + '@storybook/csf': 0.1.11 + '@storybook/icons': 1.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + storybook: 8.4.2(prettier@3.3.3) + ts-dedent: 2.2.0 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@storybook/builder-vite@8.4.2(storybook@8.4.2(prettier@3.3.3))(vite@5.4.10(@types/node@22.9.0)(terser@5.36.0))(webpack-sources@3.2.3)': + dependencies: + '@storybook/csf-plugin': 8.4.2(storybook@8.4.2(prettier@3.3.3))(webpack-sources@3.2.3) + browser-assert: 1.2.1 + storybook: 8.4.2(prettier@3.3.3) + ts-dedent: 2.2.0 + vite: 5.4.10(@types/node@22.9.0)(terser@5.36.0) + transitivePeerDependencies: + - webpack-sources + + '@storybook/channels@7.6.17': + dependencies: + '@storybook/client-logger': 7.6.17 + '@storybook/core-events': 7.6.17 + '@storybook/global': 5.0.0 + qs: 6.13.0 + telejson: 7.2.0 + tiny-invariant: 1.3.3 + + '@storybook/channels@7.6.20': + dependencies: + '@storybook/client-logger': 7.6.20 + '@storybook/core-events': 7.6.20 + '@storybook/global': 5.0.0 + qs: 6.13.0 + telejson: 7.2.0 + tiny-invariant: 1.3.3 + + '@storybook/client-logger@7.6.17': + dependencies: + '@storybook/global': 5.0.0 + + '@storybook/client-logger@7.6.20': + dependencies: + '@storybook/global': 5.0.0 + + '@storybook/components@7.6.20(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-select': 1.2.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toolbar': 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/client-logger': 7.6.20 + '@storybook/csf': 0.1.11 + '@storybook/global': 5.0.0 + '@storybook/theming': 7.6.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/types': 7.6.20 + memoizerific: 1.11.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + use-resize-observer: 9.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + util-deprecate: 1.0.2 + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + '@storybook/components@8.4.2(storybook@8.4.2(prettier@3.3.3))': + dependencies: + storybook: 8.4.2(prettier@3.3.3) + + '@storybook/core-events@7.6.17': + dependencies: + ts-dedent: 2.2.0 + + '@storybook/core-events@7.6.20': + dependencies: + ts-dedent: 2.2.0 + + '@storybook/core@8.4.2(prettier@3.3.3)': + dependencies: + '@storybook/csf': 0.1.11 + better-opn: 3.0.2 + browser-assert: 1.2.1 + esbuild: 0.21.5 + esbuild-register: 3.6.0(esbuild@0.21.5) + jsdoc-type-pratt-parser: 4.1.0 + process: 0.11.10 + recast: 0.23.9 + semver: 7.6.3 + util: 0.12.5 + ws: 8.18.0 + optionalDependencies: + prettier: 3.3.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@storybook/csf-plugin@8.4.2(storybook@8.4.2(prettier@3.3.3))(webpack-sources@3.2.3)': + dependencies: + storybook: 8.4.2(prettier@3.3.3) + unplugin: 1.15.0(webpack-sources@3.2.3) + transitivePeerDependencies: + - webpack-sources + + '@storybook/csf@0.1.11': + dependencies: + type-fest: 2.19.0 + + '@storybook/global@5.0.0': {} + + '@storybook/icons@1.2.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@storybook/instrumenter@8.4.2(storybook@8.4.2(prettier@3.3.3))': + dependencies: + '@storybook/global': 5.0.0 + '@vitest/utils': 2.1.4 + storybook: 8.4.2(prettier@3.3.3) + + '@storybook/manager-api@7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@storybook/channels': 7.6.17 + '@storybook/client-logger': 7.6.17 + '@storybook/core-events': 7.6.17 + '@storybook/csf': 0.1.11 + '@storybook/global': 5.0.0 + '@storybook/router': 7.6.17 + '@storybook/theming': 7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/types': 7.6.17 + dequal: 2.0.3 + lodash: 4.17.21 + memoizerific: 1.11.3 + store2: 2.14.3 + telejson: 7.2.0 + ts-dedent: 2.2.0 + transitivePeerDependencies: + - react + - react-dom + + '@storybook/manager-api@7.6.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@storybook/channels': 7.6.20 + '@storybook/client-logger': 7.6.20 + '@storybook/core-events': 7.6.20 + '@storybook/csf': 0.1.11 + '@storybook/global': 5.0.0 + '@storybook/router': 7.6.20 + '@storybook/theming': 7.6.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@storybook/types': 7.6.20 + dequal: 2.0.3 + lodash: 4.17.21 + memoizerific: 1.11.3 + store2: 2.14.3 + telejson: 7.2.0 + ts-dedent: 2.2.0 + transitivePeerDependencies: + - react + - react-dom + + '@storybook/manager-api@8.4.2(storybook@8.4.2(prettier@3.3.3))': + dependencies: + storybook: 8.4.2(prettier@3.3.3) + + '@storybook/preview-api@7.6.20': + dependencies: + '@storybook/channels': 7.6.20 + '@storybook/client-logger': 7.6.20 + '@storybook/core-events': 7.6.20 + '@storybook/csf': 0.1.11 + '@storybook/global': 5.0.0 + '@storybook/types': 7.6.20 + '@types/qs': 6.9.17 + dequal: 2.0.3 + lodash: 4.17.21 + memoizerific: 1.11.3 + qs: 6.13.0 + synchronous-promise: 2.0.17 + ts-dedent: 2.2.0 + util-deprecate: 1.0.2 + + '@storybook/preview-api@8.4.2(storybook@8.4.2(prettier@3.3.3))': + dependencies: + storybook: 8.4.2(prettier@3.3.3) + + '@storybook/react-dom-shim@8.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.2(prettier@3.3.3))': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 8.4.2(prettier@3.3.3) + + '@storybook/react-vite@8.4.2(@storybook/test@8.4.2(storybook@8.4.2(prettier@3.3.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.24.4)(storybook@8.4.2(prettier@3.3.3))(typescript@5.6.3)(vite@5.4.10(@types/node@22.9.0)(terser@5.36.0))(webpack-sources@3.2.3)': + dependencies: + '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.6.3)(vite@5.4.10(@types/node@22.9.0)(terser@5.36.0)) + '@rollup/pluginutils': 5.1.3(rollup@4.24.4) + '@storybook/builder-vite': 8.4.2(storybook@8.4.2(prettier@3.3.3))(vite@5.4.10(@types/node@22.9.0)(terser@5.36.0))(webpack-sources@3.2.3) + '@storybook/react': 8.4.2(@storybook/test@8.4.2(storybook@8.4.2(prettier@3.3.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.2(prettier@3.3.3))(typescript@5.6.3) + find-up: 5.0.0 + magic-string: 0.30.8 + react: 18.3.1 + react-docgen: 7.1.0 + react-dom: 18.3.1(react@18.3.1) + resolve: 1.22.8 + storybook: 8.4.2(prettier@3.3.3) + tsconfig-paths: 4.2.0 + vite: 5.4.10(@types/node@22.9.0)(terser@5.36.0) + transitivePeerDependencies: + - '@storybook/test' + - rollup + - supports-color + - typescript + - webpack-sources + + '@storybook/react@8.4.2(@storybook/test@8.4.2(storybook@8.4.2(prettier@3.3.3)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.2(prettier@3.3.3))(typescript@5.6.3)': + dependencies: + '@storybook/components': 8.4.2(storybook@8.4.2(prettier@3.3.3)) + '@storybook/global': 5.0.0 + '@storybook/manager-api': 8.4.2(storybook@8.4.2(prettier@3.3.3)) + '@storybook/preview-api': 8.4.2(storybook@8.4.2(prettier@3.3.3)) + '@storybook/react-dom-shim': 8.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.4.2(prettier@3.3.3)) + '@storybook/theming': 8.4.2(storybook@8.4.2(prettier@3.3.3)) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + storybook: 8.4.2(prettier@3.3.3) + optionalDependencies: + '@storybook/test': 8.4.2(storybook@8.4.2(prettier@3.3.3)) + typescript: 5.6.3 + + '@storybook/router@7.6.17': + dependencies: + '@storybook/client-logger': 7.6.17 + memoizerific: 1.11.3 + qs: 6.13.0 + + '@storybook/router@7.6.20': + dependencies: + '@storybook/client-logger': 7.6.20 + memoizerific: 1.11.3 + qs: 6.13.0 + + '@storybook/test@8.4.2(storybook@8.4.2(prettier@3.3.3))': + dependencies: + '@storybook/csf': 0.1.11 + '@storybook/global': 5.0.0 + '@storybook/instrumenter': 8.4.2(storybook@8.4.2(prettier@3.3.3)) + '@testing-library/dom': 10.4.0 + '@testing-library/jest-dom': 6.5.0 + '@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0) + '@vitest/expect': 2.0.5 + '@vitest/spy': 2.0.5 + storybook: 8.4.2(prettier@3.3.3) + + '@storybook/theming@7.6.17(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@emotion/use-insertion-effect-with-fallbacks': 1.1.0(react@18.3.1) + '@storybook/client-logger': 7.6.17 + '@storybook/global': 5.0.0 + memoizerific: 1.11.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@storybook/theming@7.6.20(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@emotion/use-insertion-effect-with-fallbacks': 1.1.0(react@18.3.1) + '@storybook/client-logger': 7.6.20 + '@storybook/global': 5.0.0 + memoizerific: 1.11.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@storybook/theming@8.4.2(storybook@8.4.2(prettier@3.3.3))': + dependencies: + storybook: 8.4.2(prettier@3.3.3) + + '@storybook/types@7.6.17': + dependencies: + '@storybook/channels': 7.6.17 + '@types/babel__core': 7.20.5 + '@types/express': 4.17.21 + file-system-cache: 2.3.0 + + '@storybook/types@7.6.20': + dependencies: + '@storybook/channels': 7.6.20 + '@types/babel__core': 7.20.5 + '@types/express': 4.17.21 + file-system-cache: 2.3.0 + + '@testing-library/dom@10.4.0': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/runtime': 7.26.0 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + chalk: 4.1.2 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.5.0': + dependencies: + '@adobe/css-tools': 4.4.0 + aria-query: 5.3.2 + 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/jest-dom@6.6.3': + dependencies: + '@adobe/css-tools': 4.4.0 + aria-query: 5.3.2 + 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-hooks@8.0.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + react: 18.3.1 + react-error-boundary: 3.1.4(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + react-dom: 18.3.1(react@18.3.1) + + '@testing-library/react@16.0.1(@testing-library/dom@10.4.0)(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.26.0 + '@testing-library/dom': 10.4.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + + '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': + dependencies: + '@testing-library/dom': 10.4.0 + + '@tootallnate/once@1.1.2': + optional: true + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.26.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.26.0 + + '@types/body-parser@1.19.5': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 20.17.6 + + '@types/cls-hooked@4.3.9': + dependencies: + '@types/node': 22.9.0 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 20.17.6 + + '@types/conventional-commits-parser@5.0.0': + dependencies: + '@types/node': 22.9.0 + + '@types/cookie-parser@1.4.7': + dependencies: + '@types/express': 5.0.0 + + '@types/cookie@0.4.1': + optional: true + + '@types/cookiejar@2.1.5': {} + + '@types/cors@2.8.17': + dependencies: + '@types/node': 22.9.0 + optional: true + + '@types/doctrine@0.0.9': {} + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.6 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + + '@types/estree@1.0.6': {} + + '@types/express-serve-static-core@4.19.6': + dependencies: + '@types/node': 22.9.0 + '@types/qs': 6.9.17 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + + '@types/express-serve-static-core@5.0.1': + dependencies: + '@types/node': 20.17.6 + '@types/qs': 6.9.17 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + + '@types/express-session@1.18.0': + dependencies: + '@types/express': 5.0.0 + + '@types/express@4.17.21': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 4.19.6 + '@types/qs': 6.9.17 + '@types/serve-static': 1.15.7 + + '@types/express@5.0.0': + dependencies: + '@types/body-parser': 1.19.5 + '@types/express-serve-static-core': 5.0.1 + '@types/qs': 6.9.17 + '@types/serve-static': 1.15.7 + + '@types/glob@7.2.0': + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 22.9.0 + + '@types/graceful-fs@4.1.9': + dependencies: + '@types/node': 22.9.0 + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/history@4.7.11': {} + + '@types/http-errors@2.0.4': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@29.5.14': + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + + '@types/json-schema@7.0.15': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdx@2.0.13': {} + + '@types/methods@1.1.4': {} + + '@types/mime@1.3.5': {} + + '@types/minimatch@5.1.2': {} + + '@types/node@20.17.6': + dependencies: + undici-types: 6.19.8 + + '@types/node@22.9.0': + dependencies: + undici-types: 6.19.8 + + '@types/prop-types@15.7.13': {} + + '@types/qs@6.9.17': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@18.3.1': + dependencies: + '@types/react': 18.3.12 + + '@types/react-router-dom@5.3.3': + dependencies: + '@types/history': 4.7.11 + '@types/react': 18.3.12 + '@types/react-router': 5.1.20 + + '@types/react-router@5.1.20': + dependencies: + '@types/history': 4.7.11 + '@types/react': 18.3.12 + + '@types/react@18.3.12': + dependencies: + '@types/prop-types': 15.7.13 + csstype: 3.1.3 + + '@types/resolve@1.20.6': {} + + '@types/send@0.17.4': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 20.17.6 + + '@types/serve-static@1.15.7': + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 20.17.6 + '@types/send': 0.17.4 + + '@types/stack-utils@2.0.3': {} + + '@types/superagent@8.1.9': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 20.17.6 + form-data: 4.0.1 + + '@types/supertest@6.0.2': + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.9 + + '@types/triple-beam@1.3.5': {} + + '@types/unist@3.0.3': {} + + '@types/uuid@9.0.8': {} + + '@types/validator@13.12.2': {} + + '@types/ws@8.5.13': + dependencies: + '@types/node': 22.9.0 + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.33': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.13.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.13.0 + '@typescript-eslint/type-utils': 8.13.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) + '@typescript-eslint/utils': 8.13.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.13.0 + eslint: 9.15.0(jiti@1.21.6) + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.13.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.13.0 + '@typescript-eslint/types': 8.13.0 + '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.13.0 + debug: 4.3.7 + eslint: 9.15.0(jiti@1.21.6) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.13.0': + dependencies: + '@typescript-eslint/types': 8.13.0 + '@typescript-eslint/visitor-keys': 8.13.0 + + '@typescript-eslint/type-utils@8.13.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) + '@typescript-eslint/utils': 8.13.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) + debug: 4.3.7 + ts-api-utils: 1.4.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - eslint + - supports-color + + '@typescript-eslint/types@8.13.0': {} + + '@typescript-eslint/typescript-estree@8.13.0(typescript@5.6.3)': + dependencies: + '@typescript-eslint/types': 8.13.0 + '@typescript-eslint/visitor-keys': 8.13.0 + debug: 4.3.7 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.4.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.13.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0(jiti@1.21.6)) + '@typescript-eslint/scope-manager': 8.13.0 + '@typescript-eslint/types': 8.13.0 + '@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3) + eslint: 9.15.0(jiti@1.21.6) + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@8.13.0': + dependencies: + '@typescript-eslint/types': 8.13.0 + eslint-visitor-keys: 3.4.3 + + '@ungap/structured-clone@1.2.0': {} + + '@vitejs/plugin-react@4.3.3(vite@5.4.10(@types/node@22.9.0)(terser@5.36.0))': + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.0) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.2 + vite: 5.4.10(@types/node@22.9.0)(terser@5.36.0) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@2.0.5': + dependencies: + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.2 + tinyrainbow: 1.2.0 + + '@vitest/expect@2.1.5': + dependencies: + '@vitest/spy': 2.1.5 + '@vitest/utils': 2.1.5 + chai: 5.1.2 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.5(vite@5.4.10(@types/node@22.9.0)(terser@5.36.0))': + dependencies: + '@vitest/spy': 2.1.5 + estree-walker: 3.0.3 + magic-string: 0.30.12 + optionalDependencies: + vite: 5.4.10(@types/node@22.9.0)(terser@5.36.0) + + '@vitest/pretty-format@2.0.5': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@2.1.4': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/pretty-format@2.1.5': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.5': + dependencies: + '@vitest/utils': 2.1.5 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.5': + dependencies: + '@vitest/pretty-format': 2.1.5 + magic-string: 0.30.12 + pathe: 1.1.2 + + '@vitest/spy@2.0.5': + dependencies: + tinyspy: 3.0.2 + + '@vitest/spy@2.1.5': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.0.5': + dependencies: + '@vitest/pretty-format': 2.0.5 + estree-walker: 3.0.3 + loupe: 3.1.2 + tinyrainbow: 1.2.0 + + '@vitest/utils@2.1.4': + dependencies: + '@vitest/pretty-format': 2.1.4 + loupe: 3.1.2 + tinyrainbow: 1.2.0 + + '@vitest/utils@2.1.5': + dependencies: + '@vitest/pretty-format': 2.1.5 + loupe: 3.1.2 + tinyrainbow: 1.2.0 + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + JSONStream@1.3.5: + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + + abbrev@1.1.1: + optional: true + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + adjust-sourcemap-loader@4.0.0: + dependencies: + loader-utils: 2.0.4 + regex-parser: 2.3.0 + + agent-base@6.0.2: + dependencies: + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + optional: true + + agent-base@7.1.1: + dependencies: + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + agentkeepalive@4.5.0: + dependencies: + humanize-ms: 1.2.1 + optional: true + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + optional: true + + ajv-formats@2.1.1(ajv@8.12.0): + optionalDependencies: + ajv: 8.12.0 + + ajv-keywords@3.5.2(ajv@6.12.6): + dependencies: + ajv: 6.12.6 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.12.0: + 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.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.1: {} + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + app-root-path@3.1.0: {} + + append-field@1.0.0: {} + + aproba@2.0.0: + optional: true + + are-we-there-yet@3.0.1: + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + optional: true + + arg@4.1.3: {} + + arg@5.0.2: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-hidden@1.2.4: + dependencies: + tslib: 2.8.1 + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + array-flatten@1.1.1: {} + + array-ify@1.0.0: {} + + array-timsort@1.0.3: {} + + asap@2.0.6: {} + + assertion-error@2.0.1: {} + + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + + async-hook-jl@1.7.6: + dependencies: + stack-chain: 1.3.7 + + async@3.2.6: {} + + asynckit@0.4.0: {} + + autoprefixer@10.4.20(postcss@8.4.47): + dependencies: + browserslist: 4.24.2 + caniuse-lite: 1.0.30001678 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.4.47 + postcss-value-parser: 4.2.0 + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.0.0 + + aws-ssl-profiles@1.1.2: {} + + babel-jest@29.7.0(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.26.0) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@6.1.1: + dependencies: + '@babel/helper-plugin-utils': 7.25.9 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@29.6.3: + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.6 + + babel-preset-current-node-syntax@1.1.0(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.26.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.26.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-import-attributes': 7.26.0(@babel/core@7.26.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.26.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.26.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.26.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.26.0) + + babel-preset-jest@29.6.3(@babel/core@7.26.0): + dependencies: + '@babel/core': 7.26.0 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) + + backend@file:apps/backend(@nestjs/platform-socket.io@10.4.7)(encoding@0.1.13)(supertest@7.0.0)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + dependencies: + '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/config': 3.3.0(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1) + '@nestjs/core': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.7)(@nestjs/websockets@10.4.7)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/mapped-types': 2.0.6(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + '@nestjs/platform-express': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7) + '@nestjs/platform-ws': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.4.7)(rxjs@7.8.1) + '@nestjs/swagger': 8.0.5(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + '@nestjs/typeorm': 10.0.2(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1)(typeorm@0.3.20(mysql2@3.11.4)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))) + '@nestjs/websockets': 10.4.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.7)(@nestjs/platform-socket.io@10.4.7)(reflect-metadata@0.2.2)(rxjs@7.8.1) + class-transformer: 0.5.1 + class-validator: 0.14.1 + cookie: 1.0.1 + cookie-parser: 1.4.7 + express-session: 1.18.1 + get-port: 7.1.0 + mysql2: 3.11.4 + nest-winston: 1.9.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(winston@3.17.0) + reflect-metadata: 0.2.2 + rxjs: 7.8.1 + socket.io-client: 4.8.1 + sqlite3: 5.1.7 + superwstest: 2.0.4(supertest@7.0.0) + typeorm: 0.3.20(mysql2@3.11.4)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + typeorm-transactional: 0.5.0(reflect-metadata@0.2.2)(typeorm@0.3.20(mysql2@3.11.4)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))) + winston: 3.17.0 + ws: 8.18.0 + transitivePeerDependencies: + - '@fastify/static' + - '@google-cloud/spanner' + - '@nestjs/microservices' + - '@nestjs/platform-socket.io' + - '@sap/hana-client' + - better-sqlite3 + - bluebird + - bufferutil + - encoding + - hdb-pool + - ioredis + - mongodb + - mssql + - oracledb + - pg + - pg-native + - pg-query-stream + - redis + - sql.js + - supertest + - supports-color + - ts-node + - typeorm-aurora-data-api-driver + - utf-8-validate + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + base64id@2.0.0: + optional: true + + better-opn@3.0.2: + dependencies: + open: 8.4.2 + + big.js@5.2.2: {} + + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + body-parser@1.20.3: + 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.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browser-assert@1.2.1: {} + + browserslist@4.24.2: + dependencies: + caniuse-lite: 1.0.30001678 + electron-to-chromium: 1.5.52 + node-releases: 2.0.18 + update-browserslist-db: 1.1.1(browserslist@4.24.2) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + bytes@3.1.2: {} + + cac@6.7.14: {} + + cacache@15.3.0: + dependencies: + '@npmcli/fs': 1.1.1 + '@npmcli/move-file': 1.1.2 + chownr: 2.0.0 + fs-minipass: 2.1.0 + glob: 7.2.3 + infer-owner: 1.0.4 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + mkdirp: 1.0.4 + p-map: 4.0.0 + promise-inflight: 1.0.1 + rimraf: 3.0.2 + ssri: 8.0.1 + tar: 6.2.1 + unique-filename: 1.1.1 + transitivePeerDependencies: + - bluebird + optional: true + + call-bind@1.0.7: + 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.2 + + callsites@3.1.0: {} + + camelcase-css@2.0.1: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001678: {} + + ccount@2.0.1: {} + + chai@5.1.2: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.2 + pathval: 2.0.0 + + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.3.0: {} + + char-regex@1.0.2: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + chardet@0.7.0: {} + + check-error@2.1.1: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@1.1.4: {} + + chownr@2.0.0: {} + + chromatic@11.18.0: {} + + chrome-trace-event@1.0.4: {} + + ci-info@3.9.0: {} + + cjs-module-lexer@1.4.1: {} + + class-transformer@0.5.1: {} + + class-validator@0.14.1: + dependencies: + '@types/validator': 13.12.2 + libphonenumber-js: 1.11.14 + validator: 13.12.0 + + class-variance-authority@0.7.0: + dependencies: + clsx: 2.0.0 + + clean-stack@2.2.0: + optional: true + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-highlight@2.1.11: + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.0 + + cli-spinners@2.9.2: {} + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + cli-width@3.0.0: {} + + cli-width@4.1.0: {} + + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + cls-hooked@4.2.2: + dependencies: + async-hook-jl: 1.7.6 + emitter-listener: 1.1.2 + semver: 5.7.2 + + clsx@2.0.0: {} + + clsx@2.1.1: {} + + co@4.6.0: {} + + collect-v8-coverage@1.0.2: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color-support@1.1.3: + optional: true + + color@3.2.1: + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + + colorspace@1.1.4: + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comma-separated-tokens@2.0.3: {} + + commander@2.20.3: {} + + commander@4.1.1: {} + + comment-json@4.2.5: + dependencies: + array-timsort: 1.0.3 + core-util-is: 1.0.3 + esprima: 4.0.1 + has-own-prop: 2.0.0 + repeat-string: 1.6.1 + + compare-func@2.0.0: + dependencies: + array-ify: 1.0.0 + dot-prop: 5.3.0 + + component-emitter@1.3.1: {} + + concat-map@0.0.1: {} + + concat-stream@1.6.2: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + + consola@2.15.3: {} + + console-control-strings@1.1.0: + optional: true + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + conventional-changelog-angular@7.0.0: + dependencies: + compare-func: 2.0.0 + + conventional-changelog-conventionalcommits@7.0.2: + dependencies: + compare-func: 2.0.0 + + conventional-commits-parser@5.0.0: + dependencies: + JSONStream: 1.3.5 + is-text-path: 2.0.0 + meow: 12.1.1 + split2: 4.2.0 + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + + cookie-signature@1.0.7: {} + + cookie@0.7.1: {} + + cookie@0.7.2: {} + + cookie@1.0.1: {} + + cookiejar@2.1.4: {} + + core-util-is@1.0.3: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig-typescript-loader@5.1.0(@types/node@22.9.0)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3): + dependencies: + '@types/node': 22.9.0 + cosmiconfig: 9.0.0(typescript@5.6.3) + jiti: 1.21.6 + typescript: 5.6.3 + + cosmiconfig@8.3.6(typescript@5.6.3): + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.6.3 + + cosmiconfig@9.0.0(typescript@5.6.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.6.3 + + create-jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + create-require@1.1.1: {} + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-loader@6.11.0(webpack@5.96.1(esbuild@0.21.5)): + dependencies: + icss-utils: 5.1.0(postcss@8.4.47) + postcss: 8.4.47 + postcss-modules-extract-imports: 3.1.0(postcss@8.4.47) + postcss-modules-local-by-default: 4.0.5(postcss@8.4.47) + postcss-modules-scope: 3.2.0(postcss@8.4.47) + postcss-modules-values: 4.0.0(postcss@8.4.47) + postcss-value-parser: 4.2.0 + semver: 7.6.3 + optionalDependencies: + webpack: 5.96.1(esbuild@0.21.5) + + css.escape@1.5.1: {} + + cssesc@3.0.0: {} + + cssstyle@4.1.0: + dependencies: + rrweb-cssom: 0.7.1 + + csstype@3.1.3: {} + + dargs@8.1.0: {} + + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + + dayjs@1.11.13: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + decimal.js@10.4.3: {} + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + dedent@1.5.3: {} + + deep-eql@5.0.2: {} + + deep-extend@0.6.0: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + define-lazy-prop@2.0.0: {} + + delayed-stream@1.0.0: {} + + delegates@1.0.0: + optional: true + + denque@2.1.0: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destroy@1.2.0: {} + + detect-libc@2.0.3: {} + + detect-newline@3.1.0: {} + + detect-node-es@1.1.0: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + + didyoumean@1.2.2: {} + + diff-sequences@29.6.3: {} + + diff@4.0.2: {} + + dlv@1.1.3: {} + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dot-prop@5.3.0: + dependencies: + is-obj: 2.0.0 + + dotenv-expand@10.0.0: {} + + dotenv@16.4.5: {} + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + ejs@3.1.10: + dependencies: + jake: 10.9.2 + + electron-to-chromium@1.5.52: {} + + emitter-listener@1.1.2: + dependencies: + shimmer: 1.2.1 + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + emojis-list@3.0.0: {} + + enabled@2.0.0: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + + end-of-stream@1.4.4: + dependencies: + once: 1.4.0 + + engine.io-client@6.6.2: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1 + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + + engine.io@6.6.2: + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.17 + '@types/node': 22.9.0 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.5 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + + enhanced-resolve@5.17.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + + entities@4.5.0: {} + + env-paths@2.2.1: {} + + err-code@2.0.3: + optional: true + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.0: + dependencies: + get-intrinsic: 1.2.4 + + es-errors@1.3.0: {} + + es-module-lexer@1.5.4: {} + + esbuild-register@3.6.0(esbuild@0.21.5): + dependencies: + debug: 4.3.7 + esbuild: 0.21.5 + transitivePeerDependencies: + - supports-color + + esbuild@0.21.5: + 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.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@9.1.0(eslint@9.15.0(jiti@1.21.6)): + dependencies: + eslint: 9.15.0(jiti@1.21.6) + + eslint-plugin-prettier@5.2.1(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@9.15.0(jiti@1.21.6)))(eslint@9.15.0(jiti@1.21.6))(prettier@3.3.3): + dependencies: + eslint: 9.15.0(jiti@1.21.6) + prettier: 3.3.3 + prettier-linter-helpers: 1.0.0 + synckit: 0.9.2 + optionalDependencies: + '@types/eslint': 9.6.1 + eslint-config-prettier: 9.1.0(eslint@9.15.0(jiti@1.21.6)) + + eslint-plugin-react-hooks@5.0.0(eslint@9.15.0(jiti@1.21.6)): + dependencies: + eslint: 9.15.0(jiti@1.21.6) + + eslint-plugin-react-refresh@0.4.14(eslint@9.15.0(jiti@1.21.6)): + dependencies: + eslint: 9.15.0(jiti@1.21.6) + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@8.2.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@9.15.0(jiti@1.21.6): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0(jiti@1.21.6)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.19.0 + '@eslint/core': 0.9.0 + '@eslint/eslintrc': 3.2.0 + '@eslint/js': 9.15.0 + '@eslint/plugin-kit': 0.2.3 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.1 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.3.7 + escape-string-regexp: 4.0.0 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 1.21.6 + transitivePeerDependencies: + - supports-color + + espree@10.3.0: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.6 + + esutils@2.0.3: {} + + etag@1.8.1: {} + + events@3.3.0: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit@0.1.2: {} + + expand-template@2.0.3: {} + + expect-type@1.1.0: {} + + expect@29.7.0: + 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-session@1.18.1: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + on-headers: 1.0.2 + parseurl: 1.3.3 + safe-buffer: 5.2.1 + uid-safe: 2.1.5 + transitivePeerDependencies: + - supports-color + + express@4.21.1: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.10 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-safe-stringify@2.1.1: {} + + fast-uri@3.0.3: {} + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fecha@4.2.3: {} + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-system-cache@2.3.0: + dependencies: + fs-extra: 11.1.1 + ramda: 0.29.0 + + file-uri-to-path@1.0.0: {} + + filelist@1.0.4: + dependencies: + minimatch: 5.1.6 + + filesize@10.1.6: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + find-up@7.0.0: + dependencies: + locate-path: 7.2.0 + path-exists: 5.0.0 + unicorn-magic: 0.1.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + + flatted@3.3.1: {} + + fn.name@1.1.0: {} + + for-each@0.3.3: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + fork-ts-checker-webpack-plugin@9.0.2(typescript@5.6.3)(webpack@5.96.1): + dependencies: + '@babel/code-frame': 7.26.2 + chalk: 4.1.2 + chokidar: 3.6.0 + cosmiconfig: 8.3.6(typescript@5.6.3) + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.2 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.6.3 + tapable: 2.2.1 + typescript: 5.6.3 + webpack: 5.96.1 + + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + formidable@3.5.2: + dependencies: + dezalgo: 1.0.4 + hexoid: 2.0.0 + once: 1.4.0 + + forwarded@0.2.0: {} + + fraction.js@4.3.7: {} + + fresh@0.5.2: {} + + fs-constants@1.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-extra@11.1.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 + + fs-monkey@1.0.6: {} + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gauge@4.0.4: + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + optional: true + + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.2.4: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + + get-nonce@1.0.1: {} + + get-package-type@0.1.0: {} + + get-port@7.1.0: {} + + get-stream@6.0.1: {} + + git-raw-commits@4.0.0: + dependencies: + dargs: 8.1.0 + meow: 12.1.1 + split2: 4.2.0 + + github-from-package@0.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-promise@4.2.2(glob@7.2.3): + dependencies: + '@types/glob': 7.2.0 + glob: 7.2.3 + + glob-to-regexp@0.4.1: {} + + glob@10.4.2: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + + globals@11.12.0: {} + + globals@14.0.0: {} + + globals@15.12.0: {} + + gopd@1.0.1: + dependencies: + get-intrinsic: 1.2.4 + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + has-own-prop@2.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.0 + + has-proto@1.0.3: {} + + has-symbols@1.0.3: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.0.3 + + has-unicode@2.0.1: + optional: true + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-to-html@9.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hexoid@2.0.0: {} + + highlight.js@10.7.3: {} + + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + + html-escaper@2.0.2: {} + + html-void-elements@3.0.0: {} + + http-cache-semantics@4.1.1: + optional: true + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-proxy-agent@4.0.1: + dependencies: + '@tootallnate/once': 1.1.2 + agent-base: 6.0.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + optional: true + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.1 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + optional: true + + https-proxy-agent@7.0.5: + dependencies: + agent-base: 7.1.1 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + optional: true + + husky@9.1.6: {} + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + icss-utils@5.1.0(postcss@8.4.47): + dependencies: + postcss: 8.4.47 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + import-meta-resolve@4.1.0: {} + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + infer-owner@1.0.4: + optional: true + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + ini@4.1.1: {} + + inquirer@8.2.6: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 6.2.0 + + inquirer@9.2.15: + dependencies: + '@ljharb/through': 2.3.13 + ansi-escapes: 4.3.2 + chalk: 5.3.0 + cli-cursor: 3.1.0 + cli-width: 4.1.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 1.0.0 + ora: 5.4.1 + run-async: 3.0.0 + rxjs: 7.8.1 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + ip-address@9.0.5: + dependencies: + jsbn: 1.1.0 + sprintf-js: 1.1.3 + optional: true + + ipaddr.js@1.9.1: {} + + is-arguments@1.1.1: + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + is-arrayish@0.2.1: {} + + is-arrayish@0.3.2: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-callable@1.2.7: {} + + is-core-module@2.15.1: + dependencies: + hasown: 2.0.2 + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-generator-function@1.0.10: + dependencies: + has-tostringtag: 1.0.2 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-lambda@1.0.1: + optional: true + + is-number@7.0.0: {} + + is-obj@2.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-property@1.0.2: {} + + is-stream@2.0.1: {} + + is-text-path@2.0.0: + dependencies: + text-extensions: 2.4.0 + + is-typed-array@1.1.13: + dependencies: + which-typed-array: 1.1.15 + + is-unicode-supported@0.1.0: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isarray@1.0.0: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@5.2.1: + dependencies: + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.26.0 + '@babel/parser': 7.26.2 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.6.3 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@4.0.1: + dependencies: + debug: 4.3.7 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + iterare@1.2.1: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jake@10.9.2: + dependencies: + async: 3.2.6 + chalk: 4.1.2 + filelist: 1.0.4 + minimatch: 3.1.2 + + jest-changed-files@29.7.0: + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + jest-circus@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.9.0 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.3 + is-generator-fn: 2.1.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.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + exit: 0.1.2 + import-local: 3.2.0 + jest-config: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jest-config@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + dependencies: + '@babel/core': 7.26.0 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.0) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + 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.8 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 20.17.6 + ts-node: 10.9.2(@types/node@20.17.6)(typescript@5.6.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@29.7.0: + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-docblock@29.7.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@29.7.0: + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + jest-environment-node@29.7.0: + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 22.9.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + jest-get-type@29.6.3: {} + + jest-haste-map@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 20.17.6 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.8 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + jest-leak-detector@29.7.0: + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-matcher-utils@29.7.0: + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + jest-message-util@29.7.0: + dependencies: + '@babel/code-frame': 7.26.2 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.8 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 22.9.0 + jest-util: 29.7.0 + + jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + optionalDependencies: + jest-resolve: 29.7.0 + + jest-regex-util@29.6.3: {} + + jest-resolve-dependencies@29.7.0: + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@29.7.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.8 + resolve.exports: 2.0.2 + slash: 3.0.0 + + jest-runner@29.7.0: + 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': 20.17.6 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + 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 + transitivePeerDependencies: + - supports-color + + jest-runtime@29.7.0: + 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': 20.17.6 + chalk: 4.1.2 + cjs-module-lexer: 1.4.1 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + 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 + transitivePeerDependencies: + - supports-color + + jest-snapshot@29.7.0: + dependencies: + '@babel/core': 7.26.0 + '@babel/generator': 7.26.2 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.0) + '@babel/types': 7.26.0 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.1.0(@babel/core@7.26.0) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + 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.6.3 + transitivePeerDependencies: + - supports-color + + jest-util@29.7.0: + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.17.6 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + jest-validate@29.7.0: + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + jest-watcher@29.7.0: + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.17.6 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + jest-worker@27.5.1: + dependencies: + '@types/node': 22.9.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@29.7.0: + dependencies: + '@types/node': 22.9.0 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + '@jest/types': 29.6.3 + import-local: 3.2.0 + jest-cli: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + jiti@1.21.6: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsbn@1.1.0: + optional: true + + jsdoc-type-pratt-parser@4.1.0: {} + + jsdom@25.0.1: + dependencies: + cssstyle: 4.1.0 + data-urls: 5.0.0 + decimal.js: 10.4.3 + form-data: 4.0.1 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.5 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.13 + parse5: 7.2.1 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.0.0 + ws: 8.18.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.0.2: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonc-parser@3.2.1: {} + + jsonc-parser@3.3.1: {} + + jsonfile@6.1.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonparse@1.3.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@3.0.3: {} + + kuler@2.0.0: {} + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + libphonenumber-js@1.11.14: {} + + lilconfig@2.1.0: {} + + lilconfig@3.1.2: {} + + lines-and-columns@1.2.4: {} + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + loader-runner@4.3.0: {} + + loader-utils@2.0.4: + dependencies: + big.js: 5.2.2 + emojis-list: 3.0.0 + json5: 2.2.3 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + locate-path@7.2.0: + dependencies: + p-locate: 6.0.0 + + lodash.camelcase@4.3.0: {} + + lodash.isplainobject@4.0.6: {} + + lodash.kebabcase@4.1.1: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash.mergewith@4.6.2: {} + + lodash.snakecase@4.1.1: {} + + lodash.startcase@4.4.0: {} + + lodash.uniq@4.5.0: {} + + lodash.upperfirst@4.3.1: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + long@5.2.3: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.1.2: {} + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + optional: true + + lru-cache@7.18.3: {} + + lru.min@1.1.1: {} + + lucide-react@0.454.0(react@18.3.1): + dependencies: + react: 18.3.1 + + lunr@2.3.9: {} + + lz-string@1.5.0: {} + + magic-string@0.27.0: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + magic-string@0.30.12: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + magic-string@0.30.8: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + make-dir@4.0.0: + dependencies: + semver: 7.6.3 + + make-error@1.3.6: {} + + make-fetch-happen@9.1.0: + dependencies: + agentkeepalive: 4.5.0 + cacache: 15.3.0 + http-cache-semantics: 4.1.1 + http-proxy-agent: 4.0.1 + https-proxy-agent: 5.0.1 + is-lambda: 1.0.1 + lru-cache: 6.0.0 + minipass: 3.3.6 + minipass-collect: 1.0.2 + minipass-fetch: 1.4.1 + minipass-flush: 1.0.5 + minipass-pipeline: 1.2.4 + negotiator: 0.6.3 + promise-retry: 2.0.1 + socks-proxy-agent: 6.2.1 + ssri: 8.0.1 + transitivePeerDependencies: + - bluebird + - supports-color + optional: true + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + map-or-similar@1.5.0: {} + + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.0 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdurl@2.0.0: {} + + media-typer@0.3.0: {} + + memfs@3.5.3: + dependencies: + fs-monkey: 1.0.6 + + memoizerific@1.11.3: + dependencies: + map-or-similar: 1.5.0 + + meow@12.1.1: {} + + merge-descriptors@1.0.3: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + methods@1.1.2: {} + + micromark-util-character@2.1.0: + dependencies: + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + + micromark-util-encode@2.0.0: {} + + micromark-util-sanitize-uri@2.0.0: + dependencies: + micromark-util-character: 2.1.0 + micromark-util-encode: 2.0.0 + micromark-util-symbol: 2.0.0 + + micromark-util-symbol@2.0.0: {} + + micromark-util-types@2.0.0: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + + mimic-response@3.1.0: {} + + min-indent@1.0.1: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@5.1.6: + dependencies: + brace-expansion: 2.0.1 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minipass-collect@1.0.2: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-fetch@1.4.1: + dependencies: + minipass: 3.3.6 + minipass-sized: 1.0.3 + minizlib: 2.1.2 + optionalDependencies: + encoding: 0.1.13 + optional: true + + minipass-flush@1.0.5: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-pipeline@1.2.4: + dependencies: + minipass: 3.3.6 + optional: true + + minipass-sized@1.0.3: + dependencies: + minipass: 3.3.6 + optional: true + + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} + + minipass@7.1.2: {} + + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp-classic@0.5.3: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mkdirp@1.0.4: {} + + mkdirp@2.1.6: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + multer@1.4.4-lts.1: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 1.6.2 + mkdirp: 0.5.6 + object-assign: 4.1.1 + type-is: 1.6.18 + xtend: 4.0.2 + + mute-stream@0.0.8: {} + + mute-stream@1.0.0: {} + + mysql2@3.11.4: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.6.3 + long: 5.2.3 + lru.min: 1.1.1 + named-placeholders: 1.1.3 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + named-placeholders@1.1.3: + dependencies: + lru-cache: 7.18.3 + + nanoid@3.3.7: {} + + napi-build-utils@1.0.2: {} + + natural-compare@1.4.0: {} + + negotiator@0.6.3: {} + + neo-async@2.6.2: {} + + nest-winston@1.9.7(@nestjs/common@10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(winston@3.17.0): + dependencies: + '@nestjs/common': 10.4.7(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + fast-safe-stringify: 2.1.1 + winston: 3.17.0 + + node-abi@3.71.0: + dependencies: + semver: 7.6.3 + + node-abort-controller@3.1.1: {} + + node-addon-api@7.1.1: {} + + node-emoji@1.11.0: + dependencies: + lodash: 4.17.21 + + node-fetch@2.7.0(encoding@0.1.13): + dependencies: + whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 + + node-gyp@8.4.1: + dependencies: + env-paths: 2.2.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + make-fetch-happen: 9.1.0 + nopt: 5.0.0 + npmlog: 6.0.2 + rimraf: 3.0.2 + semver: 7.6.3 + tar: 6.2.1 + which: 2.0.2 + transitivePeerDependencies: + - bluebird + - supports-color + optional: true + + node-int64@0.4.0: {} + + node-releases@2.0.18: {} + + nopt@5.0.0: + dependencies: + abbrev: 1.1.1 + optional: true + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + npmlog@6.0.2: + dependencies: + are-we-there-yet: 3.0.1 + console-control-strings: 1.1.0 + gauge: 4.0.4 + set-blocking: 2.0.0 + optional: true + + nwsapi@2.2.13: {} + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + on-headers@1.0.2: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + one-time@1.0.0: + dependencies: + fn.name: 1.1.0 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + oniguruma-to-js@0.4.3: + dependencies: + regex: 4.4.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + 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: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + os-tmpdir@1.0.2: {} + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@4.0.0: + dependencies: + yocto-queue: 1.1.1 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-locate@6.0.0: + dependencies: + p-limit: 4.0.0 + + p-map@4.0.0: + dependencies: + aggregate-error: 3.1.0 + optional: true + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.26.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + + parse5@5.1.1: {} + + parse5@6.0.1: {} + + parse5@7.2.1: + dependencies: + entities: 4.5.0 + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-exists@5.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-to-regexp@0.1.10: {} + + path-to-regexp@3.3.0: {} + + path-type@4.0.0: {} + + pathe@1.1.2: {} + + pathval@2.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.1: {} + + picomatch@4.0.2: {} + + pify@2.3.0: {} + + pirates@4.0.6: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pluralize@8.0.0: {} + + polished@4.3.1: + dependencies: + '@babel/runtime': 7.26.0 + + possible-typed-array-names@1.0.0: {} + + postcss-import@15.1.0(postcss@8.4.47): + dependencies: + postcss: 8.4.47 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + + postcss-js@4.0.1(postcss@8.4.47): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.47 + + postcss-load-config@4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3)): + dependencies: + lilconfig: 3.1.2 + yaml: 2.6.0 + optionalDependencies: + postcss: 8.4.47 + ts-node: 10.9.2(@types/node@22.9.0)(typescript@5.6.3) + + postcss-loader@7.3.4(postcss@8.4.47)(typescript@5.6.3)(webpack@5.96.1(esbuild@0.21.5)): + dependencies: + cosmiconfig: 8.3.6(typescript@5.6.3) + jiti: 1.21.6 + postcss: 8.4.47 + semver: 7.6.3 + webpack: 5.96.1(esbuild@0.21.5) + transitivePeerDependencies: + - typescript + + postcss-modules-extract-imports@3.1.0(postcss@8.4.47): + dependencies: + postcss: 8.4.47 + + postcss-modules-local-by-default@4.0.5(postcss@8.4.47): + dependencies: + icss-utils: 5.1.0(postcss@8.4.47) + postcss: 8.4.47 + postcss-selector-parser: 6.1.2 + postcss-value-parser: 4.2.0 + + postcss-modules-scope@3.2.0(postcss@8.4.47): + dependencies: + postcss: 8.4.47 + postcss-selector-parser: 6.1.2 + + postcss-modules-values@4.0.0(postcss@8.4.47): + dependencies: + icss-utils: 5.1.0(postcss@8.4.47) + postcss: 8.4.47 + + postcss-nested@6.2.0(postcss@8.4.47): + dependencies: + postcss: 8.4.47 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.4.47: + dependencies: + nanoid: 3.3.7 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prebuild-install@7.1.2: + dependencies: + detect-libc: 2.0.3 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 1.0.2 + node-abi: 3.71.0 + pump: 3.0.2 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.1 + tunnel-agent: 0.6.0 + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.3.3: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + process-nextick-args@2.0.1: {} + + process@0.11.10: {} + + promise-inflight@1.0.1: + optional: true + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + optional: true + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + property-information@6.5.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pump@3.0.2: + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + + punycode.js@2.3.1: {} + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + qs@6.13.0: + dependencies: + side-channel: 1.0.6 + + queue-microtask@1.2.3: {} + + ramda@0.29.0: {} + + random-bytes@1.0.0: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-confetti@6.1.0(react@18.3.1): + dependencies: + react: 18.3.1 + tween-functions: 1.2.0 + + react-docgen-typescript@2.2.2(typescript@5.6.3): + dependencies: + typescript: 5.6.3 + + react-docgen@7.1.0: + dependencies: + '@babel/core': 7.26.0 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.6 + '@types/doctrine': 0.0.9 + '@types/resolve': 1.20.6 + doctrine: 3.0.0 + resolve: 1.22.8 + strip-indent: 4.0.0 + transitivePeerDependencies: + - supports-color + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-error-boundary@3.1.4(react@18.3.1): + dependencies: + '@babel/runtime': 7.26.0 + react: 18.3.1 + + react-is@17.0.2: {} + + react-is@18.3.1: {} + + react-refresh@0.14.2: {} + + react-remove-scroll-bar@2.3.6(@types/react@18.3.12)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.12 + + react-remove-scroll@2.5.5(@types/react@18.3.12)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.6(@types/react@18.3.12)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.2(@types/react@18.3.12)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.12)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + + react-remove-scroll@2.6.0(@types/react@18.3.12)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.6(@types/react@18.3.12)(react@18.3.1) + react-style-singleton: 2.2.1(@types/react@18.3.12)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.2(@types/react@18.3.12)(react@18.3.1) + use-sidecar: 1.1.2(@types/react@18.3.12)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + + react-router-dom@6.28.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.21.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.28.0(react@18.3.1) + + react-router@6.28.0(react@18.3.1): + dependencies: + '@remix-run/router': 1.21.0 + react: 18.3.1 + + react-style-singleton@2.2.1(@types/react@18.3.12)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + invariant: 2.2.4 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.12 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + recast@0.23.9: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + reflect-metadata@0.2.2: {} + + regenerator-runtime@0.14.1: {} + + regex-parser@2.3.0: {} + + regex@4.4.0: {} + + repeat-string@1.6.1: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + resolve-url-loader@5.0.0: + dependencies: + adjust-sourcemap-loader: 4.0.0 + convert-source-map: 1.9.0 + loader-utils: 2.0.4 + postcss: 8.4.47 + source-map: 0.6.1 + + resolve.exports@2.0.2: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + retry@0.12.0: + optional: true + + reusify@1.0.4: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + optional: true + + rollup@4.24.4: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.24.4 + '@rollup/rollup-android-arm64': 4.24.4 + '@rollup/rollup-darwin-arm64': 4.24.4 + '@rollup/rollup-darwin-x64': 4.24.4 + '@rollup/rollup-freebsd-arm64': 4.24.4 + '@rollup/rollup-freebsd-x64': 4.24.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.24.4 + '@rollup/rollup-linux-arm-musleabihf': 4.24.4 + '@rollup/rollup-linux-arm64-gnu': 4.24.4 + '@rollup/rollup-linux-arm64-musl': 4.24.4 + '@rollup/rollup-linux-powerpc64le-gnu': 4.24.4 + '@rollup/rollup-linux-riscv64-gnu': 4.24.4 + '@rollup/rollup-linux-s390x-gnu': 4.24.4 + '@rollup/rollup-linux-x64-gnu': 4.24.4 + '@rollup/rollup-linux-x64-musl': 4.24.4 + '@rollup/rollup-win32-arm64-msvc': 4.24.4 + '@rollup/rollup-win32-ia32-msvc': 4.24.4 + '@rollup/rollup-win32-x64-msvc': 4.24.4 + fsevents: 2.3.3 + + rrweb-cssom@0.7.1: {} + + run-async@2.4.1: {} + + run-async@3.0.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.1: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + sass-loader@13.3.3(webpack@5.96.1(esbuild@0.21.5)): + dependencies: + neo-async: 2.6.2 + webpack: 5.96.1(esbuild@0.21.5) + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + ajv-keywords: 3.5.2(ajv@6.12.6) + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.6.3: {} + + send@0.19.0: + 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 + transitivePeerDependencies: + - supports-color + + seq-queue@0.0.5: {} + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + set-blocking@2.0.0: + optional: true + + set-function-length@1.2.2: + 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 + + setprototypeof@1.2.0: {} + + sha.js@2.4.11: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shiki@1.22.2: + dependencies: + '@shikijs/core': 1.22.2 + '@shikijs/engine-javascript': 1.22.2 + '@shikijs/engine-oniguruma': 1.22.2 + '@shikijs/types': 1.22.2 + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + + shimmer@1.2.1: {} + + side-channel@1.0.6: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.2 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + + sisteransi@1.0.5: {} + + slash@3.0.0: {} + + smart-buffer@4.2.0: + optional: true + + socket.io-adapter@2.5.5: + dependencies: + debug: 4.3.7 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + + socket.io-client@4.8.1: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + engine.io-client: 6.6.2 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + socket.io@4.8.0: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.7 + engine.io: 6.6.2 + socket.io-adapter: 2.5.5 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + optional: true + + socks-proxy-agent@6.2.1: + dependencies: + agent-base: 6.0.2 + debug: 4.3.7 + socks: 2.8.3 + transitivePeerDependencies: + - supports-color + optional: true + + socks@2.8.3: + dependencies: + ip-address: 9.0.5 + smart-buffer: 4.2.0 + optional: true + + source-map-js@1.2.1: {} + + source-map-support@0.5.13: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + space-separated-tokens@2.0.2: {} + + split2@4.2.0: {} + + sprintf-js@1.0.3: {} + + sprintf-js@1.1.3: + optional: true + + sqlite3@5.1.7: + dependencies: + bindings: 1.5.0 + node-addon-api: 7.1.1 + prebuild-install: 7.1.2 + tar: 6.2.1 + optionalDependencies: + node-gyp: 8.4.1 + transitivePeerDependencies: + - bluebird + - supports-color + + sqlstring@2.3.3: {} + + ssri@8.0.1: + dependencies: + minipass: 3.3.6 + optional: true + + stack-chain@1.3.7: {} + + stack-trace@0.0.10: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + stackback@0.0.2: {} + + statuses@2.0.1: {} + + std-env@3.8.0: {} + + store2@2.14.3: {} + + storybook@8.4.2(prettier@3.3.3): + dependencies: + '@storybook/core': 8.4.2(prettier@3.3.3) + optionalDependencies: + prettier: 3.3.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + streamsearch@1.1.0: {} + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + strip-indent@4.0.0: + dependencies: + min-indent: 1.0.1 + + strip-json-comments@2.0.1: {} + + strip-json-comments@3.1.1: {} + + style-loader@3.3.4(webpack@5.96.1(esbuild@0.21.5)): + dependencies: + webpack: 5.96.1(esbuild@0.21.5) + + sucrase@3.35.0: + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + commander: 4.1.1 + glob: 10.4.5 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.6 + ts-interface-checker: 0.1.13 + + superagent@9.0.2: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.3.7 + fast-safe-stringify: 2.1.1 + form-data: 4.0.1 + formidable: 3.5.2 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.13.0 + transitivePeerDependencies: + - supports-color + + supertest@7.0.0: + dependencies: + methods: 1.1.2 + superagent: 9.0.2 + transitivePeerDependencies: + - supports-color + + superwstest@2.0.4(supertest@7.0.0): + dependencies: + '@types/supertest': 6.0.2 + '@types/ws': 8.5.13 + ws: 8.18.0 + optionalDependencies: + supertest: 7.0.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + swagger-ui-dist@5.18.2: + dependencies: + '@scarf/scarf': 1.4.0 + + symbol-observable@4.0.0: {} + + symbol-tree@3.2.4: {} + + synchronous-promise@2.0.17: {} + + synckit@0.9.2: + dependencies: + '@pkgr/core': 0.1.1 + tslib: 2.8.1 + + tailwind-merge@2.5.4: {} + + tailwindcss-animate@1.0.7(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3))): + dependencies: + tailwindcss: 3.4.14(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3)) + + tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3)): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.6 + lilconfig: 2.1.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.4.47 + postcss-import: 15.1.0(postcss@8.4.47) + postcss-js: 4.0.1(postcss@8.4.47) + postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3)) + postcss-nested: 6.2.0(postcss@8.4.47) + postcss-selector-parser: 6.1.2 + resolve: 1.22.8 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + + tapable@2.2.1: {} + + tar-fs@2.1.1: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.2 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.4 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@6.2.1: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + + telejson@7.2.0: + dependencies: + memoizerific: 1.11.3 + + terser-webpack-plugin@5.3.10(esbuild@0.21.5)(webpack@5.96.1(esbuild@0.21.5)): + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.36.0 + webpack: 5.96.1(esbuild@0.21.5) + optionalDependencies: + esbuild: 0.21.5 + + terser-webpack-plugin@5.3.10(webpack@5.96.1): + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.36.0 + webpack: 5.96.1 + + terser@5.36.0: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.14.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + text-extensions@2.4.0: {} + + text-hex@1.0.0: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + through@2.3.8: {} + + tiny-invariant@1.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@0.3.1: {} + + tinypool@1.0.2: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + + tldts-core@6.1.61: {} + + tldts@6.1.61: + dependencies: + tldts-core: 6.1.61 + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + tmpl@1.0.5: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + tough-cookie@5.0.0: + dependencies: + tldts: 6.1.61 + + tr46@0.0.3: {} + + tr46@5.0.0: + dependencies: + punycode: 2.3.1 + + tree-kill@1.2.2: {} + + trim-lines@3.0.1: {} + + triple-beam@1.4.1: {} + + ts-api-utils@1.4.0(typescript@5.6.3): + dependencies: + typescript: 5.6.3 + + ts-dedent@2.2.0: {} + + ts-interface-checker@0.1.13: {} + + ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)))(typescript@5.6.3): + dependencies: + bs-logger: 0.2.6 + ejs: 3.1.10 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@20.17.6)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.6.3 + typescript: 5.6.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.26.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.26.0) + + ts-loader@9.5.1(typescript@5.6.3)(webpack@5.96.1): + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.17.1 + micromatch: 4.0.8 + semver: 7.6.3 + source-map: 0.7.4 + typescript: 5.6.3 + webpack: 5.96.1 + + ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.17.6 + acorn: 8.14.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.6.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + ts-node@10.9.2(@types/node@22.9.0)(typescript@5.6.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.9.0 + acorn: 8.14.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.6.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + + tsconfig-paths-webpack-plugin@4.1.0: + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.17.1 + tsconfig-paths: 4.2.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.7.0: {} + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + tween-functions@1.2.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.21.3: {} + + type-fest@2.19.0: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typedarray@0.0.6: {} + + typedoc@0.26.11(typescript@5.6.3): + dependencies: + lunr: 2.3.9 + markdown-it: 14.1.0 + minimatch: 9.0.5 + shiki: 1.22.2 + typescript: 5.6.3 + yaml: 2.6.0 + + typeorm-transactional@0.5.0(reflect-metadata@0.2.2)(typeorm@0.3.20(mysql2@3.11.4)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3))): + dependencies: + '@types/cls-hooked': 4.3.9 + cls-hooked: 4.2.2 + reflect-metadata: 0.2.2 + semver: 7.6.3 + typeorm: 0.3.20(mysql2@3.11.4)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)) + + typeorm@0.3.20(mysql2@3.11.4)(sqlite3@5.1.7)(ts-node@10.9.2(@types/node@20.17.6)(typescript@5.6.3)): + dependencies: + '@sqltools/formatter': 1.2.5 + app-root-path: 3.1.0 + buffer: 6.0.3 + chalk: 4.1.2 + cli-highlight: 2.1.11 + dayjs: 1.11.13 + debug: 4.3.7 + dotenv: 16.4.5 + glob: 10.4.5 + mkdirp: 2.1.6 + reflect-metadata: 0.2.2 + sha.js: 2.4.11 + tslib: 2.8.1 + uuid: 9.0.1 + yargs: 17.7.2 + optionalDependencies: + mysql2: 3.11.4 + sqlite3: 5.1.7 + ts-node: 10.9.2(@types/node@20.17.6)(typescript@5.6.3) + transitivePeerDependencies: + - supports-color + + typescript-eslint@8.13.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) + '@typescript-eslint/parser': 8.13.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) + '@typescript-eslint/utils': 8.13.0(eslint@9.15.0(jiti@1.21.6))(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - eslint + - supports-color + + typescript@5.6.3: {} + + uc.micro@2.1.0: {} + + uid-safe@2.1.5: + dependencies: + random-bytes: 1.0.0 + + uid@2.0.2: + dependencies: + '@lukeed/csprng': 1.1.0 + + undici-types@6.19.8: {} + + unicorn-magic@0.1.0: {} + + unique-filename@1.1.1: + dependencies: + unique-slug: 2.0.2 + optional: true + + unique-slug@2.0.2: + dependencies: + imurmurhash: 0.1.4 + optional: true + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unplugin@1.15.0(webpack-sources@3.2.3): + dependencies: + acorn: 8.14.0 + webpack-virtual-modules: 0.6.2 + optionalDependencies: + webpack-sources: 3.2.3 + + update-browserslist-db@1.1.1(browserslist@4.24.2): + dependencies: + browserslist: 4.24.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.2(@types/react@18.3.12)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.12 + + use-resize-observer@9.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@juggle/resize-observer': 3.4.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + use-sidecar@1.1.2(@types/react@18.3.12)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.12 + + util-deprecate@1.0.2: {} + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.1.1 + is-generator-function: 1.0.10 + is-typed-array: 1.1.13 + which-typed-array: 1.1.15 + + utils-merge@1.0.1: {} + + uuid@9.0.1: {} + + v8-compile-cache-lib@3.0.1: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + validator@13.12.0: {} + + vary@1.1.2: {} + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + vite-node@2.1.5(@types/node@22.9.0)(terser@5.36.0): + dependencies: + cac: 6.7.14 + debug: 4.3.7 + es-module-lexer: 1.5.4 + pathe: 1.1.2 + vite: 5.4.10(@types/node@22.9.0)(terser@5.36.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.10(@types/node@22.9.0)(terser@5.36.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.47 + rollup: 4.24.4 + optionalDependencies: + '@types/node': 22.9.0 + fsevents: 2.3.3 + terser: 5.36.0 + + vitest@2.1.5(@types/node@22.9.0)(jsdom@25.0.1)(terser@5.36.0): + dependencies: + '@vitest/expect': 2.1.5 + '@vitest/mocker': 2.1.5(vite@5.4.10(@types/node@22.9.0)(terser@5.36.0)) + '@vitest/pretty-format': 2.1.5 + '@vitest/runner': 2.1.5 + '@vitest/snapshot': 2.1.5 + '@vitest/spy': 2.1.5 + '@vitest/utils': 2.1.5 + chai: 5.1.2 + debug: 4.3.7 + expect-type: 1.1.0 + magic-string: 0.30.12 + pathe: 1.1.2 + std-env: 3.8.0 + tinybench: 2.9.0 + tinyexec: 0.3.1 + tinypool: 1.0.2 + tinyrainbow: 1.2.0 + vite: 5.4.10(@types/node@22.9.0)(terser@5.36.0) + vite-node: 2.1.5(@types/node@22.9.0)(terser@5.36.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.9.0 + jsdom: 25.0.1 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + watchpack@2.4.2: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webidl-conversions@3.0.1: {} + + webidl-conversions@7.0.0: {} + + webpack-node-externals@3.0.0: {} + + webpack-sources@3.2.3: {} + + webpack-virtual-modules@0.6.2: {} + + webpack@5.96.1: + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.6 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.14.0 + browserslist: 4.24.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.17.1 + es-module-lexer: 1.5.4 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.10(webpack@5.96.1) + watchpack: 2.4.2 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + webpack@5.96.1(esbuild@0.21.5): + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.6 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.14.0 + browserslist: 4.24.2 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.17.1 + es-module-lexer: 1.5.4 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.10(esbuild@0.21.5)(webpack@5.96.1(esbuild@0.21.5)) + watchpack: 2.4.2 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.0.0: + dependencies: + tr46: 5.0.0 + webidl-conversions: 7.0.0 + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which-typed-array@1.1.15: + 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@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wide-align@1.1.5: + dependencies: + string-width: 4.2.3 + optional: true + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + winston@3.17.0: + dependencies: + '@colors/colors': 1.6.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.6 + is-stream: 2.0.1 + logform: 2.7.0 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.5.0 + stack-trace: 0.0.10 + triple-beam: 1.4.1 + winston-transport: 4.9.0 + + word-wrap@1.2.5: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + write-file-atomic@4.0.2: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + ws@8.17.1: {} + + ws@8.18.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + xmlhttprequest-ssl@2.1.2: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@4.0.0: {} + + yaml@2.6.0: {} + + yargs-parser@20.2.9: {} + + yargs-parser@21.1.1: {} + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + yocto-queue@1.1.1: {} + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..291c01f --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +packages: + # include packages in subfolders (e.g. apps/ and packages/) + - "apps/**" + - 'packages/**' + # if required, exclude some directories + - '!**/test/**' \ No newline at end of file diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..0138a33 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "@shared/*": ["packages/shared/src/*"] + } + } +} diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000..9d21e00 --- /dev/null +++ b/typedoc.json @@ -0,0 +1,18 @@ +{ + "entryPoints": [ + "apps/backend/src", + "apps/frontend/src", + "packages/shared/src" + ], + "entryPointStrategy": "expand", + "out": "docs", + "includeVersion": true, + "exclude": [ + "**/*.spec.ts", + "**/node_modules/**" + ], + "excludeExternals": true, + "excludePrivate": true, + "excludeProtected": true, + "readme": "README.md" +}