From 33849f4e5b9e635ce8dde0d39598e35d5d3d505f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=82=98=EC=95=84=EC=97=B0?= <110026001+ay-eonii@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:43:46 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Main=20=EB=B8=8C=EB=9E=9C=EC=B9=98=EC=97=90?= =?UTF-8?q?=20=EB=B3=91=ED=95=A9=20(#770)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 회원 알림 테이블에 다락방 ID 컬럼 추가 * feat:마지막 다락방 아이디를 저장/삭제할 함수 구현 * feat(apiClient): 마지막 다락방 id를 가지고 요청을 보내는 함수 구현 * feat:lastDarakbangId를 활용하는 함수로 변경 * feat: Notification 타입에 redirectUrl 속성 추가 * feat: NotificationCardProps 타입울 div로 확장 * feat: removeBaseUrl 유틸 함수 작성 * feat: 알림 버튼 클릭 시, 관련 페이지로 이동하는 핸들러 작성 * feat: storybook을 arg형식에 맞게 수정 * refactor: FirebaseApp 중복 초기화 방지 코드 추가 * refactor: Id 도메인 명시 * feat: 다락방의 모임, 다락방의 해주세요 검증 AOP 파라미터 순서 강제 * refactor: 기존 알림 기능에 Darakbang 및 DarakbangMember의 변경 사항 반영 * 테니 요청사항 * feat:쿼리키에 다락방과 최근 다락방 아이디를 넣음 * feat: 다락방 검증 응답에 다락방 아이디 추가 * refactor: 파라미터 순서 변경 * feat:다락방 선택페이지를 통해 최근 다락방을 수정할 수 있도록 구현 * feat:다락방 메인페이지를 통해 최근 다락방을 수정할 수 있도록 구현 * feat: 다락방 이름 조회 swagger * feat: 다락방 아이디로 이름 조회 * feat:뒤로가기가 홈일 필요가 없는 navigate를 -1로 지정 * feat:다락방 메인 페이지 route 구현 * feat: 지금 다락방 메인으로 가는 링크 구현 * feat:지금 다락방 메인으로 가는 Navigate 구현 * chore: 스토리북 별칭 경로 올바르게 설정 * feat: Card를 클릭할 때 배경색 변경 * test:마지막 다락방 Id를 이용하게 테스트 변경 * feature: 모임 만들기 API 연동 * refactor: 모임 만들기 비즈니스 로직 위치 변경 및 이름 수정 * refactor: UI 로직 분리 * mainfest.json 작성 * feat: 앱 아이콘 이미지 추가 * feat:URL에 darakbang을 사용하는 페이지 변경 * chore: public 폴더 안의 정적 파일 빌드 폴더에 추가 * feat: 알림을 보낼 때 유효하지 않은 토큰에 대한 응답이 오면 토큰 삭제 * feat: 토큰 저장 변경 시 타임스탬프를 함께 갱신하는 기능 구현 저장 시 타임스탬프를 저장한다. 변경 시 타임스탬프를 변경하고 토큰을 변경한다. * feat: 스케줄링을 사용해 토큰이 1개월 이상 비활성화 상태이면 제거 * feat: 토큰 저장 시 원래 있는 토큰이면 새로 저장하지 않도록 수정 * refactor: 사용하지 않는 메서드 제거 * refactor: 사용하지 않는 클래스 제거 * refactor: 퍼널 로직 추상화 및 모임 만들기 로직 리팩토링 * refactor: AOP 대신 moim, please 에서 다락방ID 검증 로직 추가 * style: console log 삭제 * fix: postNotificationToken을 수정 * fix: 파라미터 순서 변경 * refactor: 다락방 멤버 타입 Long으로 변경 * fix: 다락방 멤버의 멤버아이디 가져오기 * test: 모임 만들기 훅 테스트 * feat:다락방 코드를 로컬스토리지에 저장 * feat: 다락방 코드로 입장 페이지 onClick 구현 * feat:다락방 닉네임 입력 페이지에서 로컬스토리지 코드 활용 * feat:다락방 참여시 들어가는 루트에서 로컬 스토리지 코드 사용 * feat: 브라우저가 닫힐 때 참여 코드 삭제 구현 * refactor: 다락방 멤버 필드 변수명 `darakbangMember`로 변경 * feat: 다락방 멤버 검증 전 다락방 존재 여부 확인 * test: useFunnel 테스트 코드 추가 및 불필요한 테스트 삭제 * chore: application-prod.yml 설정 파일 생성 * feat: 알림을 보내는 URL을 수정된 버전과 동기화 * fix: 다락방 아이디를 추가하여 버그 수정 * refactor: 불필요한 코드 제거 * refactor: 예외 메시지 변경 * fix : 'serviceWorker' in navigator의 로직을 변경 * feat:카카오 로그인 이후 참가 링크가 있는지 여부에 따라 분기처리 * chore: plain-jar 제거 설정 * feat:홈 페이지 분기 처리 구현 * fix: 테스트 로그인 api 삭제 * fix: 참여하지 않은 경우 예외 던지는 로직 제거 * feat:'/' 로 들어갔을 때 분기처리 * fix: 다락방 생성 후 모임목록을 제대로 못불러오던 버그 수정 * fix: safari에서는 유저 제스쳐 상황에서만 알림 허용가능으로 인한 로직 변경 * fix: 훅 위치 변경 * fix: 알림 권항 요청 위치 수정 * refactor: 최종 유효성 검사 로직 훅으로 분리 * Create cd-prod.yml * fix:잘못된 route들을 알맞게 변경 * rename: 파일 이름 통일 * feat: 404페이지 구현 * test: storybook css 순서 수정 * feat: NotFoundPage 라우터 연결 * test: storybook 테스트 코드 작성 * feat: bottomButton 최대 너비 지정 * fix: 머지할 때 소파가 망가뜨린 코드 수정 * feat: apiError를 처리해주는 에러 페이지 구현 및 연결 * feature: open graph 적용 favicon 적용 * refactor: 다락방에 있는 모임/해주세요인지 확인하는 메서드명 변경 * feat: 알림 카드 클릭 이벤트 연결 * feat:addBaseUrl에서 /를 자동으로 넣는 코드 삭제 * feat: 지금 다락방 이름을 가져오는 훅 구현 * feat: 하드코딩 된 헤더, 닉네임 변경 * feat: 다른 다락방 링크로 들어갔을 때에 링크의 다락방으로 안들어가짐 * fix:하단 해주세요 네비게이션바 클릭 시 해당 페이지로 이동 * fix(auth):주소에서 미 기재된 / 추가 * feat:로그아웃과 다락방 메뉴 추가 * fix: 다락방 멤버의 멤버아이디가 아닌 다락방 멤버의 아이디로 참여 조회 * feat: 홈 헤더 클릭시 메뉴 나오게 설정 * fix: 알림에서 에러 수정 * fix: 홈 화면 네비게이션에 홈 화면 확인하도록 수정 * fix: 다락방 랜딩페이지를 사용 안하던 버그 수정 * fix(HomePage): navigate가 아닌 로 변경 * fix:해주세요 페이지의 헤더 다락방 이름으로 변경 * feat:navigationBar에서 포인터 커서 삭제 * feat:navigationBar에서 포인터 커서 삭제 * refactor: 개발 프로파일 설정을 제거 * fix: 존재하는 참여코드일 시 뒤로가기 무한루프 구현 * chore: CorsConfig에 프로덕션 도메인 적용 * chore: production 환경에서만 sentry, GA가 적용되게 수정 * feat: 요청 로깅 위치를 컨트롤러로 옮김 & AOP 적용 * refactor: 인터셉터 로깅 삭제 * feat: 요청한 멤버 혹은 다락방멤버 ID 로깅 정보 추가 * refactor: DEFAULT FirebaseApp을 찾지 못하는 문제 해결 * chore: gitignore 수정 * fix:다락방 초대링크 코드 수정 * fix: 알림이 undefined로 오는 문제를 수정 * fix:다락방 참여 api를 사용할 때 내 모임으로 제대로 가지지 않는 버그 수정 * refactor: 채팅을 조회할 때 로깅하지 않도록 수정 * refactor: 채팅방 목록 조회 시 로깅 제외 * feat: 각 스탭의 입력란에 자동 포커스 및 엔터로 다음 스탭 이동 기능 구현 * fix: 모바일 키보드 UI 노출 시 버튼 가려지지 않도록 수정 * fix: 파이어베이스 예외 발생시 무시옵션 추가 * fix: 헤더 내부 요소 간격 문제 해결 * feat:다락방 생성 페이지에서 최대 다락방 이름 크기 제한 * fix 케밥 메뉴의 폰트 크기를 변경 * feat: 해주세요 조회 내림차순 조회 * feat: 찜한 모임 조회 내림차순 * feat: 모임 조회 내림차순 * feat: 알림 조회 내림차순 * fix: 로그인 만료된 토큰 에러 처리 * test: 테스트 수정 * feat: 모집중인 모임만 조회 * feat: 다락방 이름 래퍼 컴포넌트 구현 * feat(OptionPanel):최대 길이 및 dimmer 구현 * feat: 다락방 이름 래퍼 및 panel height 추가 * feat: 해주세요 만들기 중복 제출 방지 * feat: 모임 생성 페이지 중복 제출 방지 * feat: 모임 상세페이지 중복 제출 방지 * fix: URL로 다락방, 모임 접근시 예외 처리 * feat: 채팅방 중복 제출 방지 * feat: 케밥 메뉴 배열 타입 변경 * feat: 메세지 제출 중복 제출 방지 * test: ChattingFooter 요구 props 변경 * feat: 댓글 중복 제출 방지 * fix: PWD 이름 변경 * feature: 모임 만들기 맞지막 스탭 포커싱 * feat: 다락방 이름 래퍼의 최대 길이 조정 * fix(MainPage): 현재 다락방 알려주는 지시문자에 공백 추가 * fix(mainPage): 옵션판넬의 좌우 width 변경 * fix: 같은 다락방 참여자에게만 모임 생성 알림이 전송되도록 수정 * feat:아이콘 배경 투명화 * feat:미리보기 이미지 변경 * feat:앱 이름 및 미리보기 아이디 추가 * fix: 알림 두번오는 문제 수정 * 알림 클릭 시, 정상 url로 이동 * feature: 스켈레톤 요소 컴포넌트 * chore: action workflows에서의 runner 수정 * fix: 부모 댓글 조회 오타 수정 * chore: 배포 서버 설정 * fix: 다락방 멤버의 멤버아이디를 가져오도록 변경 * Develop backend 작업 사항 반영 (#479) * feat: 해주세요 조회 내림차순 조회 * feat: 찜한 모임 조회 내림차순 * feat: 모임 조회 내림차순 * feat: 알림 조회 내림차순 * test: 테스트 수정 * feat: 모집중인 모임만 조회 * fix: 같은 다락방 참여자에게만 모임 생성 알림이 전송되도록 수정 * fix: 부모 댓글 조회 오타 수정 * chore: 배포 서버 설정 * fix: 다락방 멤버의 멤버아이디를 가져오도록 변경 --------- Co-authored-by: MingyeomKim * feat: 알림에 대한 권한을 미 설정시, 알림에 대한 설명을 모달로 표시 * feat: 알림 모달 폰트를 s1으로 수정 * feat: 모임목록스켈레톤 * feat: 알림 차단 시, 알림 페이지에서 모달로 알림 허용 추천 모달을 띄움 * feat: 채팅목록스켈레톤 * fix: 헤더의 다락방 이름이 짧게 나오는 것을 수정 * feat: 해주세요목록스켈레톤 * feat: 모임 상세페이지 스켈레톤 구현 * feat:버튼 스켈레톤 삭제 * refactor: 방장이 자신의 모임에 댓글을 작성하면 알림을 보내지 않도록 수정 * 알림 허욜 모달 UI 문제 해결 (임시방편) * fix: 재수정 (max width) * 노티 알림 문제 해결 * fix: 모임 만들기 자동 포커싱 삭제 * 브라우저 자동 캐시 막기 * Fix/#495 (#496) (#498) * Fix/#495 (#496) * fix:다락방 초대링크 코드 수정 * fix:다락방 참여 api를 사용할 때 내 모임으로 제대로 가지지 않는 버그 수정 * fix: 알림이 undefined로 오는 문제를 수정 * fix: 채팅방 알림에 사용되는 경로 수정 --------- Co-authored-by: ss0526100 Co-authored-by: jaeml06 * feat: 채팅방 목록을 조회할 때 가장 최근에 온 메시지를 기준으로 정렬하는 기능 추가 * refactor: 채팅방 목록 정렬시 같은 기준이면 참여 순서대로 정렬하도록 수정 * feat: 해주세요 목록 조회시 관심이 많은 순서대로 조회하고, 관심이 같다면 생성된 순서대로 정렬하는 기능 추가 * refactor: 채팅방 목록 정렬에서 동일한 조건시 모임 생성 순으로 정렬하도록 수정 * refactor: 기능 수정으로 인해 실패하는 테스트 수정 * feat: 프록시 헤더를 받아들이는 설정 추가 * refactor: 사용하지 않는 메서드 제거 * feat: 비관적 쓰기 락을 통해 동시성 문제 해결 * test: 같은 회원이 동시에 참여하는 경우에 대한 동시성 테스트 * refactor: 참여 서비스 테스트에 동시성 테스트를 추가 --------- Co-authored-by: 이상진 Co-authored-by: ss0526100 Co-authored-by: jaeml06 Co-authored-by: MingyeomKim * Develop backend 병합 (#748) * style: todo 삭제 * chore: apple의 redirect url를 백엔드 서버 API로 수정 * feat: nonce를 받아 회원을 조회하여 액세스 토큰을 발급하도록 구현 * feat: BetAttributeManager 구현 * feat: 애플 서버로부터 데이터를 받아오는 기능 구현 * feat: MoimAttributeManager 구현 * fix: ChatRoomType String으로 저장 * fix: 실패하는 테스트 케이스 수정 * feat: ParticipantResolver, ParticipantResolverRegistry 구현 * test: DisplayName 추가 * test: darakbangmember nickname 주입방식 수정 * feat: MoimParticipantResolver 구현 * feat: BetParticipantResolver 구현 * feat: attributes 구현체에 title 필드 추가, 구현체 별 getAttributes 구현 * feat: ChatRoomDetails, ChatRoomDetailsFinder 구현 * feat: ChatRoomService 구현 * feat: ChatRoomDetailsResponse 구현 * refactor: findChatRoomDetails 메서드 시그니처에 darakbangId 추가, Transactional 설정 * refactor: 참여자 응답 json key 이름 변경 * feat: 모임 시간 나노초 제거 포매팅 기능 추가 * feat: equals & hashcode 재정의 * feat: ChatPreviewResponses json 응답 key 이름 변경 * refactor: ChatRoomDetailsFinder Transactional, 테스트 코드 추가 * fix: ChatPreviewResponse 변경에 따른 오류 수정 * feat: ChatRoomController 구현 * refactor: 채팅방 프리뷰, 채팅방 열기 기능을 ChatRoom 관련 책임으로 분리 * fix: 테스트 코드 NPE 해결 * test: DisplayName 추가 * fix: GET 요청에서 RequestBody를 사용함으로써 나오는 400 에러 해결 * style: 컨벤션 수정 * refactor: 댓글 작성시 알림 대상자 조회 로직 수정 및 테스트 추가 * Fix: 알림 재전송 처리에서의 오타 수정 * comment: 댓글 알림 대상자 조회 메서드에 주석 추가 * refactor: 애플 api 경로 수정 * refactor: cors 허용 경로로 apple 서버 추가 * refactor: 인터셉터 인증 허용 주소 추가 * refactor: 컨트롤러 수정 * refactor: 컨트롤러 수정 * refactor: 컨트롤러 수정 * refactor: 컨트롤러 수정 * refactor: 리다이랙션 설정 * feat: Json String을 객체로 파싱하고 code를 담아 보내도록 수정 * feat: 사용자 정보를 받을 객체 생성 * fix: Service 객체 빈 등록 * fix: apple oauth를 통해 accessToken 반환 * refactor: Loser 도메인을 Participant 도메인을 재사용하도록 변경 * refactor: readOnly 옵션 추가 * refactor: 필드값이 널을 허용하지 않도록 수정 * fix: 멤버 이름 누락 예외 메시지 변경 * refactor: 예외 처리 통일 * test: 예외 리팩토링에 따른 테스트 수정 * feat: 프리뷰 응답에 참가자 정보 추가 * feat: 채팅 목록 응답에 채팅 작성자 정보 추가 * feat: Chat 도메인 및 Chat 변환 메서드 추가 * feat: chat 패키지의 ChatType 사용 * refactor: ChatEntity 대신 Chat 사용 * feat: 파일과 이미지 URL이 따로 넘어오도록 수정 * feat: 파일과 이미지 URL이 따로 넘어오도록 수정 * feat: 새로 추가된 파일이 없을 때 S3업로드 하지 않도록 구현 * feat: 기본 이미지로 변경 혹은 이미지 변경이 없는 경우 DB Profile 업데이트 * test: 채팅 알림 대상자 찾기 테스트 추가 * refactor: TODO 제거 * refactor: 미사용 필드 제거 * fix: 마이페이지 버그 수정 * feat: 채팅방 오픈 기능에 채팅방 ID를 반환하도록 수정 * fix: null 가능성 있는 필드를 조회할 때 생기는 버그 수정 * fix: 응답 형식 컨벤션이 맞지 않는 문제 수정 * feat: 기존 이미지가 있다면 S3에서 삭제한다. * feat: 알림 예외 메시지 및 커스텀 예외 추가 * refactor: 알림 이벤트 객체 생성시 채팅 / 채팅이 아닌 경우를 구분하기 위한 팩토리 메서드 추가 * feat: 채팅 알림시 날짜 / 시간을 '~월 ~일 ~시 ~분' 형식으로 보내기 위한 유틸 클래스 추가 * refactor: 알림 메시지 생성 역할 변경에 따른 NotificationType의 메시지 제거 * refactor: 변경된 사항을 채팅 서비스에 반영 * fix: 배팅 어트리뷰트 조회시 프로필 널로 인해 발생하는 오류 수정 * feat: 테스트 사용자를 두개로 늘린다. * refactor: 채팅 서비스에서의 타입에 따른 처리 수정 * test: 테스트 오류 수정 * fix: time null 로 인한 오류 수정 * feat: 인터셉터 허용 * fix: ChatType 의존성 수정 * feat: betFindResponse 프로필 url 추가 * fix: participant 변경사항 반영 * refactor: AttributeManagerRegistry 예외메시지 수정 * refactor: 예외 발생 로직 수정 * refactor: 예외 발생 로직 수정 * refactor: 채팅 알림에서 새로 추가된 베팅 기능 지원 * refactor: 애플 소셜 로그인 로직을 컨트롤러에서 서비스로 이동 * fix: ChatRoom이 ChatRoomEntity으로 의존성 제거 * fix: 추첨자 있는 안내면진다만 채팅방목록 조회 가능하도록 수정 * fix: targetid nullable 하지 않도록 변경 * fix: 채팅에서 모두 자신의 프로필로 나오는 오류 수정 * test: 주석 제거하고 케이스별 테스트 코드 작성 * test: 마이페이지 수정 테스트 작성 `@MockBean`을 Nested 내부에 작성하면 에러가 터져서 외부에 선언하였습니다. * refactor: 참여에 사용자 목록이 보이도록 생성 * refactor: 테스트용 사용자 만드는 기능 구현 * refactor: 테스트용 사용자가 prod 환경에 배포되지 않게 수정 * refactor: 닉네임 검증과정을 생성뿐 아니라 수정에도 사용 * refactor: 최대 길이 12글자 * refactor: 최대 길이 12글자로 변경 * feat: Author 추가 채팅 작성자 도메인 * fix: 기본 이미지로 변경하는 경우에도 S3에서 삭제하도록 구현 * refactor: chat DarakbangMember를 Author로 변경 * fix: 참여자 모이머인지 확인 오류 수정 * feat: 안내면진다 중복참여 예외처리 * feat: 추첨시간이 지났거나 당첨자가 이미 있는 경우 참여 예외 처리 * refactor: AccessToken payload 에 OauthType 추가 * refactor: 인터셉터에서 토큰의 정보가 Kakao 라면 예외를 발생한다. * refactor: 값 비교, 접근제어자 변경 * fix: 오타수정 * refactor: 메서드명 변경 * refactor: 클래스명 변경 * feat: role 포함하지 않는 ParticipantResponse 생성자 추가 * feat: 회원 탈퇴 API 구현 * feat: 회원 탈퇴 시 애플 서버와 통신하여 사용자 권한을 지우는 기능 구현 * feat: 애플 서버로부터 refresh token을 받아서 저장하는 기능 구현 * fix: 안내면진다 목록 정렬 수정 * fix: 실패하는 테스트 케이스 수정 * chore: redirect-uri가 달라서 생기는 문제 해결 * fix: 회원 탈퇴 시 revoke 과정 확인을 위한 디버깅 코드 추가 * refactor: 모임 정보 수정시 알림 메시지에서 수정 전 모임 이름을 사용하도록 수정 * refactor: 채팅 알림 구현 방법 및 메시지 형식 수정 * feat: 애플 회원 탈퇴 시 상태 정보를 수정하도록 변경 * refactor: 채팅 알림 메시지 세분화 * chore: 소셜 로그인 아이디를 확인하기 위한 로그 추가 * chore: 소셜 아이디를 확인하기 위한 로그 추가 * feat: 로그인 시 재가입 여부에 따라 상태 정보를 변경 * feat: 애플 로그인 시 재가입한 회원인 경우 상태만 변경하도록 수정 * test: 회원 탈퇴 논리 삭제 테스트 * feat: 재가입 시 더티체킹이 반영되지 않는 현상 해결 * refactor: 적절한 패키지로 이동 * refactor: 구글 로그인 시 카카오 회원의 memberId를 받지 않도록 수정 updateLoginDetail 로직은 여전히 필요할 것 같아 그대로 두었습니다 * refactor: 실제 로그인과 테스트용 로그인 컨트롤러 클래스를 분리 * refactor: 회원 탈퇴를 auth가 아닌 member 도메인으로 이동 * refactor: 애플 서버로부터 회원 정보를 전달받는 API를 AuthController로 이동 * chore: 애플 RedirectURL 변경에 따른 설정 수정 * refactor: socialLoginId 대신 identifier라는 명칭 사용 * refactor: 불필요한 어노테이션 제거 * refactor: 로그인 요청 DTO의 이름 변경 * refactor: 로그인 및 회원가입 비즈니스 로직 리팩토링 * refactor: 카카오 로그인 후 토큰 받아오는 서비스 로직 수정 * feat: 카카오 사용자를 애플, 구글 사용자로 치환하는 기능 구현 * refactor: IdentityToken 대신 IdToken으로 통일 * refactor: JsonNode를 사용하여 사용자 이름 읽어오도록 수정 * test: 애플 로그인 흐름 테스트 * test: 사용자 전환 기능을 테스트 * refactor: 모임에서의 다락방 조회 예외 메시지 추가 * fix: 모임 ID 대신 다락방 ID를 사용하는 오타 수정 * refactor: 참여자가 입력된 DarakbangMember인지 확인하는 메서드 추가 * refactor: memberId 필드 추가에 따른 Author 필드명 세분화(id -> darakbangMember) * refactor: Recipient 필드 final 지정 및 빌더 추가 * refactor: CommentRecipient를 Map을 가진 일급 컬렉션으로 수정 * refactor: ChatDateTimeFormatter 세분화 * fix: DateTimeFormatter 사용 제거 * feat: id와 토큰 정보를 가지는 도메인 객체 추가 * refactor: 기존 List 형태의 필드를 에러 코드로 구분되는 Map 구조로 수정 및 Retry-After 헤더에서 값을 가져오는 유틸 클래스 추가 * refactor: 404 에러 토큰 제거를 Sender가 아니라 Handler에서 마지막에 처리하도록 수정 및 FcmFailedResponse에서의 변경 사항 반영 * chore: rolling 배포 workflows * feat: 모니터링 대시보드를 위한 설정 추가 * chore: prod 환경에 모니터링을 위한 설정 추가 * chore: rolling 배포 시간 단축 * remove: 구버전 채팅 제거 * fix: 엔티티 스캔 패키지 수정 * refactor: 파라미터 카멜케이스로 변경 * refactor: enum 비교 변경 * refactor: 환경 변수 등록 * refactor: 환경 변수 등록 * refactor: 인터셉터 허용 url 변경 * refactor: client Id 로그 확인 * refactor: 소셜 아이디 로그 수정 * refactor: 로그 삭제 * chore: 개발 환경에 설정 파일 추가 * chore: hikari connection pool size를 20으로 지정 * fix: 다락방, 멤버 테이블 이름 변경 * refactor: 모임 도메인 테이블 이름 지정 * refactor: please, interest 테이블 명 매핑 * chore: Tomcat 매트릭을 측정하기 위한 설정 추가 * chore: tomcat mbeanregistry 활성화 * chore: prometheus endpoint를 활성화 * chore: 최대 스레드 개수를 수정 * chore: 스레드 개수를 100개로 제한 * fix: 엔티티 스캔 패키지 지정 * feat: cors 허용 * feat: 사용자 전환 시 상태를 변경하도록 수정 * feat: 구글, 애플 로그인 후 사용자 전환 여부를 반환하도록 수정 * fix: identifier 조회 시 active 회원만 조회하도록 수정 * feat: 사용자 전환 시 구글, 애플 사용자 상태를 DEPRECATED로 설정 * fix: 상태 변경이 반영되지 않는 현상 해결 * fix: darakbangId 누락 수정 * feat: 다락방 멤버 목록 darakbangMemberId, profile 추가 * feat: 참여자 목록 응답에 darakbangMemberId 추가 * feat: 회원 가입 이력이 있다면 최초 애플 로그인이더라도 회원가입하지 않는다 * feat: 유저 성과 이름을 바꾸어 위치 * chore: 애플 로그인 시 리디렉션 URL을 환경에 맞추어 설정 * feat: 다락방 멤버 프로필 조회 API * feat: Participant에 DarakbangMemberId 추가 * feat: 룰렛 참여자 응답에 darakbangMemberId 추가 * feat: 회원 객체의 상태를 직접 변경하여 더티체킹 하도록 수정 * chore: 스크립트 실행 브랜치 변경 * test: 날짜 이슈 테스트 실패 수정 --------- Co-authored-by: 김민겸 Co-authored-by: SungKyum Kim Co-authored-by: pricelees Co-authored-by: hoyeonyy Co-authored-by: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Co-authored-by: SUNGKYUM KIM <76910498+ksk0605@users.noreply.github.com> * develop 병합 (#768) * style: 컨벤션 수정 * refactor: 댓글 작성시 알림 대상자 조회 로직 수정 및 테스트 추가 * Fix: 알림 재전송 처리에서의 오타 수정 * comment: 댓글 알림 대상자 조회 메서드에 주석 추가 * refactor: 애플 api 경로 수정 * refactor: cors 허용 경로로 apple 서버 추가 * refactor: 인터셉터 인증 허용 주소 추가 * refactor: 컨트롤러 수정 * refactor: 컨트롤러 수정 * refactor: 컨트롤러 수정 * refactor: 컨트롤러 수정 * refactor: 리다이랙션 설정 * feat: Json String을 객체로 파싱하고 code를 담아 보내도록 수정 * feat: 사용자 정보를 받을 객체 생성 * fix: Service 객체 빈 등록 * fix: apple oauth를 통해 accessToken 반환 * refactor: Loser 도메인을 Participant 도메인을 재사용하도록 변경 * refactor: readOnly 옵션 추가 * refactor: 필드값이 널을 허용하지 않도록 수정 * fix: 멤버 이름 누락 예외 메시지 변경 * refactor: 예외 처리 통일 * test: 예외 리팩토링에 따른 테스트 수정 * feat: 프리뷰 응답에 참가자 정보 추가 * feat: 채팅 목록 응답에 채팅 작성자 정보 추가 * feat: Chat 도메인 및 Chat 변환 메서드 추가 * feat: chat 패키지의 ChatType 사용 * refactor: ChatEntity 대신 Chat 사용 * feat: 파일과 이미지 URL이 따로 넘어오도록 수정 * feat: 파일과 이미지 URL이 따로 넘어오도록 수정 * feat: 새로 추가된 파일이 없을 때 S3업로드 하지 않도록 구현 * feat: 기본 이미지로 변경 혹은 이미지 변경이 없는 경우 DB Profile 업데이트 * test: 채팅 알림 대상자 찾기 테스트 추가 * refactor: TODO 제거 * refactor: 미사용 필드 제거 * fix: 마이페이지 버그 수정 * feat: 채팅방 오픈 기능에 채팅방 ID를 반환하도록 수정 * fix: null 가능성 있는 필드를 조회할 때 생기는 버그 수정 * fix: 응답 형식 컨벤션이 맞지 않는 문제 수정 * feat: 기존 이미지가 있다면 S3에서 삭제한다. * feat: 알림 예외 메시지 및 커스텀 예외 추가 * refactor: 알림 이벤트 객체 생성시 채팅 / 채팅이 아닌 경우를 구분하기 위한 팩토리 메서드 추가 * feat: 채팅 알림시 날짜 / 시간을 '~월 ~일 ~시 ~분' 형식으로 보내기 위한 유틸 클래스 추가 * refactor: 알림 메시지 생성 역할 변경에 따른 NotificationType의 메시지 제거 * refactor: 변경된 사항을 채팅 서비스에 반영 * fix: 배팅 어트리뷰트 조회시 프로필 널로 인해 발생하는 오류 수정 * feat: 테스트 사용자를 두개로 늘린다. * refactor: 채팅 서비스에서의 타입에 따른 처리 수정 * test: 테스트 오류 수정 * fix: time null 로 인한 오류 수정 * feat: 인터셉터 허용 * fix: ChatType 의존성 수정 * feat: betFindResponse 프로필 url 추가 * fix: participant 변경사항 반영 * refactor: AttributeManagerRegistry 예외메시지 수정 * refactor: 예외 발생 로직 수정 * refactor: 예외 발생 로직 수정 * refactor: 채팅 알림에서 새로 추가된 베팅 기능 지원 * refactor: 애플 소셜 로그인 로직을 컨트롤러에서 서비스로 이동 * fix: ChatRoom이 ChatRoomEntity으로 의존성 제거 * fix: 추첨자 있는 안내면진다만 채팅방목록 조회 가능하도록 수정 * fix: targetid nullable 하지 않도록 변경 * fix: 채팅에서 모두 자신의 프로필로 나오는 오류 수정 * test: 주석 제거하고 케이스별 테스트 코드 작성 * test: 마이페이지 수정 테스트 작성 `@MockBean`을 Nested 내부에 작성하면 에러가 터져서 외부에 선언하였습니다. * refactor: 참여에 사용자 목록이 보이도록 생성 * refactor: 테스트용 사용자 만드는 기능 구현 * refactor: 테스트용 사용자가 prod 환경에 배포되지 않게 수정 * refactor: 닉네임 검증과정을 생성뿐 아니라 수정에도 사용 * refactor: 최대 길이 12글자 * refactor: 최대 길이 12글자로 변경 * feat: Author 추가 채팅 작성자 도메인 * fix: 기본 이미지로 변경하는 경우에도 S3에서 삭제하도록 구현 * refactor: chat DarakbangMember를 Author로 변경 * fix: 참여자 모이머인지 확인 오류 수정 * feat: 안내면진다 중복참여 예외처리 * feat: 추첨시간이 지났거나 당첨자가 이미 있는 경우 참여 예외 처리 * refactor: AccessToken payload 에 OauthType 추가 * refactor: 인터셉터에서 토큰의 정보가 Kakao 라면 예외를 발생한다. * refactor: 값 비교, 접근제어자 변경 * fix: 오타수정 * refactor: 메서드명 변경 * refactor: 클래스명 변경 * feat: role 포함하지 않는 ParticipantResponse 생성자 추가 * feat: 회원 탈퇴 API 구현 * feat: 회원 탈퇴 시 애플 서버와 통신하여 사용자 권한을 지우는 기능 구현 * feat: 애플 서버로부터 refresh token을 받아서 저장하는 기능 구현 * fix: 안내면진다 목록 정렬 수정 * fix: 실패하는 테스트 케이스 수정 * chore: redirect-uri가 달라서 생기는 문제 해결 * fix: 회원 탈퇴 시 revoke 과정 확인을 위한 디버깅 코드 추가 * refactor: 모임 정보 수정시 알림 메시지에서 수정 전 모임 이름을 사용하도록 수정 * refactor: 채팅 알림 구현 방법 및 메시지 형식 수정 * feat: 애플 회원 탈퇴 시 상태 정보를 수정하도록 변경 * refactor: 채팅 알림 메시지 세분화 * chore: 소셜 로그인 아이디를 확인하기 위한 로그 추가 * chore: 소셜 아이디를 확인하기 위한 로그 추가 * feat: 로그인 시 재가입 여부에 따라 상태 정보를 변경 * feat: 애플 로그인 시 재가입한 회원인 경우 상태만 변경하도록 수정 * test: 회원 탈퇴 논리 삭제 테스트 * feat: 재가입 시 더티체킹이 반영되지 않는 현상 해결 * refactor: 적절한 패키지로 이동 * refactor: 구글 로그인 시 카카오 회원의 memberId를 받지 않도록 수정 updateLoginDetail 로직은 여전히 필요할 것 같아 그대로 두었습니다 * refactor: 실제 로그인과 테스트용 로그인 컨트롤러 클래스를 분리 * refactor: 회원 탈퇴를 auth가 아닌 member 도메인으로 이동 * refactor: 애플 서버로부터 회원 정보를 전달받는 API를 AuthController로 이동 * chore: 애플 RedirectURL 변경에 따른 설정 수정 * refactor: socialLoginId 대신 identifier라는 명칭 사용 * refactor: 불필요한 어노테이션 제거 * refactor: 로그인 요청 DTO의 이름 변경 * refactor: 로그인 및 회원가입 비즈니스 로직 리팩토링 * refactor: 카카오 로그인 후 토큰 받아오는 서비스 로직 수정 * feat: 카카오 사용자를 애플, 구글 사용자로 치환하는 기능 구현 * refactor: IdentityToken 대신 IdToken으로 통일 * refactor: JsonNode를 사용하여 사용자 이름 읽어오도록 수정 * test: 애플 로그인 흐름 테스트 * test: 사용자 전환 기능을 테스트 * refactor: 모임에서의 다락방 조회 예외 메시지 추가 * fix: 모임 ID 대신 다락방 ID를 사용하는 오타 수정 * refactor: 참여자가 입력된 DarakbangMember인지 확인하는 메서드 추가 * refactor: memberId 필드 추가에 따른 Author 필드명 세분화(id -> darakbangMember) * refactor: Recipient 필드 final 지정 및 빌더 추가 * refactor: CommentRecipient를 Map을 가진 일급 컬렉션으로 수정 * refactor: ChatDateTimeFormatter 세분화 * fix: DateTimeFormatter 사용 제거 * feat: id와 토큰 정보를 가지는 도메인 객체 추가 * refactor: 기존 List 형태의 필드를 에러 코드로 구분되는 Map 구조로 수정 및 Retry-After 헤더에서 값을 가져오는 유틸 클래스 추가 * refactor: 404 에러 토큰 제거를 Sender가 아니라 Handler에서 마지막에 처리하도록 수정 및 FcmFailedResponse에서의 변경 사항 반영 * chore: rolling 배포 workflows * feat: 모니터링 대시보드를 위한 설정 추가 * chore: prod 환경에 모니터링을 위한 설정 추가 * chore: rolling 배포 시간 단축 * remove: 구버전 채팅 제거 * fix: 엔티티 스캔 패키지 수정 * refactor: 파라미터 카멜케이스로 변경 * refactor: enum 비교 변경 * refactor: 환경 변수 등록 * refactor: 환경 변수 등록 * refactor: 인터셉터 허용 url 변경 * refactor: client Id 로그 확인 * refactor: 소셜 아이디 로그 수정 * refactor: 로그 삭제 * chore: 개발 환경에 설정 파일 추가 * chore: hikari connection pool size를 20으로 지정 * fix: 다락방, 멤버 테이블 이름 변경 * refactor: 모임 도메인 테이블 이름 지정 * refactor: please, interest 테이블 명 매핑 * chore: Tomcat 매트릭을 측정하기 위한 설정 추가 * chore: tomcat mbeanregistry 활성화 * chore: prometheus endpoint를 활성화 * chore: 최대 스레드 개수를 수정 * chore: 스레드 개수를 100개로 제한 * fix: 엔티티 스캔 패키지 지정 * feat: cors 허용 * feat: 사용자 전환 시 상태를 변경하도록 수정 * feat: 구글, 애플 로그인 후 사용자 전환 여부를 반환하도록 수정 * fix: identifier 조회 시 active 회원만 조회하도록 수정 * feat: 사용자 전환 시 구글, 애플 사용자 상태를 DEPRECATED로 설정 * fix: 상태 변경이 반영되지 않는 현상 해결 * fix: darakbangId 누락 수정 * feat: 다락방 멤버 목록 darakbangMemberId, profile 추가 * feat: 참여자 목록 응답에 darakbangMemberId 추가 * feat: 회원 가입 이력이 있다면 최초 애플 로그인이더라도 회원가입하지 않는다 * feat: 유저 성과 이름을 바꾸어 위치 * chore: 애플 로그인 시 리디렉션 URL을 환경에 맞추어 설정 * feat: 다락방 멤버 프로필 조회 API * feat: Participant에 DarakbangMemberId 추가 * feat: 룰렛 참여자 응답에 darakbangMemberId 추가 * feat: 회원 객체의 상태를 직접 변경하여 더티체킹 하도록 수정 * feat: FCM 알림 재시도시 재시도 가능 여부 판단을 별도의 객체로 분리 * feat: 비동기로 알림을 전송하는 별도의 객체 분리 * feat: 알림 전송 이벤트 객체 구현 * feat: 알림 전송 이벤트 처리 객체 구현 * refactor: 구독 정보 필터의 파라미터 타입 수정 * refactor: 구독 정보 필터를 가져올 때의 처리 로직 수정 * rename: NotificationEvent 클래스명 수정(->NotificationPayload) * refactor: 알림 저장 & 이벤트 발행 객체 생성 및 이에따른 NotificationService 삭제 * feat: 모임 패키지 안에서의 알림 전송(=이벤트 발행) 객체 추가 * feat: 모임 패키지 안에서의 공통 이벤트 처리 객체 구현 * feat: 참여 이벤트 처리 객체 구현 * feat: 댓글 이벤트 처리 객체 구현 * feat: 모임 관련(모임 생성, 수정, 상태 변경) 이벤트 처리 객체 구현 * feat: 채팅 이벤트 처리 객체 구현 * refactor: FcmFailedResponse에 실패한 토큰이 없는지 확인하는 메서드 추가 * refactor: 토큰 스케쥴러에 Transactional 적용 * refactor: 비동기 테스트 추가 * chore: 스크립트 실행 브랜치 변경 * test: 날짜 이슈 테스트 실패 수정 * feat: ChatRoomValidator, 타입과 타겟 id로 이미 존재하는 채팅방 검증 로직 구현 * chore: 서버별 스크립트 변경 * fix: 채팅방 생성시 이미 존재하는 채팅방 검증 로직 추가 * chore: 구 prod 스크립트 삭제 * refactor: 파일 사이즈 10MB 로 개선 * refactor: 재전송하지 않는 실패 알림에 대한 로깅 메시지 구체화 * refactor: yml 로 통합 --------- Co-authored-by: pricelees Co-authored-by: hoyeonyy Co-authored-by: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Co-authored-by: SungKyum Kim Co-authored-by: SUNGKYUM KIM <76910498+ksk0605@users.noreply.github.com> Co-authored-by: 김민겸 * develop 백엔드 병합 (#769) * style: 컨벤션 수정 * refactor: 댓글 작성시 알림 대상자 조회 로직 수정 및 테스트 추가 * Fix: 알림 재전송 처리에서의 오타 수정 * comment: 댓글 알림 대상자 조회 메서드에 주석 추가 * refactor: 애플 api 경로 수정 * refactor: cors 허용 경로로 apple 서버 추가 * refactor: 인터셉터 인증 허용 주소 추가 * refactor: 컨트롤러 수정 * refactor: 컨트롤러 수정 * refactor: 컨트롤러 수정 * refactor: 컨트롤러 수정 * refactor: 리다이랙션 설정 * feat: Json String을 객체로 파싱하고 code를 담아 보내도록 수정 * feat: 사용자 정보를 받을 객체 생성 * fix: Service 객체 빈 등록 * fix: apple oauth를 통해 accessToken 반환 * refactor: Loser 도메인을 Participant 도메인을 재사용하도록 변경 * refactor: readOnly 옵션 추가 * refactor: 필드값이 널을 허용하지 않도록 수정 * fix: 멤버 이름 누락 예외 메시지 변경 * refactor: 예외 처리 통일 * test: 예외 리팩토링에 따른 테스트 수정 * feat: 프리뷰 응답에 참가자 정보 추가 * feat: 채팅 목록 응답에 채팅 작성자 정보 추가 * feat: Chat 도메인 및 Chat 변환 메서드 추가 * feat: chat 패키지의 ChatType 사용 * refactor: ChatEntity 대신 Chat 사용 * feat: 파일과 이미지 URL이 따로 넘어오도록 수정 * feat: 파일과 이미지 URL이 따로 넘어오도록 수정 * feat: 새로 추가된 파일이 없을 때 S3업로드 하지 않도록 구현 * feat: 기본 이미지로 변경 혹은 이미지 변경이 없는 경우 DB Profile 업데이트 * test: 채팅 알림 대상자 찾기 테스트 추가 * refactor: TODO 제거 * refactor: 미사용 필드 제거 * fix: 마이페이지 버그 수정 * feat: 채팅방 오픈 기능에 채팅방 ID를 반환하도록 수정 * fix: null 가능성 있는 필드를 조회할 때 생기는 버그 수정 * fix: 응답 형식 컨벤션이 맞지 않는 문제 수정 * feat: 기존 이미지가 있다면 S3에서 삭제한다. * feat: 알림 예외 메시지 및 커스텀 예외 추가 * refactor: 알림 이벤트 객체 생성시 채팅 / 채팅이 아닌 경우를 구분하기 위한 팩토리 메서드 추가 * feat: 채팅 알림시 날짜 / 시간을 '~월 ~일 ~시 ~분' 형식으로 보내기 위한 유틸 클래스 추가 * refactor: 알림 메시지 생성 역할 변경에 따른 NotificationType의 메시지 제거 * refactor: 변경된 사항을 채팅 서비스에 반영 * fix: 배팅 어트리뷰트 조회시 프로필 널로 인해 발생하는 오류 수정 * feat: 테스트 사용자를 두개로 늘린다. * refactor: 채팅 서비스에서의 타입에 따른 처리 수정 * test: 테스트 오류 수정 * fix: time null 로 인한 오류 수정 * feat: 인터셉터 허용 * fix: ChatType 의존성 수정 * feat: betFindResponse 프로필 url 추가 * fix: participant 변경사항 반영 * refactor: AttributeManagerRegistry 예외메시지 수정 * refactor: 예외 발생 로직 수정 * refactor: 예외 발생 로직 수정 * refactor: 채팅 알림에서 새로 추가된 베팅 기능 지원 * refactor: 애플 소셜 로그인 로직을 컨트롤러에서 서비스로 이동 * fix: ChatRoom이 ChatRoomEntity으로 의존성 제거 * fix: 추첨자 있는 안내면진다만 채팅방목록 조회 가능하도록 수정 * fix: targetid nullable 하지 않도록 변경 * fix: 채팅에서 모두 자신의 프로필로 나오는 오류 수정 * test: 주석 제거하고 케이스별 테스트 코드 작성 * test: 마이페이지 수정 테스트 작성 `@MockBean`을 Nested 내부에 작성하면 에러가 터져서 외부에 선언하였습니다. * refactor: 참여에 사용자 목록이 보이도록 생성 * refactor: 테스트용 사용자 만드는 기능 구현 * refactor: 테스트용 사용자가 prod 환경에 배포되지 않게 수정 * refactor: 닉네임 검증과정을 생성뿐 아니라 수정에도 사용 * refactor: 최대 길이 12글자 * refactor: 최대 길이 12글자로 변경 * feat: Author 추가 채팅 작성자 도메인 * fix: 기본 이미지로 변경하는 경우에도 S3에서 삭제하도록 구현 * refactor: chat DarakbangMember를 Author로 변경 * fix: 참여자 모이머인지 확인 오류 수정 * feat: 안내면진다 중복참여 예외처리 * feat: 추첨시간이 지났거나 당첨자가 이미 있는 경우 참여 예외 처리 * refactor: AccessToken payload 에 OauthType 추가 * refactor: 인터셉터에서 토큰의 정보가 Kakao 라면 예외를 발생한다. * refactor: 값 비교, 접근제어자 변경 * fix: 오타수정 * refactor: 메서드명 변경 * refactor: 클래스명 변경 * feat: role 포함하지 않는 ParticipantResponse 생성자 추가 * feat: 회원 탈퇴 API 구현 * feat: 회원 탈퇴 시 애플 서버와 통신하여 사용자 권한을 지우는 기능 구현 * feat: 애플 서버로부터 refresh token을 받아서 저장하는 기능 구현 * fix: 안내면진다 목록 정렬 수정 * fix: 실패하는 테스트 케이스 수정 * chore: redirect-uri가 달라서 생기는 문제 해결 * fix: 회원 탈퇴 시 revoke 과정 확인을 위한 디버깅 코드 추가 * refactor: 모임 정보 수정시 알림 메시지에서 수정 전 모임 이름을 사용하도록 수정 * refactor: 채팅 알림 구현 방법 및 메시지 형식 수정 * feat: 애플 회원 탈퇴 시 상태 정보를 수정하도록 변경 * refactor: 채팅 알림 메시지 세분화 * chore: 소셜 로그인 아이디를 확인하기 위한 로그 추가 * chore: 소셜 아이디를 확인하기 위한 로그 추가 * feat: 로그인 시 재가입 여부에 따라 상태 정보를 변경 * feat: 애플 로그인 시 재가입한 회원인 경우 상태만 변경하도록 수정 * test: 회원 탈퇴 논리 삭제 테스트 * feat: 재가입 시 더티체킹이 반영되지 않는 현상 해결 * refactor: 적절한 패키지로 이동 * refactor: 구글 로그인 시 카카오 회원의 memberId를 받지 않도록 수정 updateLoginDetail 로직은 여전히 필요할 것 같아 그대로 두었습니다 * refactor: 실제 로그인과 테스트용 로그인 컨트롤러 클래스를 분리 * refactor: 회원 탈퇴를 auth가 아닌 member 도메인으로 이동 * refactor: 애플 서버로부터 회원 정보를 전달받는 API를 AuthController로 이동 * chore: 애플 RedirectURL 변경에 따른 설정 수정 * refactor: socialLoginId 대신 identifier라는 명칭 사용 * refactor: 불필요한 어노테이션 제거 * refactor: 로그인 요청 DTO의 이름 변경 * refactor: 로그인 및 회원가입 비즈니스 로직 리팩토링 * refactor: 카카오 로그인 후 토큰 받아오는 서비스 로직 수정 * feat: 카카오 사용자를 애플, 구글 사용자로 치환하는 기능 구현 * refactor: IdentityToken 대신 IdToken으로 통일 * refactor: JsonNode를 사용하여 사용자 이름 읽어오도록 수정 * test: 애플 로그인 흐름 테스트 * test: 사용자 전환 기능을 테스트 * refactor: 모임에서의 다락방 조회 예외 메시지 추가 * fix: 모임 ID 대신 다락방 ID를 사용하는 오타 수정 * refactor: 참여자가 입력된 DarakbangMember인지 확인하는 메서드 추가 * refactor: memberId 필드 추가에 따른 Author 필드명 세분화(id -> darakbangMember) * refactor: Recipient 필드 final 지정 및 빌더 추가 * refactor: CommentRecipient를 Map을 가진 일급 컬렉션으로 수정 * refactor: ChatDateTimeFormatter 세분화 * fix: DateTimeFormatter 사용 제거 * feat: id와 토큰 정보를 가지는 도메인 객체 추가 * refactor: 기존 List 형태의 필드를 에러 코드로 구분되는 Map 구조로 수정 및 Retry-After 헤더에서 값을 가져오는 유틸 클래스 추가 * refactor: 404 에러 토큰 제거를 Sender가 아니라 Handler에서 마지막에 처리하도록 수정 및 FcmFailedResponse에서의 변경 사항 반영 * chore: rolling 배포 workflows * feat: 모니터링 대시보드를 위한 설정 추가 * chore: prod 환경에 모니터링을 위한 설정 추가 * chore: rolling 배포 시간 단축 * remove: 구버전 채팅 제거 * fix: 엔티티 스캔 패키지 수정 * refactor: 파라미터 카멜케이스로 변경 * refactor: enum 비교 변경 * refactor: 환경 변수 등록 * refactor: 환경 변수 등록 * refactor: 인터셉터 허용 url 변경 * refactor: client Id 로그 확인 * refactor: 소셜 아이디 로그 수정 * refactor: 로그 삭제 * chore: 개발 환경에 설정 파일 추가 * chore: hikari connection pool size를 20으로 지정 * fix: 다락방, 멤버 테이블 이름 변경 * refactor: 모임 도메인 테이블 이름 지정 * refactor: please, interest 테이블 명 매핑 * chore: Tomcat 매트릭을 측정하기 위한 설정 추가 * chore: tomcat mbeanregistry 활성화 * chore: prometheus endpoint를 활성화 * chore: 최대 스레드 개수를 수정 * chore: 스레드 개수를 100개로 제한 * fix: 엔티티 스캔 패키지 지정 * feat: cors 허용 * feat: 사용자 전환 시 상태를 변경하도록 수정 * feat: 구글, 애플 로그인 후 사용자 전환 여부를 반환하도록 수정 * fix: identifier 조회 시 active 회원만 조회하도록 수정 * feat: 사용자 전환 시 구글, 애플 사용자 상태를 DEPRECATED로 설정 * fix: 상태 변경이 반영되지 않는 현상 해결 * fix: darakbangId 누락 수정 * feat: 다락방 멤버 목록 darakbangMemberId, profile 추가 * feat: 참여자 목록 응답에 darakbangMemberId 추가 * feat: 회원 가입 이력이 있다면 최초 애플 로그인이더라도 회원가입하지 않는다 * feat: 유저 성과 이름을 바꾸어 위치 * chore: 애플 로그인 시 리디렉션 URL을 환경에 맞추어 설정 * feat: 다락방 멤버 프로필 조회 API * feat: Participant에 DarakbangMemberId 추가 * feat: 룰렛 참여자 응답에 darakbangMemberId 추가 * feat: 회원 객체의 상태를 직접 변경하여 더티체킹 하도록 수정 * feat: FCM 알림 재시도시 재시도 가능 여부 판단을 별도의 객체로 분리 * feat: 비동기로 알림을 전송하는 별도의 객체 분리 * feat: 알림 전송 이벤트 객체 구현 * feat: 알림 전송 이벤트 처리 객체 구현 * refactor: 구독 정보 필터의 파라미터 타입 수정 * refactor: 구독 정보 필터를 가져올 때의 처리 로직 수정 * rename: NotificationEvent 클래스명 수정(->NotificationPayload) * refactor: 알림 저장 & 이벤트 발행 객체 생성 및 이에따른 NotificationService 삭제 * feat: 모임 패키지 안에서의 알림 전송(=이벤트 발행) 객체 추가 * feat: 모임 패키지 안에서의 공통 이벤트 처리 객체 구현 * feat: 참여 이벤트 처리 객체 구현 * feat: 댓글 이벤트 처리 객체 구현 * feat: 모임 관련(모임 생성, 수정, 상태 변경) 이벤트 처리 객체 구현 * feat: 채팅 이벤트 처리 객체 구현 * refactor: FcmFailedResponse에 실패한 토큰이 없는지 확인하는 메서드 추가 * refactor: 토큰 스케쥴러에 Transactional 적용 * refactor: 비동기 테스트 추가 * chore: 스크립트 실행 브랜치 변경 * test: 날짜 이슈 테스트 실패 수정 * feat: ChatRoomValidator, 타입과 타겟 id로 이미 존재하는 채팅방 검증 로직 구현 * chore: 서버별 스크립트 변경 * fix: 채팅방 생성시 이미 존재하는 채팅방 검증 로직 추가 * chore: 구 prod 스크립트 삭제 * refactor: 파일 사이즈 10MB 로 개선 * refactor: 재전송하지 않는 실패 알림에 대한 로깅 메시지 구체화 * refactor: yml 로 통합 --------- Co-authored-by: pricelees Co-authored-by: hoyeonyy Co-authored-by: HoYeon <114469256+hoyeonyy@users.noreply.github.com> Co-authored-by: SungKyum Kim Co-authored-by: SUNGKYUM KIM <76910498+ksk0605@users.noreply.github.com> Co-authored-by: 김민겸 --------- Co-authored-by: pricelees Co-authored-by: ss0526100 Co-authored-by: jaeml06 Co-authored-by: cys4585 Co-authored-by: MingyeomKim <67851124+MingyeomKim@users.noreply.github.com> Co-authored-by: MingyeomKim Co-authored-by: 차승하 <75566149+ss0526100@users.noreply.github.com> Co-authored-by: jaeml06 <107801932+jaeml06@users.noreply.github.com> Co-authored-by: 최영수(suya) <77481524+cys4585@users.noreply.github.com> Co-authored-by: SungKyum Kim Co-authored-by: SUNGKYUM KIM <76910498+ksk0605@users.noreply.github.com> Co-authored-by: hoyeonyy Co-authored-by: HoYeon <114469256+hoyeonyy@users.noreply.github.com> --- .../PULL_REQUEST_TEMPLATE.md | 0 .github/workflows/be-rolling-deploy.yml | 68 + .github/workflows/cd-frontend.yml | 17 + .../.gitkeep => .github/workflows/cd-prod.yml | 0 .github/workflows/ci-frontend.yml | 46 + .github/workflows/cicd-backend-dev.yml | 70 + .github/workflows/pr-test.yml | 41 + .gitignore | 7 + README.md | 18 + backend/.DS_Store | Bin 0 -> 6148 bytes backend/.gitignore | 9 + backend/Dockerfile | 9 + backend/HELP.md | 25 + backend/build.gradle | 77 + backend/docker-compose.yml | 11 + backend/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + backend/gradlew | 249 + backend/gradlew.bat | 92 + backend/settings.gradle | 1 + .../mouda/backend/BackendApplication.java | 15 + .../aop/logging/ExceptRequestLogging.java | 12 + .../aop/logging/RequestLoggingAspect.java | 106 + .../auth/Infrastructure/AppleOauthClient.java | 93 + .../Infrastructure/GoogleOauthClient.java | 74 + .../auth/Infrastructure/KakaoOauthClient.java | 68 + .../auth/Infrastructure/OauthClient.java | 6 + .../response/AppleRefreshTokenResponse.java | 4 + .../auth/business/AppleAuthService.java | 58 + .../auth/business/GoogleAuthService.java | 41 + .../auth/business/KakaoAuthService.java | 29 + .../auth/business/TestAuthService.java | 37 + .../auth/exception/AuthErrorMessage.java | 26 + .../backend/auth/exception/AuthException.java | 11 + .../auth/implement/AppleUserInfoProvider.java | 40 + .../implement/GoogleUserInfoProvider.java | 63 + .../backend/auth/implement/JoinManager.java | 34 + .../auth/implement/KakaoUserInfoProvider.java | 24 + .../implement/jwt/AccessTokenProvider.java | 73 + .../implement/jwt/ClientSecretProvider.java | 76 + .../controller/AuthController.java | 67 + .../controller/TestAuthController.java | 37 + .../controller/swagger/AuthSwagger.java | 37 + .../controller/swagger/TestAuthSwagger.java | 24 + .../request/GoogleLoginRequest.java | 9 + .../request/KakaoConvertRequest.java | 9 + .../response/KakaoLoginResponse.java | 6 + .../presentation/response/LoginResponse.java | 7 + .../presentation/response/OauthResponse.java | 6 + .../mouda/backend/auth/util/TokenDecoder.java | 30 + .../backend/bet/business/BetScheduler.java | 37 + .../backend/bet/business/BetService.java | 73 + .../java/mouda/backend/bet/domain/Bet.java | 71 + .../mouda/backend/bet/domain/BetDetails.java | 50 + .../mouda/backend/bet/domain/BetRole.java | 5 + .../mouda/backend/bet/domain/BettingTime.java | 31 + .../java/mouda/backend/bet/domain/Loser.java | 15 + .../mouda/backend/bet/domain/Participant.java | 23 + .../bet/entity/BetDarakbangMemberEntity.java | 59 + .../mouda/backend/bet/entity/BetEntity.java | 81 + .../bet/exception/BetErrorMessage.java | 17 + .../backend/bet/exception/BetException.java | 12 + .../implement/BetDarakbangMemberWriter.java | 26 + .../backend/bet/implement/BetFinder.java | 93 + .../backend/bet/implement/BetSorter.java | 24 + .../backend/bet/implement/BetWriter.java | 61 + .../bet/implement/ParticipantFinder.java | 26 + .../BetDarakbangMemberRepository.java | 30 + .../bet/infrastructure/BetRepository.java | 18 + .../controller/BetController.java | 99 + .../controller/swagger/BetSwagger.java | 84 + .../request/BetCreateRequest.java | 20 + .../response/BetCreateResponse.java | 6 + .../response/BetFindAllResponse.java | 23 + .../response/BetFindAllResponses.java | 16 + .../response/BetFindResponse.java | 34 + .../response/BetResultResponse.java | 11 + .../response/ParticipantResponse.java | 17 + .../chat/business/ChatRoomService.java | 54 + .../backend/chat/business/ChatService.java | 98 + .../mouda/backend/chat/domain/Attributes.java | 7 + .../mouda/backend/chat/domain/Author.java | 25 + .../backend/chat/domain/BetAttributes.java | 32 + .../java/mouda/backend/chat/domain/Chat.java | 33 + .../chat/domain/ChatNotificationEvent.java | 15 + .../backend/chat/domain/ChatOwnership.java | 15 + .../backend/chat/domain/ChatPreview.java | 42 + .../mouda/backend/chat/domain/ChatRoom.java | 37 + .../backend/chat/domain/ChatRoomDetails.java | 29 + .../backend/chat/domain/ChatRoomType.java | 9 + .../java/mouda/backend/chat/domain/Chats.java | 22 + .../mouda/backend/chat/domain/LastChat.java | 21 + .../backend/chat/domain/MoimAttributes.java | 46 + .../backend/chat/domain/Participant.java | 37 + .../mouda/backend/chat/domain/Target.java | 25 + .../mouda/backend/chat/entity/ChatEntity.java | 72 + .../backend/chat/entity/ChatRoomEntity.java | 47 + .../mouda/backend/chat/entity/ChatType.java | 6 + .../chat/exception/ChatErrorMessage.java | 20 + .../backend/chat/exception/ChatException.java | 12 + .../chat/implement/AttributeManager.java | 13 + .../implement/AttributeManagerRegistry.java | 23 + .../chat/implement/BetAttributeManager.java | 54 + .../chat/implement/BetChatPreviewManager.java | 58 + .../implement/BetParticipantResolver.java | 47 + .../chat/implement/ChatPreviewManager.java | 12 + .../implement/ChatPreviewManagerRegistry.java | 21 + .../chat/implement/ChatRoomDetailsFinder.java | 31 + .../chat/implement/ChatRoomFinder.java | 93 + .../chat/implement/ChatRoomValidator.java | 23 + .../chat/implement/ChatRoomWriter.java | 27 + .../backend/chat/implement/ChatWriter.java | 73 + .../chat/implement/MoimAttributeManager.java | 58 + .../implement/MoimChatPreviewManager.java | 57 + .../implement/MoimParticipantResolver.java | 39 + .../ParticipantResolverRegistry.java | 25 + .../chat/implement/ParticipantsResolver.java | 14 + .../ChatNotificationEventHandler.java | 147 + .../notification/ChatNotificationSender.java | 26 + .../notification/ChatRecipientFinder.java | 51 + .../chat/infrastructure/ChatRepository.java | 17 + .../infrastructure/ChatRoomRepository.java | 16 + .../controller/ChatController.java | 101 + .../controller/ChatRoomController.java | 61 + .../controller/swagger/ChatSwagger.java | 98 + .../request/ChatCreateRequest.java | 9 + .../request/DateTimeConfirmRequest.java | 28 + .../request/LastReadChatRequest.java | 9 + .../request/PlaceConfirmRequest.java | 25 + .../response/ChatFindDetailResponse.java | 39 + .../response/ChatFindUnloadedResponse.java | 16 + .../response/ChatPreviewResponse.java | 40 + .../response/ChatPreviewResponses.java | 17 + .../response/ChatRoomDetailsResponse.java | 35 + .../response/ParticipantResponse.java | 13 + .../chat/util/ChatDateTimeFormatter.java | 51 + .../backend/common/HealthCheckController.java | 14 + .../backend/common/config/CorsConfig.java | 24 + .../backend/common/config/FirebaseConfig.java | 29 + .../backend/common/config/JacksonConfig.java | 32 + .../config/RequestBodyWrappingFilter.java | 29 + .../common/config/RestClientConfig.java | 24 + .../mouda/backend/common/config/S3Config.java | 24 + .../backend/common/config/SwaggerConfig.java | 36 + .../backend/common/config/UrlConfig.java | 25 + .../backend/common/config/WebMvcConfig.java | 45 + .../LoginDarakbangMember.java | 12 + .../LoginDarakbangMemberArgumentResolver.java | 44 + .../config/argumentresolver/LoginMember.java | 12 + .../LoginMemberArgumentResolver.java | 38 + .../config/converter/FilterTypeConverter.java | 19 + .../config/data/DataSourceConfiguration.java | 85 + .../common/config/data/RoutingDataSource.java | 15 + .../AuthenticationCheckInterceptor.java | 43 + .../common/exception/ErrorResponse.java | 6 + .../exception/GlobalExceptionHandler.java | 62 + .../common/exception/MoudaException.java | 14 + .../backend/common/response/RestResponse.java | 11 + .../darakbang/business/DarakbangService.java | 78 + .../backend/darakbang/domain/Darakbang.java | 62 + .../backend/darakbang/domain/Darakbangs.java | 16 + .../exception/DarakbangErrorMessage.java | 18 + .../exception/DarakbangException.java | 12 + .../darakbang/implement/DarakbangFinder.java | 38 + .../implement/DarakbangValidator.java | 45 + .../darakbang/implement/DarakbangWriter.java | 23 + .../implement/InvitationCodeGenerator.java | 37 + .../infrastructure/DarakbangRepository.java | 18 + .../controller/DarakbangController.java | 93 + .../controller/swagger/DarakbangSwagger.java | 85 + .../request/DarakbangCreateRequest.java | 12 + .../request/DarakbangEnterRequest.java | 9 + .../response/CodeValidationResponse.java | 18 + .../response/DarakbangNameResponse.java | 16 + .../response/DarakbangResponse.java | 17 + .../response/DarakbangResponses.java | 21 + .../response/InvitationCodeResponse.java | 16 + .../business/DarakbangMemberService.java | 94 + .../domain/DarakBangMemberRole.java | 10 + .../domain/DarakbangMember.java | 140 + .../domain/DarakbangMembers.java | 15 + .../DarakbangMemberErrorMessage.java | 20 + .../exception/DarakbangMemberException.java | 12 + .../implement/DarakbangMemberFinder.java | 58 + .../implement/DarakbangMemberWriter.java | 55 + .../implement/ImageParser.java | 30 + .../darakbangmember/implement/S3Client.java | 53 + .../DarakbangMemberRepository.java | 23 + .../controller/DarakbangMemberController.java | 94 + .../swagger/DarakbangMemberSwagger.java | 74 + .../request/DarakbangMemberInfoRequest.java | 7 + .../DarakbangMemberProfileResponse.java | 22 + .../response/DarakbangMemberResponse.java | 20 + .../response/DarakbangMemberResponses.java | 21 + .../response/DarakbangMemberRoleResponse.java | 16 + .../member/business/MemberService.java | 33 + .../backend/member/domain/LoginDetail.java | 41 + .../mouda/backend/member/domain/Member.java | 102 + .../backend/member/domain/MemberStatus.java | 8 + .../backend/member/domain/OauthType.java | 9 + .../member/exception/MemberErrorMessage.java | 14 + .../member/exception/MemberException.java | 12 + .../member/implement/MemberFinder.java | 33 + .../member/implement/MemberValidator.java | 20 + .../member/implement/MemberWriter.java | 42 + .../infrastructure/MemberRepository.java | 48 + .../controller/MemberController.java | 26 + .../controller/swagger/MemberSwagger.java | 18 + .../response/DarakbangMemberInfoResponse.java | 9 + .../backend/moim/business/ChamyoService.java | 64 + .../backend/moim/business/CommentService.java | 32 + .../backend/moim/business/MoimService.java | 112 + .../backend/moim/business/ZzimService.java | 37 + .../mouda/backend/moim/domain/Chamyo.java | 65 + .../mouda/backend/moim/domain/Comment.java | 89 + .../moim/domain/CommentRecipients.java | 40 + .../mouda/backend/moim/domain/FilterType.java | 7 + .../java/mouda/backend/moim/domain/Moim.java | 234 + .../backend/moim/domain/MoimOverview.java | 17 + .../mouda/backend/moim/domain/MoimRole.java | 8 + .../mouda/backend/moim/domain/MoimStatus.java | 8 + .../backend/moim/domain/ParentComment.java | 17 + .../java/mouda/backend/moim/domain/Zzim.java | 38 + .../moim/exception/ChamyoErrorMessage.java | 19 + .../moim/exception/ChamyoException.java | 12 + .../moim/exception/CommentErrorMessage.java | 16 + .../moim/exception/CommentException.java | 12 + .../moim/exception/MoimErrorMessage.java | 33 + .../backend/moim/exception/MoimException.java | 12 + .../moim/exception/ZzimErrorMessage.java | 13 + .../backend/moim/exception/ZzimException.java | 12 + .../moim/implement/finder/ChamyoFinder.java | 61 + .../moim/implement/finder/CommentFinder.java | 35 + .../moim/implement/finder/MoimFinder.java | 87 + .../moim/implement/finder/ZzimFinder.java | 25 + ...ctMoimRelatedNotificationEventHandler.java | 21 + .../ChamyoNotificationEventHandler.java | 71 + .../notificiation/ChamyoRecipientFinder.java | 32 + .../CommentNotificationEventHandler.java | 78 + .../notificiation/CommentRecipientFinder.java | 101 + .../notificiation/CommentRecipients.java | 40 + .../MoimNotificationEventHandler.java | 124 + .../notificiation/MoimRecipientFinder.java | 48 + .../MoimRelatedNotificationSender.java | 70 + .../event/ChamyoNotificationEvent.java | 26 + .../event/CommentNotificationEvent.java | 16 + .../event/MoimCreateNotificationEvent.java | 18 + .../event/MoimEditedNotificationEvent.java | 17 + .../MoimStatusChangeNotificationEvent.java | 16 + .../implement/validator/ChamyoValidator.java | 70 + .../implement/validator/CommentValidator.java | 25 + .../implement/validator/MoimValidator.java | 86 + .../implement/validator/ZzimValidator.java | 10 + .../moim/implement/writer/ChamyoWriter.java | 58 + .../moim/implement/writer/CommentWriter.java | 34 + .../moim/implement/writer/MoimWriter.java | 78 + .../moim/implement/writer/ZzimWriter.java | 40 + .../moim/infrastructure/ChamyoRepository.java | 39 + .../infrastructure/CommentRepository.java | 22 + .../moim/infrastructure/MoimRepository.java | 36 + .../moim/infrastructure/ZzimRepository.java | 17 + .../controller/ChamyoController.java | 79 + .../controller/MoimController.java | 155 + .../controller/ZzimController.java | 52 + .../controller/swagger/ChamyoSwagger.java | 60 + .../controller/swagger/MoimSwagger.java | 121 + .../controller/swagger/ZzimSwagger.java | 39 + .../request/chamyo/ChamyoCancelRequest.java | 10 + .../request/chamyo/MoimChamyoRequest.java | 10 + .../request/comment/CommentCreateRequest.java | 25 + .../request/moim/MoimCreateRequest.java | 37 + .../request/moim/MoimEditRequest.java | 28 + .../request/moim/MoimJoinRequest.java | 15 + .../request/zzim/ZzimUpdateRequest.java | 10 + .../chamyo/ChamyoFindAllResponse.java | 22 + .../chamyo/ChamyoFindAllResponses.java | 18 + .../response/chamyo/MoimRoleFindResponse.java | 16 + .../comment/ChildCommentResponse.java | 29 + .../response/comment/CommentResponse.java | 41 + .../response/comment/CommentResponses.java | 18 + .../moim/MoimDetailsFindResponse.java | 49 + .../response/moim/MoimFindAllResponse.java | 40 + .../response/moim/MoimFindAllResponses.java | 18 + .../response/zzim/ZzimCheckResponse.java | 10 + .../business/FcmTokenService.java | 23 + .../business/MemberNotificationService.java | 26 + .../business/SubscriptionService.java | 49 + .../domain/CommonNotification.java | 35 + .../domain/FcmFailedResponse.java | 105 + .../backend/notification/domain/FcmToken.java | 23 + .../domain/MemberNotification.java | 20 + .../domain/NotificationEvent.java | 52 + .../domain/NotificationPayload.java | 52 + .../domain/NotificationSendEvent.java | 44 + .../notification/domain/NotificationType.java | 30 + .../notification/domain/Recipient.java | 14 + .../notification/domain/Subscription.java | 28 + .../exception/NotificationErrorMessage.java | 17 + .../exception/NotificationException.java | 12 + .../implement/MessageFactory.java | 10 + .../implement/NotificationFinder.java | 35 + .../implement/NotificationProcessor.java | 26 + .../NotificationSendEventHandler.java | 35 + .../implement/NotificationSender.java | 11 + .../implement/NotificationWriter.java | 46 + .../fcm/AsyncFcmNotificationSender.java | 69 + .../implement/fcm/FcmConfigBuilder.java | 8 + .../implement/fcm/FcmMessageFactory.java | 50 + .../implement/fcm/FcmNotificationSender.java | 34 + .../implement/fcm/FcmResponseHandler.java | 108 + .../implement/fcm/FcmRetryableChecker.java | 62 + .../implement/fcm/WebFcmConfigBuilder.java | 32 + .../implement/fcm/token/FcmTokenFinder.java | 33 + .../fcm/token/FcmTokenScheduler.java | 34 + .../implement/fcm/token/FcmTokenWriter.java | 46 + .../filter/ChatRoomSubscriptionFilter.java | 39 + .../filter/MoimCreatedSubscriptionFilter.java | 34 + .../filter/NonSubscriptionFilter.java | 25 + .../implement/filter/SubscriptionFilter.java | 14 + .../filter/SubscriptionFilterRegistry.java | 32 + .../subscription/SubscriptionFinder.java | 54 + .../subscription/SubscriptionWriter.java | 50 + .../infrastructure/entity/FcmTokenEntity.java | 62 + .../entity/MemberNotificationEntity.java | 46 + .../entity/SubscriptionEntity.java | 61 + .../entity/UnsubscribedChatRooms.java | 31 + .../repository/FcmTokenRepository.java | 19 + .../MemberNotificationRepository.java | 12 + .../repository/SubscriptionRepository.java | 12 + .../MemberNotificationController.java | 32 + .../controller/MemberNotificationSwagger.java | 24 + .../NotificationTokenController.java | 29 + .../controller/NotificationTokenSwagger.java | 23 + .../controller/SubscriptionController.java | 68 + .../controller/SubscriptionSwagger.java | 53 + .../request/ChatSubscriptionRequest.java | 12 + .../presentation/request/FcmTokenRequest.java | 6 + .../MemberNotificationFindAllResponse.java | 18 + .../MemberNotificationFindResponse.java | 42 + .../response/SubscriptionResponse.java | 9 + .../util/FcmRetryAfterExtractor.java | 39 + .../please/business/InterestService.java | 25 + .../please/business/PleaseService.java | 43 + .../mouda/backend/please/domain/Interest.java | 56 + .../mouda/backend/please/domain/Please.java | 68 + .../please/domain/PleaseWithInterest.java | 23 + .../please/domain/PleaseWithInterests.java | 13 + .../please/exception/PleaseErrorMessage.java | 18 + .../please/exception/PleaseException.java | 12 + .../please/implement/InterestFinder.java | 25 + .../please/implement/InterestValidator.java | 21 + .../please/implement/InterestWriter.java | 36 + .../please/implement/PleaseFinder.java | 50 + .../please/implement/PleaseValidator.java | 32 + .../please/implement/PleaseWriter.java | 22 + .../infrastructure/InterestRepository.java | 18 + .../infrastructure/PleaseRepository.java | 12 + .../controller/InterestController.java | 35 + .../controller/PleaseController.java | 64 + .../controller/swagger/InterestSwagger.java | 25 + .../controller/swagger/PleaseSwagger.java | 47 + .../request/InterestUpdateRequest.java | 7 + .../request/PleaseCreateRequest.java | 23 + .../response/PleaseFindAllResponse.java | 23 + .../response/PleaseFindAllResponses.java | 18 + .../src/main/resources/application-dev.yml | 52 + .../src/main/resources/application-local.yml | 62 + .../src/main/resources/application-prod.yml | 66 + backend/src/main/resources/application.yml | 27 + backend/src/main/resources/data.sql | 14 + backend/src/main/resources/logback-spring.xml | 42 + .../Infrastructure/AppleOauthClientTest.java | 26 + .../auth/business/AppleAuthServiceTest.java | 107 + .../auth/business/GoogleAuthServiceTest.java | 55 + .../auth/business/KakaoAuthServiceTest.java | 65 + .../implement/AppleUserInfoProviderTest.java | 26 + .../auth/implement/MemberFinderTest.java | 50 + .../auth/implement/MemberWriterTest.java | 56 + .../controller/AuthControllerTest.java | 47 + .../bet/business/BetSchedulerTest.java | 62 + .../backend/bet/business/BetServiceTest.java | 153 + .../mouda/backend/bet/domain/BetTest.java | 59 + .../backend/bet/domain/BettingTimeTest.java | 25 + .../backend/bet/implement/BetFinderTest.java | 121 + .../backend/bet/implement/BetSorterTest.java | 77 + .../backend/bet/implement/BetWriterTest.java | 86 + .../bet/implement/ParticipantFinderTest.java | 50 + .../bet/infrastructure/BetRepositoryTest.java | 39 + .../chamyo/service/ChamyoServiceTest.java | 113 + .../mouda/backend/chat/ChatAsyncTest.java | 91 + .../chat/business/ChatRoomServiceTest.java | 163 + .../chat/business/ChatServiceTest.java | 313 + .../chat/entity/ChatRoomEntityTest.java | 55 + .../AttributeManagerRegistryTest.java | 40 + .../implement/BetAttributeManagerTest.java | 142 + .../implement/BetChatPreviewManagerTest.java | 68 + .../implement/BetParticipantResolverTest.java | 64 + .../chat/implement/ChatFinderTest.java | 102 + .../implement/ChatRecipientFinderTest.java | 49 + .../implement/ChatRoomDetailsFinderTest.java | 153 + .../chat/implement/ChatRoomValidatorTest.java | 67 + .../chat/implement/ChatRoomWriterTest.java | 79 + .../implement/MoimAttributeManagerTest.java | 107 + .../implement/MoimChatPreviewManagerTest.java | 72 + .../MoimParticipantResolverTest.java | 73 + .../ParticipantResolverRegistryTest.java | 72 + .../backend/common/config/ChamyoCreator.java | 46 + .../common/config/DatabaseCleaner.java | 39 + .../common/config/NoTransactionExtension.java | 51 + .../common/config/WithTransactionalTest.java | 11 + .../config/data/RoutingDataSourceTest.java | 58 + .../common/fixture/BetEntityFixture.java | 63 + .../backend/common/fixture/BetFixture.java | 26 + .../common/fixture/ChatEntityFixture.java | 22 + .../common/fixture/ChatRoomEntityFixture.java | 23 + .../common/fixture/CommentFixture.java | 30 + .../common/fixture/DarakbangFixture.java | 20 + .../fixture/DarakbangMemberFixture.java | 53 + .../common/fixture/DarakbangSetUp.java | 55 + .../backend/common/fixture/MemberFixture.java | 50 + .../backend/common/fixture/MoimFixture.java | 57 + .../backend/common/fixture/PleaseFixture.java | 42 + .../common/global/IgnoreNotificationTest.java | 23 + .../business/DarakbangServiceTest.java | 72 + .../implement/DarakbangFinderTest.java | 94 + .../implement/DarakbangValidatorTest.java | 80 + .../implement/DarakbangWriterTest.java | 36 + .../InvitationCodeGeneratorTest.java | 23 + .../business/DarakbangMemberServiceTest.java | 163 + .../domain/DarakbangMemberTest.java | 24 + .../implement/DarakbangMemberFinderTest.java | 130 + .../implement/DarakbangMemberWriterTest.java | 74 + .../implement/ImageParserTest.java | 26 + .../member/implement/MemberValidatorTest.java | 47 + .../moim/business/ChamyoAsyncTest.java | 84 + .../moim/business/CommentAsyncTest.java | 113 + .../backend/moim/business/MoimAsyncTest.java | 146 + .../chamyo/business/ChamyoServiceTest.java | 108 + .../infrastructure/ChatRepositoryTest.java | 70 + .../moim/comment/domain/CommentTest.java | 74 + .../implement/finder/ChamyoFinderTest.java | 157 + .../finder/ChamyoRecipientFinderTest.java | 53 + .../implement/finder/CommentFinderTest.java | 50 + .../finder/CommentRecipientsFinderTest.java | 182 + .../moim/implement/finder/MoimFinderTest.java | 181 + .../finder/MoimRecipientFinderTest.java | 73 + .../moim/implement/finder/ZzimFinderTest.java | 53 + .../implement/writer/ChamyoWriterTest.java | 103 + .../implement/writer/CommentWriterTest.java | 59 + .../moim/implement/writer/MoimWriterTest.java | 125 + .../moim/implement/writer/ZzimWriterTest.java | 54 + .../moim/business/CommentServiceTest.java | 94 + .../moim/moim/business/MoimServiceTest.java | 116 + .../backend/moim/moim/domain/MoimTest.java | 287 + .../business/NotificationServiceTest.java | 100 + .../domain/NotificationSendEventTest.java | 66 + .../implement/NotificationAsyncTest.java | 75 + .../implement/NotificationFinderTest.java | 54 + .../implement/fcm/FcmMessageFactoryTest.java | 116 + .../fcm/token/FcmTokenWriterTest.java | 91 + .../ChatRoomSubscriptionFilterTest.java | 107 + .../subscription/SubscriptionFinderTest.java | 57 + .../subscription/SubscriptionWriterTest.java | 160 + .../SubscriptionEntityTest.java | 54 + .../please/implement/InterestFinderTest.java | 52 + .../please/implement/InterestWriterTest.java | 36 + .../please/implement/PleaseFinderTest.java | 71 + .../please/implement/PleaseValidatorTest.java | 43 + .../org.junit.jupiter.api.extension.Extension | 1 + backend/src/test/resources/application.yml | 48 + .../test/resources/junit-platform.properties | 1 + frontend/.browserslistrc | 1 + frontend/.eslintrc.cjs | 37 + frontend/.gitignore | 8 + frontend/.prettierrc | 28 + frontend/.storybook/main.ts | 90 + frontend/.storybook/preview.tsx | 39 + frontend/.stylelintrc.json | 36 + frontend/.vscode/settings.json | 7 + frontend/babel.config.json | 13 + frontend/cypress.config.ts | 9 + frontend/cypress/support/commands.ts | 37 + frontend/cypress/support/e2e.ts | 20 + frontend/jest.config.json | 25 + frontend/jest.polyfills.js | 32 + frontend/jest.setup.ts | 20 + frontend/package-lock.json | 23437 ++++++++++++++++ frontend/package.json | 106 + .../android/android-launchericon-144-144.png | Bin 0 -> 9440 bytes .../android/android-launchericon-192-192.png | Bin 0 -> 12613 bytes .../android/android-launchericon-48-48.png | Bin 0 -> 3431 bytes .../android/android-launchericon-512-512.png | Bin 0 -> 35721 bytes .../android/android-launchericon-72-72.png | Bin 0 -> 4925 bytes .../android/android-launchericon-96-96.png | Bin 0 -> 6459 bytes frontend/public/firebase-messaging-sw.js | 53 + frontend/public/main-logo.png | Bin 0 -> 16188 bytes frontend/public/main-logo2.png | Bin 0 -> 9745 bytes frontend/public/manifest.json | 51 + frontend/public/mockServiceWorker.js | 284 + frontend/public/preview-image.png | Bin 0 -> 24037 bytes frontend/src/App.tsx | 30 + frontend/src/RouteChageTracker.ts | 34 + frontend/src/apis/apiClient.ts | 245 + frontend/src/apis/auth.ts | 17 + frontend/src/apis/deletes.ts | 7 + frontend/src/apis/endPoints.ts | 16 + frontend/src/apis/gets.ts | 189 + frontend/src/apis/patches.ts | 39 + frontend/src/apis/posts.ts | 134 + frontend/src/apis/responseTypes.ts | 109 + frontend/src/common/assets/back.svg | 15 + frontend/src/common/assets/crown.svg | 3 + .../src/common/assets/default_profile.png | Bin 0 -> 2349 bytes frontend/src/common/assets/empty_profile.svg | 4 + .../Pretendard-Black.subset.woff2 | Bin 0 -> 273460 bytes .../woff2-subset/Pretendard-Bold.subset.woff2 | Bin 0 -> 270784 bytes .../Pretendard-ExtraBold.subset.woff2 | Bin 0 -> 271796 bytes .../Pretendard-ExtraLight.subset.woff2 | Bin 0 -> 263284 bytes .../Pretendard-Light.subset.woff2 | Bin 0 -> 267768 bytes .../Pretendard-Medium.subset.woff2 | Bin 0 -> 268324 bytes .../Pretendard-Regular.subset.woff2 | Bin 0 -> 267096 bytes .../Pretendard-SemiBold.subset.woff2 | Bin 0 -> 268752 bytes .../woff2-subset/Pretendard-Thin.subset.woff2 | Bin 0 -> 258680 bytes .../fonts/woff2/PretendardVariable.woff2 | Bin 0 -> 2057688 bytes frontend/src/common/assets/kebab_menu.svg | 3 + frontend/src/common/assets/meatball.svg | 5 + frontend/src/common/assets/notification.svg | 6 + frontend/src/common/assets/plus.svg | 3 + frontend/src/common/assets/regret_cat.png | Bin 0 -> 1093900 bytes .../common/assets/submit_message_button.svg | 4 + frontend/src/common/assets/tabler_plus.svg | 3 + frontend/src/common/assets/x.svg | 3 + .../src/common/assets/zzim_emty_button.svg | 3 + .../src/common/assets/zzim_fill_button.svg | 3 + frontend/src/common/common.style.ts | 10 + frontend/src/common/font.style.ts | 89 + frontend/src/common/getRoutes.ts | 30 + frontend/src/common/inviteCodeManager.tsx | 15 + frontend/src/common/lastDarakbangManager.tsx | 11 + frontend/src/common/reset.style.ts | 142 + frontend/src/common/theme/colorPalette.ts | 89 + .../src/common/theme/coloredTypography.ts | 20 + frontend/src/common/theme/layout.ts | 9 + frontend/src/common/theme/semantic.ts | 9 + frontend/src/common/theme/theme.style.ts | 14 + frontend/src/common/theme/theme.type.ts | 80 + frontend/src/common/theme/typography.ts | 177 + .../BackArrowButton/BackArrowButton.style.ts | 7 + .../BackArrowButton/BackArrowButton.tsx | 15 + .../src/components/Button/Button.stories.tsx | 14 + .../src/components/Button/Button.style.ts | 125 + frontend/src/components/Button/Button.tsx | 36 + frontend/src/components/Chat/Chat.stories.tsx | 24 + frontend/src/components/Chat/Chat.tsx | 35 + frontend/src/components/Chat/Chatstyle.ts | 31 + .../ChatBottomMenu/ChatBottomMenu.stories.tsx | 63 + .../ChatBottomMenu/ChatBottomMenu.style.ts | 13 + .../ChatBottomMenu/ChatBottomMenu.tsx | 11 + .../ChatBubble/ChatBubble.stories.tsx | 14 + .../components/ChatBubble/ChatBubble.style.ts | 24 + .../src/components/ChatBubble/ChatBubble.tsx | 20 + .../ChatChildren/ChatChildren.style.tsx | 10 + .../ChatBubble/ChatChildren/ChatChildren.tsx | 60 + .../components/ChatList/ChatList.stories.tsx | 107 + .../src/components/ChatList/ChatList.style.ts | 13 + frontend/src/components/ChatList/ChatList.tsx | 35 + .../ChatMenuItem/ChatMenuItem.stories.tsx | 19 + .../ChatMenuItem/ChatMenuItem.style.ts | 30 + .../components/ChatMenuItem/ChatMenuItem.tsx | 29 + .../ChattingFooter/ChattingFooter.stories.tsx | 14 + .../ChattingFooter/ChattingFooter.style.ts | 54 + .../ChattingFooter/ChattingFooter.tsx | 72 + .../ChattingPreview.stories.tsx | 62 + .../ChattingPreview/ChattingPreview.style.ts | 94 + .../ChattingPreview/ChattingPreview.tsx | 68 + .../CommentCard/CommentCard.stories.tsx | 34 + .../CommentCard/CommentCard.style.ts | 79 + .../components/CommentCard/CommentCard.tsx | 61 + .../CommentCard/CommentCardSkeleton.tsx | 25 + .../CommentList/ComentList.style.ts | 7 + .../CommentList/CommentList.stories.tsx | 54 + .../components/CommentList/CommentList.tsx | 46 + .../CommentList/CommentListSkeleton.tsx | 22 + .../DarakbangNameWrapper.style.ts | 11 + .../DarakbangNameWrapper.tsx | 16 + .../DateTimeModalContent.stories.tsx | 14 + .../DateTimeModalContent.style.ts | 27 + .../DateTimeModalContent.tsx | 86 + .../ErrorControlledInput.stories.tsx | 16 + .../ErrorControlledInput.style.ts | 19 + .../ErrorControlledInput.tsx | 22 + .../Funnel/FunnelButton/FunnelButton.tsx | 19 + .../FunnelInput/FunnelInput.stories.tsx | 18 + .../Funnel/FunnelInput/FunnelInput.style.ts | 13 + .../Funnel/FunnelInput/FunnelInput.tsx | 16 + .../FunnelQuestion/FunnelQuestion.stories.tsx | 21 + .../Funnel/FunnelQuestion/FunnelQuestion.tsx | 14 + .../FunnelQuestionHighlight.style.ts | 5 + .../FunnelQuestionHighlight.tsx | 11 + .../FunnelQuestionText/FunnelQuestionText.tsx | 10 + .../FunnelRadioCardGroup.style.ts | 7 + .../FunnelRadioCardGroup.tsx | 13 + .../FunnelRadioCardGroupOption.style.ts | 36 + .../FunnelRadioCardGroupOption.tsx | 30 + .../FunnelStepIndicator.stories.tsx | 26 + .../FunnelStepIndicator.style.ts | 34 + .../FunnelStepIndicator.tsx | 26 + .../FunnelTextArea/FunnelTextArea.style.ts | 19 + .../Funnel/FunnelTextArea/FunnelTextArea.tsx | 17 + .../HighlightSpan/HighlightSpan.stories.tsx | 21 + .../HighlightSpan/HighlightSpan.style.ts | 17 + .../HighlightSpan/HighlightSpan.tsx | 54 + .../HomeHeaderContent/HomHeaderContent.tsx | 11 + .../HomeHeaderContent.style.ts | 8 + .../HomeMainContent/HomeMainContent.tsx | 22 + .../src/components/Icons/BackArrowIcon.tsx | 19 + .../src/components/Icons/CalenderClock.tsx | 35 + .../src/components/Icons/ChatBubbleSvg.tsx | 22 + .../src/components/Icons/ChattingIcon.tsx | 28 + frontend/src/components/Icons/HeartIcon.tsx | 48 + frontend/src/components/Icons/HomeIcon.tsx | 28 + .../src/components/Icons/InterestingIcon.tsx | 28 + .../src/components/Icons/KakaoOAuthIcon.tsx | 21 + .../src/components/Icons/MainLogoIcon.tsx | 82 + frontend/src/components/Icons/MyPageIcon.tsx | 28 + frontend/src/components/Icons/Picker.tsx | 25 + frontend/src/components/Icons/PleaseIcon.tsx | 34 + frontend/src/components/Icons/PlusIcon.tsx | 23 + .../src/components/Icons/SelectionIcon.tsx | 56 + frontend/src/components/Icons/SendButton.tsx | 31 + frontend/src/components/Icons/SolidArrow.tsx | 37 + .../MessagInput/MessageInput.stories.tsx | 20 + .../Input/MessagInput/MessageInput.style.ts | 29 + .../Input/MessagInput/MessageInput.tsx | 44 + .../src/components/Input/MoimInput.style.ts | 31 + frontend/src/components/Input/MoimInput.tsx | 40 + .../KebabMenu/KababMenu.stories.tsx | 29 + .../components/KebabMenu/KebabMenu.style.ts | 32 + .../src/components/KebabMenu/KebabMenu.tsx | 58 + .../LoginForm/LoginForm.stories.tsx | 15 + .../components/LoginForm/LoginForm.style.ts | 14 + .../src/components/LoginForm/LoginForm.tsx | 47 + .../MemberCard/MemberCard.stories.tsx | 14 + .../components/MemberCard/MemberCard.style.ts | 21 + .../src/components/MemberCard/MemberCard.tsx | 31 + .../components/MenuItem/MenuItem.stories.tsx | 14 + .../components/MenuItem/MenuItem.style.tsx | 20 + frontend/src/components/MenuItem/MenuItem.tsx | 21 + .../MenuItemList/MenuItemList.stories.tsx | 24 + .../components/MenuItemList/MenuItemList.tsx | 25 + .../MineInfoCard/MineInfoCard.style.ts | 16 + .../components/MineInfoCard/MineInfoCard.tsx | 33 + .../MineInfoCard/MoinInfoCard.stories.tsx | 19 + .../MissingFallback.stories.tsx | 29 + .../MissingFallback/MissingFallback.style.ts | 23 + .../MissingFallback/MissingFallback.tsx | 21 + frontend/src/components/Modal/Modal.style.tsx | 52 + frontend/src/components/Modal/Modal.tsx | 44 + .../components/MoimCard/MoimCard.stories.tsx | 52 + .../src/components/MoimCard/MoimCard.style.ts | 102 + frontend/src/components/MoimCard/MoimCard.tsx | 76 + .../MoimCardList/MoimCardList.stories.tsx | 115 + .../MoimCardList/MoimCardList.style.ts | 7 + .../components/MoimCardList/MoimCardList.tsx | 34 + .../MoimDescription.stories.tsx | 19 + .../MoimDescription/MoimDescription.style.ts | 30 + .../MoimDescription/MoimDescription.tsx | 25 + .../MoimDescriptionSkeleton.tsx | 34 + .../MoimInfomationSkeleton.tsx | 39 + .../MoimInformation.stories.tsx | 24 + .../MoimInformation/MoimInformation.style.ts | 36 + .../MoimInformation/MoimInformation.tsx | 55 + .../MoimCardListSkeleton.style.ts | 7 + .../MoimCardListSkeleton.tsx | 16 + .../MoimCardSkeleton.stories.tsx | 16 + .../MoimCardSkeleton.style.ts | 27 + .../MoimCardSkeleton/MoimCardSkeleton.tsx | 19 + frontend/src/components/MoimList/MoimList.tsx | 18 + .../MoimSummary/MoimSummary.stories.tsx | 21 + .../MoimSummary/MoimSummary.style.ts | 39 + .../components/MoimSummary/MoimSummary.tsx | 24 + .../MoimSummary/MoimSummarySkeleton.tsx | 15 + .../MoimTabBar/MoimTabBar.stories.tsx | 21 + .../components/MoimTabBar/MoimTabBar.style.ts | 21 + .../src/components/MoimTabBar/MoimTabBar.tsx | 36 + .../src/components/MyMoim/MyMoim.style.tsx | 5 + frontend/src/components/MyMoim/MyMoim.tsx | 44 + .../MyMoimListFilterTag.stories.tsx | 38 + .../MyMoimListFilterTag.style.ts | 24 + .../MyMoimListFilterTag.tsx | 25 + .../MyMoimListFilters.stories.tsx | 19 + .../MyMoimListFilters.style.ts | 6 + .../MyMoimListFilters/MyMoimListFilters.tsx | 45 + .../MyZzimMoimList/MyZzimMoimList.tsx | 17 + .../NavigationBar/NavigationBar.stories.tsx | 15 + .../NavigationBar/NavigationBar.style.ts | 25 + .../NavigationBar/NavigationBar.tsx | 48 + .../NavigationBarItem.style.ts | 25 + .../NavigationBarItem/NavigationBarItem.tsx | 42 + .../NotificationCard.const.ts | 21 + .../NotificationCard.style.ts | 32 + .../NotificationCard/NotificationCard.tsx | 23 + .../NotificationList.stories.tsx | 37 + .../NotificationList.style.ts | 7 + .../NotificationList/NotificationList.tsx | 33 + .../OptionsPanel/OptionsPanel.stories.tsx | 101 + .../OptionsPanel/OptionsPanel.style.ts | 61 + .../components/OptionsPanel/OptionsPanel.tsx | 62 + .../PlaceModalContent.stories.tsx | 14 + .../PlaceModalContent.style.ts | 27 + .../PlaceModalContent/PlaceModalContent.tsx | 64 + .../PleaseCard/PleaseCard.stories.tsx | 24 + .../components/PleaseCard/PleaseCard.style.ts | 76 + .../src/components/PleaseCard/PleaseCard.tsx | 48 + .../PleaseCardList/PleaseCardList.style.ts | 7 + .../PleaseCardList/PleaseCardList.tsx | 19 + .../PleaseCardListSkeleton.style.ts | 7 + .../PleaseCardListSkeleton.tsx | 15 + .../PleaseCardSkeleton.stories.tsx | 16 + .../PleaseCardSkeleton.style.ts | 33 + .../PleaseCardSkeleton/PleaseCardSkeleton.tsx | 21 + .../src/components/PleaseList/PleaseList.tsx | 18 + .../components/PlusButton/PlusButton.style.ts | 12 + .../src/components/PlusButton/PlusButton.tsx | 16 + .../Profile/ProfileCard.stories.tsx | 46 + .../components/Profile/ProfileCard.style.ts | 16 + .../src/components/Profile/ProfileCard.tsx | 18 + .../Profile/ProfileFrame.stories.tsx | 50 + .../components/Profile/ProfileFrame.style.ts | 42 + .../src/components/Profile/ProfileFrame.tsx | 48 + .../ProfileList/ProfileList.stories.tsx | 59 + .../ProfileList/ProfileList.style.ts | 17 + .../components/ProfileList/ProfileList.tsx | 24 + .../ProfileList/ProfileListSkeleton.tsx | 21 + .../SelectBar/SelectBar.stories.tsx | 14 + .../components/SelectBar/SelectBar.style.ts | 13 + .../src/components/SelectBar/SelectBar.tsx | 19 + .../Skeleton/SkeletonPiece.stories.tsx | 19 + .../Skeleton/SkeletonPiece.style.ts | 35 + .../src/components/Skeleton/SkeletonPiece.tsx | 15 + frontend/src/components/Tag/Tag.stories.tsx | 39 + frontend/src/components/Tag/Tag.style.ts | 28 + frontend/src/components/Tag/Tag.tsx | 23 + frontend/src/components/Tag/TagSkeleton.tsx | 5 + .../TextArea/LabeledTextArea.stories.tsx | 19 + .../TextArea/LabeledTextArea.style.ts | 29 + .../components/TextArea/LabeledTextArea.tsx | 31 + .../UserPreview/UserPreview.stories.tsx | 17 + .../UserPreview/UserPreview.style.ts | 26 + .../components/UserPreview/UserPreview.tsx | 13 + .../UserPreviewList.stories.tsx | 37 + .../UserPreviewList/UserPreviewList.style.ts | 32 + .../UserPreviewList/UserPreviewList.tsx | 21 + .../components/Zzim/ZzimButton.stories.tsx | 35 + .../src/components/Zzim/ZzimButton.style.ts | 7 + frontend/src/components/Zzim/ZzimButton.tsx | 17 + frontend/src/constants/poclies.ts | 33 + frontend/src/constants/queryKeys.ts | 26 + frontend/src/constants/routes.ts | 51 + frontend/src/constants/styles.ts | 2 + frontend/src/custom.d.ts | 19 + frontend/src/emotion.d.ts | 15 + frontend/src/hooks/mutaions/useAddMoim.ts | 24 + frontend/src/hooks/mutaions/useAddPlease.ts | 23 + .../src/hooks/mutaions/useCancelChamyo.ts | 32 + frontend/src/hooks/mutaions/useCancelMoim.ts | 18 + frontend/src/hooks/mutaions/useChangeZzim.ts | 44 + .../src/hooks/mutaions/useCompleteMoin.ts | 18 + .../src/hooks/mutaions/useConfirmDatetime.ts | 16 + .../src/hooks/mutaions/useConfirmPlace.ts | 9 + .../src/hooks/mutaions/useCreateDarakbang.ts | 17 + .../src/hooks/mutaions/useEnterDarakbang.ts | 34 + frontend/src/hooks/mutaions/useInterest.ts | 23 + frontend/src/hooks/mutaions/useJoinMoim.ts | 19 + frontend/src/hooks/mutaions/useModifyMoim.ts | 26 + frontend/src/hooks/mutaions/useOpenChat.ts | 9 + frontend/src/hooks/mutaions/useReopenMoim.ts | 18 + frontend/src/hooks/mutaions/useSendMessage.ts | 8 + frontend/src/hooks/mutaions/useServeToken.ts | 8 + .../src/hooks/mutaions/useWriteComment.ts | 30 + frontend/src/hooks/queries/useChamyoAll.ts | 18 + frontend/src/hooks/queries/useChamyoMine.ts | 18 + frontend/src/hooks/queries/useChatPreiview.ts | 19 + frontend/src/hooks/queries/useChats.test.tsx | 44 + frontend/src/hooks/queries/useChats.ts | 34 + .../hooks/queries/useDarakbangInviteCode.ts | 13 + .../src/hooks/queries/useDarakbangMembers.ts | 13 + .../hooks/queries/useDarakbangNameByCode.ts | 12 + frontend/src/hooks/queries/useMoim.ts | 18 + frontend/src/hooks/queries/useMoims.ts | 12 + frontend/src/hooks/queries/useMyDarakbang.ts | 12 + .../src/hooks/queries/useMyDarakbangRole.ts | 13 + frontend/src/hooks/queries/useMyInfo.ts | 13 + frontend/src/hooks/queries/useMyMoim.ts | 13 + frontend/src/hooks/queries/useMyMoims.ts | 19 + frontend/src/hooks/queries/useMyZzimMoim.ts | 17 + frontend/src/hooks/queries/useNotification.ts | 17 + .../hooks/queries/useNowDarakbangNameById.ts | 13 + frontend/src/hooks/queries/usePleases.ts | 13 + frontend/src/hooks/queries/useZzimMine.ts | 18 + frontend/src/hooks/useFunnel.test.tsx | 81 + frontend/src/hooks/useFunnel.ts | 35 + frontend/src/hooks/useStatePersist.test.tsx | 132 + frontend/src/hooks/useStatePersist.ts | 65 + frontend/src/index.html | 24 + frontend/src/index.tsx | 48 + .../ChattingPreviewContainer.stories.tsx | 14 + .../ChattingPreviewContainer.style.ts | 19 + .../ChattingPreviewContainer.tsx | 11 + .../ChattingPreviewLayout.style.ts | 15 + .../ChattingPreviewLayout.tsx | 18 + .../ChattingRoomFooter.style.ts | 9 + .../ChattingRoomFooter/ChattingRoomFooter.tsx | 11 + .../ChattingRoomLayout.stories.tsx | 135 + .../ChattingRoomLayout.style.ts | 20 + .../ChattingRoomLayout/ChattingRoomLayout.tsx | 35 + .../CompleteBottomWrapper.style.ts | 10 + .../CompleteBottomWrapper.tsx | 11 + .../CompleteContentContainer.stories.tsx | 52 + .../CompleteContentContainer.style.ts | 15 + .../CompleteContentContainer.tsx | 9 + .../CompleteLayout/CompleteLayout.style.ts | 7 + .../layouts/CompleteLayout/CompleteLayout.tsx | 18 + .../FormBottomButtonWrapper.tsx | 9 + .../FormBottomWrapper.style.ts | 12 + .../FormLayout/FormHeader/FormHeader.style.ts | 17 + .../FormLayout/FormHeader/FormHeader.tsx | 24 + .../layouts/FormLayout/FormLayout.style.ts | 8 + .../src/layouts/FormLayout/FormLayout.tsx | 18 + .../FormLayout/FormMain/FormMain.style.ts | 8 + .../layouts/FormLayout/FormMain/FormMain.tsx | 8 + .../FunnelFooter/FunnelFooter.style.ts | 15 + .../FunnelFooter/FunnelFooter.tsx | 23 + .../FunnelLayout/FunnelLayout.style.ts | 7 + .../src/layouts/FunnelLayout/FunnelLayout.tsx | 17 + .../FunnelMain/FunnelMain.style.ts | 10 + .../FunnelLayout/FunnelMain/FunnelMain.tsx | 8 + .../HomeFixedButtonWrapper.style.ts | 15 + .../HomeFixedButtonWrapper.tsx | 8 + .../HomeHeader/HomeHeader.style.ts | 15 + .../HomeLayout.tsx/HomeHeader/HomeHeader.tsx | 11 + .../HomeLayout.tsx/HomeLayout.style.ts | 9 + .../src/layouts/HomeLayout.tsx/HomeLayout.tsx | 21 + .../HomeLayout.tsx/HomeMain/HomeMain.style.ts | 5 + .../HomeLayout.tsx/HomeMain/HomeMain.tsx | 9 + .../InformationBottomWrapper.style.ts | 12 + .../InformationBottomWrapper.tsx | 9 + .../InformationContentContainer.stories.tsx | 51 + .../InformationContentContainer.style.ts | 8 + .../InformationLayoutContentContainer.tsx | 9 + .../InformationLayout.stories.tsx | 72 + .../InformationLayout.style.ts | 8 + .../InformationLayout/InformationLayout.tsx | 18 + .../LoginFooter/LoginFooter.style.ts | 6 + .../LoginLayout/LoginFooter/LoginFooter.tsx | 8 + .../LoginHeader/LoginHeader.style.ts | 7 + .../LoginLayout/LoginHeader/LoginHeader.tsx | 8 + .../layouts/LoginLayout/LoginLayout.style.ts | 7 + .../src/layouts/LoginLayout/LoginLayout.tsx | 17 + .../LoginLayout/LoginMain/LoginMain.style.ts | 11 + .../LoginLayout/LoginMain/LoginMain.tsx | 8 + .../PleaseFixedButtonWrapper.style.ts | 15 + .../PleaseFixedButtonWrapper.tsx | 8 + .../PleaseHeader/PleaseHeader.style.ts | 16 + .../PleaseHeader/PleaseHeader.tsx | 14 + .../PleaseLayout/PleaseLayout.style.ts | 9 + .../src/layouts/PleaseLayout/PleaseLayout.tsx | 20 + .../PleaseMain/PleaseMain.style.ts | 5 + .../PleaseLayout/PleaseMain/PleaseMain.tsx | 9 + .../SelectBottomWrapper.style.ts | 8 + .../SelectBottomWrapper.tsx | 9 + .../SelectContentContainer.stories.tsx | 52 + .../SelectContentContainer.style.ts | 13 + .../SelectContentContainer.tsx | 9 + .../SelectLayout/SelectLayout.style.ts | 7 + .../src/layouts/SelectLayout/SelectLayout.tsx | 18 + .../StretchContentBottomWrapper.style.ts | 8 + .../StretchContentBottomWrapper.tsx | 9 + .../StretchContentContainer.style.ts | 15 + .../StretchContentContainer.tsx | 9 + .../StretchContentLayout.style.ts | 7 + .../StretchContentLayout.tsx | 18 + .../NavigationBarWrapper.style.ts | 12 + .../NavigationBarWrapper.tsx | 8 + .../StickyTriSectionHeader.style.ts | 7 + .../StickyTriSectionHeader.tsx | 22 + .../TriSectionHeader.stories.tsx | 29 + .../TriSectionHeader.style.ts | 40 + .../TriSectionHeader/TriSectionHeader.tsx | 35 + frontend/src/mocks/browser.ts | 14 + frontend/src/mocks/handler/chatHandler.ts | 40 + frontend/src/mocks/handler/index.ts | 4 + frontend/src/mocks/handler/interestHandler.ts | 17 + frontend/src/mocks/handler/mockPleases.ts | 61 + frontend/src/mocks/handler/mockedChats.ts | 186 + frontend/src/mocks/handler/moimHandler.ts | 255 + .../src/mocks/handler/notificationHandler.ts | 19 + frontend/src/mocks/handler/pleaseHandler.ts | 13 + frontend/src/mocks/server.ts | 4 + frontend/src/mocks/wrapper.tsx | 19 + .../ChatCardListSkeleton.style.ts | 8 + .../ChatListSkeleton/ChatCardListSkeleton.tsx | 17 + .../ChatCardSkeleton.stories.tsx | 16 + .../ChatCardSkeleton.style.ts | 27 + .../ChatCardSkeleton/ChatCardSkeleton.tsx | 16 + frontend/src/pages/ChatPage/ChatPage.tsx | 54 + .../ChattingRoomPage/ChattingRoomPage.tsx | 145 + .../DarakbangCreationModalContent.style.ts | 27 + .../DarakbangCreationModalContent.tsx | 55 + .../DarakbangCreationPage.style.ts | 8 + .../DarakbangCreationPage.tsx | 106 + .../DarakbangEntrancePage.tsx | 56 + .../DarakbangInvitationPage.style.ts | 24 + .../DarakbangInvitationPage.tsx | 59 + .../DarakbangLandingPage.tsx | 44 + .../DarakbangManagementPage.tsx | 28 + .../DarakbangMembersPage.style.tsx | 8 + .../DarakbangMembersPage.tsx | 30 + .../DarakbangNicknameModalContent.style.ts | 27 + .../DarakbangNicknameModalContent.tsx | 51 + .../DarakbangNicknamePage.tsx | 98 + .../DarakbangSelectOptionPage.tsx | 32 + .../DarakbangSelectPage.style.ts | 17 + .../DarakbangSelectPage.tsx | 71 + frontend/src/pages/HomePage/HomePage.tsx | 85 + .../KakaoOAuthLoginPage.tsx | 37 + .../src/pages/MainPage/MainPage.stories.tsx | 15 + frontend/src/pages/MainPage/MainPage.style.ts | 22 + frontend/src/pages/MainPage/MainPage.tsx | 201 + .../MoimCreationPage.hook.test.tsx | 90 + .../MoimCreationPage/MoimCreationPage.hook.ts | 146 + .../MoimCreationPage.style.ts | 10 + .../MoimCreationPage/MoimCreationPage.tsx | 114 + .../MoimCreationPage.util.test.tsx | 94 + .../MoimCreationPage/MoimCreationPage.util.ts | 63 + .../Steps/DateAndTimeStep.tsx | 72 + .../Steps/DescriptionStep.tsx | 53 + .../MoimCreationPage/Steps/MaxPeopleStep.tsx | 52 + .../Steps/OfflineOrOnlineStep.tsx | 44 + .../MoimCreationPage/Steps/PlaceStep.tsx | 55 + .../MoimCreationPage/Steps/TitleStep.tsx | 55 + .../pages/MoimDetailPage/MoimDetailPage.tsx | 246 + .../MoimModifyPage/MoimModifyPage.constant.ts | 54 + .../MoimModifyPage/MoimModifyPage.hook.ts | 43 + .../pages/MoimModifyPage/MoimModifyPage.tsx | 63 + .../MoimModifyPage/MoimModifyPage.util.ts | 27 + frontend/src/pages/Mypage/MyPage.tsx | 42 + .../NotFoundPage/NotFoundPage.stories.tsx | 15 + .../src/pages/NotFoundPage/NotFoundPage.tsx | 36 + .../NotificationPage.style.ts | 10 + .../NotificationPage/NotificationPage.tsx | 63 + .../ParticipationCompletePage.tsx | 29 + .../MoimCreatePage.util.test.tsx | 27 + .../PleaseCreationPage.constant.ts | 18 + .../PleaseCreationPage.hook.ts | 37 + .../PleaseCreationPage.style.ts | 10 + .../PleaseCreationPage/PleaseCreationPage.tsx | 80 + .../PleaseCreationPage.util.ts | 9 + .../useMoimInfoInput.test.tsx | 14 + frontend/src/pages/PleasePage/PleasePage.tsx | 44 + frontend/src/queryClient.ts | 58 + .../src/routes/DarakbangInvitationRoute.tsx | 40 + frontend/src/routes/ErrorRoute.tsx | 22 + frontend/src/routes/ProtectedRoute.tsx | 27 + frontend/src/routes/SlashRoute.tsx | 25 + frontend/src/routes/router.tsx | 176 + frontend/src/service/forgroundMessage.ts | 50 + frontend/src/service/initFirebase.ts | 12 + frontend/src/service/notification.ts | 35 + frontend/src/types/index.d.ts | 106 + frontend/src/utils/checkAuthentication.ts | 7 + frontend/src/utils/customError/ApiError.ts | 9 + frontend/src/utils/formatters.ts | 67 + frontend/src/utils/tokenManager.ts | 15 + frontend/tsconfig.json | 44 + frontend/webpack.common.js | 69 + frontend/webpack.dev.js | 25 + frontend/webpack.prod.js | 57 + 978 files changed, 60893 insertions(+) rename .github/{ISSUE_TEMPLATE => }/PULL_REQUEST_TEMPLATE.md (100%) create mode 100644 .github/workflows/be-rolling-deploy.yml create mode 100644 .github/workflows/cd-frontend.yml rename frontend/.gitkeep => .github/workflows/cd-prod.yml (100%) create mode 100644 .github/workflows/ci-frontend.yml create mode 100644 .github/workflows/cicd-backend-dev.yml create mode 100644 .github/workflows/pr-test.yml create mode 100644 .gitignore create mode 100644 backend/.DS_Store create mode 100644 backend/.gitignore create mode 100644 backend/Dockerfile create mode 100644 backend/HELP.md create mode 100644 backend/build.gradle create mode 100644 backend/docker-compose.yml create mode 100644 backend/gradle/wrapper/gradle-wrapper.jar create mode 100644 backend/gradle/wrapper/gradle-wrapper.properties create mode 100755 backend/gradlew create mode 100644 backend/gradlew.bat create mode 100644 backend/settings.gradle create mode 100644 backend/src/main/java/mouda/backend/BackendApplication.java create mode 100644 backend/src/main/java/mouda/backend/aop/logging/ExceptRequestLogging.java create mode 100644 backend/src/main/java/mouda/backend/aop/logging/RequestLoggingAspect.java create mode 100644 backend/src/main/java/mouda/backend/auth/Infrastructure/AppleOauthClient.java create mode 100644 backend/src/main/java/mouda/backend/auth/Infrastructure/GoogleOauthClient.java create mode 100644 backend/src/main/java/mouda/backend/auth/Infrastructure/KakaoOauthClient.java create mode 100644 backend/src/main/java/mouda/backend/auth/Infrastructure/OauthClient.java create mode 100644 backend/src/main/java/mouda/backend/auth/Infrastructure/response/AppleRefreshTokenResponse.java create mode 100644 backend/src/main/java/mouda/backend/auth/business/AppleAuthService.java create mode 100644 backend/src/main/java/mouda/backend/auth/business/GoogleAuthService.java create mode 100644 backend/src/main/java/mouda/backend/auth/business/KakaoAuthService.java create mode 100644 backend/src/main/java/mouda/backend/auth/business/TestAuthService.java create mode 100644 backend/src/main/java/mouda/backend/auth/exception/AuthErrorMessage.java create mode 100644 backend/src/main/java/mouda/backend/auth/exception/AuthException.java create mode 100644 backend/src/main/java/mouda/backend/auth/implement/AppleUserInfoProvider.java create mode 100644 backend/src/main/java/mouda/backend/auth/implement/GoogleUserInfoProvider.java create mode 100644 backend/src/main/java/mouda/backend/auth/implement/JoinManager.java create mode 100644 backend/src/main/java/mouda/backend/auth/implement/KakaoUserInfoProvider.java create mode 100644 backend/src/main/java/mouda/backend/auth/implement/jwt/AccessTokenProvider.java create mode 100644 backend/src/main/java/mouda/backend/auth/implement/jwt/ClientSecretProvider.java create mode 100644 backend/src/main/java/mouda/backend/auth/presentation/controller/AuthController.java create mode 100644 backend/src/main/java/mouda/backend/auth/presentation/controller/TestAuthController.java create mode 100644 backend/src/main/java/mouda/backend/auth/presentation/controller/swagger/AuthSwagger.java create mode 100644 backend/src/main/java/mouda/backend/auth/presentation/controller/swagger/TestAuthSwagger.java create mode 100644 backend/src/main/java/mouda/backend/auth/presentation/request/GoogleLoginRequest.java create mode 100644 backend/src/main/java/mouda/backend/auth/presentation/request/KakaoConvertRequest.java create mode 100644 backend/src/main/java/mouda/backend/auth/presentation/response/KakaoLoginResponse.java create mode 100644 backend/src/main/java/mouda/backend/auth/presentation/response/LoginResponse.java create mode 100644 backend/src/main/java/mouda/backend/auth/presentation/response/OauthResponse.java create mode 100644 backend/src/main/java/mouda/backend/auth/util/TokenDecoder.java create mode 100644 backend/src/main/java/mouda/backend/bet/business/BetScheduler.java create mode 100644 backend/src/main/java/mouda/backend/bet/business/BetService.java create mode 100644 backend/src/main/java/mouda/backend/bet/domain/Bet.java create mode 100644 backend/src/main/java/mouda/backend/bet/domain/BetDetails.java create mode 100644 backend/src/main/java/mouda/backend/bet/domain/BetRole.java create mode 100644 backend/src/main/java/mouda/backend/bet/domain/BettingTime.java create mode 100644 backend/src/main/java/mouda/backend/bet/domain/Loser.java create mode 100644 backend/src/main/java/mouda/backend/bet/domain/Participant.java create mode 100644 backend/src/main/java/mouda/backend/bet/entity/BetDarakbangMemberEntity.java create mode 100644 backend/src/main/java/mouda/backend/bet/entity/BetEntity.java create mode 100644 backend/src/main/java/mouda/backend/bet/exception/BetErrorMessage.java create mode 100644 backend/src/main/java/mouda/backend/bet/exception/BetException.java create mode 100644 backend/src/main/java/mouda/backend/bet/implement/BetDarakbangMemberWriter.java create mode 100644 backend/src/main/java/mouda/backend/bet/implement/BetFinder.java create mode 100644 backend/src/main/java/mouda/backend/bet/implement/BetSorter.java create mode 100644 backend/src/main/java/mouda/backend/bet/implement/BetWriter.java create mode 100644 backend/src/main/java/mouda/backend/bet/implement/ParticipantFinder.java create mode 100644 backend/src/main/java/mouda/backend/bet/infrastructure/BetDarakbangMemberRepository.java create mode 100644 backend/src/main/java/mouda/backend/bet/infrastructure/BetRepository.java create mode 100644 backend/src/main/java/mouda/backend/bet/presentation/controller/BetController.java create mode 100644 backend/src/main/java/mouda/backend/bet/presentation/controller/swagger/BetSwagger.java create mode 100644 backend/src/main/java/mouda/backend/bet/presentation/request/BetCreateRequest.java create mode 100644 backend/src/main/java/mouda/backend/bet/presentation/response/BetCreateResponse.java create mode 100644 backend/src/main/java/mouda/backend/bet/presentation/response/BetFindAllResponse.java create mode 100644 backend/src/main/java/mouda/backend/bet/presentation/response/BetFindAllResponses.java create mode 100644 backend/src/main/java/mouda/backend/bet/presentation/response/BetFindResponse.java create mode 100644 backend/src/main/java/mouda/backend/bet/presentation/response/BetResultResponse.java create mode 100644 backend/src/main/java/mouda/backend/bet/presentation/response/ParticipantResponse.java create mode 100644 backend/src/main/java/mouda/backend/chat/business/ChatRoomService.java create mode 100644 backend/src/main/java/mouda/backend/chat/business/ChatService.java create mode 100644 backend/src/main/java/mouda/backend/chat/domain/Attributes.java create mode 100644 backend/src/main/java/mouda/backend/chat/domain/Author.java create mode 100644 backend/src/main/java/mouda/backend/chat/domain/BetAttributes.java create mode 100644 backend/src/main/java/mouda/backend/chat/domain/Chat.java create mode 100644 backend/src/main/java/mouda/backend/chat/domain/ChatNotificationEvent.java create mode 100644 backend/src/main/java/mouda/backend/chat/domain/ChatOwnership.java create mode 100644 backend/src/main/java/mouda/backend/chat/domain/ChatPreview.java create mode 100644 backend/src/main/java/mouda/backend/chat/domain/ChatRoom.java create mode 100644 backend/src/main/java/mouda/backend/chat/domain/ChatRoomDetails.java create mode 100644 backend/src/main/java/mouda/backend/chat/domain/ChatRoomType.java create mode 100644 backend/src/main/java/mouda/backend/chat/domain/Chats.java create mode 100644 backend/src/main/java/mouda/backend/chat/domain/LastChat.java create mode 100644 backend/src/main/java/mouda/backend/chat/domain/MoimAttributes.java create mode 100644 backend/src/main/java/mouda/backend/chat/domain/Participant.java create mode 100644 backend/src/main/java/mouda/backend/chat/domain/Target.java create mode 100644 backend/src/main/java/mouda/backend/chat/entity/ChatEntity.java create mode 100644 backend/src/main/java/mouda/backend/chat/entity/ChatRoomEntity.java create mode 100644 backend/src/main/java/mouda/backend/chat/entity/ChatType.java create mode 100644 backend/src/main/java/mouda/backend/chat/exception/ChatErrorMessage.java create mode 100644 backend/src/main/java/mouda/backend/chat/exception/ChatException.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/AttributeManager.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/AttributeManagerRegistry.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/BetAttributeManager.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/BetChatPreviewManager.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/BetParticipantResolver.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/ChatPreviewManager.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/ChatPreviewManagerRegistry.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/ChatRoomDetailsFinder.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/ChatRoomFinder.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/ChatRoomValidator.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/ChatRoomWriter.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/ChatWriter.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/MoimAttributeManager.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/MoimChatPreviewManager.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/MoimParticipantResolver.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/ParticipantResolverRegistry.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/ParticipantsResolver.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/notification/ChatNotificationEventHandler.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/notification/ChatNotificationSender.java create mode 100644 backend/src/main/java/mouda/backend/chat/implement/notification/ChatRecipientFinder.java create mode 100644 backend/src/main/java/mouda/backend/chat/infrastructure/ChatRepository.java create mode 100644 backend/src/main/java/mouda/backend/chat/infrastructure/ChatRoomRepository.java create mode 100644 backend/src/main/java/mouda/backend/chat/presentation/controller/ChatController.java create mode 100644 backend/src/main/java/mouda/backend/chat/presentation/controller/ChatRoomController.java create mode 100644 backend/src/main/java/mouda/backend/chat/presentation/controller/swagger/ChatSwagger.java create mode 100644 backend/src/main/java/mouda/backend/chat/presentation/request/ChatCreateRequest.java create mode 100644 backend/src/main/java/mouda/backend/chat/presentation/request/DateTimeConfirmRequest.java create mode 100644 backend/src/main/java/mouda/backend/chat/presentation/request/LastReadChatRequest.java create mode 100644 backend/src/main/java/mouda/backend/chat/presentation/request/PlaceConfirmRequest.java create mode 100644 backend/src/main/java/mouda/backend/chat/presentation/response/ChatFindDetailResponse.java create mode 100644 backend/src/main/java/mouda/backend/chat/presentation/response/ChatFindUnloadedResponse.java create mode 100644 backend/src/main/java/mouda/backend/chat/presentation/response/ChatPreviewResponse.java create mode 100644 backend/src/main/java/mouda/backend/chat/presentation/response/ChatPreviewResponses.java create mode 100644 backend/src/main/java/mouda/backend/chat/presentation/response/ChatRoomDetailsResponse.java create mode 100644 backend/src/main/java/mouda/backend/chat/presentation/response/ParticipantResponse.java create mode 100644 backend/src/main/java/mouda/backend/chat/util/ChatDateTimeFormatter.java create mode 100644 backend/src/main/java/mouda/backend/common/HealthCheckController.java create mode 100644 backend/src/main/java/mouda/backend/common/config/CorsConfig.java create mode 100644 backend/src/main/java/mouda/backend/common/config/FirebaseConfig.java create mode 100644 backend/src/main/java/mouda/backend/common/config/JacksonConfig.java create mode 100644 backend/src/main/java/mouda/backend/common/config/RequestBodyWrappingFilter.java create mode 100644 backend/src/main/java/mouda/backend/common/config/RestClientConfig.java create mode 100644 backend/src/main/java/mouda/backend/common/config/S3Config.java create mode 100644 backend/src/main/java/mouda/backend/common/config/SwaggerConfig.java create mode 100644 backend/src/main/java/mouda/backend/common/config/UrlConfig.java create mode 100644 backend/src/main/java/mouda/backend/common/config/WebMvcConfig.java create mode 100644 backend/src/main/java/mouda/backend/common/config/argumentresolver/LoginDarakbangMember.java create mode 100644 backend/src/main/java/mouda/backend/common/config/argumentresolver/LoginDarakbangMemberArgumentResolver.java create mode 100644 backend/src/main/java/mouda/backend/common/config/argumentresolver/LoginMember.java create mode 100644 backend/src/main/java/mouda/backend/common/config/argumentresolver/LoginMemberArgumentResolver.java create mode 100644 backend/src/main/java/mouda/backend/common/config/converter/FilterTypeConverter.java create mode 100644 backend/src/main/java/mouda/backend/common/config/data/DataSourceConfiguration.java create mode 100644 backend/src/main/java/mouda/backend/common/config/data/RoutingDataSource.java create mode 100644 backend/src/main/java/mouda/backend/common/config/interceptor/AuthenticationCheckInterceptor.java create mode 100644 backend/src/main/java/mouda/backend/common/exception/ErrorResponse.java create mode 100644 backend/src/main/java/mouda/backend/common/exception/GlobalExceptionHandler.java create mode 100644 backend/src/main/java/mouda/backend/common/exception/MoudaException.java create mode 100644 backend/src/main/java/mouda/backend/common/response/RestResponse.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/business/DarakbangService.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/domain/Darakbang.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/domain/Darakbangs.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/exception/DarakbangErrorMessage.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/exception/DarakbangException.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/implement/DarakbangFinder.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/implement/DarakbangValidator.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/implement/DarakbangWriter.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/implement/InvitationCodeGenerator.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/infrastructure/DarakbangRepository.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/presentation/controller/DarakbangController.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/presentation/controller/swagger/DarakbangSwagger.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/presentation/request/DarakbangCreateRequest.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/presentation/request/DarakbangEnterRequest.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/presentation/response/CodeValidationResponse.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/presentation/response/DarakbangNameResponse.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/presentation/response/DarakbangResponse.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/presentation/response/DarakbangResponses.java create mode 100644 backend/src/main/java/mouda/backend/darakbang/presentation/response/InvitationCodeResponse.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/business/DarakbangMemberService.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/domain/DarakBangMemberRole.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/domain/DarakbangMember.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/domain/DarakbangMembers.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/exception/DarakbangMemberErrorMessage.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/exception/DarakbangMemberException.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/implement/DarakbangMemberFinder.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/implement/DarakbangMemberWriter.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/implement/ImageParser.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/implement/S3Client.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/infrastructure/DarakbangMemberRepository.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/presentation/controller/DarakbangMemberController.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/presentation/controller/swagger/DarakbangMemberSwagger.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/presentation/request/DarakbangMemberInfoRequest.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/presentation/response/DarakbangMemberProfileResponse.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/presentation/response/DarakbangMemberResponse.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/presentation/response/DarakbangMemberResponses.java create mode 100644 backend/src/main/java/mouda/backend/darakbangmember/presentation/response/DarakbangMemberRoleResponse.java create mode 100644 backend/src/main/java/mouda/backend/member/business/MemberService.java create mode 100644 backend/src/main/java/mouda/backend/member/domain/LoginDetail.java create mode 100644 backend/src/main/java/mouda/backend/member/domain/Member.java create mode 100644 backend/src/main/java/mouda/backend/member/domain/MemberStatus.java create mode 100644 backend/src/main/java/mouda/backend/member/domain/OauthType.java create mode 100644 backend/src/main/java/mouda/backend/member/exception/MemberErrorMessage.java create mode 100644 backend/src/main/java/mouda/backend/member/exception/MemberException.java create mode 100644 backend/src/main/java/mouda/backend/member/implement/MemberFinder.java create mode 100644 backend/src/main/java/mouda/backend/member/implement/MemberValidator.java create mode 100644 backend/src/main/java/mouda/backend/member/implement/MemberWriter.java create mode 100644 backend/src/main/java/mouda/backend/member/infrastructure/MemberRepository.java create mode 100644 backend/src/main/java/mouda/backend/member/presentation/controller/MemberController.java create mode 100644 backend/src/main/java/mouda/backend/member/presentation/controller/swagger/MemberSwagger.java create mode 100644 backend/src/main/java/mouda/backend/member/presentation/response/DarakbangMemberInfoResponse.java create mode 100644 backend/src/main/java/mouda/backend/moim/business/ChamyoService.java create mode 100644 backend/src/main/java/mouda/backend/moim/business/CommentService.java create mode 100644 backend/src/main/java/mouda/backend/moim/business/MoimService.java create mode 100644 backend/src/main/java/mouda/backend/moim/business/ZzimService.java create mode 100644 backend/src/main/java/mouda/backend/moim/domain/Chamyo.java create mode 100644 backend/src/main/java/mouda/backend/moim/domain/Comment.java create mode 100644 backend/src/main/java/mouda/backend/moim/domain/CommentRecipients.java create mode 100644 backend/src/main/java/mouda/backend/moim/domain/FilterType.java create mode 100644 backend/src/main/java/mouda/backend/moim/domain/Moim.java create mode 100644 backend/src/main/java/mouda/backend/moim/domain/MoimOverview.java create mode 100644 backend/src/main/java/mouda/backend/moim/domain/MoimRole.java create mode 100644 backend/src/main/java/mouda/backend/moim/domain/MoimStatus.java create mode 100644 backend/src/main/java/mouda/backend/moim/domain/ParentComment.java create mode 100644 backend/src/main/java/mouda/backend/moim/domain/Zzim.java create mode 100644 backend/src/main/java/mouda/backend/moim/exception/ChamyoErrorMessage.java create mode 100644 backend/src/main/java/mouda/backend/moim/exception/ChamyoException.java create mode 100644 backend/src/main/java/mouda/backend/moim/exception/CommentErrorMessage.java create mode 100644 backend/src/main/java/mouda/backend/moim/exception/CommentException.java create mode 100644 backend/src/main/java/mouda/backend/moim/exception/MoimErrorMessage.java create mode 100644 backend/src/main/java/mouda/backend/moim/exception/MoimException.java create mode 100644 backend/src/main/java/mouda/backend/moim/exception/ZzimErrorMessage.java create mode 100644 backend/src/main/java/mouda/backend/moim/exception/ZzimException.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/finder/ChamyoFinder.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/finder/CommentFinder.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/finder/MoimFinder.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/finder/ZzimFinder.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/AbstractMoimRelatedNotificationEventHandler.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/ChamyoNotificationEventHandler.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/ChamyoRecipientFinder.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentNotificationEventHandler.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentRecipientFinder.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentRecipients.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimNotificationEventHandler.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimRecipientFinder.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimRelatedNotificationSender.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/event/ChamyoNotificationEvent.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/event/CommentNotificationEvent.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimCreateNotificationEvent.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimEditedNotificationEvent.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimStatusChangeNotificationEvent.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/validator/ChamyoValidator.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/validator/CommentValidator.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/validator/MoimValidator.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/validator/ZzimValidator.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/writer/ChamyoWriter.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/writer/CommentWriter.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/writer/MoimWriter.java create mode 100644 backend/src/main/java/mouda/backend/moim/implement/writer/ZzimWriter.java create mode 100644 backend/src/main/java/mouda/backend/moim/infrastructure/ChamyoRepository.java create mode 100644 backend/src/main/java/mouda/backend/moim/infrastructure/CommentRepository.java create mode 100644 backend/src/main/java/mouda/backend/moim/infrastructure/MoimRepository.java create mode 100644 backend/src/main/java/mouda/backend/moim/infrastructure/ZzimRepository.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/controller/ChamyoController.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/controller/MoimController.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/controller/ZzimController.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/controller/swagger/ChamyoSwagger.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/controller/swagger/MoimSwagger.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/controller/swagger/ZzimSwagger.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/request/chamyo/ChamyoCancelRequest.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/request/chamyo/MoimChamyoRequest.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/request/comment/CommentCreateRequest.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/request/moim/MoimCreateRequest.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/request/moim/MoimEditRequest.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/request/moim/MoimJoinRequest.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/request/zzim/ZzimUpdateRequest.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/response/chamyo/ChamyoFindAllResponse.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/response/chamyo/ChamyoFindAllResponses.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/response/chamyo/MoimRoleFindResponse.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/response/comment/ChildCommentResponse.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/response/comment/CommentResponse.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/response/comment/CommentResponses.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/response/moim/MoimDetailsFindResponse.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/response/moim/MoimFindAllResponse.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/response/moim/MoimFindAllResponses.java create mode 100644 backend/src/main/java/mouda/backend/moim/presentation/response/zzim/ZzimCheckResponse.java create mode 100644 backend/src/main/java/mouda/backend/notification/business/FcmTokenService.java create mode 100644 backend/src/main/java/mouda/backend/notification/business/MemberNotificationService.java create mode 100644 backend/src/main/java/mouda/backend/notification/business/SubscriptionService.java create mode 100644 backend/src/main/java/mouda/backend/notification/domain/CommonNotification.java create mode 100644 backend/src/main/java/mouda/backend/notification/domain/FcmFailedResponse.java create mode 100644 backend/src/main/java/mouda/backend/notification/domain/FcmToken.java create mode 100644 backend/src/main/java/mouda/backend/notification/domain/MemberNotification.java create mode 100644 backend/src/main/java/mouda/backend/notification/domain/NotificationEvent.java create mode 100644 backend/src/main/java/mouda/backend/notification/domain/NotificationPayload.java create mode 100644 backend/src/main/java/mouda/backend/notification/domain/NotificationSendEvent.java create mode 100644 backend/src/main/java/mouda/backend/notification/domain/NotificationType.java create mode 100644 backend/src/main/java/mouda/backend/notification/domain/Recipient.java create mode 100644 backend/src/main/java/mouda/backend/notification/domain/Subscription.java create mode 100644 backend/src/main/java/mouda/backend/notification/exception/NotificationErrorMessage.java create mode 100644 backend/src/main/java/mouda/backend/notification/exception/NotificationException.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/MessageFactory.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/NotificationFinder.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/NotificationProcessor.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/NotificationSendEventHandler.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/NotificationSender.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/NotificationWriter.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/fcm/AsyncFcmNotificationSender.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/fcm/FcmConfigBuilder.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/fcm/FcmMessageFactory.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/fcm/FcmNotificationSender.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/fcm/FcmResponseHandler.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/fcm/FcmRetryableChecker.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/fcm/WebFcmConfigBuilder.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenFinder.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenScheduler.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenWriter.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilter.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/filter/MoimCreatedSubscriptionFilter.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/filter/NonSubscriptionFilter.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilter.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilterRegistry.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/subscription/SubscriptionFinder.java create mode 100644 backend/src/main/java/mouda/backend/notification/implement/subscription/SubscriptionWriter.java create mode 100644 backend/src/main/java/mouda/backend/notification/infrastructure/entity/FcmTokenEntity.java create mode 100644 backend/src/main/java/mouda/backend/notification/infrastructure/entity/MemberNotificationEntity.java create mode 100644 backend/src/main/java/mouda/backend/notification/infrastructure/entity/SubscriptionEntity.java create mode 100644 backend/src/main/java/mouda/backend/notification/infrastructure/entity/UnsubscribedChatRooms.java create mode 100644 backend/src/main/java/mouda/backend/notification/infrastructure/repository/FcmTokenRepository.java create mode 100644 backend/src/main/java/mouda/backend/notification/infrastructure/repository/MemberNotificationRepository.java create mode 100644 backend/src/main/java/mouda/backend/notification/infrastructure/repository/SubscriptionRepository.java create mode 100644 backend/src/main/java/mouda/backend/notification/presentation/controller/MemberNotificationController.java create mode 100644 backend/src/main/java/mouda/backend/notification/presentation/controller/MemberNotificationSwagger.java create mode 100644 backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationTokenController.java create mode 100644 backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationTokenSwagger.java create mode 100644 backend/src/main/java/mouda/backend/notification/presentation/controller/SubscriptionController.java create mode 100644 backend/src/main/java/mouda/backend/notification/presentation/controller/SubscriptionSwagger.java create mode 100644 backend/src/main/java/mouda/backend/notification/presentation/request/ChatSubscriptionRequest.java create mode 100644 backend/src/main/java/mouda/backend/notification/presentation/request/FcmTokenRequest.java create mode 100644 backend/src/main/java/mouda/backend/notification/presentation/response/MemberNotificationFindAllResponse.java create mode 100644 backend/src/main/java/mouda/backend/notification/presentation/response/MemberNotificationFindResponse.java create mode 100644 backend/src/main/java/mouda/backend/notification/presentation/response/SubscriptionResponse.java create mode 100644 backend/src/main/java/mouda/backend/notification/util/FcmRetryAfterExtractor.java create mode 100644 backend/src/main/java/mouda/backend/please/business/InterestService.java create mode 100644 backend/src/main/java/mouda/backend/please/business/PleaseService.java create mode 100644 backend/src/main/java/mouda/backend/please/domain/Interest.java create mode 100644 backend/src/main/java/mouda/backend/please/domain/Please.java create mode 100644 backend/src/main/java/mouda/backend/please/domain/PleaseWithInterest.java create mode 100644 backend/src/main/java/mouda/backend/please/domain/PleaseWithInterests.java create mode 100644 backend/src/main/java/mouda/backend/please/exception/PleaseErrorMessage.java create mode 100644 backend/src/main/java/mouda/backend/please/exception/PleaseException.java create mode 100644 backend/src/main/java/mouda/backend/please/implement/InterestFinder.java create mode 100644 backend/src/main/java/mouda/backend/please/implement/InterestValidator.java create mode 100644 backend/src/main/java/mouda/backend/please/implement/InterestWriter.java create mode 100644 backend/src/main/java/mouda/backend/please/implement/PleaseFinder.java create mode 100644 backend/src/main/java/mouda/backend/please/implement/PleaseValidator.java create mode 100644 backend/src/main/java/mouda/backend/please/implement/PleaseWriter.java create mode 100644 backend/src/main/java/mouda/backend/please/infrastructure/InterestRepository.java create mode 100644 backend/src/main/java/mouda/backend/please/infrastructure/PleaseRepository.java create mode 100644 backend/src/main/java/mouda/backend/please/presentation/controller/InterestController.java create mode 100644 backend/src/main/java/mouda/backend/please/presentation/controller/PleaseController.java create mode 100644 backend/src/main/java/mouda/backend/please/presentation/controller/swagger/InterestSwagger.java create mode 100644 backend/src/main/java/mouda/backend/please/presentation/controller/swagger/PleaseSwagger.java create mode 100644 backend/src/main/java/mouda/backend/please/presentation/request/InterestUpdateRequest.java create mode 100644 backend/src/main/java/mouda/backend/please/presentation/request/PleaseCreateRequest.java create mode 100644 backend/src/main/java/mouda/backend/please/presentation/response/PleaseFindAllResponse.java create mode 100644 backend/src/main/java/mouda/backend/please/presentation/response/PleaseFindAllResponses.java create mode 100644 backend/src/main/resources/application-dev.yml create mode 100644 backend/src/main/resources/application-local.yml create mode 100644 backend/src/main/resources/application-prod.yml create mode 100644 backend/src/main/resources/application.yml create mode 100644 backend/src/main/resources/data.sql create mode 100644 backend/src/main/resources/logback-spring.xml create mode 100644 backend/src/test/java/mouda/backend/auth/Infrastructure/AppleOauthClientTest.java create mode 100644 backend/src/test/java/mouda/backend/auth/business/AppleAuthServiceTest.java create mode 100644 backend/src/test/java/mouda/backend/auth/business/GoogleAuthServiceTest.java create mode 100644 backend/src/test/java/mouda/backend/auth/business/KakaoAuthServiceTest.java create mode 100644 backend/src/test/java/mouda/backend/auth/implement/AppleUserInfoProviderTest.java create mode 100644 backend/src/test/java/mouda/backend/auth/implement/MemberFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/auth/implement/MemberWriterTest.java create mode 100644 backend/src/test/java/mouda/backend/auth/presentation/controller/AuthControllerTest.java create mode 100644 backend/src/test/java/mouda/backend/bet/business/BetSchedulerTest.java create mode 100644 backend/src/test/java/mouda/backend/bet/business/BetServiceTest.java create mode 100644 backend/src/test/java/mouda/backend/bet/domain/BetTest.java create mode 100644 backend/src/test/java/mouda/backend/bet/domain/BettingTimeTest.java create mode 100644 backend/src/test/java/mouda/backend/bet/implement/BetFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/bet/implement/BetSorterTest.java create mode 100644 backend/src/test/java/mouda/backend/bet/implement/BetWriterTest.java create mode 100644 backend/src/test/java/mouda/backend/bet/implement/ParticipantFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/bet/infrastructure/BetRepositoryTest.java create mode 100644 backend/src/test/java/mouda/backend/chamyo/service/ChamyoServiceTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/ChatAsyncTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/business/ChatRoomServiceTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/business/ChatServiceTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/entity/ChatRoomEntityTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/implement/AttributeManagerRegistryTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/implement/BetAttributeManagerTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/implement/BetChatPreviewManagerTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/implement/BetParticipantResolverTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/implement/ChatFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/implement/ChatRecipientFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/implement/ChatRoomDetailsFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/implement/ChatRoomValidatorTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/implement/ChatRoomWriterTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/implement/MoimAttributeManagerTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/implement/MoimChatPreviewManagerTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/implement/MoimParticipantResolverTest.java create mode 100644 backend/src/test/java/mouda/backend/chat/implement/ParticipantResolverRegistryTest.java create mode 100644 backend/src/test/java/mouda/backend/common/config/ChamyoCreator.java create mode 100644 backend/src/test/java/mouda/backend/common/config/DatabaseCleaner.java create mode 100644 backend/src/test/java/mouda/backend/common/config/NoTransactionExtension.java create mode 100644 backend/src/test/java/mouda/backend/common/config/WithTransactionalTest.java create mode 100644 backend/src/test/java/mouda/backend/common/config/data/RoutingDataSourceTest.java create mode 100644 backend/src/test/java/mouda/backend/common/fixture/BetEntityFixture.java create mode 100644 backend/src/test/java/mouda/backend/common/fixture/BetFixture.java create mode 100644 backend/src/test/java/mouda/backend/common/fixture/ChatEntityFixture.java create mode 100644 backend/src/test/java/mouda/backend/common/fixture/ChatRoomEntityFixture.java create mode 100644 backend/src/test/java/mouda/backend/common/fixture/CommentFixture.java create mode 100644 backend/src/test/java/mouda/backend/common/fixture/DarakbangFixture.java create mode 100644 backend/src/test/java/mouda/backend/common/fixture/DarakbangMemberFixture.java create mode 100644 backend/src/test/java/mouda/backend/common/fixture/DarakbangSetUp.java create mode 100644 backend/src/test/java/mouda/backend/common/fixture/MemberFixture.java create mode 100644 backend/src/test/java/mouda/backend/common/fixture/MoimFixture.java create mode 100644 backend/src/test/java/mouda/backend/common/fixture/PleaseFixture.java create mode 100644 backend/src/test/java/mouda/backend/common/global/IgnoreNotificationTest.java create mode 100644 backend/src/test/java/mouda/backend/darakbang/business/DarakbangServiceTest.java create mode 100644 backend/src/test/java/mouda/backend/darakbang/implement/DarakbangFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/darakbang/implement/DarakbangValidatorTest.java create mode 100644 backend/src/test/java/mouda/backend/darakbang/implement/DarakbangWriterTest.java create mode 100644 backend/src/test/java/mouda/backend/darakbang/implement/InvitationCodeGeneratorTest.java create mode 100644 backend/src/test/java/mouda/backend/darakbangmember/business/DarakbangMemberServiceTest.java create mode 100644 backend/src/test/java/mouda/backend/darakbangmember/domain/DarakbangMemberTest.java create mode 100644 backend/src/test/java/mouda/backend/darakbangmember/implement/DarakbangMemberFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/darakbangmember/implement/DarakbangMemberWriterTest.java create mode 100644 backend/src/test/java/mouda/backend/darakbangmember/implement/ImageParserTest.java create mode 100644 backend/src/test/java/mouda/backend/member/implement/MemberValidatorTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/business/ChamyoAsyncTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/business/CommentAsyncTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/business/MoimAsyncTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/chamyo/business/ChamyoServiceTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/chat/infrastructure/ChatRepositoryTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/comment/domain/CommentTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/implement/finder/ChamyoFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/implement/finder/ChamyoRecipientFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/implement/finder/CommentFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/implement/finder/CommentRecipientsFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/implement/finder/MoimFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/implement/finder/MoimRecipientFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/implement/finder/ZzimFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/implement/writer/ChamyoWriterTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/implement/writer/CommentWriterTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/implement/writer/MoimWriterTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/implement/writer/ZzimWriterTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/moim/business/CommentServiceTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/moim/business/MoimServiceTest.java create mode 100644 backend/src/test/java/mouda/backend/moim/moim/domain/MoimTest.java create mode 100644 backend/src/test/java/mouda/backend/notification/business/NotificationServiceTest.java create mode 100644 backend/src/test/java/mouda/backend/notification/domain/NotificationSendEventTest.java create mode 100644 backend/src/test/java/mouda/backend/notification/implement/NotificationAsyncTest.java create mode 100644 backend/src/test/java/mouda/backend/notification/implement/NotificationFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/notification/implement/fcm/FcmMessageFactoryTest.java create mode 100644 backend/src/test/java/mouda/backend/notification/implement/fcm/token/FcmTokenWriterTest.java create mode 100644 backend/src/test/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilterTest.java create mode 100644 backend/src/test/java/mouda/backend/notification/implement/subscription/SubscriptionFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/notification/implement/subscription/SubscriptionWriterTest.java create mode 100644 backend/src/test/java/mouda/backend/notification/infrastructure/SubscriptionEntityTest.java create mode 100644 backend/src/test/java/mouda/backend/please/implement/InterestFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/please/implement/InterestWriterTest.java create mode 100644 backend/src/test/java/mouda/backend/please/implement/PleaseFinderTest.java create mode 100644 backend/src/test/java/mouda/backend/please/implement/PleaseValidatorTest.java create mode 100644 backend/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension create mode 100644 backend/src/test/resources/application.yml create mode 100644 backend/src/test/resources/junit-platform.properties create mode 100644 frontend/.browserslistrc create mode 100644 frontend/.eslintrc.cjs create mode 100644 frontend/.gitignore create mode 100644 frontend/.prettierrc create mode 100644 frontend/.storybook/main.ts create mode 100644 frontend/.storybook/preview.tsx create mode 100644 frontend/.stylelintrc.json create mode 100644 frontend/.vscode/settings.json create mode 100644 frontend/babel.config.json create mode 100644 frontend/cypress.config.ts create mode 100644 frontend/cypress/support/commands.ts create mode 100644 frontend/cypress/support/e2e.ts create mode 100644 frontend/jest.config.json create mode 100644 frontend/jest.polyfills.js create mode 100644 frontend/jest.setup.ts create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/android/android-launchericon-144-144.png create mode 100644 frontend/public/android/android-launchericon-192-192.png create mode 100644 frontend/public/android/android-launchericon-48-48.png create mode 100644 frontend/public/android/android-launchericon-512-512.png create mode 100644 frontend/public/android/android-launchericon-72-72.png create mode 100644 frontend/public/android/android-launchericon-96-96.png create mode 100644 frontend/public/firebase-messaging-sw.js create mode 100644 frontend/public/main-logo.png create mode 100644 frontend/public/main-logo2.png create mode 100644 frontend/public/manifest.json create mode 100644 frontend/public/mockServiceWorker.js create mode 100644 frontend/public/preview-image.png create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/RouteChageTracker.ts create mode 100644 frontend/src/apis/apiClient.ts create mode 100644 frontend/src/apis/auth.ts create mode 100644 frontend/src/apis/deletes.ts create mode 100644 frontend/src/apis/endPoints.ts create mode 100644 frontend/src/apis/gets.ts create mode 100644 frontend/src/apis/patches.ts create mode 100644 frontend/src/apis/posts.ts create mode 100644 frontend/src/apis/responseTypes.ts create mode 100644 frontend/src/common/assets/back.svg create mode 100644 frontend/src/common/assets/crown.svg create mode 100644 frontend/src/common/assets/default_profile.png create mode 100644 frontend/src/common/assets/empty_profile.svg create mode 100644 frontend/src/common/assets/fonts/woff2-subset/Pretendard-Black.subset.woff2 create mode 100644 frontend/src/common/assets/fonts/woff2-subset/Pretendard-Bold.subset.woff2 create mode 100644 frontend/src/common/assets/fonts/woff2-subset/Pretendard-ExtraBold.subset.woff2 create mode 100644 frontend/src/common/assets/fonts/woff2-subset/Pretendard-ExtraLight.subset.woff2 create mode 100644 frontend/src/common/assets/fonts/woff2-subset/Pretendard-Light.subset.woff2 create mode 100644 frontend/src/common/assets/fonts/woff2-subset/Pretendard-Medium.subset.woff2 create mode 100644 frontend/src/common/assets/fonts/woff2-subset/Pretendard-Regular.subset.woff2 create mode 100644 frontend/src/common/assets/fonts/woff2-subset/Pretendard-SemiBold.subset.woff2 create mode 100644 frontend/src/common/assets/fonts/woff2-subset/Pretendard-Thin.subset.woff2 create mode 100644 frontend/src/common/assets/fonts/woff2/PretendardVariable.woff2 create mode 100644 frontend/src/common/assets/kebab_menu.svg create mode 100644 frontend/src/common/assets/meatball.svg create mode 100644 frontend/src/common/assets/notification.svg create mode 100644 frontend/src/common/assets/plus.svg create mode 100644 frontend/src/common/assets/regret_cat.png create mode 100644 frontend/src/common/assets/submit_message_button.svg create mode 100644 frontend/src/common/assets/tabler_plus.svg create mode 100644 frontend/src/common/assets/x.svg create mode 100644 frontend/src/common/assets/zzim_emty_button.svg create mode 100644 frontend/src/common/assets/zzim_fill_button.svg create mode 100644 frontend/src/common/common.style.ts create mode 100644 frontend/src/common/font.style.ts create mode 100644 frontend/src/common/getRoutes.ts create mode 100644 frontend/src/common/inviteCodeManager.tsx create mode 100644 frontend/src/common/lastDarakbangManager.tsx create mode 100644 frontend/src/common/reset.style.ts create mode 100644 frontend/src/common/theme/colorPalette.ts create mode 100644 frontend/src/common/theme/coloredTypography.ts create mode 100644 frontend/src/common/theme/layout.ts create mode 100644 frontend/src/common/theme/semantic.ts create mode 100644 frontend/src/common/theme/theme.style.ts create mode 100644 frontend/src/common/theme/theme.type.ts create mode 100644 frontend/src/common/theme/typography.ts create mode 100644 frontend/src/components/BackArrowButton/BackArrowButton.style.ts create mode 100644 frontend/src/components/BackArrowButton/BackArrowButton.tsx create mode 100644 frontend/src/components/Button/Button.stories.tsx create mode 100644 frontend/src/components/Button/Button.style.ts create mode 100644 frontend/src/components/Button/Button.tsx create mode 100644 frontend/src/components/Chat/Chat.stories.tsx create mode 100644 frontend/src/components/Chat/Chat.tsx create mode 100644 frontend/src/components/Chat/Chatstyle.ts create mode 100644 frontend/src/components/ChatBottomMenu/ChatBottomMenu.stories.tsx create mode 100644 frontend/src/components/ChatBottomMenu/ChatBottomMenu.style.ts create mode 100644 frontend/src/components/ChatBottomMenu/ChatBottomMenu.tsx create mode 100644 frontend/src/components/ChatBubble/ChatBubble.stories.tsx create mode 100644 frontend/src/components/ChatBubble/ChatBubble.style.ts create mode 100644 frontend/src/components/ChatBubble/ChatBubble.tsx create mode 100644 frontend/src/components/ChatBubble/ChatChildren/ChatChildren.style.tsx create mode 100644 frontend/src/components/ChatBubble/ChatChildren/ChatChildren.tsx create mode 100644 frontend/src/components/ChatList/ChatList.stories.tsx create mode 100644 frontend/src/components/ChatList/ChatList.style.ts create mode 100644 frontend/src/components/ChatList/ChatList.tsx create mode 100644 frontend/src/components/ChatMenuItem/ChatMenuItem.stories.tsx create mode 100644 frontend/src/components/ChatMenuItem/ChatMenuItem.style.ts create mode 100644 frontend/src/components/ChatMenuItem/ChatMenuItem.tsx create mode 100644 frontend/src/components/ChattingFooter/ChattingFooter.stories.tsx create mode 100644 frontend/src/components/ChattingFooter/ChattingFooter.style.ts create mode 100644 frontend/src/components/ChattingFooter/ChattingFooter.tsx create mode 100644 frontend/src/components/ChattingPreview/ChattingPreview.stories.tsx create mode 100644 frontend/src/components/ChattingPreview/ChattingPreview.style.ts create mode 100644 frontend/src/components/ChattingPreview/ChattingPreview.tsx create mode 100644 frontend/src/components/CommentCard/CommentCard.stories.tsx create mode 100644 frontend/src/components/CommentCard/CommentCard.style.ts create mode 100644 frontend/src/components/CommentCard/CommentCard.tsx create mode 100644 frontend/src/components/CommentCard/CommentCardSkeleton.tsx create mode 100644 frontend/src/components/CommentList/ComentList.style.ts create mode 100644 frontend/src/components/CommentList/CommentList.stories.tsx create mode 100644 frontend/src/components/CommentList/CommentList.tsx create mode 100644 frontend/src/components/CommentList/CommentListSkeleton.tsx create mode 100644 frontend/src/components/DarakbangNameWrapper/DarakbangNameWrapper.style.ts create mode 100644 frontend/src/components/DarakbangNameWrapper/DarakbangNameWrapper.tsx create mode 100644 frontend/src/components/DateTimeModalContent/DateTimeModalContent.stories.tsx create mode 100644 frontend/src/components/DateTimeModalContent/DateTimeModalContent.style.ts create mode 100644 frontend/src/components/DateTimeModalContent/DateTimeModalContent.tsx create mode 100644 frontend/src/components/ErrorControlledInput/ErrorControlledInput.stories.tsx create mode 100644 frontend/src/components/ErrorControlledInput/ErrorControlledInput.style.ts create mode 100644 frontend/src/components/ErrorControlledInput/ErrorControlledInput.tsx create mode 100644 frontend/src/components/Funnel/FunnelButton/FunnelButton.tsx create mode 100644 frontend/src/components/Funnel/FunnelInput/FunnelInput.stories.tsx create mode 100644 frontend/src/components/Funnel/FunnelInput/FunnelInput.style.ts create mode 100644 frontend/src/components/Funnel/FunnelInput/FunnelInput.tsx create mode 100644 frontend/src/components/Funnel/FunnelQuestion/FunnelQuestion.stories.tsx create mode 100644 frontend/src/components/Funnel/FunnelQuestion/FunnelQuestion.tsx create mode 100644 frontend/src/components/Funnel/FunnelQuestion/FunnelQuestionHighlight/FunnelQuestionHighlight.style.ts create mode 100644 frontend/src/components/Funnel/FunnelQuestion/FunnelQuestionHighlight/FunnelQuestionHighlight.tsx create mode 100644 frontend/src/components/Funnel/FunnelQuestion/FunnelQuestionText/FunnelQuestionText.tsx create mode 100644 frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroup.style.ts create mode 100644 frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroup.tsx create mode 100644 frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroupOption/FunnelRadioCardGroupOption.style.ts create mode 100644 frontend/src/components/Funnel/FunnelRadioCardGroup/FunnelRadioCardGroupOption/FunnelRadioCardGroupOption.tsx create mode 100644 frontend/src/components/Funnel/FunnelStepIndicator/FunnelStepIndicator.stories.tsx create mode 100644 frontend/src/components/Funnel/FunnelStepIndicator/FunnelStepIndicator.style.ts create mode 100644 frontend/src/components/Funnel/FunnelStepIndicator/FunnelStepIndicator.tsx create mode 100644 frontend/src/components/Funnel/FunnelTextArea/FunnelTextArea.style.ts create mode 100644 frontend/src/components/Funnel/FunnelTextArea/FunnelTextArea.tsx create mode 100644 frontend/src/components/HighlightSpan/HighlightSpan.stories.tsx create mode 100644 frontend/src/components/HighlightSpan/HighlightSpan.style.ts create mode 100644 frontend/src/components/HighlightSpan/HighlightSpan.tsx create mode 100644 frontend/src/components/HomeHeaderContent/HomHeaderContent.tsx create mode 100644 frontend/src/components/HomeHeaderContent/HomeHeaderContent.style.ts create mode 100644 frontend/src/components/HomeMainContent/HomeMainContent.tsx create mode 100644 frontend/src/components/Icons/BackArrowIcon.tsx create mode 100644 frontend/src/components/Icons/CalenderClock.tsx create mode 100644 frontend/src/components/Icons/ChatBubbleSvg.tsx create mode 100644 frontend/src/components/Icons/ChattingIcon.tsx create mode 100644 frontend/src/components/Icons/HeartIcon.tsx create mode 100644 frontend/src/components/Icons/HomeIcon.tsx create mode 100644 frontend/src/components/Icons/InterestingIcon.tsx create mode 100644 frontend/src/components/Icons/KakaoOAuthIcon.tsx create mode 100644 frontend/src/components/Icons/MainLogoIcon.tsx create mode 100644 frontend/src/components/Icons/MyPageIcon.tsx create mode 100644 frontend/src/components/Icons/Picker.tsx create mode 100644 frontend/src/components/Icons/PleaseIcon.tsx create mode 100644 frontend/src/components/Icons/PlusIcon.tsx create mode 100644 frontend/src/components/Icons/SelectionIcon.tsx create mode 100644 frontend/src/components/Icons/SendButton.tsx create mode 100644 frontend/src/components/Icons/SolidArrow.tsx create mode 100644 frontend/src/components/Input/MessagInput/MessageInput.stories.tsx create mode 100644 frontend/src/components/Input/MessagInput/MessageInput.style.ts create mode 100644 frontend/src/components/Input/MessagInput/MessageInput.tsx create mode 100644 frontend/src/components/Input/MoimInput.style.ts create mode 100644 frontend/src/components/Input/MoimInput.tsx create mode 100644 frontend/src/components/KebabMenu/KababMenu.stories.tsx create mode 100644 frontend/src/components/KebabMenu/KebabMenu.style.ts create mode 100644 frontend/src/components/KebabMenu/KebabMenu.tsx create mode 100644 frontend/src/components/LoginForm/LoginForm.stories.tsx create mode 100644 frontend/src/components/LoginForm/LoginForm.style.ts create mode 100644 frontend/src/components/LoginForm/LoginForm.tsx create mode 100644 frontend/src/components/MemberCard/MemberCard.stories.tsx create mode 100644 frontend/src/components/MemberCard/MemberCard.style.ts create mode 100644 frontend/src/components/MemberCard/MemberCard.tsx create mode 100644 frontend/src/components/MenuItem/MenuItem.stories.tsx create mode 100644 frontend/src/components/MenuItem/MenuItem.style.tsx create mode 100644 frontend/src/components/MenuItem/MenuItem.tsx create mode 100644 frontend/src/components/MenuItemList/MenuItemList.stories.tsx create mode 100644 frontend/src/components/MenuItemList/MenuItemList.tsx create mode 100644 frontend/src/components/MineInfoCard/MineInfoCard.style.ts create mode 100644 frontend/src/components/MineInfoCard/MineInfoCard.tsx create mode 100644 frontend/src/components/MineInfoCard/MoinInfoCard.stories.tsx create mode 100644 frontend/src/components/MissingFallback/MissingFallback.stories.tsx create mode 100644 frontend/src/components/MissingFallback/MissingFallback.style.ts create mode 100644 frontend/src/components/MissingFallback/MissingFallback.tsx create mode 100644 frontend/src/components/Modal/Modal.style.tsx create mode 100644 frontend/src/components/Modal/Modal.tsx create mode 100644 frontend/src/components/MoimCard/MoimCard.stories.tsx create mode 100644 frontend/src/components/MoimCard/MoimCard.style.ts create mode 100644 frontend/src/components/MoimCard/MoimCard.tsx create mode 100644 frontend/src/components/MoimCardList/MoimCardList.stories.tsx create mode 100644 frontend/src/components/MoimCardList/MoimCardList.style.ts create mode 100644 frontend/src/components/MoimCardList/MoimCardList.tsx create mode 100644 frontend/src/components/MoimDescription/MoimDescription.stories.tsx create mode 100644 frontend/src/components/MoimDescription/MoimDescription.style.ts create mode 100644 frontend/src/components/MoimDescription/MoimDescription.tsx create mode 100644 frontend/src/components/MoimDescription/MoimDescriptionSkeleton.tsx create mode 100644 frontend/src/components/MoimInformation/MoimInfomationSkeleton.tsx create mode 100644 frontend/src/components/MoimInformation/MoimInformation.stories.tsx create mode 100644 frontend/src/components/MoimInformation/MoimInformation.style.ts create mode 100644 frontend/src/components/MoimInformation/MoimInformation.tsx create mode 100644 frontend/src/components/MoimList/MoimCardListSkeleton/MoimCardListSkeleton.style.ts create mode 100644 frontend/src/components/MoimList/MoimCardListSkeleton/MoimCardListSkeleton.tsx create mode 100644 frontend/src/components/MoimList/MoimCardListSkeleton/MoimCardSkeleton/MoimCardSkeleton.stories.tsx create mode 100644 frontend/src/components/MoimList/MoimCardListSkeleton/MoimCardSkeleton/MoimCardSkeleton.style.ts create mode 100644 frontend/src/components/MoimList/MoimCardListSkeleton/MoimCardSkeleton/MoimCardSkeleton.tsx create mode 100644 frontend/src/components/MoimList/MoimList.tsx create mode 100644 frontend/src/components/MoimSummary/MoimSummary.stories.tsx create mode 100644 frontend/src/components/MoimSummary/MoimSummary.style.ts create mode 100644 frontend/src/components/MoimSummary/MoimSummary.tsx create mode 100644 frontend/src/components/MoimSummary/MoimSummarySkeleton.tsx create mode 100644 frontend/src/components/MoimTabBar/MoimTabBar.stories.tsx create mode 100644 frontend/src/components/MoimTabBar/MoimTabBar.style.ts create mode 100644 frontend/src/components/MoimTabBar/MoimTabBar.tsx create mode 100644 frontend/src/components/MyMoim/MyMoim.style.tsx create mode 100644 frontend/src/components/MyMoim/MyMoim.tsx create mode 100644 frontend/src/components/MyMoimListFilterTag/MyMoimListFilterTag.stories.tsx create mode 100644 frontend/src/components/MyMoimListFilterTag/MyMoimListFilterTag.style.ts create mode 100644 frontend/src/components/MyMoimListFilterTag/MyMoimListFilterTag.tsx create mode 100644 frontend/src/components/MyMoimListFilters/MyMoimListFilters.stories.tsx create mode 100644 frontend/src/components/MyMoimListFilters/MyMoimListFilters.style.ts create mode 100644 frontend/src/components/MyMoimListFilters/MyMoimListFilters.tsx create mode 100644 frontend/src/components/MyZzimMoimList/MyZzimMoimList.tsx create mode 100644 frontend/src/components/NavigationBar/NavigationBar.stories.tsx create mode 100644 frontend/src/components/NavigationBar/NavigationBar.style.ts create mode 100644 frontend/src/components/NavigationBar/NavigationBar.tsx create mode 100644 frontend/src/components/NavigationBarItem/NavigationBarItem.style.ts create mode 100644 frontend/src/components/NavigationBarItem/NavigationBarItem.tsx create mode 100644 frontend/src/components/NotificationCard/NotificationCard.const.ts create mode 100644 frontend/src/components/NotificationCard/NotificationCard.style.ts create mode 100644 frontend/src/components/NotificationCard/NotificationCard.tsx create mode 100644 frontend/src/components/NotificationList/NotificationList.stories.tsx create mode 100644 frontend/src/components/NotificationList/NotificationList.style.ts create mode 100644 frontend/src/components/NotificationList/NotificationList.tsx create mode 100644 frontend/src/components/OptionsPanel/OptionsPanel.stories.tsx create mode 100644 frontend/src/components/OptionsPanel/OptionsPanel.style.ts create mode 100644 frontend/src/components/OptionsPanel/OptionsPanel.tsx create mode 100644 frontend/src/components/PlaceModalContent/PlaceModalContent.stories.tsx create mode 100644 frontend/src/components/PlaceModalContent/PlaceModalContent.style.ts create mode 100644 frontend/src/components/PlaceModalContent/PlaceModalContent.tsx create mode 100644 frontend/src/components/PleaseCard/PleaseCard.stories.tsx create mode 100644 frontend/src/components/PleaseCard/PleaseCard.style.ts create mode 100644 frontend/src/components/PleaseCard/PleaseCard.tsx create mode 100644 frontend/src/components/PleaseCardList/PleaseCardList.style.ts create mode 100644 frontend/src/components/PleaseCardList/PleaseCardList.tsx create mode 100644 frontend/src/components/PleaseList/PleaseCardListSkeleton/PleaseCardListSkeleton.style.ts create mode 100644 frontend/src/components/PleaseList/PleaseCardListSkeleton/PleaseCardListSkeleton.tsx create mode 100644 frontend/src/components/PleaseList/PleaseCardListSkeleton/PleaseCardSkeleton/PleaseCardSkeleton.stories.tsx create mode 100644 frontend/src/components/PleaseList/PleaseCardListSkeleton/PleaseCardSkeleton/PleaseCardSkeleton.style.ts create mode 100644 frontend/src/components/PleaseList/PleaseCardListSkeleton/PleaseCardSkeleton/PleaseCardSkeleton.tsx create mode 100644 frontend/src/components/PleaseList/PleaseList.tsx create mode 100644 frontend/src/components/PlusButton/PlusButton.style.ts create mode 100644 frontend/src/components/PlusButton/PlusButton.tsx create mode 100644 frontend/src/components/Profile/ProfileCard.stories.tsx create mode 100644 frontend/src/components/Profile/ProfileCard.style.ts create mode 100644 frontend/src/components/Profile/ProfileCard.tsx create mode 100644 frontend/src/components/Profile/ProfileFrame.stories.tsx create mode 100644 frontend/src/components/Profile/ProfileFrame.style.ts create mode 100644 frontend/src/components/Profile/ProfileFrame.tsx create mode 100644 frontend/src/components/ProfileList/ProfileList.stories.tsx create mode 100644 frontend/src/components/ProfileList/ProfileList.style.ts create mode 100644 frontend/src/components/ProfileList/ProfileList.tsx create mode 100644 frontend/src/components/ProfileList/ProfileListSkeleton.tsx create mode 100644 frontend/src/components/SelectBar/SelectBar.stories.tsx create mode 100644 frontend/src/components/SelectBar/SelectBar.style.ts create mode 100644 frontend/src/components/SelectBar/SelectBar.tsx create mode 100644 frontend/src/components/Skeleton/SkeletonPiece.stories.tsx create mode 100644 frontend/src/components/Skeleton/SkeletonPiece.style.ts create mode 100644 frontend/src/components/Skeleton/SkeletonPiece.tsx create mode 100644 frontend/src/components/Tag/Tag.stories.tsx create mode 100644 frontend/src/components/Tag/Tag.style.ts create mode 100644 frontend/src/components/Tag/Tag.tsx create mode 100644 frontend/src/components/Tag/TagSkeleton.tsx create mode 100644 frontend/src/components/TextArea/LabeledTextArea.stories.tsx create mode 100644 frontend/src/components/TextArea/LabeledTextArea.style.ts create mode 100644 frontend/src/components/TextArea/LabeledTextArea.tsx create mode 100644 frontend/src/components/UserPreview/UserPreview.stories.tsx create mode 100644 frontend/src/components/UserPreview/UserPreview.style.ts create mode 100644 frontend/src/components/UserPreview/UserPreview.tsx create mode 100644 frontend/src/components/UserPreviewList/UserPreviewList.stories.tsx create mode 100644 frontend/src/components/UserPreviewList/UserPreviewList.style.ts create mode 100644 frontend/src/components/UserPreviewList/UserPreviewList.tsx create mode 100644 frontend/src/components/Zzim/ZzimButton.stories.tsx create mode 100644 frontend/src/components/Zzim/ZzimButton.style.ts create mode 100644 frontend/src/components/Zzim/ZzimButton.tsx create mode 100644 frontend/src/constants/poclies.ts create mode 100644 frontend/src/constants/queryKeys.ts create mode 100644 frontend/src/constants/routes.ts create mode 100644 frontend/src/constants/styles.ts create mode 100644 frontend/src/custom.d.ts create mode 100644 frontend/src/emotion.d.ts create mode 100644 frontend/src/hooks/mutaions/useAddMoim.ts create mode 100644 frontend/src/hooks/mutaions/useAddPlease.ts create mode 100644 frontend/src/hooks/mutaions/useCancelChamyo.ts create mode 100644 frontend/src/hooks/mutaions/useCancelMoim.ts create mode 100644 frontend/src/hooks/mutaions/useChangeZzim.ts create mode 100644 frontend/src/hooks/mutaions/useCompleteMoin.ts create mode 100644 frontend/src/hooks/mutaions/useConfirmDatetime.ts create mode 100644 frontend/src/hooks/mutaions/useConfirmPlace.ts create mode 100644 frontend/src/hooks/mutaions/useCreateDarakbang.ts create mode 100644 frontend/src/hooks/mutaions/useEnterDarakbang.ts create mode 100644 frontend/src/hooks/mutaions/useInterest.ts create mode 100644 frontend/src/hooks/mutaions/useJoinMoim.ts create mode 100644 frontend/src/hooks/mutaions/useModifyMoim.ts create mode 100644 frontend/src/hooks/mutaions/useOpenChat.ts create mode 100644 frontend/src/hooks/mutaions/useReopenMoim.ts create mode 100644 frontend/src/hooks/mutaions/useSendMessage.ts create mode 100644 frontend/src/hooks/mutaions/useServeToken.ts create mode 100644 frontend/src/hooks/mutaions/useWriteComment.ts create mode 100644 frontend/src/hooks/queries/useChamyoAll.ts create mode 100644 frontend/src/hooks/queries/useChamyoMine.ts create mode 100644 frontend/src/hooks/queries/useChatPreiview.ts create mode 100644 frontend/src/hooks/queries/useChats.test.tsx create mode 100644 frontend/src/hooks/queries/useChats.ts create mode 100644 frontend/src/hooks/queries/useDarakbangInviteCode.ts create mode 100644 frontend/src/hooks/queries/useDarakbangMembers.ts create mode 100644 frontend/src/hooks/queries/useDarakbangNameByCode.ts create mode 100644 frontend/src/hooks/queries/useMoim.ts create mode 100644 frontend/src/hooks/queries/useMoims.ts create mode 100644 frontend/src/hooks/queries/useMyDarakbang.ts create mode 100644 frontend/src/hooks/queries/useMyDarakbangRole.ts create mode 100644 frontend/src/hooks/queries/useMyInfo.ts create mode 100644 frontend/src/hooks/queries/useMyMoim.ts create mode 100644 frontend/src/hooks/queries/useMyMoims.ts create mode 100644 frontend/src/hooks/queries/useMyZzimMoim.ts create mode 100644 frontend/src/hooks/queries/useNotification.ts create mode 100644 frontend/src/hooks/queries/useNowDarakbangNameById.ts create mode 100644 frontend/src/hooks/queries/usePleases.ts create mode 100644 frontend/src/hooks/queries/useZzimMine.ts create mode 100644 frontend/src/hooks/useFunnel.test.tsx create mode 100644 frontend/src/hooks/useFunnel.ts create mode 100644 frontend/src/hooks/useStatePersist.test.tsx create mode 100644 frontend/src/hooks/useStatePersist.ts create mode 100644 frontend/src/index.html create mode 100644 frontend/src/index.tsx create mode 100644 frontend/src/layouts/ChattingPreviewLayout/ChattingPreviewContainer/ChattingPreviewContainer.stories.tsx create mode 100644 frontend/src/layouts/ChattingPreviewLayout/ChattingPreviewContainer/ChattingPreviewContainer.style.ts create mode 100644 frontend/src/layouts/ChattingPreviewLayout/ChattingPreviewContainer/ChattingPreviewContainer.tsx create mode 100644 frontend/src/layouts/ChattingPreviewLayout/ChattingPreviewLayout.style.ts create mode 100644 frontend/src/layouts/ChattingPreviewLayout/ChattingPreviewLayout.tsx create mode 100644 frontend/src/layouts/ChattingRoomLayout/ChattingRoomFooter/ChattingRoomFooter.style.ts create mode 100644 frontend/src/layouts/ChattingRoomLayout/ChattingRoomFooter/ChattingRoomFooter.tsx create mode 100644 frontend/src/layouts/ChattingRoomLayout/ChattingRoomLayout.stories.tsx create mode 100644 frontend/src/layouts/ChattingRoomLayout/ChattingRoomLayout.style.ts create mode 100644 frontend/src/layouts/ChattingRoomLayout/ChattingRoomLayout.tsx create mode 100644 frontend/src/layouts/CompleteLayout/CompleteBottomWrapper/CompleteBottomWrapper.style.ts create mode 100644 frontend/src/layouts/CompleteLayout/CompleteBottomWrapper/CompleteBottomWrapper.tsx create mode 100644 frontend/src/layouts/CompleteLayout/CompleteContentContainer/CompleteContentContainer.stories.tsx create mode 100644 frontend/src/layouts/CompleteLayout/CompleteContentContainer/CompleteContentContainer.style.ts create mode 100644 frontend/src/layouts/CompleteLayout/CompleteContentContainer/CompleteContentContainer.tsx create mode 100644 frontend/src/layouts/CompleteLayout/CompleteLayout.style.ts create mode 100644 frontend/src/layouts/CompleteLayout/CompleteLayout.tsx create mode 100644 frontend/src/layouts/FormLayout/FormBottomWrapper/FormBottomButtonWrapper.tsx create mode 100644 frontend/src/layouts/FormLayout/FormBottomWrapper/FormBottomWrapper.style.ts create mode 100644 frontend/src/layouts/FormLayout/FormHeader/FormHeader.style.ts create mode 100644 frontend/src/layouts/FormLayout/FormHeader/FormHeader.tsx create mode 100644 frontend/src/layouts/FormLayout/FormLayout.style.ts create mode 100644 frontend/src/layouts/FormLayout/FormLayout.tsx create mode 100644 frontend/src/layouts/FormLayout/FormMain/FormMain.style.ts create mode 100644 frontend/src/layouts/FormLayout/FormMain/FormMain.tsx create mode 100644 frontend/src/layouts/FunnelLayout/FunnelFooter/FunnelFooter.style.ts create mode 100644 frontend/src/layouts/FunnelLayout/FunnelFooter/FunnelFooter.tsx create mode 100644 frontend/src/layouts/FunnelLayout/FunnelLayout.style.ts create mode 100644 frontend/src/layouts/FunnelLayout/FunnelLayout.tsx create mode 100644 frontend/src/layouts/FunnelLayout/FunnelMain/FunnelMain.style.ts create mode 100644 frontend/src/layouts/FunnelLayout/FunnelMain/FunnelMain.tsx create mode 100644 frontend/src/layouts/HomeLayout.tsx/HomeFixedButtonWrapper/HomeFixedButtonWrapper.style.ts create mode 100644 frontend/src/layouts/HomeLayout.tsx/HomeFixedButtonWrapper/HomeFixedButtonWrapper.tsx create mode 100644 frontend/src/layouts/HomeLayout.tsx/HomeHeader/HomeHeader.style.ts create mode 100644 frontend/src/layouts/HomeLayout.tsx/HomeHeader/HomeHeader.tsx create mode 100644 frontend/src/layouts/HomeLayout.tsx/HomeLayout.style.ts create mode 100644 frontend/src/layouts/HomeLayout.tsx/HomeLayout.tsx create mode 100644 frontend/src/layouts/HomeLayout.tsx/HomeMain/HomeMain.style.ts create mode 100644 frontend/src/layouts/HomeLayout.tsx/HomeMain/HomeMain.tsx create mode 100644 frontend/src/layouts/InformationLayout/InformationBottomWrapper/InformationBottomWrapper.style.ts create mode 100644 frontend/src/layouts/InformationLayout/InformationBottomWrapper/InformationBottomWrapper.tsx create mode 100644 frontend/src/layouts/InformationLayout/InformationContentContainer/InformationContentContainer.stories.tsx create mode 100644 frontend/src/layouts/InformationLayout/InformationContentContainer/InformationContentContainer.style.ts create mode 100644 frontend/src/layouts/InformationLayout/InformationContentContainer/InformationLayoutContentContainer.tsx create mode 100644 frontend/src/layouts/InformationLayout/InformationLayout.stories.tsx create mode 100644 frontend/src/layouts/InformationLayout/InformationLayout.style.ts create mode 100644 frontend/src/layouts/InformationLayout/InformationLayout.tsx create mode 100644 frontend/src/layouts/LoginLayout/LoginFooter/LoginFooter.style.ts create mode 100644 frontend/src/layouts/LoginLayout/LoginFooter/LoginFooter.tsx create mode 100644 frontend/src/layouts/LoginLayout/LoginHeader/LoginHeader.style.ts create mode 100644 frontend/src/layouts/LoginLayout/LoginHeader/LoginHeader.tsx create mode 100644 frontend/src/layouts/LoginLayout/LoginLayout.style.ts create mode 100644 frontend/src/layouts/LoginLayout/LoginLayout.tsx create mode 100644 frontend/src/layouts/LoginLayout/LoginMain/LoginMain.style.ts create mode 100644 frontend/src/layouts/LoginLayout/LoginMain/LoginMain.tsx create mode 100644 frontend/src/layouts/PleaseLayout/PleaseFixedButtonWrapper/PleaseFixedButtonWrapper.style.ts create mode 100644 frontend/src/layouts/PleaseLayout/PleaseFixedButtonWrapper/PleaseFixedButtonWrapper.tsx create mode 100644 frontend/src/layouts/PleaseLayout/PleaseHeader/PleaseHeader.style.ts create mode 100644 frontend/src/layouts/PleaseLayout/PleaseHeader/PleaseHeader.tsx create mode 100644 frontend/src/layouts/PleaseLayout/PleaseLayout.style.ts create mode 100644 frontend/src/layouts/PleaseLayout/PleaseLayout.tsx create mode 100644 frontend/src/layouts/PleaseLayout/PleaseMain/PleaseMain.style.ts create mode 100644 frontend/src/layouts/PleaseLayout/PleaseMain/PleaseMain.tsx create mode 100644 frontend/src/layouts/SelectLayout/SelectBottomWrapper/SelectBottomWrapper.style.ts create mode 100644 frontend/src/layouts/SelectLayout/SelectBottomWrapper/SelectBottomWrapper.tsx create mode 100644 frontend/src/layouts/SelectLayout/SelectContentContainer/SelectContentContainer.stories.tsx create mode 100644 frontend/src/layouts/SelectLayout/SelectContentContainer/SelectContentContainer.style.ts create mode 100644 frontend/src/layouts/SelectLayout/SelectContentContainer/SelectContentContainer.tsx create mode 100644 frontend/src/layouts/SelectLayout/SelectLayout.style.ts create mode 100644 frontend/src/layouts/SelectLayout/SelectLayout.tsx create mode 100644 frontend/src/layouts/StretchContentLayout/StretchContentBottomWrapper/StretchContentBottomWrapper.style.ts create mode 100644 frontend/src/layouts/StretchContentLayout/StretchContentBottomWrapper/StretchContentBottomWrapper.tsx create mode 100644 frontend/src/layouts/StretchContentLayout/StretchContentContainer/StretchContentContainer.style.ts create mode 100644 frontend/src/layouts/StretchContentLayout/StretchContentContainer/StretchContentContainer.tsx create mode 100644 frontend/src/layouts/StretchContentLayout/StretchContentLayout.style.ts create mode 100644 frontend/src/layouts/StretchContentLayout/StretchContentLayout.tsx create mode 100644 frontend/src/layouts/components/NavigationBarWrapper/NavigationBarWrapper.style.ts create mode 100644 frontend/src/layouts/components/NavigationBarWrapper/NavigationBarWrapper.tsx create mode 100644 frontend/src/layouts/components/StickyTriSectionHeader/StickyTriSectionHeader.style.ts create mode 100644 frontend/src/layouts/components/StickyTriSectionHeader/StickyTriSectionHeader.tsx create mode 100644 frontend/src/layouts/components/TriSectionHeader/TriSectionHeader.stories.tsx create mode 100644 frontend/src/layouts/components/TriSectionHeader/TriSectionHeader.style.ts create mode 100644 frontend/src/layouts/components/TriSectionHeader/TriSectionHeader.tsx create mode 100644 frontend/src/mocks/browser.ts create mode 100644 frontend/src/mocks/handler/chatHandler.ts create mode 100644 frontend/src/mocks/handler/index.ts create mode 100644 frontend/src/mocks/handler/interestHandler.ts create mode 100644 frontend/src/mocks/handler/mockPleases.ts create mode 100644 frontend/src/mocks/handler/mockedChats.ts create mode 100644 frontend/src/mocks/handler/moimHandler.ts create mode 100644 frontend/src/mocks/handler/notificationHandler.ts create mode 100644 frontend/src/mocks/handler/pleaseHandler.ts create mode 100644 frontend/src/mocks/server.ts create mode 100644 frontend/src/mocks/wrapper.tsx create mode 100644 frontend/src/pages/ChatPage/ChatListSkeleton/ChatCardListSkeleton.style.ts create mode 100644 frontend/src/pages/ChatPage/ChatListSkeleton/ChatCardListSkeleton.tsx create mode 100644 frontend/src/pages/ChatPage/ChatListSkeleton/ChatCardSkeleton/ChatCardSkeleton.stories.tsx create mode 100644 frontend/src/pages/ChatPage/ChatListSkeleton/ChatCardSkeleton/ChatCardSkeleton.style.ts create mode 100644 frontend/src/pages/ChatPage/ChatListSkeleton/ChatCardSkeleton/ChatCardSkeleton.tsx create mode 100644 frontend/src/pages/ChatPage/ChatPage.tsx create mode 100644 frontend/src/pages/ChattingRoomPage/ChattingRoomPage.tsx create mode 100644 frontend/src/pages/DarakbangCreationPage/DarakbangCreationModalContent/DarakbangCreationModalContent.style.ts create mode 100644 frontend/src/pages/DarakbangCreationPage/DarakbangCreationModalContent/DarakbangCreationModalContent.tsx create mode 100644 frontend/src/pages/DarakbangCreationPage/DarakbangCreationPage.style.ts create mode 100644 frontend/src/pages/DarakbangCreationPage/DarakbangCreationPage.tsx create mode 100644 frontend/src/pages/DarakbangEntrancePage/DarakbangEntrancePage.tsx create mode 100644 frontend/src/pages/DarakbangInvitationPage/DarakbangInvitationPage.style.ts create mode 100644 frontend/src/pages/DarakbangInvitationPage/DarakbangInvitationPage.tsx create mode 100644 frontend/src/pages/DarakbangLandingPage/DarakbangLandingPage.tsx create mode 100644 frontend/src/pages/DarakbangManagementPage/DarakbangManagementPage.tsx create mode 100644 frontend/src/pages/DarakbangMembersPage/DarakbangMembersPage.style.tsx create mode 100644 frontend/src/pages/DarakbangMembersPage/DarakbangMembersPage.tsx create mode 100644 frontend/src/pages/DarakbangNicknamePage/DarakbangNicknameModalContent/DarakbangNicknameModalContent.style.ts create mode 100644 frontend/src/pages/DarakbangNicknamePage/DarakbangNicknameModalContent/DarakbangNicknameModalContent.tsx create mode 100644 frontend/src/pages/DarakbangNicknamePage/DarakbangNicknamePage.tsx create mode 100644 frontend/src/pages/DarakbangSelectOptionPage/DarakbangSelectOptionPage.tsx create mode 100644 frontend/src/pages/DarakbangSelectPage/DarakbangSelectPage.style.ts create mode 100644 frontend/src/pages/DarakbangSelectPage/DarakbangSelectPage.tsx create mode 100644 frontend/src/pages/HomePage/HomePage.tsx create mode 100644 frontend/src/pages/KakaoOAuthLoginPage/KakaoOAuthLoginPage.tsx create mode 100644 frontend/src/pages/MainPage/MainPage.stories.tsx create mode 100644 frontend/src/pages/MainPage/MainPage.style.ts create mode 100644 frontend/src/pages/MainPage/MainPage.tsx create mode 100644 frontend/src/pages/MoimCreationPage/MoimCreationPage.hook.test.tsx create mode 100644 frontend/src/pages/MoimCreationPage/MoimCreationPage.hook.ts create mode 100644 frontend/src/pages/MoimCreationPage/MoimCreationPage.style.ts create mode 100644 frontend/src/pages/MoimCreationPage/MoimCreationPage.tsx create mode 100644 frontend/src/pages/MoimCreationPage/MoimCreationPage.util.test.tsx create mode 100644 frontend/src/pages/MoimCreationPage/MoimCreationPage.util.ts create mode 100644 frontend/src/pages/MoimCreationPage/Steps/DateAndTimeStep.tsx create mode 100644 frontend/src/pages/MoimCreationPage/Steps/DescriptionStep.tsx create mode 100644 frontend/src/pages/MoimCreationPage/Steps/MaxPeopleStep.tsx create mode 100644 frontend/src/pages/MoimCreationPage/Steps/OfflineOrOnlineStep.tsx create mode 100644 frontend/src/pages/MoimCreationPage/Steps/PlaceStep.tsx create mode 100644 frontend/src/pages/MoimCreationPage/Steps/TitleStep.tsx create mode 100644 frontend/src/pages/MoimDetailPage/MoimDetailPage.tsx create mode 100644 frontend/src/pages/MoimModifyPage/MoimModifyPage.constant.ts create mode 100644 frontend/src/pages/MoimModifyPage/MoimModifyPage.hook.ts create mode 100644 frontend/src/pages/MoimModifyPage/MoimModifyPage.tsx create mode 100644 frontend/src/pages/MoimModifyPage/MoimModifyPage.util.ts create mode 100644 frontend/src/pages/Mypage/MyPage.tsx create mode 100644 frontend/src/pages/NotFoundPage/NotFoundPage.stories.tsx create mode 100644 frontend/src/pages/NotFoundPage/NotFoundPage.tsx create mode 100644 frontend/src/pages/NotificationPage/NotificationPage.style.ts create mode 100644 frontend/src/pages/NotificationPage/NotificationPage.tsx create mode 100644 frontend/src/pages/ParticipationCompletePage/ParticipationCompletePage.tsx create mode 100644 frontend/src/pages/PleaseCreationPage/MoimCreatePage.util.test.tsx create mode 100644 frontend/src/pages/PleaseCreationPage/PleaseCreationPage.constant.ts create mode 100644 frontend/src/pages/PleaseCreationPage/PleaseCreationPage.hook.ts create mode 100644 frontend/src/pages/PleaseCreationPage/PleaseCreationPage.style.ts create mode 100644 frontend/src/pages/PleaseCreationPage/PleaseCreationPage.tsx create mode 100644 frontend/src/pages/PleaseCreationPage/PleaseCreationPage.util.ts create mode 100644 frontend/src/pages/PleaseCreationPage/useMoimInfoInput.test.tsx create mode 100644 frontend/src/pages/PleasePage/PleasePage.tsx create mode 100644 frontend/src/queryClient.ts create mode 100644 frontend/src/routes/DarakbangInvitationRoute.tsx create mode 100644 frontend/src/routes/ErrorRoute.tsx create mode 100644 frontend/src/routes/ProtectedRoute.tsx create mode 100644 frontend/src/routes/SlashRoute.tsx create mode 100644 frontend/src/routes/router.tsx create mode 100644 frontend/src/service/forgroundMessage.ts create mode 100644 frontend/src/service/initFirebase.ts create mode 100644 frontend/src/service/notification.ts create mode 100644 frontend/src/types/index.d.ts create mode 100644 frontend/src/utils/checkAuthentication.ts create mode 100644 frontend/src/utils/customError/ApiError.ts create mode 100644 frontend/src/utils/formatters.ts create mode 100644 frontend/src/utils/tokenManager.ts create mode 100644 frontend/tsconfig.json create mode 100644 frontend/webpack.common.js create mode 100644 frontend/webpack.dev.js create mode 100644 frontend/webpack.prod.js diff --git a/.github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from .github/ISSUE_TEMPLATE/PULL_REQUEST_TEMPLATE.md rename to .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/workflows/be-rolling-deploy.yml b/.github/workflows/be-rolling-deploy.yml new file mode 100644 index 000000000..ea6c9c6ff --- /dev/null +++ b/.github/workflows/be-rolling-deploy.yml @@ -0,0 +1,68 @@ +name: Rolling Deployment + +on: + push: + branches: + - main + +jobs: + deploy-prod1: + name: Deploy to Prod1 Instance + runs-on: runner-prod1 + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Run Prod1 instance deploy script + run: | + cd ~/deploy && ./deploy.sh + + check-prod1: + name: Check Prod1 Instance + runs-on: runner-prod1 + needs: deploy-prod1 + + steps: + - name: Wait for Prod1 instance to be ready + run: sleep 30 + + - name: Health check for Prod1 instance + run: | + RESPONSE=$(curl --write-out '%{http_code}' --silent --output /dev/null http://localhost:8080/health) + if [ $RESPONSE -ne 200 ]; then + echo "Prod1 instance deployment failed." + exit 1 + fi + echo "Prod1 instance is healthy." + + deploy-prod2: + name: Deploy to Prod2 Instance + runs-on: runner-prod2 + needs: check-prod1 + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Run Prod2 instance deploy script + run: | + cd ~/deploy && ./deploy.sh + + check-prod2: + name: Check Prod2 Instance + runs-on: runner-prod2 + needs: deploy-prod2 + + steps: + - name: Wait for Prod2 instance to be ready + run: sleep 30 + + - name: Health check for Prod2 instance + run: | + RESPONSE=$(curl --write-out '%{http_code}' --silent --output /dev/null http://localhost:8080/health) + if [ $RESPONSE -ne 200 ]; then + echo "Prod2 instance deployment failed." + exit 1 + fi + echo "Prod2 instance is healthy." diff --git a/.github/workflows/cd-frontend.yml b/.github/workflows/cd-frontend.yml new file mode 100644 index 000000000..f10f5712d --- /dev/null +++ b/.github/workflows/cd-frontend.yml @@ -0,0 +1,17 @@ +name: frontend-deploy + +on: + push: + branches: + - develop-frontend + +jobs: + deploy: + runs-on: [self-hosted, develop] + + steps: + - name: deploy + run: | + cd ~/deploy && ./deploy-fe.sh + + diff --git a/frontend/.gitkeep b/.github/workflows/cd-prod.yml similarity index 100% rename from frontend/.gitkeep rename to .github/workflows/cd-prod.yml diff --git a/.github/workflows/ci-frontend.yml b/.github/workflows/ci-frontend.yml new file mode 100644 index 000000000..d9907c57f --- /dev/null +++ b/.github/workflows/ci-frontend.yml @@ -0,0 +1,46 @@ +name: frontend-integration + +on: + pull_request: + branches: + - develop-frontend + +jobs: + test: + runs-on: ubuntu-latest + + defaults: + run: + shell: bash + working-directory: ./frontend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Check Caching + uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '20.15.0' + + - name: Install Dependencies + run: npm install --frozen-lockfile + + - name: Create .env file + run: | + echo "BASE_URL=${{ secrets.BASE_URL }}" > .env + echo "REACT_APP_GOOGLE_ANALYTICS=${{ secrets.REACT_APP_GOOGLE_ANALYTICS }}" >> .env + echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> .env + echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env + + - name: Run tests + run: npm run test + + - name: Run linter + run: npm run lint diff --git a/.github/workflows/cicd-backend-dev.yml b/.github/workflows/cicd-backend-dev.yml new file mode 100644 index 000000000..c4b088e98 --- /dev/null +++ b/.github/workflows/cicd-backend-dev.yml @@ -0,0 +1,70 @@ +name: CI CD dev + +on: + push: + branches: + - develop-backend + +jobs: + build: + runs-on: ubuntu-24.04 + + defaults: + run: + shell: bash + working-directory: ./backend + + steps: + - name: 레포지토리 체크아웃 + uses: actions/checkout@v4 + + - name: JDK 17을 설치 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'corretto' + + - name: Firebase 파일 이동 + run: | + mkdir -p src/main/resources/firebase + echo ${{ secrets.BACKEND_FIREBASE_JSON }} > src/main/resources/firebase/serviceAccountKey.json + + - name: Apple Auth Key 파일 이동 + run: | + mkdir -p src/main/resources/auth + printf "%s" "${{ secrets.APPLE_AUTH_KEY }}" > src/main/resources/auth/AuthKey.p8 + + - name: gradlew 권한 부여 + run: chmod +x ./gradlew + + - name: Gradle 빌드 + run: ./gradlew clean build + + - name: DockerHub 로그인 + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: 도커 이미지 빌드 및 푸시 + run: | + docker buildx build ./ --platform=linux/arm64 -t 2024mouda/mouda-be:latest + docker push 2024mouda/mouda-be:latest + + deploy: + needs: build + runs-on: [self-hosted, develop] + + steps: + - name: DockerHub login + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Docker Compose up + run: | + cd ~/deploy + docker compose -f docker-compose-be.yml down + docker compose -f docker-compose-be.yml pull + docker compose -f docker-compose-be.yml up -d diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml new file mode 100644 index 000000000..dc74684bf --- /dev/null +++ b/.github/workflows/pr-test.yml @@ -0,0 +1,41 @@ +name: pull-request-build + +on: + pull_request: + branches: + - develop-backend + +jobs: + build: + runs-on: ubuntu-24.04 + + defaults: + run: + shell: bash + working-directory: ./backend + + steps: + - name: 레포지토리 체크아웃 + uses: actions/checkout@v4 + + - name: JDK 17을 설치 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'corretto' + + - name: Firebase 파일 이동 + run: | + mkdir -p src/main/resources/firebase + echo ${{ secrets.BACKEND_FIREBASE_JSON }} > src/main/resources/firebase/serviceAccountKey.json + + - name: Apple Auth Key 파일 이동 + run: | + mkdir -p src/main/resources/auth + printf "%s" "${{ secrets.APPLE_AUTH_KEY }}" > src/main/resources/auth/AuthKey.p8 + + - name: gradlew 권한 부여 + run: chmod +x ./gradlew + + - name: Gradle 빌드 + run: ./gradlew clean build diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..4e8677ffd --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.idea +/.github/.idea +/backend/out +/frontend/.idea +/backend/htmlReport +*.pem +backend/src/main/resources/auth/AuthKey.p8 diff --git a/README.md b/README.md index e69de29bb..5597e19f7 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,18 @@ +# 모여봐요 우리의 다락방 +단체를 운영하면서 팀원들 간의 친밀도가 낮아서 운영에 어려움을 느끼는 경우가 있지 않으신가요? 팀원들과 함께 친해지기 어려워서 고민해 보신 적이 있나요? 모임을 만들고 싶어도 친하지 않은 사람들에게 사적으로 제안하기 어렵고, 단체의 목적에 벗어나는 주제를 꺼내기가 어려워요. 애써 모임을 만들어도 사람을 모으기도 쉽지 않아요. + +새 학기, 처음 만난 학술 동아리 부원들과 농구 모임을 만드는 걸 상상해 보세요. 친하지 않은 사람들에게 제안하고, 연락처를 물어 필요한 만큼의 인원수를 채우는 것이 힘들어요. 또 카카오톡, 디스코드, 슬랙으로는 약속을 확정하고 모임원들과 공유하기도 쉽지 않아요. + +만들고 싶은 모임을 언제든지 만들고 사람들이 참여할 수 있도록 홍보해 주는 서비스가 있다면 어떨까요? 자동으로 채팅방도 만들어주고 장소와 시간을 쉽게 정할 수 있게 해준다면요? + +모임을 만들고 참여하는 진입 장벽을 낮추고 각자의 입장에서의 어려움을 해소하기 위해 서비스를 만들었습니다. 누구나 가볍게 모임에 참여할 수 있도록요. + +모여봐요 우리의 다락방, 모우다에서 모임을 쉽고 가볍게 만들어봐요. + +
+ +## 💻 모우다팀 +| ![상돌](https://github.com/user-attachments/assets/9817062f-6213-47fb-94b2-77dbd08b9848) | ![안나](https://github.com/user-attachments/assets/83d147df-9b80-4703-aa66-3632da8e9ba4) | ![테니](https://github.com/user-attachments/assets/cf57b0b3-3a93-4f6e-8bac-8ab65261594c) | ![테바](https://github.com/user-attachments/assets/09151d0f-7f5d-4a3f-9c89-7c8e15abbd14) | ![호기](https://github.com/user-attachments/assets/276888b2-aae7-48bf-8e0e-31b7585f2e51) | ![소파](https://github.com/user-attachments/assets/96a04e69-ffce-411d-ad94-a5c1bbe27b5f) | ![수야](https://github.com/user-attachments/assets/b4427e5c-0d8a-467c-a2dd-137a4b5aecce) | ![치코](https://github.com/user-attachments/assets/76b25466-ab62-4e91-8b84-3139f8be8b71) | +|:----:|:----:|:----:|:----:|:----:|:----:|:----:|:----:| +| **BE** | **BE** | **BE** | **BE** | **BE** | **FE** | **FE** | **FE** | +| [상돌](https://github.com/pricelees) | [안나](https://github.com/Mingyum-Kim) | [테니](https://github.com/ay-eonii) | [테바](https://github.com/ksk0605) | [호기](https://github.com/hoyeonyy) | [소파](https://github.com/ss0526100) | [수야](https://github.com/cys4585) | [치코](https://github.com/jaeml06) | diff --git a/backend/.DS_Store b/backend/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..3ada068e6f9a74ac240d8e86bf4af9826bb872dd GIT binary patch literal 6148 zcmeHK%SyvQ6g{`Cwo<_^-3a-Df`1T83kog-e?ZfeQivA%z-{*ais1LT(sO5qnoO+A zB64rJbCQ{JCNpPBCIdi5+x!w30O&ClTPG}j2yNGFL@U&2qqsiun_@Z5ie=W|TE*XV zK-caF8MauUKz04}FLA{je!6hOKmBH6h&fhxL{8nTvhJ#Tk95cTxpEP4yHze;>OISA z#O~MG9970KZ6ei2r0U`t6WrpC{HJ)wa&s+aGclGS8;#r&g6{htO zIftq&iY&|lbHE%pm;>CiHF|xIR+Cwv};qW2h$r4T|;-1d_6Gw-n9<4M7%z?TC zZGSt^{eQCm{9hN@ojG6*{3{2fGrk{>IHj<+HYTTgZNhwDYACPtxRdb0ZN=p3R(#HM Z!TLlmh-tvmBWoD;A>eGV!W=lL17C;&cjEv6 literal 0 HcmV?d00001 diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 000000000..db136633d --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,9 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +.idea +out +logs +src/main/resources/firebase diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 000000000..5874a9baf --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,9 @@ +FROM openjdk:17-jdk + +ARG JAR_FILE=./build/libs/backend-0.0.1-SNAPSHOT.jar + +COPY ${JAR_FILE} /app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "/app.jar"] diff --git a/backend/HELP.md b/backend/HELP.md new file mode 100644 index 000000000..d09777e94 --- /dev/null +++ b/backend/HELP.md @@ -0,0 +1,25 @@ +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Gradle documentation](https://docs.gradle.org) +* [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/3.3.1/gradle-plugin/reference/html/) +* [Create an OCI image](https://docs.spring.io/spring-boot/docs/3.3.1/gradle-plugin/reference/html/#build-image) +* [Spring Data JPA](https://docs.spring.io/spring-boot/docs/3.3.1/reference/htmlsingle/index.html#data.sql.jpa-and-spring-data) +* [Spring Web](https://docs.spring.io/spring-boot/docs/3.3.1/reference/htmlsingle/index.html#web) + +### Guides +The following guides illustrate how to use some features concretely: + +* [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) +* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) +* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) +* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) +* [Accessing data with MySQL](https://spring.io/guides/gs/accessing-data-mysql/) + +### Additional Links +These additional references should also help you: + +* [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle) + diff --git a/backend/build.gradle b/backend/build.gradle new file mode 100644 index 000000000..e6d931772 --- /dev/null +++ b/backend/build.gradle @@ -0,0 +1,77 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.1' + id 'io.spring.dependency-management' version '1.1.5' +} + +group = 'mouda' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + // web + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + implementation group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.3.1' + + // lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + // jwt + implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation 'javax.xml.bind:jaxb-api:2.3.1' + + // database + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.mysql:mysql-connector-j' + + // test + testImplementation 'io.rest-assured:rest-assured:5.3.1' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.awaitility:awaitility:4.2.0' + + // jackson + implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.2' + + // notification + implementation 'com.google.firebase:firebase-admin:9.3.0' + + //Google Oauth + implementation 'com.google.api-client:google-api-client:1.32.1' + + // S3 + implementation 'com.amazonaws:aws-java-sdk-s3:1.12.773' + + // monitoring + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'io.micrometer:micrometer-registry-prometheus' +} + +tasks.named('test') { + useJUnitPlatform() +} + +jar { + enabled = false +} diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 000000000..0dbfde141 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.7" # 파일 규격 버전 +services: + mouda-be: + build: . + container_name: mouda-be # 컨테이너 이름 설정 + ports: + - "8080:8080" + command: + - run + - --cors + - "*" diff --git a/backend/gradle/wrapper/gradle-wrapper.jar b/backend/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/backend/gradlew.bat b/backend/gradlew.bat new file mode 100644 index 000000000..7101f8e46 --- /dev/null +++ b/backend/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/backend/settings.gradle b/backend/settings.gradle new file mode 100644 index 000000000..0f5036dcc --- /dev/null +++ b/backend/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'backend' diff --git a/backend/src/main/java/mouda/backend/BackendApplication.java b/backend/src/main/java/mouda/backend/BackendApplication.java new file mode 100644 index 000000000..a11c24fd7 --- /dev/null +++ b/backend/src/main/java/mouda/backend/BackendApplication.java @@ -0,0 +1,15 @@ +package mouda.backend; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EnableScheduling +public class BackendApplication { + + public static void main(String[] args) { + SpringApplication.run(BackendApplication.class, args); + } + +} diff --git a/backend/src/main/java/mouda/backend/aop/logging/ExceptRequestLogging.java b/backend/src/main/java/mouda/backend/aop/logging/ExceptRequestLogging.java new file mode 100644 index 000000000..8e5afa602 --- /dev/null +++ b/backend/src/main/java/mouda/backend/aop/logging/ExceptRequestLogging.java @@ -0,0 +1,12 @@ +package mouda.backend.aop.logging; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExceptRequestLogging { + +} diff --git a/backend/src/main/java/mouda/backend/aop/logging/RequestLoggingAspect.java b/backend/src/main/java/mouda/backend/aop/logging/RequestLoggingAspect.java new file mode 100644 index 000000000..38c614cea --- /dev/null +++ b/backend/src/main/java/mouda/backend/aop/logging/RequestLoggingAspect.java @@ -0,0 +1,106 @@ +package mouda.backend.aop.logging; + +import static java.util.stream.Collectors.*; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.member.domain.Member; + +@Aspect +@Component +@Slf4j +public class RequestLoggingAspect { + + @Pointcut("execution(* mouda.backend..controller.*Controller.*(..))") + public void allController() { + } + + @Before("allController()") + public void logController(JoinPoint joinPoint) { + HttpServletRequest request = getHttpServletRequest(); + + MethodSignature signature = (MethodSignature)joinPoint.getSignature(); + Method method = signature.getMethod(); + if (method.isAnnotationPresent(ExceptRequestLogging.class)) { + return; + } + + String uri = request.getRequestURI(); + String httpMethod = request.getMethod(); + String queryParameters = getQueryParameters(request); + String body = getBody(joinPoint); + + String memberInfo = getMemberInfo(joinPoint); + + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(String.format("Request : %s %s", httpMethod, uri)); + if (memberInfo != null) { + stringBuilder.append(String.format(", member : %s", memberInfo)); + } + if (body != null) { + stringBuilder.append(String.format(", body : %s", body)); + } + if (queryParameters != null) { + stringBuilder.append(String.format(", parameters : %s", queryParameters)); + } + log.info(stringBuilder.toString()); + } + + private String getMemberInfo(JoinPoint joinPoint) { + for (Object arg : joinPoint.getArgs()) { + if (arg instanceof Member) { + return "Member ID = " + ((Member)arg).getId(); + } + if (arg instanceof DarakbangMember) { + return "DarakbangMember ID = " + ((DarakbangMember)arg).getId(); + } + } + return null; + } + + private HttpServletRequest getHttpServletRequest() { + ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.currentRequestAttributes(); + return requestAttributes.getRequest(); + } + + private String getQueryParameters(HttpServletRequest request) { + String queryParameters = request.getParameterMap() + .entrySet() + .stream() + .map(entry -> "%s = %s".formatted(entry.getKey(), entry.getValue()[0])) + .collect(joining(", ")); + + if (queryParameters.isEmpty()) { + return null; + } + return queryParameters; + } + + private String getBody(JoinPoint joinPoint) { + MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature(); + Parameter[] parameters = methodSignature.getMethod().getParameters(); + Object[] args = joinPoint.getArgs(); + for (int i = 0; i < parameters.length; i++) { + Parameter param = parameters[i]; + Object arg = args[i]; + if (param.isAnnotationPresent(RequestBody.class)) { + return arg.toString(); + } + } + return null; + } +} diff --git a/backend/src/main/java/mouda/backend/auth/Infrastructure/AppleOauthClient.java b/backend/src/main/java/mouda/backend/auth/Infrastructure/AppleOauthClient.java new file mode 100644 index 000000000..f746cf369 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/Infrastructure/AppleOauthClient.java @@ -0,0 +1,93 @@ +package mouda.backend.auth.Infrastructure; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mouda.backend.auth.Infrastructure.response.AppleRefreshTokenResponse; +import mouda.backend.auth.implement.jwt.ClientSecretProvider; +import mouda.backend.auth.presentation.response.OauthResponse; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AppleOauthClient implements OauthClient { + + public static final String CLIENT_ID = "site.mouda.backend"; + private static final String APPLE_API_URL = "https://appleid.apple.com/auth"; + private static final String GRANT_TYPE = "authorization_code"; + + private final RestClient restClient; + private final ClientSecretProvider clientSecretProvider; + + @Value("${oauth.apple.redirect-uri}") + private String redirectUri; + + @Override + public String getIdToken(String code) { + String tokenUrl = APPLE_API_URL + "/token"; + MultiValueMap formData = getFormData(code); + + OauthResponse oauthResponse = restClient.method(HttpMethod.POST) + .uri(tokenUrl) + .headers(httpHeaders -> httpHeaders.addAll(getHttpHeaders())) + .body(formData) + .retrieve() + .body(OauthResponse.class); + return oauthResponse.id_token(); + } + + public String getRefreshToken(String code) { + String tokenUrl = APPLE_API_URL + "/token"; + MultiValueMap formData = getFormData(code); + + AppleRefreshTokenResponse response = restClient.method(HttpMethod.POST) + .uri(tokenUrl) + .headers(httpHeaders -> httpHeaders.addAll(getHttpHeaders())) + .body(formData) + .retrieve() + .body(AppleRefreshTokenResponse.class); + return response.refresh_token(); + } + + // TODO: 애플 심사 시 필요할 수 있으므로 제거하지 않습니다. + // public void revoke(String refreshToken) { + // String revokeUrl = APPLE_API_URL + "/oauth2/v2/revoke"; + // MultiValueMap formData = new LinkedMultiValueMap<>(); + // formData.add("client_id", CLIENT_ID); + // formData.add("client_secret", clientSecretProvider.provide()); + // formData.add("token", refreshToken); + // formData.add("token_hint_type", "refresh_token"); + // + // ResponseEntity result = restClient.method(HttpMethod.POST) + // .uri(revokeUrl) + // .headers(httpHeaders -> httpHeaders.addAll(getHttpHeaders())) + // .body(formData) + // .retrieve() + // .toEntity(String.class); + // log.info("revoke status code : {}", result.getStatusCode()); + // } + + private MultiValueMap getFormData(String code) { + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("client_id", CLIENT_ID); + formData.add("client_secret", clientSecretProvider.provide()); + formData.add("code", code); + formData.add("grant_type", GRANT_TYPE); + formData.add("redirect_uri", redirectUri); + return formData; + } + + private HttpHeaders getHttpHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + return headers; + } +} diff --git a/backend/src/main/java/mouda/backend/auth/Infrastructure/GoogleOauthClient.java b/backend/src/main/java/mouda/backend/auth/Infrastructure/GoogleOauthClient.java new file mode 100644 index 000000000..76f886805 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/Infrastructure/GoogleOauthClient.java @@ -0,0 +1,74 @@ +package mouda.backend.auth.Infrastructure; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; + +import com.fasterxml.jackson.databind.JsonNode; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Component +@Slf4j +@RequiredArgsConstructor +public class GoogleOauthClient implements OauthClient { + + private static final String CLIENT_ID = "630308965506-4eiek02jh2a5fbj7as1o84l4mks3s2tu.apps.googleusercontent.com"; + private static final String GRANT_TYPE = "authorization_code"; + private static final String GOOGLE_API_URL = "https://oauth2.googleapis.com/token"; + + @Value("${oauth.google.client-secret}") + private String clientSecret; + + @Value("${oauth.google.redirect-uri}") + private String redirectUri; + + private final RestClient restClient; + + @Override + public String getIdToken(String code) { + try { + HttpHeaders headers = getHttpHeaders(); + MultiValueMap formData = getFormData(code); + + JsonNode oauthResponse = restClient.method(HttpMethod.POST) + .uri(GOOGLE_API_URL) + .headers(httpHeaders -> httpHeaders.addAll(headers)) + .body(formData) + .retrieve() + .body(JsonNode.class); + return oauthResponse.get("id_token").asText(); + } catch (Exception e) { + log.warn(e.getMessage()); + // throw new AuthException(HttpStatus.BAD_GATEWAY, TOKEN_ISSUE_FAILED); + throw e; + } + } + + private HttpHeaders getHttpHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + return headers; + } + + private MultiValueMap getFormData(String code) { + String scope = "https://www.googleapis.com/auth/userinfo.email " + + "https://www.googleapis.com/auth/userinfo.profile " + + "openid"; + + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("client_id", CLIENT_ID); + formData.add("client_secret", clientSecret); + formData.add("code", code); + formData.add("grant_type", GRANT_TYPE); + formData.add("redirect_uri", redirectUri); + formData.add("scope", scope); + return formData; + } +} diff --git a/backend/src/main/java/mouda/backend/auth/Infrastructure/KakaoOauthClient.java b/backend/src/main/java/mouda/backend/auth/Infrastructure/KakaoOauthClient.java new file mode 100644 index 000000000..014b3296e --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/Infrastructure/KakaoOauthClient.java @@ -0,0 +1,68 @@ +package mouda.backend.auth.Infrastructure; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestClient; + +import lombok.RequiredArgsConstructor; +import mouda.backend.auth.exception.AuthErrorMessage; +import mouda.backend.auth.exception.AuthException; +import mouda.backend.auth.presentation.response.OauthResponse; + +@Component +@RequiredArgsConstructor +public class KakaoOauthClient implements OauthClient { + + public static final String CLIENT_ID = "ca3adf9a52671fdbb847b809c0fdb980"; + public static final String GRANT_TYPE = "authorization_code"; + private static final String KAKAO_API_URL = "https://kauth.kakao.com/oauth/token"; + + private final RestClient restClient; + + @Value("${oauth.kakao.redirect-uri}") + private String redirectUri; + + public String getIdToken(String code) { + try { + HttpHeaders headers = getHttpHeaders(); + MultiValueMap formData = getFormData(code); + + OauthResponse response = restClient.method(HttpMethod.POST) + .uri(KAKAO_API_URL) + .headers(httpHeaders -> httpHeaders.addAll(headers)) + .body(formData) + .retrieve() + .body(OauthResponse.class); + + return response.id_token(); + } catch (ResourceAccessException e) { + throw new AuthException(HttpStatus.BAD_GATEWAY, AuthErrorMessage.KAKAO_CONNECT_TIMEOUT); + } catch (Exception e) { + throw new AuthException(HttpStatus.BAD_GATEWAY, AuthErrorMessage.KAKAO_UNAUTHORIZED); + } + + } + + private HttpHeaders getHttpHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set(HttpHeaders.ACCEPT_CHARSET, "utf-8"); + return headers; + } + + private MultiValueMap getFormData(String code) { + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("code", code); + formData.add("client_id", CLIENT_ID); + formData.add("grant_type", GRANT_TYPE); + formData.add("redirect_uri", redirectUri); + return formData; + } +} diff --git a/backend/src/main/java/mouda/backend/auth/Infrastructure/OauthClient.java b/backend/src/main/java/mouda/backend/auth/Infrastructure/OauthClient.java new file mode 100644 index 000000000..5ad2242c3 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/Infrastructure/OauthClient.java @@ -0,0 +1,6 @@ +package mouda.backend.auth.Infrastructure; + +public interface OauthClient { + + String getIdToken(String code); +} diff --git a/backend/src/main/java/mouda/backend/auth/Infrastructure/response/AppleRefreshTokenResponse.java b/backend/src/main/java/mouda/backend/auth/Infrastructure/response/AppleRefreshTokenResponse.java new file mode 100644 index 000000000..5ff150582 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/Infrastructure/response/AppleRefreshTokenResponse.java @@ -0,0 +1,4 @@ +package mouda.backend.auth.Infrastructure.response; + +public record AppleRefreshTokenResponse(String refresh_token) { +} diff --git a/backend/src/main/java/mouda/backend/auth/business/AppleAuthService.java b/backend/src/main/java/mouda/backend/auth/business/AppleAuthService.java new file mode 100644 index 000000000..0adf48134 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/business/AppleAuthService.java @@ -0,0 +1,58 @@ +package mouda.backend.auth.business; + +import java.util.Optional; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import mouda.backend.auth.exception.AuthErrorMessage; +import mouda.backend.auth.exception.AuthException; +import mouda.backend.auth.implement.AppleUserInfoProvider; +import mouda.backend.auth.implement.JoinManager; +import mouda.backend.auth.implement.jwt.AccessTokenProvider; +import mouda.backend.auth.presentation.response.LoginResponse; +import mouda.backend.member.domain.Member; +import mouda.backend.member.domain.OauthType; +import mouda.backend.member.implement.MemberFinder; + +@Service +@RequiredArgsConstructor +public class AppleAuthService { + + private final JoinManager joinManager; + private final AppleUserInfoProvider userInfoProvider; + private final MemberFinder memberFinder; + private final AccessTokenProvider accessTokenProvider; + + public LoginResponse login(String idToken, String user) { + String identifier = userInfoProvider.getIdentifier(idToken); + if (user != null) { + return handleNewUser(user, identifier); + } + return handleExistingUser(identifier); + } + + private LoginResponse handleNewUser(String user, String identifier) { + Optional member = memberFinder.getByIdentifier(identifier); + if (member.isPresent()) { + return new LoginResponse(accessTokenProvider.provide(member.get()), member.get().isConverted()); + } + Member joinedMember = join(identifier, user); + return new LoginResponse(accessTokenProvider.provide(joinedMember), joinedMember.isConverted()); + } + + private Member join(String identifier, String user) { + String name = userInfoProvider.getName(user); + return joinManager.join(name, OauthType.APPLE, identifier); + } + + private LoginResponse handleExistingUser(String identifier) { + Member member = memberFinder.findActiveOrDeletedByIdentifier(identifier); + if (member != null) { + joinManager.rejoin(member); + return new LoginResponse(accessTokenProvider.provide(member), member.isConverted()); + } + throw new AuthException(HttpStatus.BAD_REQUEST, AuthErrorMessage.CANNOT_FIND_APPLE_MEMBER); + } +} diff --git a/backend/src/main/java/mouda/backend/auth/business/GoogleAuthService.java b/backend/src/main/java/mouda/backend/auth/business/GoogleAuthService.java new file mode 100644 index 000000000..0511a7aea --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/business/GoogleAuthService.java @@ -0,0 +1,41 @@ +package mouda.backend.auth.business; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.auth.implement.GoogleUserInfoProvider; +import mouda.backend.auth.implement.JoinManager; +import mouda.backend.auth.implement.jwt.AccessTokenProvider; +import mouda.backend.auth.presentation.request.GoogleLoginRequest; +import mouda.backend.auth.presentation.response.LoginResponse; +import mouda.backend.member.domain.Member; +import mouda.backend.member.domain.OauthType; +import mouda.backend.member.implement.MemberFinder; +import mouda.backend.member.implement.MemberWriter; + +@Service +@Transactional +@RequiredArgsConstructor +public class GoogleAuthService { + + private final JoinManager joinManager; + private final GoogleUserInfoProvider userInfoProvider; + private final MemberFinder memberFinder; + private final AccessTokenProvider accessTokenProvider; + private final MemberWriter memberWriter; + + public LoginResponse login(GoogleLoginRequest request) { + String name = userInfoProvider.getName(request.idToken()); + String identifier = userInfoProvider.getIdentifier(request.idToken()); + Member member = memberFinder.findActiveOrDeletedByIdentifier(identifier); + + if (member != null) { + joinManager.rejoin(member); + memberWriter.updateName(member.getId(), name); + return new LoginResponse(accessTokenProvider.provide(member), member.isConverted()); + } + Member joinedMember = joinManager.join(name, OauthType.GOOGLE, identifier); + return new LoginResponse(accessTokenProvider.provide(joinedMember), joinedMember.isConverted()); + } +} diff --git a/backend/src/main/java/mouda/backend/auth/business/KakaoAuthService.java b/backend/src/main/java/mouda/backend/auth/business/KakaoAuthService.java new file mode 100644 index 000000000..655659fb4 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/business/KakaoAuthService.java @@ -0,0 +1,29 @@ +package mouda.backend.auth.business; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.auth.implement.KakaoUserInfoProvider; +import mouda.backend.auth.presentation.request.KakaoConvertRequest; +import mouda.backend.member.domain.Member; +import mouda.backend.member.implement.MemberFinder; +import mouda.backend.member.implement.MemberWriter; + +@Service +@Transactional +@RequiredArgsConstructor +public class KakaoAuthService { + + private final KakaoUserInfoProvider userInfoProvider; + private final MemberWriter memberWriter; + private final MemberFinder memberFinder; + + public void convert(Member alternation, KakaoConvertRequest kakaoConvertRequest) { + String identifier = userInfoProvider.getIdentifier(kakaoConvertRequest.code()); + Member kakao = memberFinder.findActiveOrDeletedByIdentifier(identifier); + memberWriter.updateLoginDetail(kakao, alternation.getLoginDetail()); + kakao.convert(); + memberWriter.deprecate(alternation); + } +} diff --git a/backend/src/main/java/mouda/backend/auth/business/TestAuthService.java b/backend/src/main/java/mouda/backend/auth/business/TestAuthService.java new file mode 100644 index 000000000..b72ac7298 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/business/TestAuthService.java @@ -0,0 +1,37 @@ +package mouda.backend.auth.business; + +import java.util.UUID; + +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import mouda.backend.auth.implement.jwt.AccessTokenProvider; +import mouda.backend.auth.presentation.response.LoginResponse; +import mouda.backend.member.domain.LoginDetail; +import mouda.backend.member.domain.Member; +import mouda.backend.member.domain.OauthType; +import mouda.backend.member.implement.MemberFinder; +import mouda.backend.member.implement.MemberWriter; + +@Service +@RequiredArgsConstructor +public class TestAuthService { + + private final MemberFinder memberFinder; + private final MemberWriter memberWriter; + private final AccessTokenProvider accessTokenProvider; + + public LoginResponse basicLoginAnna() { + Member member = memberFinder.findActiveOrDeletedByIdentifier("identifier"); + return new LoginResponse(accessTokenProvider.provide(member), true); + } + + public LoginResponse basicLoginHogee() { + Member member = Member.builder() + .name("조호연") + .loginDetail(new LoginDetail(OauthType.GOOGLE, UUID.randomUUID().toString())) + .build(); + memberWriter.append(member); + return new LoginResponse(accessTokenProvider.provide(member), true); + } +} diff --git a/backend/src/main/java/mouda/backend/auth/exception/AuthErrorMessage.java b/backend/src/main/java/mouda/backend/auth/exception/AuthErrorMessage.java new file mode 100644 index 000000000..18ab8ddf6 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/exception/AuthErrorMessage.java @@ -0,0 +1,26 @@ +package mouda.backend.auth.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum AuthErrorMessage { + + UNAUTHORIZED("인증되지 않은 사용자입니다."), + KAKAO_UNAUTHORIZED("카카오 인증에 실패하였습니다."), + INVALID_KAKO_AUTH_CODE("유효하지 않은 인증 코드입니다."), + TOKEN_ISSUE_FAILED("토큰 발급에 실패하였습니다."), + KAKAO_VALIDATION_FAILED("카카오 토큰 검증에 실패하였습니다."), + MISSING_AUTH_CODE("인증 코드가 누락되었습니다."), + INVALID_TOKEN("유효하지 않은 토큰 입니다."), + EXPIRED_TOKEN("만료된 토큰입니다."), + KAKAO_CONNECT_TIMEOUT("커넥션 타임아웃 되었습니다."), + DARAKBANG_NOT_ENTERED("가입한 다락방이 아닙니다."), + MEMBER_NOT_FOUND("회원가입 이력을 찾을 수 없습니다."), + KAKAO_CANNOT_JOIN("기존 카카오 로그인 이력이 있는 사용자만 이용할 수 있는 서비스입니다. 새로운 회원은 다른 로그인 서비스를 이용해주세요."), + APPLE_USER_BAD_REQUEST("사용자의 이름을 가져오는 과정에서 오류가 발생하였습니다."), + CANNOT_FIND_APPLE_MEMBER("애플 로그인 이력이 있지만 회원 정보를 조회할 수 없습니다."); + + private final String message; +} diff --git a/backend/src/main/java/mouda/backend/auth/exception/AuthException.java b/backend/src/main/java/mouda/backend/auth/exception/AuthException.java new file mode 100644 index 000000000..f11e389e9 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/exception/AuthException.java @@ -0,0 +1,11 @@ +package mouda.backend.auth.exception; + +import mouda.backend.common.exception.MoudaException; +import org.springframework.http.HttpStatus; + +public class AuthException extends MoudaException { + + public AuthException(HttpStatus httpStatus, AuthErrorMessage authErrorMessage) { + super(httpStatus, authErrorMessage.getMessage()); + } +} diff --git a/backend/src/main/java/mouda/backend/auth/implement/AppleUserInfoProvider.java b/backend/src/main/java/mouda/backend/auth/implement/AppleUserInfoProvider.java new file mode 100644 index 000000000..bfce1e521 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/implement/AppleUserInfoProvider.java @@ -0,0 +1,40 @@ +package mouda.backend.auth.implement; + +import java.util.Map; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; +import mouda.backend.auth.exception.AuthErrorMessage; +import mouda.backend.auth.exception.AuthException; +import mouda.backend.auth.util.TokenDecoder; + +@Component +@RequiredArgsConstructor +public class AppleUserInfoProvider { + + private static final String SUB_CLAIM_KEY = "sub"; + + private final ObjectMapper objectMapper; + + public String getIdentifier(String idToken) { + Map payload = TokenDecoder.parseIdToken(idToken); + return payload.get(SUB_CLAIM_KEY); + } + + public String getName(String user) { + try { + JsonNode node = objectMapper.readTree(user); + String firstName = node.path("name").path("firstName").asText(); + String lastName = node.path("name").path("lastName").asText(); + return lastName + firstName; + } catch (JsonProcessingException exception) { + throw new AuthException(HttpStatus.BAD_REQUEST, AuthErrorMessage.APPLE_USER_BAD_REQUEST); + } + } +} diff --git a/backend/src/main/java/mouda/backend/auth/implement/GoogleUserInfoProvider.java b/backend/src/main/java/mouda/backend/auth/implement/GoogleUserInfoProvider.java new file mode 100644 index 000000000..55a41b242 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/implement/GoogleUserInfoProvider.java @@ -0,0 +1,63 @@ +package mouda.backend.auth.implement; + +import java.util.Collections; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.jackson2.JacksonFactory; + +import lombok.RequiredArgsConstructor; +import mouda.backend.auth.exception.AuthErrorMessage; +import mouda.backend.auth.exception.AuthException; + +@Component +@RequiredArgsConstructor +public class GoogleUserInfoProvider { + + private static final String CLIENT_ID = "630308965506-4eiek02jh2a5fbj7as1o84l4mks3s2tu.apps.googleusercontent.com"; + + public String getName(String idToken) { + GoogleIdToken googleIdToken = getGoogleIdToken(idToken); + String familyName = (String)googleIdToken.getPayload().get("family_name"); + String givenName = (String)googleIdToken.getPayload().get("given_name"); + return getFullName(familyName, givenName); + } + + private String getFullName(String familyName, String givenName) { + if (familyName == null) { + familyName = ""; + } + if (givenName == null) { + givenName = ""; + } + return familyName + givenName; + } + + public String getIdentifier(String idToken) { + GoogleIdToken googleIdToken = getGoogleIdToken(idToken); + return googleIdToken.getPayload().getSubject(); + } + + private GoogleIdToken getGoogleIdToken(String idToken) { + GoogleIdToken googleIdToken = validateIdToken(idToken); + if (googleIdToken == null) { + throw new AuthException(HttpStatus.INTERNAL_SERVER_ERROR, AuthErrorMessage.INVALID_TOKEN); + } + return googleIdToken; + } + + private GoogleIdToken validateIdToken(String idToken) { + GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new JacksonFactory()) + .setAudience(Collections.singletonList(CLIENT_ID)) + .build(); + try { + return verifier.verify(idToken); + } catch (Exception e) { + throw new AuthException(HttpStatus.INTERNAL_SERVER_ERROR, AuthErrorMessage.INVALID_TOKEN); + } + } +} diff --git a/backend/src/main/java/mouda/backend/auth/implement/JoinManager.java b/backend/src/main/java/mouda/backend/auth/implement/JoinManager.java new file mode 100644 index 000000000..b17ef3773 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/implement/JoinManager.java @@ -0,0 +1,34 @@ +package mouda.backend.auth.implement; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.auth.exception.AuthErrorMessage; +import mouda.backend.auth.exception.AuthException; +import mouda.backend.member.domain.LoginDetail; +import mouda.backend.member.domain.Member; +import mouda.backend.member.domain.OauthType; +import mouda.backend.member.implement.MemberWriter; + +@Component +@RequiredArgsConstructor +public class JoinManager { + + private final MemberWriter memberWriter; + + public Member join(String name, OauthType oauthType, String identifier) { + if (OauthType.KAKAO == oauthType) { + throw new AuthException(HttpStatus.BAD_REQUEST, AuthErrorMessage.KAKAO_CANNOT_JOIN); + } + Member member = new Member(name, new LoginDetail(oauthType, identifier)); + return memberWriter.append(member); + } + + public void rejoin(Member member) { + if (member.isDeleted()) { + member.rejoin(); + memberWriter.append(member); + } + } +} diff --git a/backend/src/main/java/mouda/backend/auth/implement/KakaoUserInfoProvider.java b/backend/src/main/java/mouda/backend/auth/implement/KakaoUserInfoProvider.java new file mode 100644 index 000000000..4f713ae7c --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/implement/KakaoUserInfoProvider.java @@ -0,0 +1,24 @@ +package mouda.backend.auth.implement; + +import java.util.Map; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.auth.Infrastructure.KakaoOauthClient; +import mouda.backend.auth.util.TokenDecoder; + +@Component +@RequiredArgsConstructor +public class KakaoUserInfoProvider { + + private static final String SUB_CLAIM_KEY = "sub"; + + private final KakaoOauthClient oauthClient; + + public String getIdentifier(String code) { + String idToken = oauthClient.getIdToken(code); + Map payload = TokenDecoder.parseIdToken(idToken); + return payload.get(SUB_CLAIM_KEY); + } +} diff --git a/backend/src/main/java/mouda/backend/auth/implement/jwt/AccessTokenProvider.java b/backend/src/main/java/mouda/backend/auth/implement/jwt/AccessTokenProvider.java new file mode 100644 index 000000000..e6f1a971c --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/implement/jwt/AccessTokenProvider.java @@ -0,0 +1,73 @@ +package mouda.backend.auth.implement.jwt; + +import java.util.Date; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import mouda.backend.auth.exception.AuthErrorMessage; +import mouda.backend.auth.exception.AuthException; +import mouda.backend.member.domain.Member; +import mouda.backend.member.domain.OauthType; + +@Component +public class AccessTokenProvider { + + private static final String MEMBER_ID_CLAIM_KEY = "id"; + private static final String SOCIAL_LOGIN_ID_CLAIM_KEY = "identifier"; + private static final String OAUTH_TYPE = "oauthType"; + + @Value("${security.jwt.token.secret-key}") + private String secretKey; + + @Value("${security.jwt.token.expire-length}") + private long validityInMilliseconds; + + public String provide(Member member) { + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); + + return Jwts.builder() + .claim(MEMBER_ID_CLAIM_KEY, member.getId()) + .claim(SOCIAL_LOGIN_ID_CLAIM_KEY, member.getIdentifier()) + .claim(OAUTH_TYPE, member.getOauthType()) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + } + + public String extractSocialId(String token) { + Claims claims = getPayload(token); + return claims.get(SOCIAL_LOGIN_ID_CLAIM_KEY, String.class); + } + + public Claims getPayload(String token) { + try { + return Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody(); + + } catch (JwtException | IllegalArgumentException e) { + throw new AuthException(HttpStatus.UNAUTHORIZED, AuthErrorMessage.UNAUTHORIZED); + } + } + + public void validateToken(String token) { + Claims claims = getPayload(token); + + if (claims.getExpiration().before(new Date())) { + throw new AuthException(HttpStatus.UNAUTHORIZED, AuthErrorMessage.UNAUTHORIZED); + } + if (claims.get(OAUTH_TYPE).equals(OauthType.KAKAO.toString())) { + throw new AuthException(HttpStatus.UNAUTHORIZED, AuthErrorMessage.UNAUTHORIZED); + } + } +} + diff --git a/backend/src/main/java/mouda/backend/auth/implement/jwt/ClientSecretProvider.java b/backend/src/main/java/mouda/backend/auth/implement/jwt/ClientSecretProvider.java new file mode 100644 index 000000000..a86e95a0f --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/implement/jwt/ClientSecretProvider.java @@ -0,0 +1,76 @@ +package mouda.backend.auth.implement.jwt; + +import static mouda.backend.auth.Infrastructure.AppleOauthClient.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Date; +import java.util.stream.Collectors; + +import org.apache.commons.codec.binary.Base64; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import mouda.backend.auth.exception.AuthErrorMessage; +import mouda.backend.auth.exception.AuthException; + +@Component +public class ClientSecretProvider { + + private static final String ALG_PARAMETER_NAME = "alg"; + private static final String ALG_PARAMETER_VALUE = "ES256"; + + private static final String KEY_ID_PARAMETER_NAME = "kid"; + private static final String KEY_ID_PARAMETER_VALUE = "4YYCNG8SC9"; + + private static final String APPLE_URL = "https://appleid.apple.com"; + private static final String TEAM_ID = "3D7CZ9274W"; + + @Value("${security.jwt.token.expire-length}") + private long validityInMilliseconds; + + public String provide() { + try { + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); + + return Jwts.builder() + .setHeaderParam(ALG_PARAMETER_NAME, ALG_PARAMETER_VALUE) + .setHeaderParam(KEY_ID_PARAMETER_NAME, KEY_ID_PARAMETER_VALUE) + .setSubject(CLIENT_ID) + .setIssuer(TEAM_ID) + .setAudience(APPLE_URL) + .setExpiration(validity) + .signWith(SignatureAlgorithm.ES256, getPrivateKey()) + .compact(); + } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException exception) { + throw new AuthException(HttpStatus.INTERNAL_SERVER_ERROR, AuthErrorMessage.TOKEN_ISSUE_FAILED); + } + } + + private static PrivateKey getPrivateKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + InputStream privateKey = new ClassPathResource("auth/AuthKey.p8").getInputStream(); + + String result = new BufferedReader(new InputStreamReader(privateKey)).lines().collect(Collectors.joining("\n")); + + String key = result.replace("-----BEGIN PRIVATE KEY-----\n", "") + .replace("-----END PRIVATE KEY-----", ""); + + byte[] encoded = Base64.decodeBase64(key); + + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return keyFactory.generatePrivate(keySpec); + } +} diff --git a/backend/src/main/java/mouda/backend/auth/presentation/controller/AuthController.java b/backend/src/main/java/mouda/backend/auth/presentation/controller/AuthController.java new file mode 100644 index 000000000..e6dcd29cb --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/presentation/controller/AuthController.java @@ -0,0 +1,67 @@ +package mouda.backend.auth.presentation.controller; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import mouda.backend.auth.business.AppleAuthService; +import mouda.backend.auth.business.GoogleAuthService; +import mouda.backend.auth.business.KakaoAuthService; +import mouda.backend.auth.presentation.controller.swagger.AuthSwagger; +import mouda.backend.auth.presentation.request.GoogleLoginRequest; +import mouda.backend.auth.presentation.request.KakaoConvertRequest; +import mouda.backend.auth.presentation.response.LoginResponse; +import mouda.backend.common.config.argumentresolver.LoginMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.member.domain.Member; + +@RestController +@RequestMapping("/v1/auth") +@RequiredArgsConstructor +public class AuthController implements AuthSwagger { + + private final KakaoAuthService kakaoAuthService; + private final GoogleAuthService googleAuthService; + private final AppleAuthService appleAuthService; + + @Value("${oauth.apple.redirection}") + private String redirectUrl; + + @PostMapping("/kakao") + public ResponseEntity convert( + @LoginMember Member member, + @RequestBody KakaoConvertRequest kakaoConvertRequest + ) { + kakaoAuthService.convert(member, kakaoConvertRequest); + + return ResponseEntity.ok().build(); + } + + @PostMapping("/google") + public ResponseEntity> loginGoogle( + @RequestBody GoogleLoginRequest googleLoginRequest + ) { + LoginResponse response = googleAuthService.login(googleLoginRequest); + + return ResponseEntity.ok().body(new RestResponse<>(response)); + } + + @PostMapping("/apple") + public ResponseEntity loginApple( + @RequestParam("id_token") String idToken, + @RequestParam(name = "user", required = false) String user + ) { + LoginResponse response = appleAuthService.login(idToken, user); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add("Location", String.format(redirectUrl, response.accessToken(), response.isConverted())); + return new ResponseEntity<>(httpHeaders, HttpStatus.FOUND); + } +} diff --git a/backend/src/main/java/mouda/backend/auth/presentation/controller/TestAuthController.java b/backend/src/main/java/mouda/backend/auth/presentation/controller/TestAuthController.java new file mode 100644 index 000000000..d8220ae46 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/presentation/controller/TestAuthController.java @@ -0,0 +1,37 @@ +package mouda.backend.auth.presentation.controller; + +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import mouda.backend.aop.logging.ExceptRequestLogging; +import mouda.backend.auth.business.TestAuthService; +import mouda.backend.auth.presentation.controller.swagger.TestAuthSwagger; +import mouda.backend.auth.presentation.response.LoginResponse; +import mouda.backend.common.response.RestResponse; + +@Profile(value = {"local", "dev"}) +@RestController +@RequiredArgsConstructor +public class TestAuthController implements TestAuthSwagger { + + private final TestAuthService testAuthService; + + @PostMapping("/login/anna") + @ExceptRequestLogging + public ResponseEntity> loginBasicOauthAnna() { + LoginResponse response = testAuthService.basicLoginAnna(); + + return ResponseEntity.ok().body(new RestResponse<>(response)); + } + + @PostMapping("/login/hogee") + @ExceptRequestLogging + public ResponseEntity> loginBasicOauthHogee() { + LoginResponse response = testAuthService.basicLoginHogee(); + + return ResponseEntity.ok().body(new RestResponse<>(response)); + } +} diff --git a/backend/src/main/java/mouda/backend/auth/presentation/controller/swagger/AuthSwagger.java b/backend/src/main/java/mouda/backend/auth/presentation/controller/swagger/AuthSwagger.java new file mode 100644 index 000000000..fb4b5c4c3 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/presentation/controller/swagger/AuthSwagger.java @@ -0,0 +1,37 @@ +package mouda.backend.auth.presentation.controller.swagger; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import mouda.backend.auth.presentation.request.GoogleLoginRequest; +import mouda.backend.auth.presentation.request.KakaoConvertRequest; +import mouda.backend.auth.presentation.response.LoginResponse; +import mouda.backend.common.config.argumentresolver.LoginMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.member.domain.Member; + +public interface AuthSwagger { + + @Operation(summary = "카카오 로그인", description = "카카오 Oauth Code를 사용하여 로그인한다(accessToken 발급).") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "로그인 성공!"), + }) + ResponseEntity convert( + @LoginMember Member member, + @RequestBody KakaoConvertRequest kakaoConvertRequest); + + @Operation(summary = "구글 oauth 로그인", description = "구글 Oauth Identity Token 를 사용하여 로그인한다(accessToken 발급).") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "로그인 성공!"), + }) + ResponseEntity> loginGoogle(@RequestBody GoogleLoginRequest googleLoginRequest); + + @Operation(summary = "애플 oauth 로그인", description = "애플 서버로부터 직접 Identity Token과 사용자 정보를 받아 토큰을 발급") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "로그인 성공!"), + }) + ResponseEntity loginApple(String id_token, String user); +} diff --git a/backend/src/main/java/mouda/backend/auth/presentation/controller/swagger/TestAuthSwagger.java b/backend/src/main/java/mouda/backend/auth/presentation/controller/swagger/TestAuthSwagger.java new file mode 100644 index 000000000..8a78a01c6 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/presentation/controller/swagger/TestAuthSwagger.java @@ -0,0 +1,24 @@ +package mouda.backend.auth.presentation.controller.swagger; + +import org.springframework.http.ResponseEntity; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import mouda.backend.auth.presentation.response.LoginResponse; +import mouda.backend.common.response.RestResponse; + +public interface TestAuthSwagger { + + @Operation(summary = "테스트 용 로그인(안나)", description = "다락방과 모임을 생성하고 참여한 안나의 토큰 발급. 매번 같은 안나임.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "로그인 성공!"), + }) + ResponseEntity> loginBasicOauthAnna(); + + @Operation(summary = "테스트 용 로그인(호기)", description = "매번 새롭게 회원가입하는 호기의 토큰 발급. 매번 다른 호기임.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "로그인 성공!"), + }) + ResponseEntity> loginBasicOauthHogee(); +} diff --git a/backend/src/main/java/mouda/backend/auth/presentation/request/GoogleLoginRequest.java b/backend/src/main/java/mouda/backend/auth/presentation/request/GoogleLoginRequest.java new file mode 100644 index 000000000..af758f0ca --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/presentation/request/GoogleLoginRequest.java @@ -0,0 +1,9 @@ +package mouda.backend.auth.presentation.request; + +import jakarta.validation.constraints.NotNull; + +public record GoogleLoginRequest( + @NotNull + String idToken +) { +} diff --git a/backend/src/main/java/mouda/backend/auth/presentation/request/KakaoConvertRequest.java b/backend/src/main/java/mouda/backend/auth/presentation/request/KakaoConvertRequest.java new file mode 100644 index 000000000..30d0b4ae8 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/presentation/request/KakaoConvertRequest.java @@ -0,0 +1,9 @@ +package mouda.backend.auth.presentation.request; + +import jakarta.validation.constraints.NotNull; + +public record KakaoConvertRequest( + @NotNull + String code +) { +} diff --git a/backend/src/main/java/mouda/backend/auth/presentation/response/KakaoLoginResponse.java b/backend/src/main/java/mouda/backend/auth/presentation/response/KakaoLoginResponse.java new file mode 100644 index 000000000..bbae935c2 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/presentation/response/KakaoLoginResponse.java @@ -0,0 +1,6 @@ +package mouda.backend.auth.presentation.response; + +public record KakaoLoginResponse( + String accessToken +) { +} diff --git a/backend/src/main/java/mouda/backend/auth/presentation/response/LoginResponse.java b/backend/src/main/java/mouda/backend/auth/presentation/response/LoginResponse.java new file mode 100644 index 000000000..cbe29c35a --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/presentation/response/LoginResponse.java @@ -0,0 +1,7 @@ +package mouda.backend.auth.presentation.response; + +public record LoginResponse( + String accessToken, + boolean isConverted +) { +} diff --git a/backend/src/main/java/mouda/backend/auth/presentation/response/OauthResponse.java b/backend/src/main/java/mouda/backend/auth/presentation/response/OauthResponse.java new file mode 100644 index 000000000..d268a8a1e --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/presentation/response/OauthResponse.java @@ -0,0 +1,6 @@ +package mouda.backend.auth.presentation.response; + +public record OauthResponse( + String id_token +) { +} diff --git a/backend/src/main/java/mouda/backend/auth/util/TokenDecoder.java b/backend/src/main/java/mouda/backend/auth/util/TokenDecoder.java new file mode 100644 index 000000000..915267921 --- /dev/null +++ b/backend/src/main/java/mouda/backend/auth/util/TokenDecoder.java @@ -0,0 +1,30 @@ +package mouda.backend.auth.util; + +import java.util.Base64; +import java.util.Map; + +import org.springframework.http.HttpStatus; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import mouda.backend.auth.exception.AuthErrorMessage; +import mouda.backend.auth.exception.AuthException; + +public class TokenDecoder { + + public static Map parseIdToken(String idToken) { + try { + String[] parts = idToken.split("\\."); + if (parts.length != 3) { + throw new AuthException(HttpStatus.INTERNAL_SERVER_ERROR, AuthErrorMessage.INVALID_TOKEN); + } + String payload = parts[1]; + byte[] decodedBytes = Base64.getUrlDecoder().decode(payload); + String decodedPayload = new String(decodedBytes); + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readValue(decodedPayload, Map.class); + } catch (Exception e) { + throw new AuthException(HttpStatus.INTERNAL_SERVER_ERROR, AuthErrorMessage.KAKAO_VALIDATION_FAILED); + } + } +} diff --git a/backend/src/main/java/mouda/backend/bet/business/BetScheduler.java b/backend/src/main/java/mouda/backend/bet/business/BetScheduler.java new file mode 100644 index 000000000..257707e2a --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/business/BetScheduler.java @@ -0,0 +1,37 @@ +package mouda.backend.bet.business; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import mouda.backend.bet.domain.Bet; +import mouda.backend.bet.implement.BetFinder; +import mouda.backend.bet.implement.BetWriter; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.implement.ChatRoomWriter; + +@Service +@RequiredArgsConstructor +public class BetScheduler { + + @Value("${bet.schedule}") + private String rate; + + private final BetFinder betFinder; + private final BetWriter betWriter; + private final ChatRoomWriter chatRoomWriter; + + @Scheduled(cron = "${bet.schedule}") + public void performScheduledTask() { + List bets = betFinder.findAllDrawableBet(); + + bets.forEach(Bet::draw); + + betWriter.saveAll(bets); + + bets.forEach(bet -> chatRoomWriter.append(bet.getId(), bet.getDarakbangId(), ChatRoomType.BET)); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/business/BetService.java b/backend/src/main/java/mouda/backend/bet/business/BetService.java new file mode 100644 index 000000000..ad4bdb063 --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/business/BetService.java @@ -0,0 +1,73 @@ +package mouda.backend.bet.business; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.bet.domain.Bet; +import mouda.backend.bet.domain.Loser; +import mouda.backend.bet.implement.BetFinder; +import mouda.backend.bet.implement.BetSorter; +import mouda.backend.bet.implement.BetWriter; +import mouda.backend.bet.presentation.request.BetCreateRequest; +import mouda.backend.bet.presentation.response.BetFindAllResponses; +import mouda.backend.bet.presentation.response.BetFindResponse; +import mouda.backend.bet.presentation.response.BetResultResponse; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.implement.ChatRoomFinder; +import mouda.backend.chat.implement.ChatRoomWriter; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Service +@RequiredArgsConstructor +@Transactional +public class BetService { + + private final BetFinder betFinder; + private final BetWriter betWriter; + private final BetSorter betSorter; + private final ChatRoomFinder chatRoomFinder; + private final ChatRoomWriter chatRoomWriter; + + @Transactional(readOnly = true) + public BetFindAllResponses findAllBets(long darakbangId) { + List bets = betFinder.findAllByDarakbangId(darakbangId); + List sortedBets = betSorter.sort(bets); + return BetFindAllResponses.toResponse(sortedBets); + } + + @Transactional(readOnly = true) + public BetFindResponse findBet(long darakbangId, long betId, DarakbangMember darakbangMember) { + Bet bet = betFinder.find(darakbangId, betId); + Long chatRoomId = chatRoomFinder.findChatRoomIdByTargetId(bet.getId(), ChatRoomType.BET); + return BetFindResponse.toResponse(bet, darakbangMember, chatRoomId); + } + + public long createBet(long darakbangId, BetCreateRequest betRequest, DarakbangMember darakbangMember) { + Bet bet = betRequest.toBet(darakbangMember.getId()); + long savedBetId = betWriter.save(darakbangId, bet); + betWriter.participate(darakbangId, savedBetId, darakbangMember); + + return savedBetId; + } + + public void participateBet(long darakbangId, long betId, DarakbangMember darakbangMember) { + betWriter.participate(darakbangId, betId, darakbangMember); + } + + @Transactional(readOnly = true) + public BetResultResponse findBetResult(long darakbangId, long betId) { + Loser loser = betFinder.findResult(darakbangId, betId); + return BetResultResponse.from(loser); + } + + public void drawBet(long darakbangId, long betId) { + Bet bet = betFinder.find(darakbangId, betId); + bet.draw(); + betWriter.updateLoser(bet); + chatRoomWriter.append(bet.getId(), darakbangId, ChatRoomType.BET); + } +} + diff --git a/backend/src/main/java/mouda/backend/bet/domain/Bet.java b/backend/src/main/java/mouda/backend/bet/domain/Bet.java new file mode 100644 index 000000000..11092a307 --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/domain/Bet.java @@ -0,0 +1,71 @@ +package mouda.backend.bet.domain; + +import java.util.List; +import java.util.Random; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class Bet { + + private final BetDetails betDetails; + private final long darakbangId; + private final List participants; + private final long moimerId; + private Long loserId; + + @Builder + public Bet(BetDetails betDetails, long darakbangId, List participants, long moimerId, Long loserId) { + this.betDetails = betDetails; + this.darakbangId = darakbangId; + this.participants = participants; + this.moimerId = moimerId; + this.loserId = loserId; + } + + public void draw() { + Random random = new Random(); + int loserIndex = random.nextInt(participants.size()); + this.loserId = participants.get(loserIndex).getId(); + } + + public boolean hasLoser() { + return loserId != null; + } + + public BetRole getMyRole(Long id) { + if (moimerId == id) { + return BetRole.MOIMER; + } + if (isParticipated(id)) { + return BetRole.MOIMEE; + } + return BetRole.NON_MOIMEE; + } + + private boolean isParticipated(Long id) { + return participants.stream() + .anyMatch(participant -> participant.getId() == id); + } + + public boolean isLoser(long otherId) { + return loserId == otherId; + } + + public long getId() { + return betDetails.getId(); + } + + public String getTitle() { + return betDetails.getTitle(); + } + + public long timeDifferenceInMinutesWithNow() { + return betDetails.timeDifferenceInMinutesWithNow(); + } + + public boolean canNotParticipate() { + return hasLoser() || betDetails.pastBettingTime(); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/domain/BetDetails.java b/backend/src/main/java/mouda/backend/bet/domain/BetDetails.java new file mode 100644 index 000000000..b0d9838b2 --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/domain/BetDetails.java @@ -0,0 +1,50 @@ +package mouda.backend.bet.domain; + +import java.time.LocalDateTime; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class BetDetails { + + private final long id; + private final String title; + private final BettingTime bettingTime; + private final long moimerId; + private final Long loserId; + + @Builder + public BetDetails(long id, String title, LocalDateTime bettingTime, long moimerId, Long loserId) { + this.id = id; + this.title = title; + this.bettingTime = new BettingTime(bettingTime); + this.moimerId = moimerId; + this.loserId = loserId; + } + + public static BetDetails create(String title, int waitingMinutes) { + LocalDateTime bettingTime = LocalDateTime.now().plusMinutes(waitingMinutes); + + return BetDetails.builder() + .title(title) + .bettingTime(bettingTime) + .build(); + } + + public LocalDateTime getBettingTime() { + return bettingTime.getBettingTime(); + } + + public long timeDifferenceInMinutesWithNow() { + return bettingTime.timeDifferenceInMinutesWithNow(); + } + + public boolean hasLoser() { + return loserId != null; + } + + public boolean pastBettingTime() { + return bettingTime.isPast(); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/domain/BetRole.java b/backend/src/main/java/mouda/backend/bet/domain/BetRole.java new file mode 100644 index 000000000..c3025d655 --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/domain/BetRole.java @@ -0,0 +1,5 @@ +package mouda.backend.bet.domain; + +public enum BetRole { + MOIMER, MOIMEE, NON_MOIMEE +} diff --git a/backend/src/main/java/mouda/backend/bet/domain/BettingTime.java b/backend/src/main/java/mouda/backend/bet/domain/BettingTime.java new file mode 100644 index 000000000..ac93468db --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/domain/BettingTime.java @@ -0,0 +1,31 @@ +package mouda.backend.bet.domain; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +import lombok.Getter; + +@Getter +public class BettingTime { + + private static final int NO_SECONDS = 0; + private static final int NO_NANOS = 0; + + private final LocalDateTime bettingTime; + + public BettingTime(LocalDateTime bettingTime) { + this.bettingTime = normalize(bettingTime); + } + + private LocalDateTime normalize(LocalDateTime bettingTime) { + return bettingTime.withSecond(NO_SECONDS).withNano(NO_NANOS); + } + + public long timeDifferenceInMinutesWithNow() { + return Math.abs(ChronoUnit.MINUTES.between(normalize(LocalDateTime.now()), this.bettingTime)); + } + + public boolean isPast() { + return LocalDateTime.now().isAfter(bettingTime); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/domain/Loser.java b/backend/src/main/java/mouda/backend/bet/domain/Loser.java new file mode 100644 index 000000000..0ba77841c --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/domain/Loser.java @@ -0,0 +1,15 @@ +package mouda.backend.bet.domain; + +import lombok.Getter; + +@Getter +public class Loser { + + private final long id; + private final String name; + + public Loser(long id, String name) { + this.id = id; + this.name = name; + } +} diff --git a/backend/src/main/java/mouda/backend/bet/domain/Participant.java b/backend/src/main/java/mouda/backend/bet/domain/Participant.java new file mode 100644 index 000000000..90ccb911f --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/domain/Participant.java @@ -0,0 +1,23 @@ +package mouda.backend.bet.domain; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class Participant { + + private final long id; + private final String name; + private final String profile; + + @Builder + public Participant(long id, String name, String profile) { + this.id = id; + this.name = name; + this.profile = profile; + } + + public Loser toLoser() { + return new Loser(id, name); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/entity/BetDarakbangMemberEntity.java b/backend/src/main/java/mouda/backend/bet/entity/BetDarakbangMemberEntity.java new file mode 100644 index 000000000..ea3b6d854 --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/entity/BetDarakbangMemberEntity.java @@ -0,0 +1,59 @@ +package mouda.backend.bet.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mouda.backend.bet.domain.BetRole; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Entity +@Getter +@NoArgsConstructor +@Table( + name = "bet_darakbang_member", + uniqueConstraints = { + @UniqueConstraint( + columnNames = {"bet_id", "darakbang_member_id"} + ) + } +) +public class BetDarakbangMemberEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private DarakbangMember darakbangMember; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private BetEntity bet; + + private long lastReadChatId; + + public BetDarakbangMemberEntity(DarakbangMember darakbangMember, BetEntity bet) { + this.darakbangMember = darakbangMember; + this.bet = bet; + } + + public void updateLastChat(Long lastReadChatId) { + this.lastReadChatId = lastReadChatId; + } + + public String getRole(long moimerId) { + if (darakbangMember.getId() == moimerId) { + return BetRole.MOIMER.name(); + } + return BetRole.MOIMEE.name(); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/entity/BetEntity.java b/backend/src/main/java/mouda/backend/bet/entity/BetEntity.java new file mode 100644 index 000000000..ad3195d63 --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/entity/BetEntity.java @@ -0,0 +1,81 @@ +package mouda.backend.bet.entity; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mouda.backend.bet.domain.Bet; +import mouda.backend.bet.domain.BetDetails; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "bet") +public class BetEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private LocalDateTime bettingTime; + + private Long loserDarakbangMemberId; + + @Column(nullable = false, updatable = false) + private long darakbangId; + + @Column(nullable = false) + private long moimerId; + + @Builder + private BetEntity(Long id, String title, LocalDateTime bettingTime, Long loserDarakbangMemberId, long darakbangId, + long moimerId) { + this.id = id; + this.title = title; + this.bettingTime = bettingTime; + this.loserDarakbangMemberId = loserDarakbangMemberId; + this.darakbangId = darakbangId; + this.moimerId = moimerId; + } + + public static BetEntity from(Bet bet) { + BetDetails betDetails = bet.getBetDetails(); + + return BetEntity.builder() + .id(bet.getId()) + .title(betDetails.getTitle()) + .bettingTime(betDetails.getBettingTime()) + .loserDarakbangMemberId(bet.getLoserId()) + .moimerId(bet.getMoimerId()) + .build(); + } + + public static BetEntity create(Bet bet, long darakbangId) { + return BetEntity.builder() + .title(bet.getBetDetails().getTitle()) + .bettingTime(bet.getBetDetails().getBettingTime()) + .moimerId(bet.getMoimerId()) + .darakbangId(darakbangId) + .build(); + } + + public BetDetails toBetDetails() { + return BetDetails.builder() + .id(id) + .title(title) + .bettingTime(bettingTime) + .loserId(loserDarakbangMemberId) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/exception/BetErrorMessage.java b/backend/src/main/java/mouda/backend/bet/exception/BetErrorMessage.java new file mode 100644 index 000000000..3a74a79ea --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/exception/BetErrorMessage.java @@ -0,0 +1,17 @@ +package mouda.backend.bet.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum BetErrorMessage { + + BET_DARAKBANG_MEMBER_NOT_FOUND("참여하지 않은 안내면진다입니다."), + BET_NOT_FOUND("안내면진다가 존재하지 않습니다."), + LOSER_NOT_FOUND("당첨자가 존재하지 않습니다."), + CAN_NOT_PARTICIPATE("참여할 수 없는 안내면진다입니다."), + ALREADY_PARTICIPATED_BET("이미 참여한 안내면진다입니다."); + + private final String message; +} diff --git a/backend/src/main/java/mouda/backend/bet/exception/BetException.java b/backend/src/main/java/mouda/backend/bet/exception/BetException.java new file mode 100644 index 000000000..dddf16052 --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/exception/BetException.java @@ -0,0 +1,12 @@ +package mouda.backend.bet.exception; + +import org.springframework.http.HttpStatus; + +import mouda.backend.common.exception.MoudaException; + +public class BetException extends MoudaException { + + public BetException(HttpStatus httpStatus, BetErrorMessage betErrorMessage) { + super(httpStatus, betErrorMessage.getMessage()); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/implement/BetDarakbangMemberWriter.java b/backend/src/main/java/mouda/backend/bet/implement/BetDarakbangMemberWriter.java new file mode 100644 index 000000000..21ca22a77 --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/implement/BetDarakbangMemberWriter.java @@ -0,0 +1,26 @@ +package mouda.backend.bet.implement; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.bet.entity.BetDarakbangMemberEntity; +import mouda.backend.bet.exception.BetErrorMessage; +import mouda.backend.bet.exception.BetException; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Component +@RequiredArgsConstructor +public class BetDarakbangMemberWriter { + + private final BetDarakbangMemberRepository betDarakbangMemberRepository; + + public void updateLastReadChat(long betId, DarakbangMember darakbangMember, long lastReadChatId) { + BetDarakbangMemberEntity betDarakbangMemberEntity = betDarakbangMemberRepository + .findByBetIdAndDarakbangMemberId(betId, darakbangMember.getId()) + .orElseThrow( + () -> new BetException(HttpStatus.NOT_FOUND, BetErrorMessage.BET_DARAKBANG_MEMBER_NOT_FOUND)); + betDarakbangMemberEntity.updateLastChat(lastReadChatId); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/implement/BetFinder.java b/backend/src/main/java/mouda/backend/bet/implement/BetFinder.java new file mode 100644 index 000000000..bffbeabf8 --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/implement/BetFinder.java @@ -0,0 +1,93 @@ +package mouda.backend.bet.implement; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.bet.domain.Bet; +import mouda.backend.bet.domain.BetDetails; +import mouda.backend.bet.domain.Loser; +import mouda.backend.bet.domain.Participant; +import mouda.backend.bet.entity.BetDarakbangMemberEntity; +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.exception.BetErrorMessage; +import mouda.backend.bet.exception.BetException; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.bet.infrastructure.BetRepository; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Component +@Transactional +@RequiredArgsConstructor +public class BetFinder { + + private final BetDarakbangMemberRepository betDarakbangMemberRepository; + private final BetRepository betRepository; + private final ParticipantFinder participantFinder; + + public Bet find(long darakbangId, long betEntityId) { + BetEntity betEntity = betRepository.findByIdAndDarakbangId(betEntityId, darakbangId) + .orElseThrow(() -> new BetException(HttpStatus.NOT_FOUND, BetErrorMessage.BET_NOT_FOUND)); + + return createBet(betEntity); + } + + public List findAllByDarakbangId(long darakbangId) { + List betEntities = betRepository.findAllByDarakbangId(darakbangId); + + return createBets(betEntities); + } + + public List findAllDrawableBet() { + List betEntities = betRepository.findAllByBettingTimeAndLoserDarakbangMemberIdIsNull( + LocalDateTime.now().withSecond(0).withNano(0)); + + return createBets(betEntities); + } + + private List createBets(List betEntities) { + return betEntities.stream() + .map(this::createBet) + .toList(); + } + + private Bet createBet(BetEntity betEntity) { + List participants = participantFinder.findAllByBetEntity(betEntity); + return Bet.builder() + .betDetails(betEntity.toBetDetails()) + .moimerId(betEntity.getMoimerId()) + .loserId(betEntity.getLoserDarakbangMemberId()) + .darakbangId(betEntity.getDarakbangId()) + .participants(participants) + .build(); + } + + @Transactional(readOnly = true) + public Loser findResult(long darakbangId, long betId) { + BetEntity betEntity = betRepository.findByIdAndDarakbangId(betId, darakbangId) + .orElseThrow(() -> new BetException(HttpStatus.NOT_FOUND, BetErrorMessage.BET_NOT_FOUND)); + + Long loserDarakbangMemberId = betEntity.getLoserDarakbangMemberId(); + if (loserDarakbangMemberId == null) { + throw new BetException(HttpStatus.NOT_FOUND, BetErrorMessage.LOSER_NOT_FOUND); + } + + BetDarakbangMemberEntity betDarakbangMemberEntity = betDarakbangMemberRepository + .findByBetIdAndDarakbangMemberId(betId, loserDarakbangMemberId) + .orElseThrow(() -> new BetException(HttpStatus.NOT_FOUND, BetErrorMessage.BET_DARAKBANG_MEMBER_NOT_FOUND)); + + return new Loser(betDarakbangMemberEntity.getDarakbangMember().getId(), + betDarakbangMemberEntity.getDarakbangMember().getNickname()); + } + + public List readAllMyBets(DarakbangMember darakbangMember) { + return betDarakbangMemberRepository.findAllByDarakbangMemberId(darakbangMember.getId()).stream() + .map(BetDarakbangMemberEntity::getBet) + .map(BetEntity::toBetDetails) + .toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/implement/BetSorter.java b/backend/src/main/java/mouda/backend/bet/implement/BetSorter.java new file mode 100644 index 000000000..b8feb1cd5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/implement/BetSorter.java @@ -0,0 +1,24 @@ +package mouda.backend.bet.implement; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.springframework.stereotype.Component; + +import mouda.backend.bet.domain.Bet; + +@Component +public class BetSorter { + + public List sort(List bets) { + List mutableBets = new ArrayList<>(bets); + + mutableBets.sort(Comparator + .comparing(Bet::hasLoser) + .thenComparing(Bet::timeDifferenceInMinutesWithNow) + ); + + return mutableBets; + } +} diff --git a/backend/src/main/java/mouda/backend/bet/implement/BetWriter.java b/backend/src/main/java/mouda/backend/bet/implement/BetWriter.java new file mode 100644 index 000000000..49a8e1a18 --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/implement/BetWriter.java @@ -0,0 +1,61 @@ +package mouda.backend.bet.implement; + +import java.util.List; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.bet.domain.Bet; +import mouda.backend.bet.entity.BetDarakbangMemberEntity; +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.exception.BetErrorMessage; +import mouda.backend.bet.exception.BetException; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.bet.infrastructure.BetRepository; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Component +@RequiredArgsConstructor +public class BetWriter { + + private final BetRepository betRepository; + private final BetDarakbangMemberRepository betDarakbangMemberRepository; + private final BetFinder betFinder; + + public void saveAll(List bets) { + List betEntities = bets.stream() + .map(BetEntity::from) + .toList(); + + betRepository.saveAll(betEntities); + } + + public long save(long darakbangId, Bet bet) { + BetEntity betEntity = betRepository.save(BetEntity.create(bet, darakbangId)); + return betEntity.getId(); + } + + public void participate(long darakbangId, long betId, DarakbangMember darakbangMember) { + Bet bet = betFinder.find(darakbangId, betId); + if (bet.canNotParticipate()) { + throw new BetException(HttpStatus.BAD_REQUEST, BetErrorMessage.CAN_NOT_PARTICIPATE); + } + participate(darakbangMember, bet); + } + + private void participate(DarakbangMember darakbangMember, Bet bet) { + BetEntity betEntity = BetEntity.from(bet); + BetDarakbangMemberEntity betDarakbangMemberEntity = new BetDarakbangMemberEntity(darakbangMember, betEntity); + try { + betDarakbangMemberRepository.save(betDarakbangMemberEntity); + } catch (DataIntegrityViolationException e) { + throw new BetException(HttpStatus.BAD_REQUEST, BetErrorMessage.ALREADY_PARTICIPATED_BET); + } + } + + public void updateLoser(Bet bet) { + betRepository.save(BetEntity.from(bet)); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/implement/ParticipantFinder.java b/backend/src/main/java/mouda/backend/bet/implement/ParticipantFinder.java new file mode 100644 index 000000000..883d0ef9c --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/implement/ParticipantFinder.java @@ -0,0 +1,26 @@ +package mouda.backend.bet.implement; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.bet.domain.Participant; +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Component +@RequiredArgsConstructor +public class ParticipantFinder { + + private final BetDarakbangMemberRepository betDarakbangMemberRepository; + + public List findAllByBetEntity(BetEntity betEntity) { + List darakbangMembers = betDarakbangMemberRepository.findAllDarakbangMemberByBetId( + betEntity.getId()); + return darakbangMembers.stream() + .map(darakbangMember -> new Participant(darakbangMember.getId(), darakbangMember.getNickname(), darakbangMember.getProfile())) + .toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/infrastructure/BetDarakbangMemberRepository.java b/backend/src/main/java/mouda/backend/bet/infrastructure/BetDarakbangMemberRepository.java new file mode 100644 index 000000000..7873be60c --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/infrastructure/BetDarakbangMemberRepository.java @@ -0,0 +1,30 @@ +package mouda.backend.bet.infrastructure; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import mouda.backend.bet.entity.BetDarakbangMemberEntity; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +public interface BetDarakbangMemberRepository extends JpaRepository { + + @Query("SELECT bdm.darakbangMember FROM BetDarakbangMemberEntity bdm WHERE bdm.bet.id = :betId ") + List findAllDarakbangMemberByBetId(@Param("betId") Long betId); + + List findAllByBetId(Long id); + + Optional findByBetIdAndDarakbangMemberId(Long betId, Long loserDarakbangMemberId); + + boolean existsByBetIdAndDarakbangMemberId(long betId, long darakbangMemberId); + + List findAllByDarakbangMemberId(Long id); + + int countByBetId(long betId); + + @Query("SELECT bdm.lastReadChatId FROM BetDarakbangMemberEntity bdm WHERE bdm.bet.id = :betId") + long findLastReadChatIdByBetId(@Param("betId") long betId); +} diff --git a/backend/src/main/java/mouda/backend/bet/infrastructure/BetRepository.java b/backend/src/main/java/mouda/backend/bet/infrastructure/BetRepository.java new file mode 100644 index 000000000..538a1e3b7 --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/infrastructure/BetRepository.java @@ -0,0 +1,18 @@ +package mouda.backend.bet.infrastructure; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import mouda.backend.bet.entity.BetEntity; + +public interface BetRepository extends JpaRepository { + + List findAllByDarakbangId(long darakbangId); + + Optional findByIdAndDarakbangId(long darakbangId, long betEntityId); + + List findAllByBettingTimeAndLoserDarakbangMemberIdIsNull(LocalDateTime localDateTime); +} diff --git a/backend/src/main/java/mouda/backend/bet/presentation/controller/BetController.java b/backend/src/main/java/mouda/backend/bet/presentation/controller/BetController.java new file mode 100644 index 000000000..c788ed0e6 --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/presentation/controller/BetController.java @@ -0,0 +1,99 @@ +package mouda.backend.bet.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import mouda.backend.bet.business.BetService; +import mouda.backend.bet.presentation.controller.swagger.BetSwagger; +import mouda.backend.bet.presentation.request.BetCreateRequest; +import mouda.backend.bet.presentation.response.BetCreateResponse; +import mouda.backend.bet.presentation.response.BetFindAllResponses; +import mouda.backend.bet.presentation.response.BetFindResponse; +import mouda.backend.bet.presentation.response.BetResultResponse; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@RestController +@RequestMapping("/v1/darakbang/{darakbangId}/bet") +@RequiredArgsConstructor +public class BetController implements BetSwagger { + + private final BetService betService; + + @Override + @GetMapping + public ResponseEntity> findAll( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember darakbangMember) { + BetFindAllResponses responses = betService.findAllBets(darakbangId); + + return ResponseEntity.ok(new RestResponse<>(responses)); + } + + @Override + @GetMapping("/{betId}") + public ResponseEntity> findBetDetails( + @PathVariable Long darakbangId, + @PathVariable Long betId, + @LoginDarakbangMember DarakbangMember darakbangMember + ) { + BetFindResponse response = betService.findBet(darakbangId, betId, darakbangMember); + + return ResponseEntity.ok(new RestResponse<>(response)); + } + + @Override + @PostMapping + public ResponseEntity> createBet( + @PathVariable Long darakbangId, + @RequestBody BetCreateRequest request, + @LoginDarakbangMember DarakbangMember darakbangMember + ) { + long betId = betService.createBet(darakbangId, request, darakbangMember); + + return ResponseEntity.ok(new RestResponse<>(new BetCreateResponse(betId))); + } + + @Override + @PostMapping("/{betId}") + public ResponseEntity participateBet( + @PathVariable Long darakbangId, + @PathVariable Long betId, + @LoginDarakbangMember DarakbangMember darakbangMember + ) { + betService.participateBet(darakbangId, betId, darakbangMember); + + return ResponseEntity.ok().build(); + } + + @Override + @GetMapping("/{betId}/result") + public ResponseEntity> findBetResult( + @PathVariable Long darakbangId, + @PathVariable Long betId, + @LoginDarakbangMember DarakbangMember darakbangMember + ) { + BetResultResponse response = betService.findBetResult(darakbangId, betId); + + return ResponseEntity.ok(new RestResponse<>(response)); + } + + @Override + @PostMapping("/{betId}/result") + public ResponseEntity drawBet( + @PathVariable Long darakbangId, + @PathVariable Long betId, + @LoginDarakbangMember DarakbangMember darakbangMember + ) { + betService.drawBet(darakbangId, betId); + + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/presentation/controller/swagger/BetSwagger.java b/backend/src/main/java/mouda/backend/bet/presentation/controller/swagger/BetSwagger.java new file mode 100644 index 000000000..337f1c0e0 --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/presentation/controller/swagger/BetSwagger.java @@ -0,0 +1,84 @@ +package mouda.backend.bet.presentation.controller.swagger; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import mouda.backend.bet.presentation.request.BetCreateRequest; +import mouda.backend.bet.presentation.response.BetCreateResponse; +import mouda.backend.bet.presentation.response.BetFindAllResponses; +import mouda.backend.bet.presentation.response.BetFindResponse; +import mouda.backend.bet.presentation.response.BetResultResponse; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +public interface BetSwagger { + + @Operation(summary = "베팅 목록 조회", description = "다락방의 모든 베팅을 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "베팅 목록 조회 성공") + }) + ResponseEntity> findAll( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember darakbangMember + ); + + @Operation(summary = "베팅 세부사항 조회", description = "특정 베팅의 세부사항을 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "베팅 세부사항 조회 성공"), + @ApiResponse(responseCode = "404", description = "해당 베팅을 찾을 수 없음") + }) + ResponseEntity> findBetDetails( + @PathVariable Long darakbangId, + @PathVariable Long betId, + @LoginDarakbangMember DarakbangMember darakbangMember + ); + + @Operation(summary = "베팅 생성", description = "새로운 베팅을 생성한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "베팅 생성 성공"), + @ApiResponse(responseCode = "400", description = "유효하지 않은 요청") + }) + ResponseEntity> createBet( + @PathVariable Long darakbangId, + @RequestBody BetCreateRequest request, + @LoginDarakbangMember DarakbangMember darakbangMember + ); + + @Operation(summary = "베팅 참여", description = "기존 베팅에 참여한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "베팅 참여 성공"), + @ApiResponse(responseCode = "404", description = "해당 베팅을 찾을 수 없음") + }) + ResponseEntity participateBet( + @PathVariable Long darakbangId, + @PathVariable Long betId, + @LoginDarakbangMember DarakbangMember darakbangMember + ); + + @Operation(summary = "베팅 결과 조회", description = "특정 베팅의 결과를 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "베팅 결과 조회 성공"), + @ApiResponse(responseCode = "404", description = "해당 베팅 결과를 찾을 수 없음") + }) + ResponseEntity> findBetResult( + @PathVariable Long darakbangId, + @PathVariable Long betId, + @LoginDarakbangMember DarakbangMember darakbangMember + ); + + @Operation(summary = "베팅 결과 도출", description = "특정 베팅의 결과를 도출한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "베팅 결과 도출 성공"), + @ApiResponse(responseCode = "404", description = "해당 베팅을 찾을 수 없음") + }) + ResponseEntity drawBet( + @PathVariable Long darakbangId, + @PathVariable Long betId, + @LoginDarakbangMember DarakbangMember darakbangMember + ); +} diff --git a/backend/src/main/java/mouda/backend/bet/presentation/request/BetCreateRequest.java b/backend/src/main/java/mouda/backend/bet/presentation/request/BetCreateRequest.java new file mode 100644 index 000000000..9eea4f06d --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/presentation/request/BetCreateRequest.java @@ -0,0 +1,20 @@ +package mouda.backend.bet.presentation.request; + +import mouda.backend.bet.domain.Bet; +import mouda.backend.bet.domain.BetDetails; + +public record BetCreateRequest( + String title, + int waitingMinutes +) { + public Bet toBet(long moimerId) { + BetDetails betDetails = BetDetails.create( + title, + waitingMinutes + ); + return Bet.builder() + .betDetails(betDetails) + .moimerId(moimerId) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/presentation/response/BetCreateResponse.java b/backend/src/main/java/mouda/backend/bet/presentation/response/BetCreateResponse.java new file mode 100644 index 000000000..5c5f8f064 --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/presentation/response/BetCreateResponse.java @@ -0,0 +1,6 @@ +package mouda.backend.bet.presentation.response; + +public record BetCreateResponse( + long betId +) { +} diff --git a/backend/src/main/java/mouda/backend/bet/presentation/response/BetFindAllResponse.java b/backend/src/main/java/mouda/backend/bet/presentation/response/BetFindAllResponse.java new file mode 100644 index 000000000..771f9e5ac --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/presentation/response/BetFindAllResponse.java @@ -0,0 +1,23 @@ +package mouda.backend.bet.presentation.response; + +import java.time.LocalDateTime; + +import mouda.backend.bet.domain.Bet; + +public record BetFindAllResponse( + long id, + String title, + int currentParticipants, + LocalDateTime deadline, + boolean isAnnounced +) { + public static BetFindAllResponse from(Bet bet) { + return new BetFindAllResponse( + bet.getBetDetails().getId(), + bet.getBetDetails().getTitle(), + bet.getParticipants().size(), + bet.getBetDetails().getBettingTime(), + bet.hasLoser() + ); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/presentation/response/BetFindAllResponses.java b/backend/src/main/java/mouda/backend/bet/presentation/response/BetFindAllResponses.java new file mode 100644 index 000000000..f7980cecd --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/presentation/response/BetFindAllResponses.java @@ -0,0 +1,16 @@ +package mouda.backend.bet.presentation.response; + +import java.util.List; + +import mouda.backend.bet.domain.Bet; + +public record BetFindAllResponses( + List bets +) { + public static BetFindAllResponses toResponse(List bets) { + List responses = bets.stream() + .map(BetFindAllResponse::from) + .toList(); + return new BetFindAllResponses(responses); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/presentation/response/BetFindResponse.java b/backend/src/main/java/mouda/backend/bet/presentation/response/BetFindResponse.java new file mode 100644 index 000000000..4f1075bb0 --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/presentation/response/BetFindResponse.java @@ -0,0 +1,34 @@ +package mouda.backend.bet.presentation.response; + +import java.time.LocalDateTime; +import java.util.List; + +import mouda.backend.bet.domain.Bet; +import mouda.backend.bet.domain.BetRole; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +public record BetFindResponse( + String title, + int currentParticipants, + LocalDateTime deadline, + boolean isAnnounced, + List participants, + BetRole myRole, + Long chatroomId +) { + public static BetFindResponse toResponse(Bet bet, DarakbangMember darakbangMember, Long chatroomId) { + List participants = bet.getParticipants().stream() + .map(ParticipantResponse::from) + .toList(); + + return new BetFindResponse( + bet.getBetDetails().getTitle(), + bet.getParticipants().size(), + bet.getBetDetails().getBettingTime(), + bet.hasLoser(), + participants, + bet.getMyRole(darakbangMember.getId()), + chatroomId + ); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/presentation/response/BetResultResponse.java b/backend/src/main/java/mouda/backend/bet/presentation/response/BetResultResponse.java new file mode 100644 index 000000000..56e228bca --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/presentation/response/BetResultResponse.java @@ -0,0 +1,11 @@ +package mouda.backend.bet.presentation.response; + +import mouda.backend.bet.domain.Loser; + +public record BetResultResponse( + String nickname +) { + public static BetResultResponse from(Loser loser) { + return new BetResultResponse(loser.getName()); + } +} diff --git a/backend/src/main/java/mouda/backend/bet/presentation/response/ParticipantResponse.java b/backend/src/main/java/mouda/backend/bet/presentation/response/ParticipantResponse.java new file mode 100644 index 000000000..5e4926d0e --- /dev/null +++ b/backend/src/main/java/mouda/backend/bet/presentation/response/ParticipantResponse.java @@ -0,0 +1,17 @@ +package mouda.backend.bet.presentation.response; + +import mouda.backend.bet.domain.Participant; + +public record ParticipantResponse( + String nickname, + long id, + String profileUrl +) { + public static ParticipantResponse from(Participant participant) { + return new ParticipantResponse( + participant.getName(), + participant.getId(), + participant.getProfile() + ); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/business/ChatRoomService.java b/backend/src/main/java/mouda/backend/chat/business/ChatRoomService.java new file mode 100644 index 000000000..9ccaf5244 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/business/ChatRoomService.java @@ -0,0 +1,54 @@ +package mouda.backend.chat.business; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.chat.domain.ChatPreview; +import mouda.backend.chat.domain.ChatRoomDetails; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.implement.ChatPreviewManager; +import mouda.backend.chat.implement.ChatPreviewManagerRegistry; +import mouda.backend.chat.implement.ChatRoomDetailsFinder; +import mouda.backend.chat.implement.ChatRoomWriter; +import mouda.backend.chat.presentation.response.ChatPreviewResponses; +import mouda.backend.chat.presentation.response.ChatRoomDetailsResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.finder.MoimFinder; +import mouda.backend.moim.implement.writer.MoimWriter; + +@Service +@Transactional +@RequiredArgsConstructor +public class ChatRoomService { + + private final ChatRoomDetailsFinder chatRoomDetailsFinder; + private final ChatPreviewManagerRegistry chatPreviewManagerRegistry; + private final ChatRoomWriter chatRoomWriter; + private final MoimFinder moimFinder; + private final MoimWriter moimWriter; + + @Transactional(readOnly = true) + public ChatRoomDetailsResponse findChatRoomDetails(long darakbangId, long chatRoomId, DarakbangMember darakbangMember) { + ChatRoomDetails chatRoomDetails = chatRoomDetailsFinder.find(darakbangId, chatRoomId, darakbangMember); + + return ChatRoomDetailsResponse.from(chatRoomDetails); + } + + @Transactional(readOnly = true) + public ChatPreviewResponses findChatPreview(DarakbangMember darakbangMember, ChatRoomType chatRoomType) { + ChatPreviewManager manager = chatPreviewManagerRegistry.getManager(chatRoomType); + List chatPreviews = manager.create(darakbangMember); + + return ChatPreviewResponses.toResponse(chatPreviews); + } + + public long openChatRoom(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { + Moim moim = moimFinder.read(moimId, darakbangId); + moimWriter.openChatByMoimer(moim, darakbangMember); + return chatRoomWriter.append(moimId, darakbangId, ChatRoomType.MOIM); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/business/ChatService.java b/backend/src/main/java/mouda/backend/chat/business/ChatService.java new file mode 100644 index 000000000..29ce9ada2 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/business/ChatService.java @@ -0,0 +1,98 @@ +package mouda.backend.chat.business; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.chat.domain.Chat; +import mouda.backend.chat.domain.ChatOwnership; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.Chats; +import mouda.backend.chat.implement.ChatRoomFinder; +import mouda.backend.chat.implement.ChatWriter; +import mouda.backend.chat.implement.notification.ChatNotificationSender; +import mouda.backend.chat.presentation.request.ChatCreateRequest; +import mouda.backend.chat.presentation.request.DateTimeConfirmRequest; +import mouda.backend.chat.presentation.request.LastReadChatRequest; +import mouda.backend.chat.presentation.request.PlaceConfirmRequest; +import mouda.backend.chat.presentation.response.ChatFindUnloadedResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.finder.MoimFinder; +import mouda.backend.moim.implement.writer.MoimWriter; + +@Service +@Transactional +@RequiredArgsConstructor +public class ChatService { + + private final ChatRoomFinder chatRoomFinder; + private final ChatWriter chatWriter; + private final MoimWriter moimWriter; + private final MoimFinder moimFinder; + private final ChatNotificationSender chatNotificationSender; + + @Transactional(readOnly = true) + public ChatFindUnloadedResponse findUnloadedChats( + long darakbangId, long recentChatId, long chatRoomId, DarakbangMember darakbangMember + ) { + ChatRoom chatRoom = chatRoomFinder.read(darakbangId, chatRoomId, darakbangMember); + + Chats chats = chatRoomFinder.findAllUnloadedChats(chatRoom.getId(), recentChatId); + List chatsWithAuthor = chats.getChatsWithAuthor(darakbangMember); + + return ChatFindUnloadedResponse.toResponse(chatsWithAuthor); + } + + public void createChat( + long darakbangId, + long chatRoomId, + ChatCreateRequest request, + DarakbangMember darakbangMember + ) { + ChatRoom chatRoom = chatRoomFinder.read(darakbangId, chatRoomId, darakbangMember); + + Chat appendedChat = chatWriter.append(chatRoom.getId(), request.content(), darakbangMember); + + chatNotificationSender.sendChatNotification(darakbangId, chatRoom, appendedChat); + } + + public void confirmPlace(long darakbangId, long chatRoomId, PlaceConfirmRequest request, + DarakbangMember darakbangMember) { + ChatRoom chatRoom = chatRoomFinder.readMoimChatRoom(darakbangId, chatRoomId); + String place = request.place(); + + Moim moim = moimFinder.read(chatRoom.getTargetId(), darakbangId); + moimWriter.confirmPlace(moim, darakbangMember, place); + + Chat appendedChat = chatWriter.appendPlaceTypeChat(chatRoom.getId(), place, darakbangMember); + + chatNotificationSender.sendChatNotification(darakbangId, chatRoom, appendedChat); + } + + public void confirmDateTime(long darakbangId, long chatRoomId, DateTimeConfirmRequest request, + DarakbangMember darakbangMember) { + ChatRoom chatRoom = chatRoomFinder.readMoimChatRoom(darakbangId, chatRoomId); + + Moim moim = moimFinder.read(chatRoom.getTargetId(), darakbangId); + LocalDate date = request.date(); + LocalTime time = request.time(); + moimWriter.confirmDateTime(moim, darakbangMember, date, time); + + Chat appendedChat = chatWriter.appendDateTimeTypeChat(chatRoom.getId(), date, time, darakbangMember); + + chatNotificationSender.sendChatNotification(darakbangId, chatRoom, appendedChat); + } + + public void updateLastReadChat( + long darakbangId, long chatRoomId, LastReadChatRequest request, DarakbangMember darakbangMember + ) { + ChatRoom chatRoom = chatRoomFinder.read(darakbangId, chatRoomId, darakbangMember); + + chatWriter.updateLastReadChat(chatRoom, darakbangMember, request.lastReadChatId()); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/domain/Attributes.java b/backend/src/main/java/mouda/backend/chat/domain/Attributes.java new file mode 100644 index 000000000..1baeb1e34 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/domain/Attributes.java @@ -0,0 +1,7 @@ +package mouda.backend.chat.domain; + +import java.util.Map; + +public interface Attributes { + Map getAttributes(); +} diff --git a/backend/src/main/java/mouda/backend/chat/domain/Author.java b/backend/src/main/java/mouda/backend/chat/domain/Author.java new file mode 100644 index 000000000..6d858fb2d --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/domain/Author.java @@ -0,0 +1,25 @@ +package mouda.backend.chat.domain; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class Author { + + private final long darakbangMemberId; + private final long memberId; + private final String nickname; + private final String profile; + + @Builder + public Author(long darakbangMemberId, long memberId, String nickname, String profile) { + this.darakbangMemberId = darakbangMemberId; + this.memberId = memberId; + this.nickname = nickname; + this.profile = profile; + } + + public boolean isNotSameMember(long darakbangMemberId, long memberId) { + return this.darakbangMemberId != darakbangMemberId || this.memberId != memberId; + } +} diff --git a/backend/src/main/java/mouda/backend/chat/domain/BetAttributes.java b/backend/src/main/java/mouda/backend/chat/domain/BetAttributes.java new file mode 100644 index 000000000..b939f2402 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/domain/BetAttributes.java @@ -0,0 +1,32 @@ +package mouda.backend.chat.domain; + +import java.util.HashMap; +import java.util.Map; + +import lombok.Getter; + +@Getter +public class BetAttributes implements Attributes { + + private final String title; + private final boolean isLoser; + private final long betId; + private final Participant loser; + + public BetAttributes(String title, Boolean isLoser, Long betId, Participant loser) { + this.title = title; + this.isLoser = isLoser; + this.betId = betId; + this.loser = loser; + } + + @Override + public Map getAttributes() { + Map attributes = new HashMap<>(); + attributes.put("title", title); + attributes.put("isLoser", isLoser); + attributes.put("betId", betId); + attributes.put("loser", loser); + return attributes; + } +} diff --git a/backend/src/main/java/mouda/backend/chat/domain/Chat.java b/backend/src/main/java/mouda/backend/chat/domain/Chat.java new file mode 100644 index 000000000..f849dd36a --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/domain/Chat.java @@ -0,0 +1,33 @@ +package mouda.backend.chat.domain; + +import java.time.LocalDate; +import java.time.LocalTime; + +import lombok.Builder; +import lombok.Getter; +import mouda.backend.chat.entity.ChatType; + +@Getter +public class Chat { + + private final long id; + private final String content; + private final Author author; + private final LocalDate date; + private final LocalTime time; + private final ChatType chatType; + + @Builder + public Chat(long id, String content, Author author, LocalDate date, LocalTime time, ChatType chatType) { + this.id = id; + this.content = content; + this.author = author; + this.date = date; + this.time = time; + this.chatType = chatType; + } + + public boolean isMine(long darakbangMemberId) { + return author.getDarakbangMemberId() == darakbangMemberId; + } +} diff --git a/backend/src/main/java/mouda/backend/chat/domain/ChatNotificationEvent.java b/backend/src/main/java/mouda/backend/chat/domain/ChatNotificationEvent.java new file mode 100644 index 000000000..9b4854e75 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/domain/ChatNotificationEvent.java @@ -0,0 +1,15 @@ +package mouda.backend.chat.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +@Builder +public class ChatNotificationEvent { + + private final Long darakbangId; + private final ChatRoom chatRoom; + private final Chat appendedChat; +} diff --git a/backend/src/main/java/mouda/backend/chat/domain/ChatOwnership.java b/backend/src/main/java/mouda/backend/chat/domain/ChatOwnership.java new file mode 100644 index 000000000..8e9273d20 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/domain/ChatOwnership.java @@ -0,0 +1,15 @@ +package mouda.backend.chat.domain; + +import lombok.Getter; + +@Getter +public class ChatOwnership { + + private final Chat chat; + private final boolean isMine; + + public ChatOwnership(Chat chat, boolean isMine) { + this.chat = chat; + this.isMine = isMine; + } +} diff --git a/backend/src/main/java/mouda/backend/chat/domain/ChatPreview.java b/backend/src/main/java/mouda/backend/chat/domain/ChatPreview.java new file mode 100644 index 000000000..1e92226cb --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/domain/ChatPreview.java @@ -0,0 +1,42 @@ +package mouda.backend.chat.domain; + +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.List; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class ChatPreview implements Comparable { + + private final ChatRoom chatRoom; + private final Target target; + private final long lastReadChatId; + private final List participants; + + @Builder + public ChatPreview(ChatRoom chatRoom, Target target, long lastReadChatId, List participants) { + this.chatRoom = chatRoom; + this.target = target; + this.lastReadChatId = lastReadChatId; + this.participants = participants; + } + + public String getLastContent() { + return chatRoom.getLastChat().getContent(); + } + + public LocalDateTime getLastChatDateTime() { + return chatRoom.getLastChatDateTime(); + } + + @Override + public int compareTo(ChatPreview that) { + Comparator chatRoomComparator = Comparator.comparing( + ChatPreview::getLastChatDateTime, + Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(chatPreview -> chatPreview.chatRoom.getTargetId()); + return chatRoomComparator.compare(this, that); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/domain/ChatRoom.java b/backend/src/main/java/mouda/backend/chat/domain/ChatRoom.java new file mode 100644 index 000000000..9e176cc5b --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/domain/ChatRoom.java @@ -0,0 +1,37 @@ +package mouda.backend.chat.domain; + +import java.time.LocalDateTime; + +import lombok.Getter; + +@Getter +public class ChatRoom { + + private final long id; + private final long targetId; + private final ChatRoomType type; + private final LastChat lastChat; + + public ChatRoom(Long id, long targetId, ChatRoomType type, LastChat lastChat) { + this.id = id; + this.targetId = targetId; + this.type = type; + this.lastChat = lastChat; + } + + public ChatRoom(Long id, long targetId, ChatRoomType type) { + this(id, targetId, type, LastChat.empty()); + } + + public boolean isMoim() { + return type == ChatRoomType.MOIM; + } + + public boolean isBet() { + return type == ChatRoomType.BET; + } + + public LocalDateTime getLastChatDateTime() { + return lastChat.getDateTime(); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/domain/ChatRoomDetails.java b/backend/src/main/java/mouda/backend/chat/domain/ChatRoomDetails.java new file mode 100644 index 000000000..934301471 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/domain/ChatRoomDetails.java @@ -0,0 +1,29 @@ +package mouda.backend.chat.domain; + +import java.util.List; +import java.util.Map; + +import lombok.Getter; + +@Getter +public class ChatRoomDetails { + private final long id; + private final ChatRoomType chatRoomType; + private final Attributes attributes; + private final List participants; + + public ChatRoomDetails(long id, ChatRoomType chatRoomType, Attributes attributes, List participants) { + this.id = id; + this.chatRoomType = chatRoomType; + this.attributes = attributes; + this.participants = participants; + } + + public String getTitle() { + return (String)getAttributes().get("title"); + } + + public Map getAttributes() { + return attributes.getAttributes(); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/domain/ChatRoomType.java b/backend/src/main/java/mouda/backend/chat/domain/ChatRoomType.java new file mode 100644 index 000000000..54996ecb9 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/domain/ChatRoomType.java @@ -0,0 +1,9 @@ +package mouda.backend.chat.domain; + +public enum ChatRoomType { + MOIM, BET; + + public boolean isNotMoim() { + return this != MOIM; + } +} diff --git a/backend/src/main/java/mouda/backend/chat/domain/Chats.java b/backend/src/main/java/mouda/backend/chat/domain/Chats.java new file mode 100644 index 000000000..28fcf7c67 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/domain/Chats.java @@ -0,0 +1,22 @@ +package mouda.backend.chat.domain; + +import java.util.List; + +import lombok.Getter; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Getter +public class Chats { + + private final List chats; + + public Chats(List chats) { + this.chats = chats; + } + + public List getChatsWithAuthor(DarakbangMember darakbangMember) { + return chats.stream() + .map(chat -> new ChatOwnership(chat, chat.isMine(darakbangMember.getId()))) + .toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/domain/LastChat.java b/backend/src/main/java/mouda/backend/chat/domain/LastChat.java new file mode 100644 index 000000000..ef6a7120c --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/domain/LastChat.java @@ -0,0 +1,21 @@ +package mouda.backend.chat.domain; + +import java.time.LocalDateTime; + +import lombok.Getter; + +@Getter +public class LastChat { + + private final LocalDateTime dateTime; + private final String content; + + public LastChat(LocalDateTime dateTime, String content) { + this.dateTime = dateTime; + this.content = content; + } + + public static LastChat empty() { + return new LastChat(null, ""); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/domain/MoimAttributes.java b/backend/src/main/java/mouda/backend/chat/domain/MoimAttributes.java new file mode 100644 index 000000000..25f32ba4e --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/domain/MoimAttributes.java @@ -0,0 +1,46 @@ +package mouda.backend.chat.domain; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.HashMap; +import java.util.Map; + +import lombok.Getter; + +@Getter +public class MoimAttributes implements Attributes { + + private final String title; + private final String place; + private final Boolean isMoimer; + private final Boolean isStarted; + private final String description; + private final LocalDate date; + private final LocalTime time; + private final Long moimId; + + public MoimAttributes(String title, String place, Boolean isMoimer, Boolean isStarted, String description, LocalDate date, LocalTime time, Long moimId) { + this.title = title; + this.place = place; + this.isMoimer = isMoimer; + this.isStarted = isStarted; + this.description = description; + this.date = date; + this.time = time; + this.moimId = moimId; + } + + @Override + public Map getAttributes() { + Map attributes = new HashMap<>(); + attributes.put("title", title); + attributes.put("place", place); + attributes.put("isMoimer", isMoimer); + attributes.put("isStarted", isStarted); + attributes.put("description", description); + attributes.put("date", date); + attributes.put("time", time); + attributes.put("moimId", moimId); + return attributes; + } +} diff --git a/backend/src/main/java/mouda/backend/chat/domain/Participant.java b/backend/src/main/java/mouda/backend/chat/domain/Participant.java new file mode 100644 index 000000000..be2edab2a --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/domain/Participant.java @@ -0,0 +1,37 @@ +package mouda.backend.chat.domain; + +import java.util.Objects; + +import lombok.Getter; + +@Getter +public class Participant { + + private final long darakbangMemberId; + private final String nickname; + private final String profile; + private final String role; + + public Participant(long darakbangMemberId, String nickname, String profile, String role) { + this.darakbangMemberId = darakbangMemberId; + this.nickname = nickname; + this.profile = profile; + this.role = role; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Participant that = (Participant)o; + return darakbangMemberId == that.darakbangMemberId && Objects.equals(nickname, that.nickname) + && Objects.equals(profile, that.profile) && Objects.equals(role, that.role); + } + + @Override + public int hashCode() { + return Objects.hash(darakbangMemberId, nickname, profile, role); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/domain/Target.java b/backend/src/main/java/mouda/backend/chat/domain/Target.java new file mode 100644 index 000000000..3ddecc53c --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/domain/Target.java @@ -0,0 +1,25 @@ +package mouda.backend.chat.domain; + +import lombok.Getter; +import mouda.backend.bet.domain.BetDetails; +import mouda.backend.moim.domain.Moim; + +@Getter +public class Target { + + private final long targetId; + private final String title; + private final boolean isStarted; + + public Target(Moim moim) { + this.targetId = moim.getId(); + this.title = moim.getTitle(); + this.isStarted = moim.isPastMoim(); + } + + public Target(BetDetails bet) { + this.targetId = bet.getId(); + this.title = bet.getTitle(); + this.isStarted = true; + } +} diff --git a/backend/src/main/java/mouda/backend/chat/entity/ChatEntity.java b/backend/src/main/java/mouda/backend/chat/entity/ChatEntity.java new file mode 100644 index 000000000..eb4cdc868 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/entity/ChatEntity.java @@ -0,0 +1,72 @@ +package mouda.backend.chat.entity; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mouda.backend.chat.domain.Chat; +import mouda.backend.chat.domain.LastChat; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Entity +@Getter +@Table(name = "chat") +@NoArgsConstructor +public class ChatEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String content; + + private long chatRoomId; + + @ManyToOne(fetch = FetchType.LAZY) + private DarakbangMember darakbangMember; + + private LocalDate date; + + private LocalTime time; + + @Enumerated(EnumType.STRING) + private ChatType chatType; + + @Builder + public ChatEntity(String content, long chatRoomId, DarakbangMember darakbangMember, LocalDate date, LocalTime time, + ChatType chatType) { + this.content = content; + this.chatRoomId = chatRoomId; + this.darakbangMember = darakbangMember; + this.date = date; + this.time = time; + this.chatType = chatType; + } + + public Chat toChat() { + return Chat.builder() + .id(id) + .author(darakbangMember.toAuthor()) + .content(content) + .chatType(chatType) + .date(date) + .time(time) + .build(); + } + + public LastChat toLastChat() { + return new LastChat(LocalDateTime.of(date, time), content); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/entity/ChatRoomEntity.java b/backend/src/main/java/mouda/backend/chat/entity/ChatRoomEntity.java new file mode 100644 index 000000000..42e793b82 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/entity/ChatRoomEntity.java @@ -0,0 +1,47 @@ +package mouda.backend.chat.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mouda.backend.chat.domain.ChatRoomType; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table( + name = "chat_room", + uniqueConstraints = { + @UniqueConstraint( + columnNames = {"target_id", "type"} + ) + } +) +public class ChatRoomEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private long targetId; + + private long darakbangId; + + @Enumerated(EnumType.STRING) + private ChatRoomType type; + + @Builder + public ChatRoomEntity(long targetId, long darakbangId, ChatRoomType type) { + this.targetId = targetId; + this.darakbangId = darakbangId; + this.type = type; + } +} diff --git a/backend/src/main/java/mouda/backend/chat/entity/ChatType.java b/backend/src/main/java/mouda/backend/chat/entity/ChatType.java new file mode 100644 index 000000000..1d7691ec9 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/entity/ChatType.java @@ -0,0 +1,6 @@ +package mouda.backend.chat.entity; + +public enum ChatType { + + BASIC, PLACE, DATETIME +} diff --git a/backend/src/main/java/mouda/backend/chat/exception/ChatErrorMessage.java b/backend/src/main/java/mouda/backend/chat/exception/ChatErrorMessage.java new file mode 100644 index 000000000..8f91eb32b --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/exception/ChatErrorMessage.java @@ -0,0 +1,20 @@ +package mouda.backend.chat.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ChatErrorMessage { + + CHATROOM_NOT_FOUND("존재하지 않는 채팅방입니다."), + BET_DARAKBANG_MEMBER_NOT_FOUND("참여하지 않은 안내면진다입니다."), + INVALID_CHATROOM_TYPE("잘못된 채팅 방 타입입니다."), + UNAUTHORIZED("권한이 없습니다."), + INVALID_DATE_TIME_FORMAT("날짜와 시간 형식이 올바르지 않습니다."), + CHATROOM_ALREADY_EXISTS("이미 존재하는 채팅방입니다."), // 새로 추가된 메시지 + + ; + + private final String message; +} diff --git a/backend/src/main/java/mouda/backend/chat/exception/ChatException.java b/backend/src/main/java/mouda/backend/chat/exception/ChatException.java new file mode 100644 index 000000000..00ef1dc51 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/exception/ChatException.java @@ -0,0 +1,12 @@ +package mouda.backend.chat.exception; + +import org.springframework.http.HttpStatus; + +import mouda.backend.common.exception.MoudaException; + +public class ChatException extends MoudaException { + + public ChatException(HttpStatus httpStatus, ChatErrorMessage chatErrorMessage) { + super(httpStatus, chatErrorMessage.getMessage()); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/AttributeManager.java b/backend/src/main/java/mouda/backend/chat/implement/AttributeManager.java new file mode 100644 index 000000000..ddafc98e6 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/AttributeManager.java @@ -0,0 +1,13 @@ +package mouda.backend.chat.implement; + +import mouda.backend.chat.domain.Attributes; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +public interface AttributeManager { + + boolean support(ChatRoomType chatRoomType); + + Attributes create(ChatRoom chatRoom, DarakbangMember darakbangMember); +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/AttributeManagerRegistry.java b/backend/src/main/java/mouda/backend/chat/implement/AttributeManagerRegistry.java new file mode 100644 index 000000000..4c9b4a914 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/AttributeManagerRegistry.java @@ -0,0 +1,23 @@ +package mouda.backend.chat.implement; + +import java.util.List; +import java.util.NoSuchElementException; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.chat.domain.ChatRoomType; + +@Component +@RequiredArgsConstructor +public class AttributeManagerRegistry { + + private final List attributeManagers; + + public AttributeManager getManager(ChatRoomType chatRoomType) { + return attributeManagers.stream() + .filter(attributeManager -> attributeManager.support(chatRoomType)) + .findFirst() + .orElseThrow(() -> new NoSuchElementException("적절한 AttributeManager 가 없습니다. (ChatRoomType : " + chatRoomType + ")")); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/BetAttributeManager.java b/backend/src/main/java/mouda/backend/chat/implement/BetAttributeManager.java new file mode 100644 index 000000000..32631339e --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/BetAttributeManager.java @@ -0,0 +1,54 @@ +package mouda.backend.chat.implement; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.bet.domain.Bet; +import mouda.backend.bet.domain.BetRole; +import mouda.backend.bet.implement.BetFinder; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.chat.domain.Attributes; +import mouda.backend.chat.domain.BetAttributes; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.domain.Participant; +import mouda.backend.chat.exception.ChatErrorMessage; +import mouda.backend.chat.exception.ChatException; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Component +@RequiredArgsConstructor +public class BetAttributeManager implements AttributeManager { + + private final BetFinder betFinder; + private final BetDarakbangMemberRepository betDarakbangMemberRepository; + + @Override + public boolean support(ChatRoomType chatRoomType) { + return chatRoomType == ChatRoomType.BET; + } + + @Override + public Attributes create(ChatRoom chatRoom, DarakbangMember darakbangMember) { + Bet bet = betFinder.find(darakbangMember.getDarakbang().getId(), chatRoom.getTargetId()); + boolean isLoser = bet.isLoser(darakbangMember.getId()); + Participant loser = getLoser(bet, darakbangMember.getId()); + return new BetAttributes(bet.getBetDetails().getTitle(), isLoser, bet.getId(), loser); + } + + private Participant getLoser(Bet bet, long requestDarakbangMemberId) { + DarakbangMember darakbangMember = betDarakbangMemberRepository + .findByBetIdAndDarakbangMemberId(bet.getId(), bet.getLoserId()) + .orElseThrow(() -> new ChatException(HttpStatus.NOT_FOUND, ChatErrorMessage.BET_DARAKBANG_MEMBER_NOT_FOUND)) + .getDarakbangMember(); + BetRole betRole = getBetRole(requestDarakbangMemberId, bet.getMoimerId()); + return new Participant( + darakbangMember.getId(), darakbangMember.getNickname(), darakbangMember.getProfile(), betRole.toString() + ); + } + + private BetRole getBetRole(long requestDarakbangMemberId, long moimerId) { + return moimerId == requestDarakbangMemberId ? BetRole.MOIMER : BetRole.MOIMEE; + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/BetChatPreviewManager.java b/backend/src/main/java/mouda/backend/chat/implement/BetChatPreviewManager.java new file mode 100644 index 000000000..83895fc6c --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/BetChatPreviewManager.java @@ -0,0 +1,58 @@ +package mouda.backend.chat.implement; + +import java.util.List; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.bet.domain.BetDetails; +import mouda.backend.bet.implement.BetFinder; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.chat.domain.ChatPreview; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.domain.Participant; +import mouda.backend.chat.domain.Target; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Component +@RequiredArgsConstructor +public class BetChatPreviewManager implements ChatPreviewManager { + + private final BetDarakbangMemberRepository betDarakbangMemberRepository; + private final BetFinder betFinder; + private final ChatRoomFinder chatRoomFinder; + + @Override + @Transactional(readOnly = true) + public List create(DarakbangMember darakbangMember) { + List myBets = betFinder.readAllMyBets(darakbangMember); + + return myBets.stream() + .filter(BetDetails::hasLoser) + .map(this::getChatPreview) + .sorted() + .toList(); + } + + private ChatPreview getChatPreview(BetDetails bet) { + long targetId = bet.getId(); + ChatRoom chatRoom = chatRoomFinder.readChatRoomByTargetId(bet.getId(), ChatRoomType.BET); + long lastReadChatId = betDarakbangMemberRepository.findLastReadChatIdByBetId(targetId); + List participants = betDarakbangMemberRepository.findAllByBetId(targetId).stream() + .map(betDarakbangMember -> new Participant( + betDarakbangMember.getId(), + betDarakbangMember.getDarakbangMember().getNickname(), + betDarakbangMember.getDarakbangMember().getProfile(), + betDarakbangMember.getRole(bet.getMoimerId()))) + .toList(); + + return ChatPreview.builder() + .chatRoom(chatRoom) + .target(new Target(bet)) + .lastReadChatId(lastReadChatId) + .participants(participants) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/BetParticipantResolver.java b/backend/src/main/java/mouda/backend/chat/implement/BetParticipantResolver.java new file mode 100644 index 000000000..fc82fe00f --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/BetParticipantResolver.java @@ -0,0 +1,47 @@ +package mouda.backend.chat.implement; + +import java.util.List; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.bet.domain.BetRole; +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.domain.Participant; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Component +@RequiredArgsConstructor +public class BetParticipantResolver implements ParticipantsResolver { + + private final BetDarakbangMemberRepository betDarakbangMemberRepository; + + @Override + public boolean support(ChatRoomType chatRoomType) { + return chatRoomType == ChatRoomType.BET; + } + + @Override + @Transactional(readOnly = true) + public List resolve(ChatRoom chatRoom) { + return betDarakbangMemberRepository.findAllByBetId(chatRoom.getTargetId()).stream() + .map(betDarakbangMemberEntity -> { + DarakbangMember darakbangMember = betDarakbangMemberEntity.getDarakbangMember(); + BetEntity bet = betDarakbangMemberEntity.getBet(); + return new Participant( + darakbangMember.getId(), + darakbangMember.getNickname(), + darakbangMember.getProfile(), + getBetRole(darakbangMember, bet).toString()); + }) + .toList(); + } + + private BetRole getBetRole(DarakbangMember darakbangMember, BetEntity betEntity) { + return betEntity.getMoimerId() == darakbangMember.getId() ? BetRole.MOIMER : BetRole.MOIMEE; + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/ChatPreviewManager.java b/backend/src/main/java/mouda/backend/chat/implement/ChatPreviewManager.java new file mode 100644 index 000000000..e8afaf31b --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/ChatPreviewManager.java @@ -0,0 +1,12 @@ +package mouda.backend.chat.implement; + +import java.util.List; + +import mouda.backend.chat.domain.ChatPreview; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@FunctionalInterface +public interface ChatPreviewManager { + + List create(DarakbangMember darakbangMember); +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/ChatPreviewManagerRegistry.java b/backend/src/main/java/mouda/backend/chat/implement/ChatPreviewManagerRegistry.java new file mode 100644 index 000000000..34dd1d7f4 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/ChatPreviewManagerRegistry.java @@ -0,0 +1,21 @@ +package mouda.backend.chat.implement; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.chat.domain.ChatRoomType; + +@Component +@RequiredArgsConstructor +public class ChatPreviewManagerRegistry { + + private final MoimChatPreviewManager moimChatPreviewManager; + private final BetChatPreviewManager betChatPreviewManager; + + public ChatPreviewManager getManager(ChatRoomType type) { + if (type == ChatRoomType.MOIM) { + return moimChatPreviewManager; + } + return betChatPreviewManager; + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/ChatRoomDetailsFinder.java b/backend/src/main/java/mouda/backend/chat/implement/ChatRoomDetailsFinder.java new file mode 100644 index 000000000..9cc1f8401 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/ChatRoomDetailsFinder.java @@ -0,0 +1,31 @@ +package mouda.backend.chat.implement; + +import java.util.List; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.chat.domain.Attributes; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomDetails; +import mouda.backend.chat.domain.Participant; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Component +@RequiredArgsConstructor +public class ChatRoomDetailsFinder { + + private final ChatRoomFinder chatRoomFinder; + private final AttributeManagerRegistry attributeManagerRegistry; + private final ParticipantResolverRegistry participantResolverRegistry; + + @Transactional(readOnly = true) + public ChatRoomDetails find(long darakbangId, long chatRoomId, DarakbangMember darakbangMember) { + ChatRoom chatRoom = chatRoomFinder.read(darakbangId, chatRoomId, darakbangMember); + Attributes attributes = attributeManagerRegistry.getManager(chatRoom.getType()).create(chatRoom, darakbangMember); + List participants = participantResolverRegistry.getResolver(chatRoom.getType()).resolve(chatRoom); + + return new ChatRoomDetails(chatRoomId, chatRoom.getType(), attributes, participants); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/ChatRoomFinder.java b/backend/src/main/java/mouda/backend/chat/implement/ChatRoomFinder.java new file mode 100644 index 000000000..cbff50a15 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/ChatRoomFinder.java @@ -0,0 +1,93 @@ +package mouda.backend.chat.implement; + +import java.util.List; +import java.util.Optional; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.chat.domain.Chat; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.domain.Chats; +import mouda.backend.chat.domain.LastChat; +import mouda.backend.chat.entity.ChatEntity; +import mouda.backend.chat.entity.ChatRoomEntity; +import mouda.backend.chat.exception.ChatErrorMessage; +import mouda.backend.chat.exception.ChatException; +import mouda.backend.chat.infrastructure.ChatRepository; +import mouda.backend.chat.infrastructure.ChatRoomRepository; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.infrastructure.ChamyoRepository; + +@Component +@RequiredArgsConstructor +public class ChatRoomFinder { + + private final ChatRepository chatRepository; + private final ChatRoomRepository chatRoomRepository; + private final ChamyoRepository chamyoRepository; + private final BetDarakbangMemberRepository betDarakbangMemberRepository; + + public ChatRoom read(long darakbangId, long chatRoomId, DarakbangMember darakbangMember) { + ChatRoomEntity chatRoomEntity = chatRoomRepository.findByIdAndDarakbangId(chatRoomId, darakbangId) + .orElseThrow(() -> new ChatException(HttpStatus.NOT_FOUND, ChatErrorMessage.CHATROOM_NOT_FOUND)); + boolean isParticipated = checkParticipation(chatRoomEntity, darakbangMember); + if (!isParticipated) { + throw new ChatException(HttpStatus.FORBIDDEN, ChatErrorMessage.UNAUTHORIZED); + } + return new ChatRoom(chatRoomEntity.getId(), chatRoomEntity.getTargetId(), chatRoomEntity.getType()); + } + + private boolean checkParticipation(ChatRoomEntity chatRoomEntity, DarakbangMember darakbangMember) { + ChatRoomType type = chatRoomEntity.getType(); + if (type == ChatRoomType.MOIM) { + return chamyoRepository.existsByMoimIdAndDarakbangMemberId(chatRoomEntity.getTargetId(), + darakbangMember.getId()); + } + if (type == ChatRoomType.BET) { + return betDarakbangMemberRepository.existsByBetIdAndDarakbangMemberId(chatRoomEntity.getTargetId(), + darakbangMember.getId()); + } + return false; + } + + public ChatRoom readMoimChatRoom(long darakbangId, long chatRoomId) { + ChatRoomEntity chatRoomEntity = chatRoomRepository.findByIdAndDarakbangId(chatRoomId, darakbangId) + .orElseThrow(() -> new ChatException(HttpStatus.NOT_FOUND, ChatErrorMessage.CHATROOM_NOT_FOUND)); + + ChatRoomType type = chatRoomEntity.getType(); + if (type.isNotMoim()) { + throw new ChatException(HttpStatus.BAD_REQUEST, ChatErrorMessage.INVALID_CHATROOM_TYPE); + } + return new ChatRoom(chatRoomEntity.getId(), chatRoomEntity.getTargetId(), chatRoomEntity.getType()); + } + + public ChatRoom readChatRoomByTargetId(long targetId, ChatRoomType chatRoomType) { + ChatRoomEntity chatRoomEntity = chatRoomRepository.findByTargetIdAndType(targetId, chatRoomType) + .orElseThrow(() -> new ChatException(HttpStatus.NOT_FOUND, ChatErrorMessage.CHATROOM_NOT_FOUND)); + + LastChat lastChat = chatRepository.findFirstByChatRoomIdOrderByIdDesc(chatRoomEntity.getId()) + .map(ChatEntity::toLastChat) + .orElse(LastChat.empty()); + + return new ChatRoom(chatRoomEntity.getId(), chatRoomEntity.getTargetId(), chatRoomEntity.getType(), lastChat); + } + + @Transactional(readOnly = true) + public Chats findAllUnloadedChats(long chatRoomId, long recentChatId) { + List chats = chatRepository.findAllUnloadedChats(chatRoomId, recentChatId).stream() + .map(ChatEntity::toChat) + .toList(); + return new Chats(chats); + } + + public Long findChatRoomIdByTargetId(long targetId, ChatRoomType chatRoomType) { + Optional chatRoom = chatRoomRepository.findByTargetIdAndType(targetId, chatRoomType); + return chatRoom.map(ChatRoomEntity::getId) + .orElse(null); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/ChatRoomValidator.java b/backend/src/main/java/mouda/backend/chat/implement/ChatRoomValidator.java new file mode 100644 index 000000000..bcdae82ae --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/ChatRoomValidator.java @@ -0,0 +1,23 @@ +package mouda.backend.chat.implement; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.exception.ChatErrorMessage; +import mouda.backend.chat.exception.ChatException; +import mouda.backend.chat.infrastructure.ChatRoomRepository; + +@Component +@RequiredArgsConstructor +public class ChatRoomValidator { + + private final ChatRoomRepository chatRoomRepository; + + public void validateAlreadyExists(long targetId, ChatRoomType chatRoomType) { + if (chatRoomRepository.existsByTargetIdAndType(targetId, chatRoomType)) { + throw new ChatException(HttpStatus.BAD_REQUEST, ChatErrorMessage.CHATROOM_ALREADY_EXISTS); + } + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/ChatRoomWriter.java b/backend/src/main/java/mouda/backend/chat/implement/ChatRoomWriter.java new file mode 100644 index 000000000..16c22ed7a --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/ChatRoomWriter.java @@ -0,0 +1,27 @@ +package mouda.backend.chat.implement; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.entity.ChatRoomEntity; +import mouda.backend.chat.infrastructure.ChatRoomRepository; + +@Component +@RequiredArgsConstructor +public class ChatRoomWriter { + + private final ChatRoomRepository chatRoomRepository; + private final ChatRoomValidator chatRoomValidator; + + public long append(long targetId, long darakbangId, ChatRoomType chatRoomType) { + chatRoomValidator.validateAlreadyExists(targetId, chatRoomType); + + ChatRoomEntity chatRoomEntity = ChatRoomEntity.builder() + .targetId(targetId) + .darakbangId(darakbangId) + .type(chatRoomType) + .build(); + return chatRoomRepository.save(chatRoomEntity).getId(); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/ChatWriter.java b/backend/src/main/java/mouda/backend/chat/implement/ChatWriter.java new file mode 100644 index 000000000..f44c86fe5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/ChatWriter.java @@ -0,0 +1,73 @@ +package mouda.backend.chat.implement; + +import java.time.LocalDate; +import java.time.LocalTime; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.bet.implement.BetDarakbangMemberWriter; +import mouda.backend.chat.domain.Chat; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.entity.ChatEntity; +import mouda.backend.chat.entity.ChatType; +import mouda.backend.chat.infrastructure.ChatRepository; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.implement.writer.ChamyoWriter; + +@Component +@RequiredArgsConstructor +public class ChatWriter { + + private final ChatRepository chatRepository; + private final ChamyoWriter chamyoWriter; + private final BetDarakbangMemberWriter betDarakbangMemberWriter; + + public Chat appendPlaceTypeChat(long chatRoomId, String content, DarakbangMember darakbangMember) { + ChatEntity chatEntity = ChatEntity.builder() + .chatRoomId(chatRoomId) + .content(content) + .date(LocalDate.now()) + .time(LocalTime.now()) + .darakbangMember(darakbangMember) + .chatType(ChatType.PLACE) + .build(); + return chatRepository.save(chatEntity).toChat(); + } + + public Chat appendDateTimeTypeChat(long chatRoomId, LocalDate date, LocalTime time, + DarakbangMember darakbangMember) { + ChatEntity chatEntity = ChatEntity.builder() + .chatRoomId(chatRoomId) + .content(date.toString() + " " + time.toString()) + .date(LocalDate.now()) + .time(LocalTime.now()) + .darakbangMember(darakbangMember) + .chatType(ChatType.DATETIME) + .build(); + return chatRepository.save(chatEntity).toChat(); + } + + public Chat append(long chatRoomId, String content, DarakbangMember darakbangMember) { + ChatEntity chatEntity = ChatEntity.builder() + .chatRoomId(chatRoomId) + .content(content) + .date(LocalDate.now()) + .time(LocalTime.now()) + .darakbangMember(darakbangMember) + .chatType(ChatType.BASIC) + .build(); + return chatRepository.save(chatEntity).toChat(); + } + + public void updateLastReadChat(ChatRoom chatRoom, DarakbangMember darakbangMember, long lastReadChatId) { + ChatRoomType type = chatRoom.getType(); + if (type == ChatRoomType.MOIM) { + chamyoWriter.updateLastReadChat(chatRoom.getTargetId(), darakbangMember, lastReadChatId); + } + if (type == ChatRoomType.BET) { + betDarakbangMemberWriter.updateLastReadChat(chatRoom.getTargetId(), darakbangMember, lastReadChatId); + } + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/MoimAttributeManager.java b/backend/src/main/java/mouda/backend/chat/implement/MoimAttributeManager.java new file mode 100644 index 000000000..26cf57578 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/MoimAttributeManager.java @@ -0,0 +1,58 @@ +package mouda.backend.chat.implement; + +import java.time.LocalTime; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.chat.domain.Attributes; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.domain.MoimAttributes; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.implement.finder.ChamyoFinder; +import mouda.backend.moim.implement.finder.MoimFinder; + +@Component +@RequiredArgsConstructor +public class MoimAttributeManager implements AttributeManager { + + private final MoimFinder moimFinder; + private final ChamyoFinder chamyoFinder; + + @Override + public boolean support(ChatRoomType chatRoomType) { + return chatRoomType == ChatRoomType.MOIM; + } + + @Override + public Attributes create(ChatRoom chatRoom, DarakbangMember darakbangMember) { + Moim moim = moimFinder.read(chatRoom.getTargetId(), darakbangMember.getDarakbang().getId()); + Chamyo chamyo = chamyoFinder.read(moim, darakbangMember); + boolean isMoimer = getIsMoimer(chamyo); + return new MoimAttributes( + moim.getTitle(), + moim.getPlace(), + isMoimer, + moim.isPastMoim(), + moim.getDescription(), + moim.getDate(), + formatToSecondPrecision(moim.getTime()), + moim.getId() + ); + } + + private boolean getIsMoimer(Chamyo chamyo) { + return chamyo.getMoimRole() == MoimRole.MOIMER; + } + + private LocalTime formatToSecondPrecision(LocalTime time) { + if (time == null) { + return null; + } + return time.withNano(0); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/MoimChatPreviewManager.java b/backend/src/main/java/mouda/backend/chat/implement/MoimChatPreviewManager.java new file mode 100644 index 000000000..828178f82 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/MoimChatPreviewManager.java @@ -0,0 +1,57 @@ +package mouda.backend.chat.implement; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.chat.domain.ChatPreview; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.domain.Participant; +import mouda.backend.chat.domain.Target; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.finder.MoimFinder; +import mouda.backend.moim.infrastructure.ChamyoRepository; + +@Component +@RequiredArgsConstructor +public class MoimChatPreviewManager implements ChatPreviewManager { + + private final ChamyoRepository chamyoRepository; + private final MoimFinder moimFinder; + private final ChatRoomFinder chatRoomFinder; + + @Override + public List create(DarakbangMember darakbangMember) { + List myMoims = moimFinder.readAllMyMoims(darakbangMember); + + return myMoims.stream() + .filter(Moim::isChatOpened) + .map(this::getChatPreview) + .sorted() + .toList(); + } + + private ChatPreview getChatPreview(Moim moim) { + long targetId = moim.getId(); + ChatRoom chatRoom = chatRoomFinder.readChatRoomByTargetId(targetId, ChatRoomType.MOIM); + long lastReadChatId = chamyoRepository.findLastReadChatIdByMoimId(targetId); + List participants = chamyoRepository.findAllByMoimId(targetId) + .stream() + .map(chamyo -> new Participant( + moim.getDarakbangId(), + chamyo.getDarakbangMember().getNickname(), + chamyo.getDarakbangMember().getProfile(), + chamyo.getDarakbangMember().getRole().toString())) + .toList(); + + return ChatPreview.builder() + .chatRoom(chatRoom) + .target(new Target(moim)) + .lastReadChatId(lastReadChatId) + .participants(participants) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/MoimParticipantResolver.java b/backend/src/main/java/mouda/backend/chat/implement/MoimParticipantResolver.java new file mode 100644 index 000000000..ef17f131e --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/MoimParticipantResolver.java @@ -0,0 +1,39 @@ +package mouda.backend.chat.implement; + +import java.util.List; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.domain.Participant; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.infrastructure.ChamyoRepository; + +@Component +@RequiredArgsConstructor +public class MoimParticipantResolver implements ParticipantsResolver { + + private final ChamyoRepository chamyoRepository; + + @Override + public boolean support(ChatRoomType chatRoomType) { + return chatRoomType == ChatRoomType.MOIM; + } + + @Override + @Transactional + public List resolve(ChatRoom chatRoom) { + return chamyoRepository.findAllByMoimId(chatRoom.getTargetId()).stream() + .map(chamyo -> { + DarakbangMember darakbangMember = chamyo.getDarakbangMember(); + return new Participant( + darakbangMember.getId(), + darakbangMember.getNickname(), + darakbangMember.getProfile(), + chamyo.getMoimRole().toString()); + }).toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/ParticipantResolverRegistry.java b/backend/src/main/java/mouda/backend/chat/implement/ParticipantResolverRegistry.java new file mode 100644 index 000000000..7823751b0 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/ParticipantResolverRegistry.java @@ -0,0 +1,25 @@ +package mouda.backend.chat.implement; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.exception.ChatErrorMessage; +import mouda.backend.chat.exception.ChatException; + +@Component +@RequiredArgsConstructor +public class ParticipantResolverRegistry { + + private final List participantsResolvers; + + public ParticipantsResolver getResolver(ChatRoomType chatRoomType) { + return participantsResolvers.stream() + .filter(participantsResolver -> participantsResolver.support(chatRoomType)) + .findFirst() + .orElseThrow(() -> new ChatException(HttpStatus.BAD_REQUEST, ChatErrorMessage.INVALID_CHATROOM_TYPE)); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/ParticipantsResolver.java b/backend/src/main/java/mouda/backend/chat/implement/ParticipantsResolver.java new file mode 100644 index 000000000..bf371b0b8 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/ParticipantsResolver.java @@ -0,0 +1,14 @@ +package mouda.backend.chat.implement; + +import java.util.List; + +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.domain.Participant; + +public interface ParticipantsResolver { + + boolean support(ChatRoomType chatRoomType); + + List resolve(ChatRoom chatRoom); +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/notification/ChatNotificationEventHandler.java b/backend/src/main/java/mouda/backend/chat/implement/notification/ChatNotificationEventHandler.java new file mode 100644 index 000000000..d4a07453a --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/notification/ChatNotificationEventHandler.java @@ -0,0 +1,147 @@ +package mouda.backend.chat.implement.notification; + +import java.util.List; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.bet.domain.Bet; +import mouda.backend.bet.implement.BetFinder; +import mouda.backend.chat.domain.Chat; +import mouda.backend.chat.domain.ChatNotificationEvent; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.entity.ChatType; +import mouda.backend.chat.util.ChatDateTimeFormatter; +import mouda.backend.common.config.UrlConfig; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.implement.DarakbangFinder; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.finder.MoimFinder; +import mouda.backend.notification.domain.NotificationPayload; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.implement.NotificationProcessor; + +@Component +@RequiredArgsConstructor +public class ChatNotificationEventHandler { + + private final UrlConfig urlConfig; + private final MoimFinder moimFinder; + private final BetFinder betFinder; + private final DarakbangFinder darakbangFinder; + private final ChatRecipientFinder chatRecipientFinder; + private final NotificationProcessor notificationProcessor; + + @Async + @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(classes = ChatNotificationEvent.class, phase = TransactionPhase.AFTER_COMMIT) + public void sendChatNotification( + ChatNotificationEvent chatNotificationEvent + ) { + long darakbangId = chatNotificationEvent.getDarakbangId(); + ChatRoom chatRoom = chatNotificationEvent.getChatRoom(); + Chat appendedChat = chatNotificationEvent.getAppendedChat(); + + ChatRoomType chatRoomType = chatRoom.getType(); + long chatRoomId = chatRoom.getId(); + + if (chatRoomType == ChatRoomType.BET) { + Bet bet = betFinder.find(darakbangId, chatRoom.getTargetId()); + handleBetNotification(bet, appendedChat, chatRoomId); + return; + } + + Moim moim = moimFinder.read(chatRoom.getTargetId(), darakbangId); + handleMoimNotification(moim, appendedChat, chatRoomId); + } + + private void handleMoimNotification( + Moim moim, Chat chat, long chatRoomId + ) { + List recipients = chatRecipientFinder.getMoimChatNotificationRecipients(moim.getId(), + chat.getAuthor()); + long darakbangId = moim.getDarakbangId(); + + processNotification(darakbangId, chatRoomId, moim.getTitle(), chat, recipients); + } + + private void handleBetNotification(Bet bet, Chat chat, long chatRoomId) { + List recipients = chatRecipientFinder.getBetChatNotificationRecipients(bet.getId(), + chat.getAuthor()); + long darakbangId = bet.getDarakbangId(); + + processNotification(darakbangId, chatRoomId, bet.getTitle(), chat, recipients); + } + + private void processNotification( + long darakbangId, long chatRoomId, String title, Chat chat, List recipients + ) { + Darakbang darakbang = darakbangFinder.findById(darakbangId); + ChatNotificationMessage chatNotificationMessage = ChatNotificationMessage.create(darakbang.getName(), title, + chat); + + NotificationPayload payload = NotificationPayload.createChatPayload( + chatNotificationMessage.getType(), + darakbang.getName(), + chatNotificationMessage.getMessage(), + urlConfig.getChatRoomUrl(darakbangId, chatRoomId), + recipients, + darakbangId, + chatRoomId + ); + + notificationProcessor.process(payload); + } + + @Getter + @RequiredArgsConstructor + static class ChatNotificationMessage { + + private final String title; + private final NotificationType type; + private final String message; + + public static ChatNotificationMessage create(String darakbangName, String title, + Chat chat) { + ChatType chatType = chat.getChatType(); + String content = chat.getContent(); + + if (chatType == ChatType.PLACE) { + String message = "'" + title + "'" + " 장소가 '" + content + "' 으로 확정되었어요!"; + return placeConfirmChat(darakbangName, message); + } + if (chatType == ChatType.DATETIME) { + String parsedDateTime = ChatDateTimeFormatter.formatDateTime(content); + String message = "'" + title + "'" + "시간이 '" + parsedDateTime + "' 으로 확정되었어요!"; + return dateTimeConfirmChat(darakbangName, message); + } + + String authorNickname = chat.getAuthor().getNickname(); + String message = authorNickname + ": " + content; + return basicChat(title, message); + } + + private static ChatNotificationMessage placeConfirmChat(String title, String message) { + return new ChatNotificationMessage(title, NotificationType.MOIM_PLACE_CONFIRMED, + message); + } + + private static ChatNotificationMessage dateTimeConfirmChat(String title, + String message) { + return new ChatNotificationMessage(title, NotificationType.MOIM_TIME_CONFIRMED, + message); + } + + private static ChatNotificationMessage basicChat(String title, String message) { + return new ChatNotificationMessage(title, NotificationType.NEW_CHAT, message); + } + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/notification/ChatNotificationSender.java b/backend/src/main/java/mouda/backend/chat/implement/notification/ChatNotificationSender.java new file mode 100644 index 000000000..1426e5d4b --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/notification/ChatNotificationSender.java @@ -0,0 +1,26 @@ +package mouda.backend.chat.implement.notification; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.chat.domain.Chat; +import mouda.backend.chat.domain.ChatNotificationEvent; +import mouda.backend.chat.domain.ChatRoom; + +@Component +@RequiredArgsConstructor +public class ChatNotificationSender { + + private final ApplicationEventPublisher eventPublisher; + + public void sendChatNotification(long darakbangId, ChatRoom chatRoom, Chat appendedChat) { + ChatNotificationEvent event = ChatNotificationEvent.builder() + .darakbangId(darakbangId) + .chatRoom(chatRoom) + .appendedChat(appendedChat) + .build(); + + eventPublisher.publishEvent(event); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/implement/notification/ChatRecipientFinder.java b/backend/src/main/java/mouda/backend/chat/implement/notification/ChatRecipientFinder.java new file mode 100644 index 000000000..970cf3cb1 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/implement/notification/ChatRecipientFinder.java @@ -0,0 +1,51 @@ +package mouda.backend.chat.implement.notification; + +import java.util.List; +import java.util.stream.Stream; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.bet.entity.BetDarakbangMemberEntity; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.chat.domain.Author; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.notification.domain.Recipient; + +@Component +@RequiredArgsConstructor +public class ChatRecipientFinder { + + private final ChamyoRepository chamyoRepository; + private final BetDarakbangMemberRepository betDarakbangMemberRepository; + + public List getMoimChatNotificationRecipients(long moimId, Author author) { + List chamyos = chamyoRepository.findAllByMoimId(moimId); + + Stream darakbangMemberStream = chamyos.stream() + .map(Chamyo::getDarakbangMember); + + return getNotificationRecipients(darakbangMemberStream, author); + } + + public List getBetChatNotificationRecipients(long betId, Author author) { + List members = betDarakbangMemberRepository.findAllByBetId(betId); + + Stream darakbangMemberStream = members.stream() + .map(BetDarakbangMemberEntity::getDarakbangMember); + + return getNotificationRecipients(darakbangMemberStream, author); + } + + public List getNotificationRecipients(Stream memberStream, Author author) { + return memberStream + .filter(darakbangMember -> author.isNotSameMember(darakbangMember.getId(), darakbangMember.getMemberId())) + .map(darakbangMember -> Recipient.builder() + .memberId(darakbangMember.getMemberId()) + .darakbangMemberId(darakbangMember.getId()) + .build()) + .toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/infrastructure/ChatRepository.java b/backend/src/main/java/mouda/backend/chat/infrastructure/ChatRepository.java new file mode 100644 index 000000000..c48f998e9 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/infrastructure/ChatRepository.java @@ -0,0 +1,17 @@ +package mouda.backend.chat.infrastructure; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import mouda.backend.chat.entity.ChatEntity; + +public interface ChatRepository extends JpaRepository { + @Query("SELECT c FROM ChatEntity c WHERE c.chatRoomId = :chatRoomId AND c.id > :chatId") + List findAllUnloadedChats(@Param("chatRoomId") long chatRoomId, @Param("chatId") long chatId); + + Optional findFirstByChatRoomIdOrderByIdDesc(long chatRoomId); +} diff --git a/backend/src/main/java/mouda/backend/chat/infrastructure/ChatRoomRepository.java b/backend/src/main/java/mouda/backend/chat/infrastructure/ChatRoomRepository.java new file mode 100644 index 000000000..e2553b5ae --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/infrastructure/ChatRoomRepository.java @@ -0,0 +1,16 @@ +package mouda.backend.chat.infrastructure; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.entity.ChatRoomEntity; + +public interface ChatRoomRepository extends JpaRepository { + Optional findByIdAndDarakbangId(Long chatRoomId, long darakbangId); + + Optional findByTargetIdAndType(long targetId, ChatRoomType chatRoomType); + + boolean existsByTargetIdAndType(long targetId, ChatRoomType chatRoomType); +} diff --git a/backend/src/main/java/mouda/backend/chat/presentation/controller/ChatController.java b/backend/src/main/java/mouda/backend/chat/presentation/controller/ChatController.java new file mode 100644 index 000000000..91c6bf352 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/presentation/controller/ChatController.java @@ -0,0 +1,101 @@ +package mouda.backend.chat.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Positive; +import lombok.RequiredArgsConstructor; +import mouda.backend.aop.logging.ExceptRequestLogging; +import mouda.backend.chat.business.ChatService; +import mouda.backend.chat.presentation.request.ChatCreateRequest; +import mouda.backend.chat.presentation.request.DateTimeConfirmRequest; +import mouda.backend.chat.presentation.request.LastReadChatRequest; +import mouda.backend.chat.presentation.request.PlaceConfirmRequest; +import mouda.backend.chat.presentation.response.ChatFindUnloadedResponse; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Tag(name = "신 채팅 API") +@RestController +@RequestMapping("/v1/darakbang/{darakbangId}/chatroom") +@RequiredArgsConstructor +public class ChatController { + + private final ChatService chatService; + + // @override + @PostMapping("/{chatRoomId}") + public ResponseEntity createChat( + @PathVariable Long darakbangId, + @PathVariable Long chatRoomId, + @LoginDarakbangMember DarakbangMember darakbangMember, + @Valid @RequestBody ChatCreateRequest chatCreateRequest + ) { + chatService.createChat(darakbangId, chatRoomId, chatCreateRequest, darakbangMember); + + return ResponseEntity.ok().build(); + } + + // @override + @ExceptRequestLogging + @GetMapping("/{chatRoomId}") + public ResponseEntity> findUnloadedChats( + @PathVariable Long darakbangId, + @PathVariable @Positive Long chatRoomId, + @LoginDarakbangMember DarakbangMember darakbangMember, + @RequestParam("recentChatId") Long recentChatId + ) { + ChatFindUnloadedResponse unloadedChats = chatService + .findUnloadedChats(darakbangId, recentChatId, chatRoomId, darakbangMember); + + return ResponseEntity.ok(new RestResponse<>(unloadedChats)); + } + + // @override + @PostMapping("/{chatRoomId}/last") + public ResponseEntity createLastReadChatId( + @PathVariable Long darakbangId, + @PathVariable Long chatRoomId, + @LoginDarakbangMember DarakbangMember darakbangMember, + @RequestBody LastReadChatRequest lastReadChatRequest + ) { + chatService.updateLastReadChat(darakbangId, chatRoomId, lastReadChatRequest, darakbangMember); + + return ResponseEntity.ok().build(); + } + + // @override + @PostMapping("/{chatRoomId}/datetime") + public ResponseEntity confirmDateTime( + @PathVariable Long darakbangId, + @PathVariable Long chatRoomId, + @LoginDarakbangMember DarakbangMember darakbangMember, + @RequestBody DateTimeConfirmRequest dateTimeConfirmRequest + ) { + chatService.confirmDateTime(darakbangId, chatRoomId, dateTimeConfirmRequest, darakbangMember); + + return ResponseEntity.ok().build(); + } + + // @override + @PostMapping("/{chatRoomId}/place") + public ResponseEntity confirmPlace( + @PathVariable Long darakbangId, + @PathVariable Long chatRoomId, + @LoginDarakbangMember DarakbangMember darakbangMember, + @RequestBody PlaceConfirmRequest placeConfirmRequest + ) { + chatService.confirmPlace(darakbangId, chatRoomId, placeConfirmRequest, darakbangMember); + + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/presentation/controller/ChatRoomController.java b/backend/src/main/java/mouda/backend/chat/presentation/controller/ChatRoomController.java new file mode 100644 index 000000000..91c77ed36 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/presentation/controller/ChatRoomController.java @@ -0,0 +1,61 @@ +package mouda.backend.chat.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import mouda.backend.aop.logging.ExceptRequestLogging; +import mouda.backend.chat.business.ChatRoomService; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.presentation.response.ChatPreviewResponses; +import mouda.backend.chat.presentation.response.ChatRoomDetailsResponse; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/darakbang/{darakbangId}/chatroom") +public class ChatRoomController { + + private final ChatRoomService chatRoomService; + + @GetMapping("/{chatRoomId}/details") + public ResponseEntity> findChatRoomDetails( + @PathVariable Long darakbangId, + @PathVariable Long chatRoomId, + @LoginDarakbangMember DarakbangMember darakbangMember + ) { + ChatRoomDetailsResponse chatRoomDetails = chatRoomService.findChatRoomDetails(darakbangId, chatRoomId, darakbangMember); + + return ResponseEntity.ok(new RestResponse<>(chatRoomDetails)); + } + + @GetMapping("/preview") + @ExceptRequestLogging + public ResponseEntity> findChatPreviews( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember darakbangMember, + @RequestParam("chatRoomType") ChatRoomType chatRoomType + ) { + ChatPreviewResponses chatPreviewResponses = chatRoomService.findChatPreview(darakbangMember, chatRoomType); + + return ResponseEntity.ok(new RestResponse<>(chatPreviewResponses)); + } + + @PatchMapping("/open") + public ResponseEntity> openChatRoom( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember darakbangMember, + @RequestParam("moimId") Long moimId + ) { + long chatRoomId = chatRoomService.openChatRoom(darakbangId, moimId, darakbangMember); + + return ResponseEntity.ok().body(new RestResponse<>(chatRoomId)); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/presentation/controller/swagger/ChatSwagger.java b/backend/src/main/java/mouda/backend/chat/presentation/controller/swagger/ChatSwagger.java new file mode 100644 index 000000000..d1235aa4a --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/presentation/controller/swagger/ChatSwagger.java @@ -0,0 +1,98 @@ +package mouda.backend.chat.presentation.controller.swagger; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.presentation.request.ChatCreateRequest; +import mouda.backend.chat.presentation.request.DateTimeConfirmRequest; +import mouda.backend.chat.presentation.request.LastReadChatRequest; +import mouda.backend.chat.presentation.request.PlaceConfirmRequest; +import mouda.backend.chat.presentation.response.ChatFindUnloadedResponse; +import mouda.backend.chat.presentation.response.ChatPreviewResponses; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +public interface ChatSwagger { + + @Operation(summary = "채팅 생성", description = "한 건의 채팅 메시지를 보낸다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "채팅 생성 성공!") + }) + ResponseEntity createChat( + @PathVariable Long darakbangId, + @PathVariable Long chatRoomId, + @LoginDarakbangMember DarakbangMember member, + @RequestBody ChatCreateRequest chatCreateRequest + ); + + @Operation(summary = "채팅 조회", description = "아직 조회되지 않은 채팅 내역을 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "채팅 조회 성공!") + }) + ResponseEntity> findUnloadedChats( + @PathVariable Long darakbangId, + @PathVariable Long chatRoomId, + @LoginDarakbangMember DarakbangMember member, + @RequestParam Long recentChatId + ); + + @Operation(summary = "장소 확정", description = "작성자가 장소를 확정하는 채팅을 전송합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "장소 확정 성공!") + }) + ResponseEntity confirmPlace( + @PathVariable Long darakbangId, + @PathVariable Long chatRoomId, + @LoginDarakbangMember DarakbangMember member, + @RequestBody PlaceConfirmRequest placeConfirmRequest + ); + + @Operation(summary = "날짜 시간 확정", description = "작성자가 날짜와 시간을 확정하는 채팅을 전송합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "날짜 시간 확정 성공!") + }) + ResponseEntity confirmDateTime( + @PathVariable Long darakbangId, + @PathVariable Long chatRoomId, + @LoginDarakbangMember DarakbangMember member, + @RequestBody DateTimeConfirmRequest dateTimeConfirmRequest + ); + + @Operation(summary = "채팅방 목록 조회", description = "채팅방 목록을 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "채팅방 조회 성공!") + }) + ResponseEntity> findChatPreviews( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @RequestParam("chatRoomType") ChatRoomType chatRoomType + ); + + @Operation(summary = "마지막 읽은 채팅 저장", description = "마지막 읽은 채팅을 저장한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "마지막 채팅 저장 성공!") + }) + ResponseEntity createLastReadChatId( + @PathVariable Long darakbangId, + @PathVariable Long chatRoomId, + @LoginDarakbangMember DarakbangMember member, + @RequestBody LastReadChatRequest lastReadChatRequest + ); + + @Operation(summary = "채팅방 열기", description = "채팅방을 연다!") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "채팅방 열기 성공!") + }) + ResponseEntity openChatRoom( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @RequestParam("moimId") Long moimId + ); +} diff --git a/backend/src/main/java/mouda/backend/chat/presentation/request/ChatCreateRequest.java b/backend/src/main/java/mouda/backend/chat/presentation/request/ChatCreateRequest.java new file mode 100644 index 000000000..760d6b018 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/presentation/request/ChatCreateRequest.java @@ -0,0 +1,9 @@ +package mouda.backend.chat.presentation.request; + +import jakarta.validation.constraints.NotBlank; + +public record ChatCreateRequest( + @NotBlank + String content +) { +} diff --git a/backend/src/main/java/mouda/backend/chat/presentation/request/DateTimeConfirmRequest.java b/backend/src/main/java/mouda/backend/chat/presentation/request/DateTimeConfirmRequest.java new file mode 100644 index 000000000..a1002f24b --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/presentation/request/DateTimeConfirmRequest.java @@ -0,0 +1,28 @@ +package mouda.backend.chat.presentation.request; + +import java.time.LocalDate; +import java.time.LocalTime; + +import jakarta.validation.constraints.NotNull; +import mouda.backend.chat.entity.ChatEntity; +import mouda.backend.chat.entity.ChatType; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +public record DateTimeConfirmRequest( + @NotNull + LocalDate date, + + @NotNull + LocalTime time +) { + public ChatEntity toEntity(long chatRoomId, DarakbangMember darakbangMember) { + return ChatEntity.builder() + .content(date.toString() + " " + time.toString()) + .chatRoomId(chatRoomId) + .date(LocalDate.now()) + .time(LocalTime.now()) + .darakbangMember(darakbangMember) + .chatType(ChatType.DATETIME) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/presentation/request/LastReadChatRequest.java b/backend/src/main/java/mouda/backend/chat/presentation/request/LastReadChatRequest.java new file mode 100644 index 000000000..463bef52a --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/presentation/request/LastReadChatRequest.java @@ -0,0 +1,9 @@ +package mouda.backend.chat.presentation.request; + +import jakarta.validation.constraints.NotNull; + +public record LastReadChatRequest( + @NotNull + Long lastReadChatId +) { +} diff --git a/backend/src/main/java/mouda/backend/chat/presentation/request/PlaceConfirmRequest.java b/backend/src/main/java/mouda/backend/chat/presentation/request/PlaceConfirmRequest.java new file mode 100644 index 000000000..0ddaffcbc --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/presentation/request/PlaceConfirmRequest.java @@ -0,0 +1,25 @@ +package mouda.backend.chat.presentation.request; + +import java.time.LocalDate; +import java.time.LocalTime; + +import jakarta.validation.constraints.NotBlank; +import mouda.backend.chat.entity.ChatEntity; +import mouda.backend.chat.entity.ChatType; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +public record PlaceConfirmRequest( + @NotBlank + String place +) { + public ChatEntity toEntity(long chatRoomId, DarakbangMember darakbangMember) { + return ChatEntity.builder() + .content(place) + .chatRoomId(chatRoomId) + .date(LocalDate.now()) + .time(LocalTime.now()) + .darakbangMember(darakbangMember) + .chatType(ChatType.PLACE) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/presentation/response/ChatFindDetailResponse.java b/backend/src/main/java/mouda/backend/chat/presentation/response/ChatFindDetailResponse.java new file mode 100644 index 000000000..a9c7196a4 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/presentation/response/ChatFindDetailResponse.java @@ -0,0 +1,39 @@ +package mouda.backend.chat.presentation.response; + +import java.time.LocalDate; +import java.time.LocalTime; + +import lombok.Builder; +import mouda.backend.chat.domain.Author; +import mouda.backend.chat.domain.Chat; +import mouda.backend.chat.domain.ChatOwnership; +import mouda.backend.chat.entity.ChatType; + +@Builder +public record ChatFindDetailResponse( + long chatId, + String content, + boolean isMyMessage, + ParticipantResponse participation, + LocalDate date, + LocalTime time, + ChatType chatType +) { + public static ChatFindDetailResponse toResponse(ChatOwnership chatOwnership) { + Chat chat = chatOwnership.getChat(); + return ChatFindDetailResponse.builder() + .chatId(chat.getId()) + .content(chat.getContent()) + .isMyMessage(chatOwnership.isMine()) + .participation(getParticipantResponse(chatOwnership)) + .date(chat.getDate()) + .time(chat.getTime()) + .chatType(chat.getChatType()) + .build(); + } + + private static ParticipantResponse getParticipantResponse(ChatOwnership chatOwnership) { + Author author = chatOwnership.getChat().getAuthor(); + return new ParticipantResponse(author.getDarakbangMemberId(), author.getNickname(), author.getProfile()); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/presentation/response/ChatFindUnloadedResponse.java b/backend/src/main/java/mouda/backend/chat/presentation/response/ChatFindUnloadedResponse.java new file mode 100644 index 000000000..7817caf46 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/presentation/response/ChatFindUnloadedResponse.java @@ -0,0 +1,16 @@ +package mouda.backend.chat.presentation.response; + +import java.util.List; + +import mouda.backend.chat.domain.ChatOwnership; + +public record ChatFindUnloadedResponse( + List chats +) { + public static ChatFindUnloadedResponse toResponse(List chats) { + List responses = chats.stream() + .map(ChatFindDetailResponse::toResponse) + .toList(); + return new ChatFindUnloadedResponse(responses); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/presentation/response/ChatPreviewResponse.java b/backend/src/main/java/mouda/backend/chat/presentation/response/ChatPreviewResponse.java new file mode 100644 index 000000000..6928d9002 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/presentation/response/ChatPreviewResponse.java @@ -0,0 +1,40 @@ +package mouda.backend.chat.presentation.response; + +import java.util.List; + +import lombok.Builder; +import mouda.backend.chat.domain.ChatPreview; +import mouda.backend.chat.domain.Target; + +@Builder +public record ChatPreviewResponse( + Long chatRoomId, + String title, + List participations, + boolean isStarted, + String lastContent, + long lastReadChatId +) { + + public static ChatPreviewResponse toResponse(ChatPreview chatPreview) { + Target target = chatPreview.getTarget(); + return ChatPreviewResponse.builder() + .chatRoomId(chatPreview.getChatRoom().getId()) + .title(target.getTitle()) + .isStarted(target.isStarted()) + .participations(getParticipants(chatPreview)) + .lastContent(chatPreview.getLastContent()) + .lastReadChatId(chatPreview.getLastReadChatId()) + .build(); + } + + private static List getParticipants(ChatPreview chatPreview) { + return chatPreview.getParticipants().stream() + .map(participant -> new ParticipantResponse( + participant.getDarakbangMemberId(), + participant.getNickname(), + participant.getProfile(), + participant.getRole())) + .toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/presentation/response/ChatPreviewResponses.java b/backend/src/main/java/mouda/backend/chat/presentation/response/ChatPreviewResponses.java new file mode 100644 index 000000000..f2bd9d5b1 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/presentation/response/ChatPreviewResponses.java @@ -0,0 +1,17 @@ +package mouda.backend.chat.presentation.response; + +import java.util.List; + +import mouda.backend.chat.domain.ChatPreview; + +public record ChatPreviewResponses( + List previews +) { + + public static ChatPreviewResponses toResponse(List chatPreviewResponses) { + List responses = chatPreviewResponses.stream() + .map(ChatPreviewResponse::toResponse) + .toList(); + return new ChatPreviewResponses(responses); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/presentation/response/ChatRoomDetailsResponse.java b/backend/src/main/java/mouda/backend/chat/presentation/response/ChatRoomDetailsResponse.java new file mode 100644 index 000000000..64960c485 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/presentation/response/ChatRoomDetailsResponse.java @@ -0,0 +1,35 @@ +package mouda.backend.chat.presentation.response; + +import java.util.List; +import java.util.Map; + +import mouda.backend.chat.domain.ChatRoomDetails; + +public record ChatRoomDetailsResponse( + long chatRoomId, + Map attributes, + String title, + String type, + List participations +) { + + public static ChatRoomDetailsResponse from(ChatRoomDetails chatRoomDetails) { + return new ChatRoomDetailsResponse( + chatRoomDetails.getId(), + chatRoomDetails.getAttributes(), + chatRoomDetails.getTitle(), + chatRoomDetails.getChatRoomType().toString(), + getParticipants(chatRoomDetails) + ); + } + + private static List getParticipants(ChatRoomDetails chatRoomDetails) { + return chatRoomDetails.getParticipants().stream() + .map(participant -> new ParticipantResponse( + participant.getDarakbangMemberId(), + participant.getNickname(), + participant.getProfile(), + participant.getRole())) + .toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/presentation/response/ParticipantResponse.java b/backend/src/main/java/mouda/backend/chat/presentation/response/ParticipantResponse.java new file mode 100644 index 000000000..caf4944e0 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/presentation/response/ParticipantResponse.java @@ -0,0 +1,13 @@ +package mouda.backend.chat.presentation.response; + +public record ParticipantResponse( + long darakbangMemberId, + String nickname, + String profile, + String role +) { + + public ParticipantResponse(long darakbangMemberId, String nickname, String profile) { + this(darakbangMemberId, nickname, profile, null); + } +} diff --git a/backend/src/main/java/mouda/backend/chat/util/ChatDateTimeFormatter.java b/backend/src/main/java/mouda/backend/chat/util/ChatDateTimeFormatter.java new file mode 100644 index 000000000..870eba2f5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/chat/util/ChatDateTimeFormatter.java @@ -0,0 +1,51 @@ +package mouda.backend.chat.util; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +import org.springframework.http.HttpStatus; + +import lombok.NoArgsConstructor; +import mouda.backend.chat.exception.ChatErrorMessage; +import mouda.backend.chat.exception.ChatException; + +/** + * 채팅방에서 날짜와 시간이 확정될 때 알림 메시지에 사용되는 날짜 / 시간 형식 변환을 위한 유틸리티 클래스 + */ +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class ChatDateTimeFormatter { + + private static final String DATETIME_DELIMITER = " "; + private static final int DATE_PART = 0; + private static final int TIME_PART = 1; + + public static String formatDateTime(String dateTime) { + LocalDateTime localDateTime = parseDateTime(dateTime); + StringBuilder formattedDateTime = new StringBuilder(); + + int year = localDateTime.getYear(); + if (year > LocalDate.now().getYear()) { + formattedDateTime.append(year).append("년 "); + } + + return formattedDateTime + .append(localDateTime.getMonthValue()).append("월 ") + .append(localDateTime.getDayOfMonth()).append("일 ") + .append(localDateTime.getHour()).append("시 ") + .append(localDateTime.getMinute()).append("분") + .toString(); + } + + private static LocalDateTime parseDateTime(String dateTime) { + try { + String[] dateTimeParts = dateTime.split(DATETIME_DELIMITER); + LocalDate date = LocalDate.parse(dateTimeParts[DATE_PART]); + LocalTime time = LocalTime.parse(dateTimeParts[TIME_PART]); + + return LocalDateTime.of(date, time); + } catch (Exception e) { + throw new ChatException(HttpStatus.BAD_REQUEST, ChatErrorMessage.INVALID_DATE_TIME_FORMAT); + } + } +} diff --git a/backend/src/main/java/mouda/backend/common/HealthCheckController.java b/backend/src/main/java/mouda/backend/common/HealthCheckController.java new file mode 100644 index 000000000..3a800fbdb --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/HealthCheckController.java @@ -0,0 +1,14 @@ +package mouda.backend.common; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HealthCheckController { + + @GetMapping("/health") + public ResponseEntity checkHealth() { + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/mouda/backend/common/config/CorsConfig.java b/backend/src/main/java/mouda/backend/common/config/CorsConfig.java new file mode 100644 index 000000000..ef6d73b4c --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/CorsConfig.java @@ -0,0 +1,24 @@ +package mouda.backend.common.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins("http://localhost:8081", + "https://dev.mouda.site", + "https://appleid.apple.com", + "https://test.mouda.site", "http://test.mouda.site", + "https://mouda.site", "http://mouda.site" + ) + .allowedMethods("GET", "POST", "PATCH", "DELETE", "OPTIONS") + .allowedHeaders("Authorization", "Content-Type") + .allowCredentials(true) + .maxAge(3600); + } +} diff --git a/backend/src/main/java/mouda/backend/common/config/FirebaseConfig.java b/backend/src/main/java/mouda/backend/common/config/FirebaseConfig.java new file mode 100644 index 000000000..5c945ba0d --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/FirebaseConfig.java @@ -0,0 +1,29 @@ +package mouda.backend.common.config; + +import java.io.InputStream; + +import org.springframework.context.annotation.Configuration; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; + +import jakarta.annotation.PostConstruct; + +@Configuration +public class FirebaseConfig { + + @PostConstruct + public void init() { + try { + InputStream serviceAccount = getClass().getClassLoader() + .getResourceAsStream("firebase/serviceAccountKey.json"); + FirebaseOptions options = new FirebaseOptions.Builder() + .setCredentials(GoogleCredentials.fromStream(serviceAccount)) + .build(); + FirebaseApp.initializeApp(options); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/backend/src/main/java/mouda/backend/common/config/JacksonConfig.java b/backend/src/main/java/mouda/backend/common/config/JacksonConfig.java new file mode 100644 index 000000000..2991fec42 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/JacksonConfig.java @@ -0,0 +1,32 @@ +package mouda.backend.common.config; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; + +@Configuration +public class JacksonConfig { + + @Bean + public JavaTimeModule javaTimeModule() { + JavaTimeModule javaTimeModule = new JavaTimeModule(); + DateTimeFormatter dateFormat = DateTimeFormatter.ISO_LOCAL_DATE; + DateTimeFormatter timeFormat = DateTimeFormatter.ofPattern("HH:mm"); + + javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(dateFormat)) + .addSerializer(LocalTime.class, new LocalTimeSerializer(timeFormat)) + .addDeserializer(LocalDate.class, new LocalDateDeserializer(dateFormat)) + .addDeserializer(LocalTime.class, new LocalTimeDeserializer(timeFormat)); + + return javaTimeModule; + } +} diff --git a/backend/src/main/java/mouda/backend/common/config/RequestBodyWrappingFilter.java b/backend/src/main/java/mouda/backend/common/config/RequestBodyWrappingFilter.java new file mode 100644 index 000000000..eb538ee01 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/RequestBodyWrappingFilter.java @@ -0,0 +1,29 @@ +package mouda.backend.common.config; + +import java.io.IOException; + +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class RequestBodyWrappingFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + + ContentCachingRequestWrapper wrappingRequest = new ContentCachingRequestWrapper(request); + filterChain.doFilter(wrappingRequest, response); + } +} diff --git a/backend/src/main/java/mouda/backend/common/config/RestClientConfig.java b/backend/src/main/java/mouda/backend/common/config/RestClientConfig.java new file mode 100644 index 000000000..2614e3e94 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/RestClientConfig.java @@ -0,0 +1,24 @@ +package mouda.backend.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestClient; + +@Configuration +public class RestClientConfig { + + @Bean + public RestClient kakaoOauthRestClient() { + return RestClient.builder() + .requestFactory(getClientHttpRequestFactory()) + .build(); + } + + private HttpComponentsClientHttpRequestFactory getClientHttpRequestFactory() { + HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(); + clientHttpRequestFactory.setConnectTimeout(3000); + clientHttpRequestFactory.setConnectionRequestTimeout(30000); + return clientHttpRequestFactory; + } +} diff --git a/backend/src/main/java/mouda/backend/common/config/S3Config.java b/backend/src/main/java/mouda/backend/common/config/S3Config.java new file mode 100644 index 000000000..26cb7003a --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/S3Config.java @@ -0,0 +1,24 @@ +package mouda.backend.common.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; + +@Configuration +public class S3Config { + + @Value("${aws.region.static}") + private String region; + + @Bean + public AmazonS3 amazonS3() { + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(DefaultAWSCredentialsProviderChain.getInstance()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/common/config/SwaggerConfig.java b/backend/src/main/java/mouda/backend/common/config/SwaggerConfig.java new file mode 100644 index 000000000..46cd22f29 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/SwaggerConfig.java @@ -0,0 +1,36 @@ +package mouda.backend.common.config; + +import org.springdoc.core.utils.SpringDocUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.config.argumentresolver.LoginMember; + +@Configuration +public class SwaggerConfig { + + static { + SpringDocUtils.getConfig().addAnnotationsToIgnore(LoginMember.class); + SpringDocUtils.getConfig().addAnnotationsToIgnore(LoginDarakbangMember.class); + } + + @Bean + public OpenAPI openAPI() { + return new OpenAPI().addSecurityItem( + new SecurityRequirement().addList("Bearer Authorization")) + .components(new Components().addSecuritySchemes( + "Bearer Authorization", createBearerTokenScheme() + )); + } + + private SecurityScheme createBearerTokenScheme() { + return new SecurityScheme().type(SecurityScheme.Type.HTTP) + .bearerFormat("JWT") + .scheme("bearer"); + } +} diff --git a/backend/src/main/java/mouda/backend/common/config/UrlConfig.java b/backend/src/main/java/mouda/backend/common/config/UrlConfig.java new file mode 100644 index 000000000..a5426a333 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/UrlConfig.java @@ -0,0 +1,25 @@ +package mouda.backend.common.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@ConfigurationProperties(prefix = "url") +@Getter +@RequiredArgsConstructor +public class UrlConfig { + + private final String base; + private final String moim; + private final String chat; + private final String chatroom; + + public String getChatRoomUrl(long darakbangId, long chatRoomId) { + return base + String.format(chatroom, darakbangId, chatRoomId); + } + + public String getMoimUrl(long darakbangId, long moimId) { + return base + String.format(moim, darakbangId, moimId); + } +} diff --git a/backend/src/main/java/mouda/backend/common/config/WebMvcConfig.java b/backend/src/main/java/mouda/backend/common/config/WebMvcConfig.java new file mode 100644 index 000000000..ae9f74361 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/WebMvcConfig.java @@ -0,0 +1,45 @@ +package mouda.backend.common.config; + +import java.util.List; + +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMemberArgumentResolver; +import mouda.backend.common.config.argumentresolver.LoginMemberArgumentResolver; +import mouda.backend.common.config.converter.FilterTypeConverter; +import mouda.backend.common.config.interceptor.AuthenticationCheckInterceptor; + +@Configuration +@RequiredArgsConstructor +public class WebMvcConfig implements WebMvcConfigurer { + + private final LoginMemberArgumentResolver loginMemberArgumentResolver; + private final LoginDarakbangMemberArgumentResolver loginDarakbangMemberArgumentResolver; + private final AuthenticationCheckInterceptor authenticationCheckInterceptor; + private final FilterTypeConverter filterTypeConverter; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authenticationCheckInterceptor) + .addPathPatterns("/v1/**") + .excludePathPatterns("/v1/auth/kakao/oauth", "/v1/auth/login/anna", "/v1/auth/login/hogee", "/health", + "/v1/auth/google", + "/v1/auth/apple"); + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(loginMemberArgumentResolver); + resolvers.add(loginDarakbangMemberArgumentResolver); + } + + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(filterTypeConverter); + } +} diff --git a/backend/src/main/java/mouda/backend/common/config/argumentresolver/LoginDarakbangMember.java b/backend/src/main/java/mouda/backend/common/config/argumentresolver/LoginDarakbangMember.java new file mode 100644 index 000000000..5247df392 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/argumentresolver/LoginDarakbangMember.java @@ -0,0 +1,12 @@ +package mouda.backend.common.config.argumentresolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface LoginDarakbangMember { + +} diff --git a/backend/src/main/java/mouda/backend/common/config/argumentresolver/LoginDarakbangMemberArgumentResolver.java b/backend/src/main/java/mouda/backend/common/config/argumentresolver/LoginDarakbangMemberArgumentResolver.java new file mode 100644 index 000000000..4dcb544d7 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/argumentresolver/LoginDarakbangMemberArgumentResolver.java @@ -0,0 +1,44 @@ +package mouda.backend.common.config.argumentresolver; + +import java.util.Map; + +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.HandlerMapping; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.business.DarakbangMemberService; +import mouda.backend.member.domain.Member; + +@Component +@RequiredArgsConstructor +public class LoginDarakbangMemberArgumentResolver implements HandlerMethodArgumentResolver { + + private final LoginMemberArgumentResolver loginMemberArgumentResolver; + private final DarakbangMemberService darakbangMemberService; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(LoginDarakbangMember.class); + } + + @Override + public Object resolveArgument( + MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory + ) throws Exception { + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + Map variables = (Map)request.getAttribute( + HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); + long darakbangId = Long.parseLong(variables.get("darakbangId")); + Member member = (Member)loginMemberArgumentResolver.resolveArgument( + parameter, mavContainer, webRequest, binderFactory); + + return darakbangMemberService.findDarakbangMember(darakbangId, member); + } +} diff --git a/backend/src/main/java/mouda/backend/common/config/argumentresolver/LoginMember.java b/backend/src/main/java/mouda/backend/common/config/argumentresolver/LoginMember.java new file mode 100644 index 000000000..79d10a353 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/argumentresolver/LoginMember.java @@ -0,0 +1,12 @@ +package mouda.backend.common.config.argumentresolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface LoginMember { + +} diff --git a/backend/src/main/java/mouda/backend/common/config/argumentresolver/LoginMemberArgumentResolver.java b/backend/src/main/java/mouda/backend/common/config/argumentresolver/LoginMemberArgumentResolver.java new file mode 100644 index 000000000..66759e1b5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/argumentresolver/LoginMemberArgumentResolver.java @@ -0,0 +1,38 @@ +package mouda.backend.common.config.argumentresolver; + +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import lombok.RequiredArgsConstructor; +import mouda.backend.member.business.MemberService; + +@Component +@RequiredArgsConstructor +public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + + private final MemberService memberService; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(LoginMember.class); + } + + @Override + public Object resolveArgument( + MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory + ) throws Exception { + String authorizationHeader = webRequest.getHeader("Authorization"); + + String token = extractToken(authorizationHeader); + return memberService.findMember(token); + } + + private String extractToken(String authorizationHeader) { + return authorizationHeader.substring(7); + } +} diff --git a/backend/src/main/java/mouda/backend/common/config/converter/FilterTypeConverter.java b/backend/src/main/java/mouda/backend/common/config/converter/FilterTypeConverter.java new file mode 100644 index 000000000..3f71d2d57 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/converter/FilterTypeConverter.java @@ -0,0 +1,19 @@ +package mouda.backend.common.config.converter; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.stereotype.Component; + +import mouda.backend.moim.domain.FilterType; + +@Component +public class FilterTypeConverter implements Converter { + + @Override + public FilterType convert(String source) { + try { + return FilterType.valueOf(source.toUpperCase()); + } catch (IllegalArgumentException e) { + return FilterType.ALL; // 에러 발생시 기본 설정은 ALL + } + } +} diff --git a/backend/src/main/java/mouda/backend/common/config/data/DataSourceConfiguration.java b/backend/src/main/java/mouda/backend/common/config/data/DataSourceConfiguration.java new file mode 100644 index 000000000..729334674 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/data/DataSourceConfiguration.java @@ -0,0 +1,85 @@ +package mouda.backend.common.config.data; + +import com.zaxxer.hikari.HikariDataSource; +import java.util.HashMap; +import java.util.Map; +import javax.sql.DataSource; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; + +@Profile(value = "prod") +@Configuration +public class DataSourceConfiguration { + + public static final String MASTER_DATA_SOURCE = "MASTER"; + public static final String SLAVE_DATA_SOURCE = "SLAVE"; + public static final String ROUTING_DATA_SOURCE = "ROUTING"; + + @Value("${spring.jpa.hibernate.ddl-auto}") + private String hibernateDdlAuto; + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory( + EntityManagerFactoryBuilder builder, DataSource dataSource + ) { + Map properties = new HashMap<>(); + properties.put("hibernate.hbm2ddl.auto", hibernateDdlAuto); + + return builder.dataSource(dataSource) + .packages("mouda.backend") + .properties(properties) + .build(); + } + + @Primary + @DependsOn(ROUTING_DATA_SOURCE) + @Bean + public DataSource dataSource(@Qualifier(ROUTING_DATA_SOURCE) DataSource dataSource) { + return new LazyConnectionDataSourceProxy(dataSource); + } + + @DependsOn({MASTER_DATA_SOURCE, SLAVE_DATA_SOURCE}) + @Bean(ROUTING_DATA_SOURCE) + public DataSource routingDataSource( + @Qualifier(MASTER_DATA_SOURCE) DataSource masterDataSource, + @Qualifier(SLAVE_DATA_SOURCE) DataSource slaveDataSource + ) { + RoutingDataSource routingDataSource = new RoutingDataSource(); + + Map dataSources = Map.of( + MASTER_DATA_SOURCE, masterDataSource, + SLAVE_DATA_SOURCE, slaveDataSource + ); + + routingDataSource.setTargetDataSources(dataSources); + routingDataSource.setDefaultTargetDataSource(masterDataSource); + + return routingDataSource; + } + + @Bean(MASTER_DATA_SOURCE) + @ConfigurationProperties(prefix = "spring.datasource.master.hikari") + public DataSource masterDataSource() { + return DataSourceBuilder.create() + .type(HikariDataSource.class) + .build(); + } + + @Bean(SLAVE_DATA_SOURCE) + @ConfigurationProperties(prefix = "spring.datasource.slave.hikari") + public DataSource slaveDataSource() { + return DataSourceBuilder.create() + .type(HikariDataSource.class) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/common/config/data/RoutingDataSource.java b/backend/src/main/java/mouda/backend/common/config/data/RoutingDataSource.java new file mode 100644 index 000000000..253c03ef7 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/data/RoutingDataSource.java @@ -0,0 +1,15 @@ +package mouda.backend.common.config.data; + +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +public class RoutingDataSource extends AbstractRoutingDataSource { + + @Override + protected Object determineCurrentLookupKey() { + if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) { + return DataSourceConfiguration.SLAVE_DATA_SOURCE; + } + return DataSourceConfiguration.MASTER_DATA_SOURCE; + } +} diff --git a/backend/src/main/java/mouda/backend/common/config/interceptor/AuthenticationCheckInterceptor.java b/backend/src/main/java/mouda/backend/common/config/interceptor/AuthenticationCheckInterceptor.java new file mode 100644 index 000000000..61ad4b397 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/config/interceptor/AuthenticationCheckInterceptor.java @@ -0,0 +1,43 @@ +package mouda.backend.common.config.interceptor; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.servlet.HandlerInterceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import mouda.backend.auth.exception.AuthErrorMessage; +import mouda.backend.auth.exception.AuthException; +import mouda.backend.member.business.MemberService; + +@Component +@RequiredArgsConstructor +public class AuthenticationCheckInterceptor implements HandlerInterceptor { + + private static final String AUTHORIZATION_PREFIX = "Bearer "; + + private final MemberService memberService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if (CorsUtils.isPreFlightRequest(request)) { + return true; + } + + String authorizationHeader = request.getHeader("Authorization"); + + if (authorizationHeader == null || !authorizationHeader.startsWith(AUTHORIZATION_PREFIX)) { + throw new AuthException(HttpStatus.UNAUTHORIZED, AuthErrorMessage.UNAUTHORIZED); + } + + String token = extractToken(authorizationHeader); + memberService.checkAuthentication(token); + return true; + } + + private String extractToken(String authorizationHeader) { + return authorizationHeader.substring(7); + } +} diff --git a/backend/src/main/java/mouda/backend/common/exception/ErrorResponse.java b/backend/src/main/java/mouda/backend/common/exception/ErrorResponse.java new file mode 100644 index 000000000..950fa1cd5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/exception/ErrorResponse.java @@ -0,0 +1,6 @@ +package mouda.backend.common.exception; + +public record ErrorResponse( + String message +) { +} diff --git a/backend/src/main/java/mouda/backend/common/exception/GlobalExceptionHandler.java b/backend/src/main/java/mouda/backend/common/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..91f252097 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,62 @@ +package mouda.backend.common.exception; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + @Override + protected ResponseEntity handleMissingServletRequestParameter( + MissingServletRequestParameterException exception, + HttpHeaders headers, HttpStatusCode status, WebRequest request + ) { + return ResponseEntity.badRequest().body(new ErrorResponse(exception.getParameterName() + "은 NULL일 수 없습니다.")); + } + + @Override + protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException exception, + HttpHeaders headers, HttpStatusCode status, WebRequest request) { + String error = exception.getBindingResult().getFieldErrors().get(0).getDefaultMessage(); + + return ResponseEntity.badRequest().body(new ErrorResponse(error)); + } + + @ExceptionHandler(MoudaException.class) + public ResponseEntity handleMoudaException(MoudaException exception) { + StackTraceElement[] stackTrace = exception.getStackTrace(); + String className = stackTrace[0].getClassName(); + String methodName = stackTrace[0].getMethodName(); + + String exceptionMessage = exception.getMessage(); + + log.info("Exception occurred in class = {}, method = {}, message = {}, exception class = {}", + className, methodName, exceptionMessage, exception.getClass().getCanonicalName()); + + return ResponseEntity.status(exception.getHttpStatus()).body(new ErrorResponse(exception.getMessage())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception exception) { + StackTraceElement[] stackTrace = exception.getStackTrace(); + String className = stackTrace[0].getClassName(); + String methodName = stackTrace[0].getMethodName(); + + String exceptionMessage = exception.getMessage(); + + log.warn("Exception occurred in class = {}, method = {}, message = {}, exception class = {}", + className, methodName, exceptionMessage, exception.getClass().getCanonicalName()); + + return ResponseEntity.internalServerError().body(new ErrorResponse("서버 오류가 발생했습니다.")); + } +} diff --git a/backend/src/main/java/mouda/backend/common/exception/MoudaException.java b/backend/src/main/java/mouda/backend/common/exception/MoudaException.java new file mode 100644 index 000000000..5e8285117 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/exception/MoudaException.java @@ -0,0 +1,14 @@ +package mouda.backend.common.exception; + +import org.springframework.http.HttpStatus; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class MoudaException extends RuntimeException { + + private HttpStatus httpStatus; + private String message; +} diff --git a/backend/src/main/java/mouda/backend/common/response/RestResponse.java b/backend/src/main/java/mouda/backend/common/response/RestResponse.java new file mode 100644 index 000000000..b6bd91715 --- /dev/null +++ b/backend/src/main/java/mouda/backend/common/response/RestResponse.java @@ -0,0 +1,11 @@ +package mouda.backend.common.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class RestResponse { + + private T data; +} diff --git a/backend/src/main/java/mouda/backend/darakbang/business/DarakbangService.java b/backend/src/main/java/mouda/backend/darakbang/business/DarakbangService.java new file mode 100644 index 000000000..b8f9379cc --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/business/DarakbangService.java @@ -0,0 +1,78 @@ +package mouda.backend.darakbang.business; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.domain.Darakbangs; +import mouda.backend.darakbang.implement.DarakbangFinder; +import mouda.backend.darakbang.implement.DarakbangValidator; +import mouda.backend.darakbang.implement.DarakbangWriter; +import mouda.backend.darakbang.presentation.request.DarakbangCreateRequest; +import mouda.backend.darakbang.presentation.request.DarakbangEnterRequest; +import mouda.backend.darakbang.presentation.response.CodeValidationResponse; +import mouda.backend.darakbang.presentation.response.DarakbangNameResponse; +import mouda.backend.darakbang.presentation.response.DarakbangResponses; +import mouda.backend.darakbang.presentation.response.InvitationCodeResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.implement.DarakbangMemberWriter; +import mouda.backend.member.domain.Member; +import mouda.backend.member.implement.MemberValidator; + +@Service +@Transactional +@RequiredArgsConstructor +public class DarakbangService { + + private final DarakbangWriter darakbangWriter; + private final DarakbangFinder darakbangFinder; + private final DarakbangValidator darakbangValidator; + private final DarakbangMemberWriter darakbangMemberWriter; + private final MemberValidator memberValidator; + + public Darakbang createDarakbang(DarakbangCreateRequest darakbangCreateRequest, Member member) { + darakbangValidator.validateAlreadyExistsName(darakbangCreateRequest.name()); + Darakbang darakbang = darakbangWriter.save(darakbangCreateRequest.name()); + darakbangMemberWriter.saveManager(darakbang, darakbangCreateRequest.nickname(), member); + + return darakbang; + } + + @Transactional(readOnly = true) + public DarakbangResponses findAllMyDarakbangs(Member member) { + Darakbangs darakbangs = darakbangFinder.findAllMyDarakbangs(member); + + return DarakbangResponses.toResponse(darakbangs); + } + + @Transactional(readOnly = true) + public InvitationCodeResponse findInvitationCode(Long darakbangId, DarakbangMember member) { + memberValidator.validateNotManager(member); + Darakbang darakbang = darakbangFinder.findById(darakbangId); + + return InvitationCodeResponse.toResponse(darakbang); + } + + @Transactional(readOnly = true) + public CodeValidationResponse validateCode(String code) { + Darakbang darakbang = darakbangFinder.findByCode(code); + + return CodeValidationResponse.toResponse(darakbang); + } + + public Darakbang enter(String code, DarakbangEnterRequest request, Member member) { + Darakbang darakbang = darakbangFinder.findByCode(code); + darakbangValidator.validateCanEnterDarakbang(darakbang, request.nickname(), member); + darakbangMemberWriter.saveMember(darakbang, request.nickname(), member); + + return darakbang; + } + + @Transactional(readOnly = true) + public DarakbangNameResponse findDarakbangName(Long darakbangId) { + Darakbang darakbang = darakbangFinder.findById(darakbangId); + + return DarakbangNameResponse.toResponse(darakbang); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbang/domain/Darakbang.java b/backend/src/main/java/mouda/backend/darakbang/domain/Darakbang.java new file mode 100644 index 000000000..b0b4770c8 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/domain/Darakbang.java @@ -0,0 +1,62 @@ +package mouda.backend.darakbang.domain; + +import java.util.Objects; + +import org.springframework.http.HttpStatus; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mouda.backend.darakbang.exception.DarakbangErrorMessage; +import mouda.backend.darakbang.exception.DarakbangException; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "darakbang") +public class Darakbang { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + @Column(nullable = false, unique = true) + private String code; + + @Builder + public Darakbang(String name, String code) { + validateName(name); + this.name = name; + this.code = code; + } + + private void validateName(String name) { + if (name == null || name.isBlank()) { + throw new DarakbangException(HttpStatus.BAD_REQUEST, DarakbangErrorMessage.NAME_NOT_EXIST); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Darakbang darakbang = (Darakbang)o; + return Objects.equals(id, darakbang.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbang/domain/Darakbangs.java b/backend/src/main/java/mouda/backend/darakbang/domain/Darakbangs.java new file mode 100644 index 000000000..0b5d914bf --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/domain/Darakbangs.java @@ -0,0 +1,16 @@ +package mouda.backend.darakbang.domain; + +import java.util.List; + +public class Darakbangs { + + private final List darakbangs; + + public Darakbangs(List darakbangs) { + this.darakbangs = darakbangs; + } + + public List getDarakbangs() { + return darakbangs; + } +} diff --git a/backend/src/main/java/mouda/backend/darakbang/exception/DarakbangErrorMessage.java b/backend/src/main/java/mouda/backend/darakbang/exception/DarakbangErrorMessage.java new file mode 100644 index 000000000..ea607900d --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/exception/DarakbangErrorMessage.java @@ -0,0 +1,18 @@ +package mouda.backend.darakbang.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum DarakbangErrorMessage { + + NAME_ALREADY_EXIST("이미 존재하는 다락방 이름입니다."), + NAME_NOT_EXIST("다락방 이름이 존재하지 않습니다."), + INVALID_CODE("유효하지 않은 초대코드입니다."), + CODE_ALREADY_EXIST("이미 존재하는 초대코드입니다."), + DARAKBANG_NOT_FOUND("다락방이 존재하지 않습니다."), + ; + + private final String message; +} diff --git a/backend/src/main/java/mouda/backend/darakbang/exception/DarakbangException.java b/backend/src/main/java/mouda/backend/darakbang/exception/DarakbangException.java new file mode 100644 index 000000000..24d21baca --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/exception/DarakbangException.java @@ -0,0 +1,12 @@ +package mouda.backend.darakbang.exception; + +import org.springframework.http.HttpStatus; + +import mouda.backend.common.exception.MoudaException; + +public class DarakbangException extends MoudaException { + + public DarakbangException(HttpStatus httpStatus, DarakbangErrorMessage message) { + super(httpStatus, message.getMessage()); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbang/implement/DarakbangFinder.java b/backend/src/main/java/mouda/backend/darakbang/implement/DarakbangFinder.java new file mode 100644 index 000000000..27fb6f62d --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/implement/DarakbangFinder.java @@ -0,0 +1,38 @@ +package mouda.backend.darakbang.implement; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.domain.Darakbangs; +import mouda.backend.darakbang.exception.DarakbangErrorMessage; +import mouda.backend.darakbang.exception.DarakbangException; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; +import mouda.backend.darakbangmember.implement.DarakbangMemberFinder; +import mouda.backend.member.domain.Member; + +@Component +@RequiredArgsConstructor +public class DarakbangFinder { + + private final DarakbangRepository darakbangRepository; + private final DarakbangMemberFinder darakbangMemberFinder; + + public Darakbangs findAllMyDarakbangs(Member member) { + List darakbangs = darakbangMemberFinder.findAllByMember(member); + return new Darakbangs(darakbangs); + } + + public Darakbang findById(Long darakbangId) { + return darakbangRepository.findById(darakbangId) + .orElseThrow(() -> new DarakbangException(HttpStatus.NOT_FOUND, DarakbangErrorMessage.DARAKBANG_NOT_FOUND)); + } + + public Darakbang findByCode(String code) { + return darakbangRepository.findByCode(code) + .orElseThrow(() -> new DarakbangException(HttpStatus.NOT_FOUND, DarakbangErrorMessage.DARAKBANG_NOT_FOUND)); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbang/implement/DarakbangValidator.java b/backend/src/main/java/mouda/backend/darakbang/implement/DarakbangValidator.java new file mode 100644 index 000000000..c5e0e3b51 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/implement/DarakbangValidator.java @@ -0,0 +1,45 @@ +package mouda.backend.darakbang.implement; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.exception.DarakbangErrorMessage; +import mouda.backend.darakbang.exception.DarakbangException; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; +import mouda.backend.darakbangmember.exception.DarakbangMemberErrorMessage; +import mouda.backend.darakbangmember.exception.DarakbangMemberException; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.member.domain.Member; + +@Component +@RequiredArgsConstructor +public class DarakbangValidator { + + private final DarakbangRepository darakbangRepository; + private final DarakbangMemberRepository darakbangMemberRepository; + + public void validateAlreadyExistsName(String name) { + if (darakbangRepository.existsByName(name)) { + throw new DarakbangException(HttpStatus.BAD_REQUEST, DarakbangErrorMessage.NAME_ALREADY_EXIST); + } + } + + public void validateCanEnterDarakbang(Darakbang darakbang, String nickname, Member member) { + if (darakbangMemberRepository.existsByDarakbangIdAndNickname(darakbang.getId(), nickname)) { + throw new DarakbangMemberException(HttpStatus.BAD_REQUEST, + DarakbangMemberErrorMessage.NICKNAME_ALREADY_EXIST); + } + if (darakbangMemberRepository.existsByDarakbangIdAndMemberId(darakbang.getId(), member.getId())) { + throw new DarakbangMemberException(HttpStatus.BAD_REQUEST, + DarakbangMemberErrorMessage.MEMBER_ALREADY_EXIST); + } + } + + public void validateAlreadyExistsCode(String code) { + if (darakbangRepository.existsByCode(code.toString())) { + throw new DarakbangException(HttpStatus.INTERNAL_SERVER_ERROR, DarakbangErrorMessage.CODE_ALREADY_EXIST); + } + } +} diff --git a/backend/src/main/java/mouda/backend/darakbang/implement/DarakbangWriter.java b/backend/src/main/java/mouda/backend/darakbang/implement/DarakbangWriter.java new file mode 100644 index 000000000..0fc00e576 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/implement/DarakbangWriter.java @@ -0,0 +1,23 @@ +package mouda.backend.darakbang.implement; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; + +@Component +@RequiredArgsConstructor +public class DarakbangWriter { + + private final DarakbangRepository darakbangRepository; + private final InvitationCodeGenerator invitationCodeGenerator; + + public Darakbang save(String name) { + Darakbang entity = Darakbang.builder() + .code(invitationCodeGenerator.generate()) + .name(name) + .build(); + return darakbangRepository.save(entity); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbang/implement/InvitationCodeGenerator.java b/backend/src/main/java/mouda/backend/darakbang/implement/InvitationCodeGenerator.java new file mode 100644 index 000000000..b08b4585c --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/implement/InvitationCodeGenerator.java @@ -0,0 +1,37 @@ +package mouda.backend.darakbang.implement; + +import java.security.SecureRandom; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class InvitationCodeGenerator { + + private static final int CODE_LENGTH = 7; + private static final int NUMBER_UPPER_BOUND = 10; + private static final int CHAR_RANDOM_BOUND = 26; + + private final SecureRandom secureRandom = new SecureRandom(); + private final DarakbangValidator darakbangValidator; + + public String generate() { + StringBuilder code = new StringBuilder(); + + for (int i = 0; i < CODE_LENGTH; i++) { + if (secureRandom.nextBoolean()) { + code.append(secureRandom.nextInt(NUMBER_UPPER_BOUND)); + } else { + code.append(getRandomUpperAlphabet()); + } + } + darakbangValidator.validateAlreadyExistsCode(code.toString()); + return code.toString(); + } + + private char getRandomUpperAlphabet() { + return (char)(secureRandom.nextInt(CHAR_RANDOM_BOUND) + 65); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbang/infrastructure/DarakbangRepository.java b/backend/src/main/java/mouda/backend/darakbang/infrastructure/DarakbangRepository.java new file mode 100644 index 000000000..56356e1db --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/infrastructure/DarakbangRepository.java @@ -0,0 +1,18 @@ +package mouda.backend.darakbang.infrastructure; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import mouda.backend.darakbang.domain.Darakbang; + +@Repository +public interface DarakbangRepository extends JpaRepository { + + boolean existsByName(String name); + + boolean existsByCode(String code); + + Optional findByCode(String code); +} diff --git a/backend/src/main/java/mouda/backend/darakbang/presentation/controller/DarakbangController.java b/backend/src/main/java/mouda/backend/darakbang/presentation/controller/DarakbangController.java new file mode 100644 index 000000000..cf1d6515e --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/presentation/controller/DarakbangController.java @@ -0,0 +1,93 @@ +package mouda.backend.darakbang.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.config.argumentresolver.LoginMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbang.business.DarakbangService; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.presentation.controller.swagger.DarakbangSwagger; +import mouda.backend.darakbang.presentation.request.DarakbangCreateRequest; +import mouda.backend.darakbang.presentation.request.DarakbangEnterRequest; +import mouda.backend.darakbang.presentation.response.CodeValidationResponse; +import mouda.backend.darakbang.presentation.response.DarakbangNameResponse; +import mouda.backend.darakbang.presentation.response.DarakbangResponses; +import mouda.backend.darakbang.presentation.response.InvitationCodeResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.member.domain.Member; + +@RestController +@RequestMapping("/v1/darakbang") +@RequiredArgsConstructor +public class DarakbangController implements DarakbangSwagger { + + private final DarakbangService darakbangService; + + @Override + @PostMapping + public ResponseEntity> createDarakbang( + @Valid @RequestBody DarakbangCreateRequest darakbangCreateRequest, + @LoginMember Member member + ) { + Darakbang darakbang = darakbangService.createDarakbang(darakbangCreateRequest, member); + + return ResponseEntity.ok(new RestResponse<>(darakbang.getId())); + } + + @Override + @GetMapping("/mine") + public ResponseEntity> findAllMyDarakbangs(@LoginMember Member member) { + DarakbangResponses darakbangResponses = darakbangService.findAllMyDarakbangs(member); + + return ResponseEntity.ok(new RestResponse<>(darakbangResponses)); + } + + @Override + @GetMapping("/{darakbangId}/code") + public ResponseEntity> findInvitationCode( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member + ) { + InvitationCodeResponse invitationCodeResponse = darakbangService.findInvitationCode(darakbangId, member); + + return ResponseEntity.ok(new RestResponse<>(invitationCodeResponse)); + } + + @Override + @GetMapping("/validation") + public ResponseEntity> validateInvitationCode(@RequestParam String code) { + CodeValidationResponse codeValidationResponse = darakbangService.validateCode(code); + + return ResponseEntity.ok(new RestResponse<>(codeValidationResponse)); + } + + @Override + @PostMapping("/entrance") + public ResponseEntity> enterDarakbang( + @RequestParam String code, + @RequestBody DarakbangEnterRequest darakbangEnterRequest, + @LoginMember Member member + ) { + Darakbang darakbang = darakbangService.enter(code, darakbangEnterRequest, member); + + return ResponseEntity.ok(new RestResponse<>(darakbang.getId())); + } + + @Override + @GetMapping("/{darakbangId}") + public ResponseEntity> findDarakbangName(@PathVariable Long darakbangId) { + DarakbangNameResponse darakbangNameResponse = darakbangService.findDarakbangName(darakbangId); + + return ResponseEntity.ok(new RestResponse<>(darakbangNameResponse)); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbang/presentation/controller/swagger/DarakbangSwagger.java b/backend/src/main/java/mouda/backend/darakbang/presentation/controller/swagger/DarakbangSwagger.java new file mode 100644 index 000000000..35e90aea4 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/presentation/controller/swagger/DarakbangSwagger.java @@ -0,0 +1,85 @@ +package mouda.backend.darakbang.presentation.controller.swagger; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.config.argumentresolver.LoginMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbang.presentation.request.DarakbangCreateRequest; +import mouda.backend.darakbang.presentation.request.DarakbangEnterRequest; +import mouda.backend.darakbang.presentation.response.CodeValidationResponse; +import mouda.backend.darakbang.presentation.response.DarakbangNameResponse; +import mouda.backend.darakbang.presentation.response.DarakbangResponses; +import mouda.backend.darakbang.presentation.response.InvitationCodeResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.member.domain.Member; + +public interface DarakbangSwagger { + + @Operation(summary = "다락방 생성", description = "다락방을 생성한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "다락방 생성 성공!"), + @ApiResponse(responseCode = "400", description = "이미 존재하는 다락방 이름입니다.") + }) + ResponseEntity> createDarakbang( + @RequestBody DarakbangCreateRequest darakbangCreateRequest, + @LoginMember Member member + ); + + @Operation(summary = "다락방 목록 조회", description = "참여한 다락방 목록을 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "다락방 목록 조회 성공!") + }) + ResponseEntity> findAllMyDarakbangs( + @LoginMember Member member + ); + + @Operation(summary = "다락방 참여코드 조회", description = "참여한 다락방 참여코드를 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "다락방 참여코드 조회 성공!"), + @ApiResponse(responseCode = "403", description = "조회 권한이 없습니다."), + @ApiResponse(responseCode = "404", description = "존재하지 않는 다락방 멤버입니다."), + @ApiResponse(responseCode = "404", description = "다락방이 존재하지 않습니다."), + }) + ResponseEntity> findInvitationCode( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member + ); + + @Operation(summary = "다락방 초대코드 유효성 검사", description = "다락방 초대코드 유효성을 검사한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "다락방 초대코드 유효성 검사 성공!"), + @ApiResponse(responseCode = "400", description = "유효하지 않은 초대코드입니다.") + }) + ResponseEntity> validateInvitationCode( + @RequestParam String code + ); + + @Operation(summary = "다락방 참여", description = "다락방에 참여한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "다락방 참여 성공!"), + @ApiResponse(responseCode = "400", description = "이미 존재하는 닉네임입니다."), + @ApiResponse(responseCode = "400", description = "이미 가입한 멤버입니다."), + @ApiResponse(responseCode = "404", description = "다락방이 존재하지 않습니다."), + }) + ResponseEntity> enterDarakbang( + @RequestParam String code, + @RequestBody DarakbangEnterRequest darakbangEnterRequest, + @LoginMember Member member + ); + + @Operation(summary = "다락방 이름 조회", description = "다락방 이름을 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "다락방 이름 조회 성공!"), + @ApiResponse(responseCode = "404", description = "다락방이 존재하지 않습니다.") + }) + ResponseEntity> findDarakbangName( + @PathVariable Long darakbangId + ); +} diff --git a/backend/src/main/java/mouda/backend/darakbang/presentation/request/DarakbangCreateRequest.java b/backend/src/main/java/mouda/backend/darakbang/presentation/request/DarakbangCreateRequest.java new file mode 100644 index 000000000..ad633d1ff --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/presentation/request/DarakbangCreateRequest.java @@ -0,0 +1,12 @@ +package mouda.backend.darakbang.presentation.request; + +import jakarta.validation.constraints.NotNull; + +public record DarakbangCreateRequest( + @NotNull + String name, + + @NotNull + String nickname +) { +} diff --git a/backend/src/main/java/mouda/backend/darakbang/presentation/request/DarakbangEnterRequest.java b/backend/src/main/java/mouda/backend/darakbang/presentation/request/DarakbangEnterRequest.java new file mode 100644 index 000000000..e395a4c73 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/presentation/request/DarakbangEnterRequest.java @@ -0,0 +1,9 @@ +package mouda.backend.darakbang.presentation.request; + +import jakarta.validation.constraints.NotNull; + +public record DarakbangEnterRequest( + @NotNull + String nickname +) { +} diff --git a/backend/src/main/java/mouda/backend/darakbang/presentation/response/CodeValidationResponse.java b/backend/src/main/java/mouda/backend/darakbang/presentation/response/CodeValidationResponse.java new file mode 100644 index 000000000..7e2dc3238 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/presentation/response/CodeValidationResponse.java @@ -0,0 +1,18 @@ +package mouda.backend.darakbang.presentation.response; + +import lombok.Builder; +import mouda.backend.darakbang.domain.Darakbang; + +@Builder +public record CodeValidationResponse( + Long darakbangId, + String name +) { + + public static CodeValidationResponse toResponse(Darakbang darakbang) { + return CodeValidationResponse.builder() + .darakbangId(darakbang.getId()) + .name(darakbang.getName()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbang/presentation/response/DarakbangNameResponse.java b/backend/src/main/java/mouda/backend/darakbang/presentation/response/DarakbangNameResponse.java new file mode 100644 index 000000000..43652408f --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/presentation/response/DarakbangNameResponse.java @@ -0,0 +1,16 @@ +package mouda.backend.darakbang.presentation.response; + +import lombok.Builder; +import mouda.backend.darakbang.domain.Darakbang; + +@Builder +public record DarakbangNameResponse( + String name +) { + + public static DarakbangNameResponse toResponse(Darakbang darakbang) { + return DarakbangNameResponse.builder() + .name(darakbang.getName()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbang/presentation/response/DarakbangResponse.java b/backend/src/main/java/mouda/backend/darakbang/presentation/response/DarakbangResponse.java new file mode 100644 index 000000000..636261800 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/presentation/response/DarakbangResponse.java @@ -0,0 +1,17 @@ +package mouda.backend.darakbang.presentation.response; + +import lombok.Builder; + +@Builder +public record DarakbangResponse( + long darakbangId, + String name +) { + + public static DarakbangResponse toResponse(Long darakbangId, String name) { + return DarakbangResponse.builder() + .darakbangId(darakbangId) + .name(name) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbang/presentation/response/DarakbangResponses.java b/backend/src/main/java/mouda/backend/darakbang/presentation/response/DarakbangResponses.java new file mode 100644 index 000000000..2f297929c --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/presentation/response/DarakbangResponses.java @@ -0,0 +1,21 @@ +package mouda.backend.darakbang.presentation.response; + +import java.util.List; + +import lombok.Builder; +import mouda.backend.darakbang.domain.Darakbangs; + +@Builder +public record DarakbangResponses( + List darakbangResponses +) { + + public static DarakbangResponses toResponse(Darakbangs darakbangs) { + List responses = darakbangs.getDarakbangs() + .stream() + .map(darakbang -> DarakbangResponse.toResponse( + darakbang.getId(), darakbang.getName())) + .toList(); + return new DarakbangResponses(responses); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbang/presentation/response/InvitationCodeResponse.java b/backend/src/main/java/mouda/backend/darakbang/presentation/response/InvitationCodeResponse.java new file mode 100644 index 000000000..ec8508f7d --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbang/presentation/response/InvitationCodeResponse.java @@ -0,0 +1,16 @@ +package mouda.backend.darakbang.presentation.response; + +import lombok.Builder; +import mouda.backend.darakbang.domain.Darakbang; + +@Builder +public record InvitationCodeResponse( + String code +) { + + public static InvitationCodeResponse toResponse(Darakbang darakbang) { + return InvitationCodeResponse.builder() + .code(darakbang.getCode()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/business/DarakbangMemberService.java b/backend/src/main/java/mouda/backend/darakbangmember/business/DarakbangMemberService.java new file mode 100644 index 000000000..5f7bae81c --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/business/DarakbangMemberService.java @@ -0,0 +1,94 @@ +package mouda.backend.darakbangmember.business; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.implement.DarakbangFinder; +import mouda.backend.darakbangmember.domain.DarakBangMemberRole; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.domain.DarakbangMembers; +import mouda.backend.darakbangmember.implement.DarakbangMemberFinder; +import mouda.backend.darakbangmember.implement.DarakbangMemberWriter; +import mouda.backend.darakbangmember.implement.ImageParser; +import mouda.backend.darakbangmember.implement.S3Client; +import mouda.backend.darakbangmember.presentation.response.DarakbangMemberProfileResponse; +import mouda.backend.darakbangmember.presentation.response.DarakbangMemberResponses; +import mouda.backend.darakbangmember.presentation.response.DarakbangMemberRoleResponse; +import mouda.backend.member.domain.Member; +import mouda.backend.member.implement.MemberFinder; +import mouda.backend.member.presentation.response.DarakbangMemberInfoResponse; + +@Service +@Transactional +@RequiredArgsConstructor +public class DarakbangMemberService { + + private final DarakbangMemberFinder darakbangMemberFinder; + private final DarakbangFinder darakbangFinder; + private final MemberFinder memberFinder; + private final DarakbangMemberWriter darakbangMemberWriter; + private final S3Client s3Client; + private final ImageParser imageParser; + + @Transactional(readOnly = true) + public DarakbangMemberResponses findAllDarakbangMembers(Long darakbangId, DarakbangMember member) { + DarakbangMembers darakbangMembers = darakbangMemberFinder.findAllDarakbangMembers(darakbangId); + + return DarakbangMemberResponses.toResponse(darakbangMembers); + } + + @Transactional(readOnly = true) + public DarakbangMemberRoleResponse findDarakbangMemberRole(Long darakbangId, Member member) { + DarakBangMemberRole role = darakbangMemberFinder.findDarakbangMemberRole(darakbangId, member.getId()); + + return DarakbangMemberRoleResponse.toResponse(role); + } + + public DarakbangMember findDarakbangMember(long darakbangId, Member member) { + Darakbang darakbang = darakbangFinder.findById(darakbangId); + return darakbangMemberFinder.find(darakbang, member); + } + + @Transactional(readOnly = true) + public DarakbangMemberInfoResponse findMyInfo(DarakbangMember darakbangMember) { + Member member = memberFinder.findByMemberId(darakbangMember.getMemberId()); + return new DarakbangMemberInfoResponse(member.getName(), darakbangMember.getNickname(), + darakbangMember.getProfile(), darakbangMember.getDescription()); + } + + public void updateMyInfo( + DarakbangMember darakbangMember, String isReset, + MultipartFile file, String nickname, String description + ) { + if (file != null) { + String url = s3Client.uploadFile(file); + String newProfileUrl = imageParser.parse(url); + deleteProfile(darakbangMember); + darakbangMemberWriter.updateMyInfo(darakbangMember, nickname, description, newProfileUrl); + return; + } + if (isReset.equals("true")) { + deleteProfile(darakbangMember); + darakbangMemberWriter.updateMyInfo(darakbangMember, nickname, description, null); + } else { + darakbangMemberWriter.updateMyInfo(darakbangMember, nickname, description); + } + } + + private void deleteProfile(DarakbangMember darakbangMember) { + if (darakbangMember.hasImage()) { + s3Client.deleteFile(darakbangMember.getProfile()); + } + } + + @Transactional(readOnly = true) + public DarakbangMemberProfileResponse findProfile(long darakbangMemberId) { + DarakbangMember darakbangMember = darakbangMemberFinder.find(darakbangMemberId); + Member member = memberFinder.findByMemberId(darakbangMember.getMemberId()); + + return DarakbangMemberProfileResponse.toResponse(darakbangMember, member.getName()); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/domain/DarakBangMemberRole.java b/backend/src/main/java/mouda/backend/darakbangmember/domain/DarakBangMemberRole.java new file mode 100644 index 000000000..9917f5d00 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/domain/DarakBangMemberRole.java @@ -0,0 +1,10 @@ +package mouda.backend.darakbangmember.domain; + +import lombok.Getter; + +@Getter +public enum DarakBangMemberRole { + MANAGER, + MEMBER, + OUTSIDER +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/domain/DarakbangMember.java b/backend/src/main/java/mouda/backend/darakbangmember/domain/DarakbangMember.java new file mode 100644 index 000000000..0477af7fa --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/domain/DarakbangMember.java @@ -0,0 +1,140 @@ +package mouda.backend.darakbangmember.domain; + +import java.util.Objects; + +import org.springframework.http.HttpStatus; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mouda.backend.chat.domain.Author; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbangmember.exception.DarakbangMemberErrorMessage; +import mouda.backend.darakbangmember.exception.DarakbangMemberException; + +@Entity +@Getter +@NoArgsConstructor +@Table( + name = "darakbang_member", + uniqueConstraints = { + @UniqueConstraint( + columnNames = {"member_id", "darakbang_id"} + ) + } +) +public class DarakbangMember { + + private static final int MAX_LENGTH = 12; + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @JoinColumn(nullable = false) + @ManyToOne(fetch = FetchType.LAZY) + private Darakbang darakbang; + + @Column(nullable = false) + private Long memberId; + + @Column(nullable = false) + private String nickname; + + private String profile; + + private String description; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private DarakBangMemberRole role; + + @Builder + public DarakbangMember(Darakbang darakbang, Long memberId, String nickname, String profile, String description, + DarakBangMemberRole role) { + validateNickname(nickname); + this.darakbang = darakbang; + this.memberId = memberId; + this.nickname = nickname; + this.profile = profile; + this.description = description; + this.role = role; + } + + private void validateNickname(String nickname) { + if (nickname == null || nickname.isBlank()) { + throw new DarakbangMemberException(HttpStatus.BAD_REQUEST, DarakbangMemberErrorMessage.NICKNAME_NOT_EXIST); + } + if (nickname.length() > MAX_LENGTH) { + throw new DarakbangMemberException(HttpStatus.BAD_REQUEST, DarakbangMemberErrorMessage.INVALID_LENGTH); + } + } + + public boolean isNotManager() { + return role != DarakBangMemberRole.MANAGER; + } + + public DarakbangMember updateMyInfo(String nickname, String description) { + validateNickname(nickname); + this.nickname = nickname; + this.description = description; + + return this; + } + + public DarakbangMember updateMyInfo(String nickname, String description, String profile) { + validateNickname(nickname); + this.nickname = nickname; + this.description = description; + this.profile = profile; + + return this; + } + + public boolean isSameMemberWith(DarakbangMember other) { + return id.equals(other.getId()); + } + + public boolean isNotSameMemberWith(DarakbangMember other) { + return !isSameMemberWith(other); + } + + public boolean hasImage() { + return profile != null; + } + + public Author toAuthor() { + return Author.builder() + .darakbangMemberId(id) + .memberId(memberId) + .nickname(nickname) + .profile(profile) + .build(); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + DarakbangMember that = (DarakbangMember)o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/domain/DarakbangMembers.java b/backend/src/main/java/mouda/backend/darakbangmember/domain/DarakbangMembers.java new file mode 100644 index 000000000..bc16a4425 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/domain/DarakbangMembers.java @@ -0,0 +1,15 @@ +package mouda.backend.darakbangmember.domain; + +import java.util.List; + +public class DarakbangMembers { + private final List darakbangMembers; + + public DarakbangMembers(List darakbangMembers) { + this.darakbangMembers = darakbangMembers; + } + + public List getDarakbangMembers() { + return darakbangMembers; + } +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/exception/DarakbangMemberErrorMessage.java b/backend/src/main/java/mouda/backend/darakbangmember/exception/DarakbangMemberErrorMessage.java new file mode 100644 index 000000000..50b47487b --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/exception/DarakbangMemberErrorMessage.java @@ -0,0 +1,20 @@ +package mouda.backend.darakbangmember.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum DarakbangMemberErrorMessage { + + NICKNAME_NOT_EXIST("닉네임이 존재하지 않습니다."), + INVALID_LENGTH("닉네임은 9글자 이하로만 가능합니다."), + NICKNAME_ALREADY_EXIST("이미 존재하는 닉네임입니다."), + MEMBER_ALREADY_EXIST("이미 가입한 멤버입니다."), + MEMBER_NOT_EXIST("존재하지 않는 다락방 멤버입니다."), + NOT_ALLOWED_TO_READ("조회 권한이 없습니다."), + INVALID_DELETE_FILE("기존 이미지를 삭제할 수 없습니다."), + INVALID_FILE("잘못된 이미지 파일입니다."); + + private final String message; +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/exception/DarakbangMemberException.java b/backend/src/main/java/mouda/backend/darakbangmember/exception/DarakbangMemberException.java new file mode 100644 index 000000000..28b71fc3b --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/exception/DarakbangMemberException.java @@ -0,0 +1,12 @@ +package mouda.backend.darakbangmember.exception; + +import org.springframework.http.HttpStatus; + +import mouda.backend.common.exception.MoudaException; + +public class DarakbangMemberException extends MoudaException { + + public DarakbangMemberException(HttpStatus httpStatus, DarakbangMemberErrorMessage message) { + super(httpStatus, message.getMessage()); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/implement/DarakbangMemberFinder.java b/backend/src/main/java/mouda/backend/darakbangmember/implement/DarakbangMemberFinder.java new file mode 100644 index 000000000..e0118dae0 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/implement/DarakbangMemberFinder.java @@ -0,0 +1,58 @@ +package mouda.backend.darakbangmember.implement; + +import java.util.List; +import java.util.Optional; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbangmember.domain.DarakBangMemberRole; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.domain.DarakbangMembers; +import mouda.backend.darakbangmember.exception.DarakbangMemberErrorMessage; +import mouda.backend.darakbangmember.exception.DarakbangMemberException; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.member.domain.Member; + +@Component +@RequiredArgsConstructor +public class DarakbangMemberFinder { + + private final DarakbangMemberRepository darakbangMemberRepository; + + public DarakbangMember find(Darakbang darakbang, Member member) { + return darakbangMemberRepository.findByDarakbangIdAndMemberId(darakbang.getId(), member.getId()) + .orElseThrow(() -> + new DarakbangMemberException(HttpStatus.UNAUTHORIZED, DarakbangMemberErrorMessage.MEMBER_NOT_EXIST)); + } + + public List findAllByMember(Member member) { + return darakbangMemberRepository.findAllByMemberId(member.getId()) + .stream() + .map(DarakbangMember::getDarakbang) + .toList(); + } + + public DarakbangMembers findAllDarakbangMembers(Long darakbangId) { + return new DarakbangMembers(darakbangMemberRepository.findAllByDarakbangId(darakbangId)); + } + + public DarakBangMemberRole findDarakbangMemberRole(Long darakbangId, Long memberId) { + Optional optionalDarakbangMember = darakbangMemberRepository + .findByDarakbangIdAndMemberId(darakbangId, memberId); + + if (optionalDarakbangMember.isPresent()) { + DarakbangMember darakbangMember = optionalDarakbangMember.get(); + return darakbangMember.getRole(); + } + return DarakBangMemberRole.OUTSIDER; + } + + public DarakbangMember find(long darakbangMemberId) { + return darakbangMemberRepository.findById(darakbangMemberId) + .orElseThrow( + () -> new DarakbangMemberException(HttpStatus.NOT_FOUND, DarakbangMemberErrorMessage.MEMBER_NOT_EXIST)); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/implement/DarakbangMemberWriter.java b/backend/src/main/java/mouda/backend/darakbangmember/implement/DarakbangMemberWriter.java new file mode 100644 index 000000000..6b816456a --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/implement/DarakbangMemberWriter.java @@ -0,0 +1,55 @@ +package mouda.backend.darakbangmember.implement; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbangmember.domain.DarakBangMemberRole; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.exception.DarakbangMemberErrorMessage; +import mouda.backend.darakbangmember.exception.DarakbangMemberException; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.member.domain.Member; + +@Component +@RequiredArgsConstructor +public class DarakbangMemberWriter { + + private final DarakbangMemberRepository darakbangMemberRepository; + + public DarakbangMember saveManager(Darakbang darakbang, String nickname, Member member) { + DarakbangMember darakbangMember = DarakbangMember.builder() + .darakbang(darakbang) + .memberId(member.getId()) + .nickname(nickname) + .role(DarakBangMemberRole.MANAGER) + .build(); + + return darakbangMemberRepository.save(darakbangMember); + } + + public DarakbangMember saveMember(Darakbang darakbang, String nickname, Member member) { + DarakbangMember entity = DarakbangMember.builder() + .darakbang(darakbang) + .memberId(member.getId()) + .nickname(nickname) + .role(DarakBangMemberRole.MEMBER) + .build(); + try { + return darakbangMemberRepository.save(entity); + } catch (DataIntegrityViolationException exception) { + throw new DarakbangMemberException(HttpStatus.BAD_REQUEST, + DarakbangMemberErrorMessage.MEMBER_ALREADY_EXIST); + } + } + + public void updateMyInfo(DarakbangMember darakbangMember, String nickname, String description) { + darakbangMemberRepository.save(darakbangMember.updateMyInfo(nickname, description)); + } + + public void updateMyInfo(DarakbangMember darakbangMember, String nickname, String description, String profile) { + darakbangMemberRepository.save(darakbangMember.updateMyInfo(nickname, description, profile)); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/implement/ImageParser.java b/backend/src/main/java/mouda/backend/darakbangmember/implement/ImageParser.java new file mode 100644 index 000000000..2fe0c3c39 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/implement/ImageParser.java @@ -0,0 +1,30 @@ +package mouda.backend.darakbangmember.implement; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class ImageParser { + + private static final String URL_DELIMITER = "/"; + private static final int PROFILE_START_INDEX = 3; + + @Value("${aws.s3.prefix}") + private String prefix; + + public String parse(String url) { + String[] split = url.split(URL_DELIMITER); + + StringBuilder profile = new StringBuilder(prefix); + for (int index = PROFILE_START_INDEX; index < split.length; index++) { + profile.append(split[index]); + if (index < split.length - 1) { + profile.append(URL_DELIMITER); + } + } + return profile.toString(); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/implement/S3Client.java b/backend/src/main/java/mouda/backend/darakbangmember/implement/S3Client.java new file mode 100644 index 000000000..8abc0317f --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/implement/S3Client.java @@ -0,0 +1,53 @@ +package mouda.backend.darakbangmember.implement; + +import java.io.IOException; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.ObjectMetadata; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.exception.DarakbangMemberErrorMessage; +import mouda.backend.darakbangmember.exception.DarakbangMemberException; + +@Component +@RequiredArgsConstructor +public class S3Client { + + private final AmazonS3 amazonS3; + + @Value("${aws.s3.bucket}") + private String bucket; + + @Value("${aws.s3.key-prefix}") + private String keyPrefix; + + public String uploadFile(MultipartFile file) { + try { + String fileName = UUID.randomUUID().toString(); + String key = keyPrefix + fileName; + + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentLength(file.getSize()); + metadata.setContentType(file.getContentType()); + + amazonS3.putObject(bucket, key, file.getInputStream(), metadata); + return amazonS3.getUrl(bucket, fileName).toString(); + } catch (IOException e) { + throw new DarakbangMemberException(HttpStatus.BAD_REQUEST, DarakbangMemberErrorMessage.INVALID_FILE); + } + } + + public void deleteFile(String fileUrl) { + try { + amazonS3.deleteObject(bucket, fileUrl); + } catch (Exception e) { + throw new DarakbangMemberException(HttpStatus.BAD_REQUEST, DarakbangMemberErrorMessage.INVALID_DELETE_FILE); + } + } +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/infrastructure/DarakbangMemberRepository.java b/backend/src/main/java/mouda/backend/darakbangmember/infrastructure/DarakbangMemberRepository.java new file mode 100644 index 000000000..a009a0ea5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/infrastructure/DarakbangMemberRepository.java @@ -0,0 +1,23 @@ +package mouda.backend.darakbangmember.infrastructure; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Repository +public interface DarakbangMemberRepository extends JpaRepository { + + List findAllByMemberId(long memberId); + + Optional findByDarakbangIdAndMemberId(Long darakbangId, Long id); + + boolean existsByDarakbangIdAndMemberId(Long darakbangId, Long id); + + boolean existsByDarakbangIdAndNickname(Long id, String nickname); + + List findAllByDarakbangId(Long darakbangId); +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/presentation/controller/DarakbangMemberController.java b/backend/src/main/java/mouda/backend/darakbangmember/presentation/controller/DarakbangMemberController.java new file mode 100644 index 000000000..3fcbe913f --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/presentation/controller/DarakbangMemberController.java @@ -0,0 +1,94 @@ +package mouda.backend.darakbangmember.presentation.controller; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.config.argumentresolver.LoginMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.business.DarakbangMemberService; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.presentation.controller.swagger.DarakbangMemberSwagger; +import mouda.backend.darakbangmember.presentation.response.DarakbangMemberProfileResponse; +import mouda.backend.darakbangmember.presentation.response.DarakbangMemberResponses; +import mouda.backend.darakbangmember.presentation.response.DarakbangMemberRoleResponse; +import mouda.backend.member.domain.Member; +import mouda.backend.member.presentation.response.DarakbangMemberInfoResponse; + +@Slf4j +@RestController +@RequestMapping("/v1/darakbang") +@RequiredArgsConstructor +public class DarakbangMemberController implements DarakbangMemberSwagger { + + private final DarakbangMemberService darakbangMemberService; + + @Override + @GetMapping("/{darakbangId}/members") + public ResponseEntity> findAllDarakbangMembers( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember darakbangMember + ) { + DarakbangMemberResponses responses = darakbangMemberService.findAllDarakbangMembers(darakbangId, + darakbangMember); + + return ResponseEntity.ok(new RestResponse<>(responses)); + } + + @Override + @GetMapping("/{darakbangId}/role") + public ResponseEntity> findDarakbangMemberRole( + @PathVariable Long darakbangId, + @LoginMember Member member + ) { + DarakbangMemberRoleResponse response = darakbangMemberService.findDarakbangMemberRole(darakbangId, member); + + return ResponseEntity.ok(new RestResponse<>(response)); + } + + @Override + @GetMapping("/{darakbangId}/member/mine") + public ResponseEntity> findMyInfo( + @LoginDarakbangMember DarakbangMember darakbangMember + ) { + DarakbangMemberInfoResponse darakbangMemberInfoResponse = darakbangMemberService.findMyInfo(darakbangMember); + + return ResponseEntity.ok().body(new RestResponse<>(darakbangMemberInfoResponse)); + } + + @Override + @PostMapping(path = "/{darakbangId}/member/mine", consumes = { + MediaType.MULTIPART_FORM_DATA_VALUE + }) + public ResponseEntity updateMyInfo( + @LoginDarakbangMember DarakbangMember darakbangMember, + @RequestPart(value = "isReset") String isReset, + @RequestPart(value = "file", required = false) MultipartFile file, + @RequestPart("nickname") String nickname, + @RequestPart(value = "description", required = false) String description + ) { + darakbangMemberService.updateMyInfo(darakbangMember, isReset, file, nickname, description); + + return ResponseEntity.ok().build(); + } + + @GetMapping("/{darakbangId}/members/{darakbangMemberId}/profile") + public ResponseEntity> findProfile( + @PathVariable Long darakbangId, + @PathVariable Long darakbangMemberId, + @LoginDarakbangMember DarakbangMember darakbangMember + ) { + DarakbangMemberProfileResponse response = darakbangMemberService.findProfile(darakbangMemberId); + + return ResponseEntity.ok().body(new RestResponse<>(response)); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/presentation/controller/swagger/DarakbangMemberSwagger.java b/backend/src/main/java/mouda/backend/darakbangmember/presentation/controller/swagger/DarakbangMemberSwagger.java new file mode 100644 index 000000000..b28aae545 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/presentation/controller/swagger/DarakbangMemberSwagger.java @@ -0,0 +1,74 @@ +package mouda.backend.darakbangmember.presentation.controller.swagger; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.config.argumentresolver.LoginMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.presentation.response.DarakbangMemberProfileResponse; +import mouda.backend.darakbangmember.presentation.response.DarakbangMemberResponses; +import mouda.backend.darakbangmember.presentation.response.DarakbangMemberRoleResponse; +import mouda.backend.member.domain.Member; +import mouda.backend.member.presentation.response.DarakbangMemberInfoResponse; + +public interface DarakbangMemberSwagger { + + @Operation(summary = "다락방 멤버 목록 조회", description = "다락방 멤버 목록을 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "다락방 멤버 목록 조회 성공!"), + @ApiResponse(responseCode = "403", description = "존재하지 않는 다락방 멤버입니다."), + @ApiResponse(responseCode = "403", description = "조회 권한이 없습니다.") + }) + ResponseEntity> findAllDarakbangMembers( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member + ); + + @Operation(summary = "다락방 멤버 권한 조회", description = "다락방 멤버 권한을 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "다락방 멤버 권한 조회 성공!") + }) + ResponseEntity> findDarakbangMemberRole( + @PathVariable Long darakbangId, + @LoginMember Member member + ); + + @Operation(summary = "마이페이지 조회", description = "마이페이지에 표시될 내 정보를 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "마이페이지 조회 성공!") + }) + ResponseEntity> findMyInfo( + @LoginDarakbangMember DarakbangMember member + ); + + @Operation(summary = "마이페이지 수정", description = "마이페이지에 표시될 내 정보를 수정한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "마이페이지 수정 성공!") + }) + ResponseEntity updateMyInfo( + @LoginDarakbangMember DarakbangMember member, + @RequestPart String isReset, + @RequestPart MultipartFile file, + @RequestPart String nickname, + @RequestPart String description + ); + + @Operation(summary = "다락방 멤버 프로필 조회", description = "다락방 멤버의 프로필을 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "마이페이지 수정 성공!"), + @ApiResponse(responseCode = "404", description = "존재하지 않는 다락방 멤버입니다."), + @ApiResponse(responseCode = "404", description = "회원가입 이력을 찾을 수 없습니다.") + }) + ResponseEntity> findProfile( + @PathVariable Long darakbangId, + @PathVariable Long darakbangMemberId, + @LoginDarakbangMember DarakbangMember darakbangMember + ); +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/presentation/request/DarakbangMemberInfoRequest.java b/backend/src/main/java/mouda/backend/darakbangmember/presentation/request/DarakbangMemberInfoRequest.java new file mode 100644 index 000000000..2d83cd422 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/presentation/request/DarakbangMemberInfoRequest.java @@ -0,0 +1,7 @@ +package mouda.backend.darakbangmember.presentation.request; + +public record DarakbangMemberInfoRequest( + String nickname, + String description +) { +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/presentation/response/DarakbangMemberProfileResponse.java b/backend/src/main/java/mouda/backend/darakbangmember/presentation/response/DarakbangMemberProfileResponse.java new file mode 100644 index 000000000..95cf041e1 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/presentation/response/DarakbangMemberProfileResponse.java @@ -0,0 +1,22 @@ +package mouda.backend.darakbangmember.presentation.response; + +import lombok.Builder; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Builder +public record DarakbangMemberProfileResponse( + String name, + String nickname, + String profile, + String description +) { + + public static DarakbangMemberProfileResponse toResponse(DarakbangMember darakbangMember, String name) { + return DarakbangMemberProfileResponse.builder() + .name(name) + .nickname(darakbangMember.getNickname()) + .profile(darakbangMember.getProfile()) + .description(darakbangMember.getDescription()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/presentation/response/DarakbangMemberResponse.java b/backend/src/main/java/mouda/backend/darakbangmember/presentation/response/DarakbangMemberResponse.java new file mode 100644 index 000000000..51ce4a42d --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/presentation/response/DarakbangMemberResponse.java @@ -0,0 +1,20 @@ +package mouda.backend.darakbangmember.presentation.response; + +import lombok.Builder; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Builder +public record DarakbangMemberResponse( + long darakbangMemberId, + String nickname, + String profile +) { + + public static DarakbangMemberResponse toResponse(DarakbangMember darakbangMember) { + return DarakbangMemberResponse.builder() + .darakbangMemberId(darakbangMember.getId()) + .nickname(darakbangMember.getNickname()) + .profile(darakbangMember.getProfile()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/presentation/response/DarakbangMemberResponses.java b/backend/src/main/java/mouda/backend/darakbangmember/presentation/response/DarakbangMemberResponses.java new file mode 100644 index 000000000..ba449a319 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/presentation/response/DarakbangMemberResponses.java @@ -0,0 +1,21 @@ +package mouda.backend.darakbangmember.presentation.response; + +import java.util.List; + +import lombok.Builder; +import mouda.backend.darakbangmember.domain.DarakbangMembers; + +@Builder +public record DarakbangMemberResponses( + List responses +) { + + public static DarakbangMemberResponses toResponse(DarakbangMembers darakbangMembers) { + return DarakbangMemberResponses.builder().responses( + darakbangMembers.getDarakbangMembers() + .stream() + .map(DarakbangMemberResponse::toResponse) + .toList() + ).build(); + } +} diff --git a/backend/src/main/java/mouda/backend/darakbangmember/presentation/response/DarakbangMemberRoleResponse.java b/backend/src/main/java/mouda/backend/darakbangmember/presentation/response/DarakbangMemberRoleResponse.java new file mode 100644 index 000000000..8a52ef9e4 --- /dev/null +++ b/backend/src/main/java/mouda/backend/darakbangmember/presentation/response/DarakbangMemberRoleResponse.java @@ -0,0 +1,16 @@ +package mouda.backend.darakbangmember.presentation.response; + +import lombok.Builder; +import mouda.backend.darakbangmember.domain.DarakBangMemberRole; + +@Builder +public record DarakbangMemberRoleResponse( + String role +) { + + public static DarakbangMemberRoleResponse toResponse(DarakBangMemberRole role) { + return DarakbangMemberRoleResponse.builder() + .role(role.name()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/member/business/MemberService.java b/backend/src/main/java/mouda/backend/member/business/MemberService.java new file mode 100644 index 000000000..fd7d6be3c --- /dev/null +++ b/backend/src/main/java/mouda/backend/member/business/MemberService.java @@ -0,0 +1,33 @@ +package mouda.backend.member.business; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.auth.implement.jwt.AccessTokenProvider; +import mouda.backend.member.domain.Member; +import mouda.backend.member.implement.MemberFinder; +import mouda.backend.member.implement.MemberWriter; + +@Service +@Transactional +@RequiredArgsConstructor +public class MemberService { + + private final AccessTokenProvider accessTokenProvider; + private final MemberFinder memberFinder; + private final MemberWriter memberWriter; + + public Member findMember(String token) { + String socialId = accessTokenProvider.extractSocialId(token); + return memberFinder.findActiveOrDeletedByIdentifier(socialId); + } + + public void checkAuthentication(String token) { + accessTokenProvider.validateToken(token); + } + + public void withdraw(Member member) { + memberWriter.withdraw(member); + } +} diff --git a/backend/src/main/java/mouda/backend/member/domain/LoginDetail.java b/backend/src/main/java/mouda/backend/member/domain/LoginDetail.java new file mode 100644 index 000000000..2b09c7864 --- /dev/null +++ b/backend/src/main/java/mouda/backend/member/domain/LoginDetail.java @@ -0,0 +1,41 @@ +package mouda.backend.member.domain; + +import java.util.Objects; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.Getter; + +@Getter +@Embeddable +public class LoginDetail { + + @Enumerated(EnumType.STRING) + private OauthType oauthType; + + private String identifier; + + protected LoginDetail() { + } + + public LoginDetail(OauthType oauthType, String identifier) { + this.oauthType = oauthType; + this.identifier = identifier; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + LoginDetail that = (LoginDetail)o; + return oauthType == that.oauthType && Objects.equals(identifier, that.identifier); + } + + @Override + public int hashCode() { + return Objects.hash(oauthType, identifier); + } +} diff --git a/backend/src/main/java/mouda/backend/member/domain/Member.java b/backend/src/main/java/mouda/backend/member/domain/Member.java new file mode 100644 index 000000000..03d950d01 --- /dev/null +++ b/backend/src/main/java/mouda/backend/member/domain/Member.java @@ -0,0 +1,102 @@ +package mouda.backend.member.domain; + +import java.util.Objects; + +import org.springframework.http.HttpStatus; + +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mouda.backend.member.exception.MemberErrorMessage; +import mouda.backend.member.exception.MemberException; + +@Entity +@Getter +@NoArgsConstructor +@Table(name = "member") +public class Member { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + @Embedded + private LoginDetail loginDetail; + + @Enumerated(EnumType.STRING) + private MemberStatus memberStatus; + + private boolean isConverted; + + @Builder + public Member(String name, LoginDetail loginDetail) { + this.loginDetail = loginDetail; + validateName(name); + this.name = name; + this.memberStatus = MemberStatus.ACTIVE; + this.isConverted = false; + } + + private void validateName(String name) { + if (name.isBlank()) { + throw new MemberException(HttpStatus.BAD_REQUEST, MemberErrorMessage.MEMBER_NAME_NOT_EXISTS); + } + } + + public String getIdentifier() { + return loginDetail.getIdentifier(); + } + + public OauthType getOauthType() { + return loginDetail.getOauthType(); + } + + public void withdraw() { + this.memberStatus = MemberStatus.DELETED; + } + + public boolean isDeleted() { + return MemberStatus.DELETED.equals(this.memberStatus); + } + + public void rejoin() { + this.memberStatus = MemberStatus.ACTIVE; + } + + public void convert() { + this.isConverted = true; + } + + public void deprecate() { + this.memberStatus = MemberStatus.DEPRECATED; + } + + public void updateLoginDetail(LoginDetail loginDetail) { + this.loginDetail = loginDetail; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Member member = (Member)o; + return Objects.equals(id, member.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/backend/src/main/java/mouda/backend/member/domain/MemberStatus.java b/backend/src/main/java/mouda/backend/member/domain/MemberStatus.java new file mode 100644 index 000000000..868f6df09 --- /dev/null +++ b/backend/src/main/java/mouda/backend/member/domain/MemberStatus.java @@ -0,0 +1,8 @@ +package mouda.backend.member.domain; + +public enum MemberStatus { + + ACTIVE, + DELETED, + DEPRECATED +} diff --git a/backend/src/main/java/mouda/backend/member/domain/OauthType.java b/backend/src/main/java/mouda/backend/member/domain/OauthType.java new file mode 100644 index 000000000..4e0b625e5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/member/domain/OauthType.java @@ -0,0 +1,9 @@ +package mouda.backend.member.domain; + +public enum OauthType { + + KAKAO, + APPLE, + GOOGLE, + ; +} diff --git a/backend/src/main/java/mouda/backend/member/exception/MemberErrorMessage.java b/backend/src/main/java/mouda/backend/member/exception/MemberErrorMessage.java new file mode 100644 index 000000000..2ab383fe5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/member/exception/MemberErrorMessage.java @@ -0,0 +1,14 @@ +package mouda.backend.member.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum MemberErrorMessage { + + MEMBER_NAME_NOT_EXISTS("멤버 이름이 존재하지 않습니다."), + ; + + private final String message; +} diff --git a/backend/src/main/java/mouda/backend/member/exception/MemberException.java b/backend/src/main/java/mouda/backend/member/exception/MemberException.java new file mode 100644 index 000000000..94a8f1c34 --- /dev/null +++ b/backend/src/main/java/mouda/backend/member/exception/MemberException.java @@ -0,0 +1,12 @@ +package mouda.backend.member.exception; + +import org.springframework.http.HttpStatus; + +import mouda.backend.common.exception.MoudaException; + +public class MemberException extends MoudaException { + + public MemberException(HttpStatus httpStatus, MemberErrorMessage chatErrorMessage) { + super(httpStatus, chatErrorMessage.getMessage()); + } +} diff --git a/backend/src/main/java/mouda/backend/member/implement/MemberFinder.java b/backend/src/main/java/mouda/backend/member/implement/MemberFinder.java new file mode 100644 index 000000000..3547ba274 --- /dev/null +++ b/backend/src/main/java/mouda/backend/member/implement/MemberFinder.java @@ -0,0 +1,33 @@ +package mouda.backend.member.implement; + +import java.util.Optional; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.auth.exception.AuthErrorMessage; +import mouda.backend.auth.exception.AuthException; +import mouda.backend.member.domain.Member; +import mouda.backend.member.infrastructure.MemberRepository; + +@Component +@RequiredArgsConstructor +public class MemberFinder { + + private final MemberRepository memberRepository; + + public Optional getByIdentifier(String identifier) { + return memberRepository.findByLoginDetail_Identifier(identifier); + } + + public Member findActiveOrDeletedByIdentifier(String identifier) { + return memberRepository.findActiveOrDeletedByIdentifier(identifier) + .orElseThrow(() -> new AuthException(HttpStatus.NOT_FOUND, AuthErrorMessage.MEMBER_NOT_FOUND)); + } + + public Member findByMemberId(long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new AuthException(HttpStatus.NOT_FOUND, AuthErrorMessage.MEMBER_NOT_FOUND)); + } +} diff --git a/backend/src/main/java/mouda/backend/member/implement/MemberValidator.java b/backend/src/main/java/mouda/backend/member/implement/MemberValidator.java new file mode 100644 index 000000000..825347fe5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/member/implement/MemberValidator.java @@ -0,0 +1,20 @@ +package mouda.backend.member.implement; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.exception.DarakbangMemberErrorMessage; +import mouda.backend.darakbangmember.exception.DarakbangMemberException; + +@Component +@RequiredArgsConstructor +public class MemberValidator { + + public void validateNotManager(DarakbangMember member) { + if (member.isNotManager()) { + throw new DarakbangMemberException(HttpStatus.FORBIDDEN, DarakbangMemberErrorMessage.NOT_ALLOWED_TO_READ); + } + } +} diff --git a/backend/src/main/java/mouda/backend/member/implement/MemberWriter.java b/backend/src/main/java/mouda/backend/member/implement/MemberWriter.java new file mode 100644 index 000000000..23f794453 --- /dev/null +++ b/backend/src/main/java/mouda/backend/member/implement/MemberWriter.java @@ -0,0 +1,42 @@ +package mouda.backend.member.implement; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.member.domain.LoginDetail; +import mouda.backend.member.domain.Member; +import mouda.backend.member.infrastructure.MemberRepository; + +@Component +@RequiredArgsConstructor +public class MemberWriter { + + private final MemberRepository memberRepository; + + public Member append(Member member) { + return memberRepository.save(member); + } + + public void updateLoginDetail(Member member, LoginDetail loginDetail) { + member.updateLoginDetail(loginDetail); + memberRepository.save(member); + } + + public void updateName(long memberId, String name) { + memberRepository.updateName(memberId, name); + } + + public void withdraw(Member member) { + member.withdraw(); + memberRepository.save(member); + } + + public void delete(Member alternation) { + memberRepository.delete(alternation); + } + + public void deprecate(Member member) { + member.deprecate(); + memberRepository.save(member); + } +} diff --git a/backend/src/main/java/mouda/backend/member/infrastructure/MemberRepository.java b/backend/src/main/java/mouda/backend/member/infrastructure/MemberRepository.java new file mode 100644 index 000000000..a917bfc38 --- /dev/null +++ b/backend/src/main/java/mouda/backend/member/infrastructure/MemberRepository.java @@ -0,0 +1,48 @@ +package mouda.backend.member.infrastructure; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; + +import mouda.backend.member.domain.Member; +import mouda.backend.member.domain.OauthType; + +public interface MemberRepository extends JpaRepository { + + Optional findByLoginDetail_Identifier(String identifier); + + @Query(""" + SELECT m FROM Member m + WHERE m.loginDetail.identifier = :identifier AND (m.memberStatus = 'ACTIVE' OR m.memberStatus = 'DELETED') + """) + Optional findActiveOrDeletedByIdentifier(@Param("identifier") String identifier); + + @Query(""" + SELECT m FROM Member m + WHERE m.loginDetail.identifier = :identifier AND m.memberStatus = 'DEPRECATED' + """) + Optional findDeprecatedByIdentifier(@Param("identifier") String identifier); + + @Query(""" + UPDATE Member m + SET m.loginDetail.oauthType = :oauthType, m.loginDetail.identifier = :identifier + WHERE m.id = :memberId + """) + @Modifying + @Transactional + void updateLoginDetail(@Param("memberId") long memberId, @Param("oauthType") OauthType oauthType, + @Param("identifier") String identifier); + + @Query(""" + UPDATE Member m + SET m.name = :name + WHERE m.id = :memberId + """) + @Modifying + @Transactional + void updateName(@Param("memberId") long memberId, @Param("name") String name); +} diff --git a/backend/src/main/java/mouda/backend/member/presentation/controller/MemberController.java b/backend/src/main/java/mouda/backend/member/presentation/controller/MemberController.java new file mode 100644 index 000000000..4b9ded39b --- /dev/null +++ b/backend/src/main/java/mouda/backend/member/presentation/controller/MemberController.java @@ -0,0 +1,26 @@ +package mouda.backend.member.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.argumentresolver.LoginMember; +import mouda.backend.member.business.MemberService; +import mouda.backend.member.domain.Member; +import mouda.backend.member.presentation.controller.swagger.MemberSwagger; + +@RestController +@RequiredArgsConstructor +public class MemberController implements MemberSwagger { + + private final MemberService memberService; + + @Override + @DeleteMapping + public ResponseEntity withdraw(@LoginMember Member member) { + memberService.withdraw(member); + + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/mouda/backend/member/presentation/controller/swagger/MemberSwagger.java b/backend/src/main/java/mouda/backend/member/presentation/controller/swagger/MemberSwagger.java new file mode 100644 index 000000000..8da6495a1 --- /dev/null +++ b/backend/src/main/java/mouda/backend/member/presentation/controller/swagger/MemberSwagger.java @@ -0,0 +1,18 @@ +package mouda.backend.member.presentation.controller.swagger; + +import org.springframework.http.ResponseEntity; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import mouda.backend.common.config.argumentresolver.LoginMember; +import mouda.backend.member.domain.Member; + +public interface MemberSwagger { + + @Operation(summary = "회원 탈퇴", description = "로그인한 회원을 서비스에서 탈퇴 처리한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "회원 탈퇴 성공!"), + }) + ResponseEntity withdraw(@LoginMember Member member); +} diff --git a/backend/src/main/java/mouda/backend/member/presentation/response/DarakbangMemberInfoResponse.java b/backend/src/main/java/mouda/backend/member/presentation/response/DarakbangMemberInfoResponse.java new file mode 100644 index 000000000..2df9e9f40 --- /dev/null +++ b/backend/src/main/java/mouda/backend/member/presentation/response/DarakbangMemberInfoResponse.java @@ -0,0 +1,9 @@ +package mouda.backend.member.presentation.response; + +public record DarakbangMemberInfoResponse( + String name, + String nickname, + String profile, + String description +) { +} diff --git a/backend/src/main/java/mouda/backend/moim/business/ChamyoService.java b/backend/src/main/java/mouda/backend/moim/business/ChamyoService.java new file mode 100644 index 000000000..fb5f6bd57 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/business/ChamyoService.java @@ -0,0 +1,64 @@ +package mouda.backend.moim.business; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.implement.finder.ChamyoFinder; +import mouda.backend.moim.implement.finder.MoimFinder; +import mouda.backend.moim.implement.notificiation.MoimRelatedNotificationSender; +import mouda.backend.moim.implement.writer.ChamyoWriter; +import mouda.backend.moim.implement.writer.MoimWriter; +import mouda.backend.moim.presentation.response.chamyo.ChamyoFindAllResponses; +import mouda.backend.moim.presentation.response.chamyo.MoimRoleFindResponse; +import mouda.backend.notification.domain.NotificationType; + +@Service +@RequiredArgsConstructor +@Transactional +public class ChamyoService { + + private final MoimFinder moimFinder; + private final MoimWriter moimWriter; + private final ChamyoFinder chamyoFinder; + private final ChamyoWriter chamyoWriter; + private final MoimRelatedNotificationSender notificationSender; + + @Transactional(readOnly = true) + public MoimRoleFindResponse findMoimRole(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { + Moim moim = moimFinder.read(moimId, darakbangId); + MoimRole moimRole = chamyoFinder.readMoimRole(moim, darakbangMember); + + return MoimRoleFindResponse.toResponse(moimRole); + } + + @Transactional(readOnly = true) + public ChamyoFindAllResponses findAllChamyo(Long darakbangId, Long moimId) { + Moim moim = moimFinder.read(moimId, darakbangId); + List chamyos = chamyoFinder.readAll(moim); + + return ChamyoFindAllResponses.toResponse(chamyos); + } + + public void chamyoMoim(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { + Moim moim = moimFinder.read(moimId, darakbangId); + Chamyo chamyo = chamyoWriter.saveAsMoimee(moim, darakbangMember); + moimWriter.updateMoimStatusIfFull(moim); + + notificationSender.sendChamyoNotification(chamyo, NotificationType.NEW_MOIMEE_JOINED); + } + + public void cancelChamyo(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { + Moim moim = moimFinder.read(moimId, darakbangId); + Chamyo chamyo = chamyoFinder.read(moim, darakbangMember); + chamyoWriter.delete(chamyo); + + notificationSender.sendChamyoNotification(chamyo, NotificationType.MOIMEE_LEFT); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/business/CommentService.java b/backend/src/main/java/mouda/backend/moim/business/CommentService.java new file mode 100644 index 000000000..010fd8c76 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/business/CommentService.java @@ -0,0 +1,32 @@ +package mouda.backend.moim.business; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.finder.MoimFinder; +import mouda.backend.moim.implement.notificiation.MoimRelatedNotificationSender; +import mouda.backend.moim.implement.writer.CommentWriter; +import mouda.backend.moim.presentation.request.comment.CommentCreateRequest; + +@Transactional +@Service +@RequiredArgsConstructor +public class CommentService { + + private final MoimFinder moimFinder; + private final CommentWriter commentWriter; + private final MoimRelatedNotificationSender notificationSender; + + public void createComment( + Long darakbangId, Long moimId, DarakbangMember darakbangMember, CommentCreateRequest request + ) { + Moim moim = moimFinder.read(moimId, darakbangId); + Comment comment = commentWriter.saveComment(moim, darakbangMember, request.parentId(), request.content()); + + notificationSender.sendCommentNotification(comment, darakbangMember); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/business/MoimService.java b/backend/src/main/java/mouda/backend/moim/business/MoimService.java new file mode 100644 index 000000000..bddb1d9d8 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/business/MoimService.java @@ -0,0 +1,112 @@ +package mouda.backend.moim.business; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.implement.ChatRoomFinder; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.FilterType; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimOverview; +import mouda.backend.moim.domain.ParentComment; +import mouda.backend.moim.implement.finder.CommentFinder; +import mouda.backend.moim.implement.finder.MoimFinder; +import mouda.backend.moim.implement.notificiation.MoimRelatedNotificationSender; +import mouda.backend.moim.implement.writer.MoimWriter; +import mouda.backend.moim.presentation.request.moim.MoimCreateRequest; +import mouda.backend.moim.presentation.request.moim.MoimEditRequest; +import mouda.backend.moim.presentation.response.comment.CommentResponses; +import mouda.backend.moim.presentation.response.moim.MoimDetailsFindResponse; +import mouda.backend.moim.presentation.response.moim.MoimFindAllResponses; +import mouda.backend.notification.domain.NotificationType; + +@Transactional +@Service +@RequiredArgsConstructor +public class MoimService { + + private final MoimWriter moimWriter; + private final MoimFinder moimFinder; + private final CommentFinder commentFinder; + private final ChatRoomFinder chatRoomFinder; + private final MoimRelatedNotificationSender notificationSender; + + @Transactional(readOnly = true) + public MoimDetailsFindResponse findMoimDetails(long darakbangId, long moimId) { + Moim moim = moimFinder.read(moimId, darakbangId); + + List parentComments = commentFinder.readAllParentComments(moim); + CommentResponses commentResponses = CommentResponses.toResponse(parentComments); + + Long chatRoomId = chatRoomFinder.findChatRoomIdByTargetId(moim.getId(), ChatRoomType.MOIM); + + return MoimDetailsFindResponse.toResponse( + moim, + moimFinder.countCurrentPeople(moim), + commentResponses, + chatRoomId + ); + } + + @Transactional(readOnly = true) + public MoimFindAllResponses findAllMoim(Long darakbangId, DarakbangMember darakbangMember) { + List moimOverviews = moimFinder.readAll(darakbangId, darakbangMember); + + return MoimFindAllResponses.toResponse(moimOverviews); + } + + @Transactional(readOnly = true) + public MoimFindAllResponses findAllMyMoim(DarakbangMember darakbangMember, FilterType filter) { + List moimOverviews = moimFinder.readAllMyMoim(darakbangMember, filter); + + return MoimFindAllResponses.toResponse(moimOverviews); + } + + @Transactional(readOnly = true) + public MoimFindAllResponses findZzimedMoim(DarakbangMember darakbangMember) { + List moimOverviews = moimFinder.readAllZzimedMoim(darakbangMember); + + return MoimFindAllResponses.toResponse(moimOverviews); + } + + public Moim createMoim(Long darakbangId, DarakbangMember darakbangMember, MoimCreateRequest moimCreateRequest) { + Moim moim = moimWriter.save(moimCreateRequest.toEntity(darakbangId), darakbangMember); + + notificationSender.sendMoimCreatedNotification(moim, darakbangMember, NotificationType.MOIM_CREATED); + return moim; + } + + public void completeMoim(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { + Moim moim = moimFinder.read(moimId, darakbangId); + moimWriter.completeMoim(moim, darakbangMember); + + notificationSender.sendMoimStatusChangeNotification(moim, NotificationType.MOIMING_COMPLETED); + } + + public void cancelMoim(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { + Moim moim = moimFinder.read(moimId, darakbangId); + moimWriter.cancelMoim(moim, darakbangMember); + + notificationSender.sendMoimStatusChangeNotification(moim, NotificationType.MOIM_CANCELLED); + } + + public void reopenMoim(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { + Moim moim = moimFinder.read(moimId, darakbangId); + moimWriter.reopenMoim(moim, darakbangMember); + + notificationSender.sendMoimStatusChangeNotification(moim, NotificationType.MOINING_REOPENED); + } + + public void editMoim(Long darakbangId, MoimEditRequest request, DarakbangMember darakbangMember) { + Moim moim = moimFinder.read(request.moimId(), darakbangId); + String oldTitle = moim.getTitle(); + moimWriter.updateMoim(moim, darakbangMember, request.title(), request.date(), request.time(), request.place(), + request.maxPeople(), request.description()); + + notificationSender.sendMoimEditedNotification(moim, oldTitle, NotificationType.MOIM_MODIFIED); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/business/ZzimService.java b/backend/src/main/java/mouda/backend/moim/business/ZzimService.java new file mode 100644 index 000000000..02220ed5c --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/business/ZzimService.java @@ -0,0 +1,37 @@ +package mouda.backend.moim.business; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.finder.MoimFinder; +import mouda.backend.moim.implement.finder.ZzimFinder; +import mouda.backend.moim.implement.validator.MoimValidator; +import mouda.backend.moim.implement.writer.ZzimWriter; +import mouda.backend.moim.presentation.response.zzim.ZzimCheckResponse; + +@Service +@Transactional +@RequiredArgsConstructor +public class ZzimService { + + private final MoimValidator moimValidator; + private final MoimFinder moimFinder; + private final ZzimFinder zzimFinder; + private final ZzimWriter zzimWriter; + + @Transactional(readOnly = true) + public ZzimCheckResponse checkZzimByMember(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { + moimValidator.validateMoimExists(moimId, darakbangId); + boolean moimZzimedByMember = zzimFinder.isMoimZzimedByMember(moimId, darakbangMember); + + return ZzimCheckResponse.toResponse(moimZzimedByMember); + } + + public void updateZzim(Long darakbangId, Long moimId, DarakbangMember darakbangMember) { + Moim moim = moimFinder.read(moimId, darakbangId); + zzimWriter.updateZzimStatus(moim, darakbangMember); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/domain/Chamyo.java b/backend/src/main/java/mouda/backend/moim/domain/Chamyo.java new file mode 100644 index 000000000..8294e6996 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/domain/Chamyo.java @@ -0,0 +1,65 @@ +package mouda.backend.moim.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Entity +@Getter +@NoArgsConstructor +@Table( + name = "chamyo", + uniqueConstraints = { + @UniqueConstraint( + columnNames = {"moim_id", "darakbang_member_id"} + ) + } +) +public class Chamyo { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(nullable = false) + private Moim moim; + + @ManyToOne + @JoinColumn(nullable = false) + private DarakbangMember darakbangMember; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private MoimRole moimRole; + + private long lastReadChatId; + + @Builder + public Chamyo(Moim moim, DarakbangMember darakbangMember, MoimRole moimRole) { + this.moim = moim; + this.darakbangMember = darakbangMember; + this.moimRole = moimRole; + this.lastReadChatId = 0L; + } + + public void updateLastChat(Long lastReadChatId) { + this.lastReadChatId = lastReadChatId; + } + + public boolean isNotSameMember(DarakbangMember darakbangMember) { + return !this.darakbangMember.equals(darakbangMember); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/domain/Comment.java b/backend/src/main/java/mouda/backend/moim/domain/Comment.java new file mode 100644 index 000000000..06e949a09 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/domain/Comment.java @@ -0,0 +1,89 @@ +package mouda.backend.moim.domain; + +import java.time.LocalDateTime; + +import org.springframework.http.HttpStatus; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.exception.CommentErrorMessage; +import mouda.backend.moim.exception.CommentException; + +@Entity +@Table(name = "comment") +@Getter +@NoArgsConstructor +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String content; + + @ManyToOne + @JoinColumn(nullable = false) + private Moim moim; + + @ManyToOne + @JoinColumn(nullable = false) + private DarakbangMember darakbangMember; + + @Column(nullable = false) + private LocalDateTime createdAt; + + private Long parentId; + + @Builder + public Comment(String content, Moim moim, DarakbangMember darakbangMember, LocalDateTime createdAt, Long parentId) { + validateContent(content); + validateMoim(moim); + validateMember(darakbangMember); + this.content = content; + this.moim = moim; + this.darakbangMember = darakbangMember; + this.createdAt = createdAt; + this.parentId = parentId; + } + + private void validateContent(String content) { + if (content == null || content.isBlank()) { + throw new CommentException(HttpStatus.BAD_REQUEST, CommentErrorMessage.CONTENT_NOT_FOUND); + } + } + + private void validateMoim(Moim moim) { + if (moim == null) { + throw new CommentException(HttpStatus.NOT_FOUND, CommentErrorMessage.MOIM_NOT_FOUND); + } + } + + private void validateMember(DarakbangMember darakbangMember) { + if (darakbangMember == null) { + throw new CommentException(HttpStatus.NOT_FOUND, CommentErrorMessage.MEMBER_NOT_FOUND); + } + } + + public boolean isComment() { + return parentId == null; + } + + public boolean isReply() { + return parentId != null; + } + + public String getAuthorNickname() { + return darakbangMember.getNickname(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/domain/CommentRecipients.java b/backend/src/main/java/mouda/backend/moim/domain/CommentRecipients.java new file mode 100644 index 000000000..e1470a327 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/domain/CommentRecipients.java @@ -0,0 +1,40 @@ +package mouda.backend.moim.domain; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; + +@RequiredArgsConstructor +@Getter +@Builder +public class CommentRecipients { + + private final Map> recipients; + + public CommentRecipients() { + this.recipients = new ConcurrentHashMap<>(); + } + + public void addRecipient(NotificationType notificationType, long memberId, long darakbangMemberId) { + Recipient recipient = Recipient.builder() + .memberId(memberId) + .darakbangMemberId(darakbangMemberId) + .build(); + + if (recipients.containsKey(notificationType)) { + recipients.get(notificationType).add(recipient); + return; + } + + List recipientList = new ArrayList<>(); + recipientList.add(recipient); + recipients.put(notificationType, recipientList); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/domain/FilterType.java b/backend/src/main/java/mouda/backend/moim/domain/FilterType.java new file mode 100644 index 000000000..c846c53bf --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/domain/FilterType.java @@ -0,0 +1,7 @@ +package mouda.backend.moim.domain; + +public enum FilterType { + ALL, + PAST, + UPCOMING +} diff --git a/backend/src/main/java/mouda/backend/moim/domain/Moim.java b/backend/src/main/java/mouda/backend/moim/domain/Moim.java new file mode 100644 index 000000000..d2f8a119b --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/domain/Moim.java @@ -0,0 +1,234 @@ +package mouda.backend.moim.domain; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Objects; + +import org.springframework.http.HttpStatus; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mouda.backend.moim.exception.MoimErrorMessage; +import mouda.backend.moim.exception.MoimException; + +@Entity +@Table(name = "moim") +@Getter +@NoArgsConstructor +public class Moim { + + private static final int TITLE_MAX_LENGTH = 30; + private static final int PLACE_MAX_LENGTH = 100; + private static final int MAX_PEOPLE_LOWER_BOUND = 1; + private static final int MAX_PEOPLE_UPPER_BOUND = 99; + private static final int AUTHOR_NICKNAME_MAX_LENGTH = 10; + private static final int DESCRIPTION_MAX_LENGTH = 1000; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + private LocalDate date; + + private LocalTime time; + + private String place; + + @Column(nullable = false) + private int maxPeople; + + private String description; + + @Enumerated(EnumType.STRING) + private MoimStatus moimStatus; + + private boolean isChatOpened; + + private Long darakbangId; + + @Builder + public Moim( + String title, + LocalDate date, + LocalTime time, + String place, + int maxPeople, + String description, + Long darakbangId + ) { + validateTitle(title); + validateMoimIsFuture(date, time); + validatePlace(place); + validateMaxPeople(maxPeople); + validateDescription(description); + + this.title = title; + this.date = date; + this.time = time; + this.place = place; + this.maxPeople = maxPeople; + this.description = description; + this.moimStatus = MoimStatus.MOIMING; + this.isChatOpened = false; + this.darakbangId = darakbangId; + } + + private void validateTitle(String title) { + if (title.isBlank()) { + throw new MoimException(HttpStatus.BAD_REQUEST, MoimErrorMessage.TITLE_NOT_EXIST); + } + if (title.length() > TITLE_MAX_LENGTH) { + throw new MoimException(HttpStatus.BAD_REQUEST, MoimErrorMessage.TITLE_TOO_LONG); + } + } + + private void validateMoimIsFuture(LocalDate date, LocalTime time) { + if (date == null || time == null) { + return; + } + LocalDateTime moimDateTime = LocalDateTime.of(date, time); + if (moimDateTime.isBefore(LocalDateTime.now())) { + throw new MoimException(HttpStatus.BAD_REQUEST, MoimErrorMessage.PAST_DATE_TIME); + } + } + + private void validatePlace(String place) { + if (place == null) { + return; + } + if (place.isBlank()) { + throw new MoimException(HttpStatus.BAD_REQUEST, MoimErrorMessage.PLACE_NOT_EXIST); + } + if (place.length() > PLACE_MAX_LENGTH) { + throw new MoimException(HttpStatus.BAD_REQUEST, MoimErrorMessage.PLACE_TOO_LONG); + } + } + + private void validateMaxPeople(int maxPeople) { + if (maxPeople < MAX_PEOPLE_LOWER_BOUND) { + throw new MoimException(HttpStatus.BAD_REQUEST, MoimErrorMessage.MAX_PEOPLE_IS_POSITIVE); + } + if (maxPeople > MAX_PEOPLE_UPPER_BOUND) { + throw new MoimException(HttpStatus.BAD_REQUEST, MoimErrorMessage.MAX_PEOPLE_TOO_MANY); + } + } + + private void validateDescription(String description) { + if (description != null && description.length() > DESCRIPTION_MAX_LENGTH) { + throw new MoimException(HttpStatus.BAD_REQUEST, MoimErrorMessage.DESCRIPTION_TOO_LONG); + } + } + + public void update(String title, LocalDate date, LocalTime time, String place, int maxPeople, + String description, int currentPeople) { + if (!Objects.equals(this.title, title)) { + validateTitle(title); + this.title = title; + } + + if (!Objects.equals(this.date, date)) { + this.date = date; + } + + if (!Objects.equals(this.time, time)) { + this.time = time; + } + + validateMoimIsFuture(this.date, this.time); + + if (!Objects.equals(this.place, place)) { + validatePlace(place); + this.place = place; + } + + if (!Objects.equals(this.maxPeople, maxPeople)) { + validateMaxPeople(maxPeople); + validateMaxPeopleIsUpperThanCurrentPeople(maxPeople, currentPeople); + this.maxPeople = maxPeople; + } + + if (!Objects.equals(this.description, description)) { + validateDescription(description); + this.description = description; + } + } + + private void validateMaxPeopleIsUpperThanCurrentPeople(int maxPeople, int currentPeople) { + if (maxPeople < currentPeople) { + throw new MoimException(HttpStatus.BAD_REQUEST, MoimErrorMessage.MAX_PEOPLE_IS_LOWER_THAN_CURRENT_PEOPLE); + } + } + + public boolean isPastMoim() { + if (date == null || time == null) { + return false; + } + LocalDateTime dateTime = LocalDateTime.of(date, time); + return dateTime.isBefore(LocalDateTime.now()); + } + + public boolean isUpcomingMoim() { + return !isPastMoim(); + } + + public void confirmPlace(String place) { + validatePlace(place); + this.place = place; + } + + public void confirmDateTime(LocalDate date, LocalTime time) { + validateMoimIsFuture(date, time); + this.date = date; + this.time = time; + } + + public void openChat() { + this.isChatOpened = true; + } + + public boolean isNotInDarakbang(long darakbangId) { + return this.darakbangId != darakbangId; + } + + public boolean isFull(int currentPeople) { + return currentPeople >= maxPeople; + } + + public boolean isCanceled() { + return moimStatus == MoimStatus.CANCELED; + } + + public boolean isCompleted() { + return moimStatus == MoimStatus.COMPLETED; + } + + + public boolean isMoiming() { + return moimStatus == MoimStatus.MOIMING; + } + + public void complete() { + this.moimStatus = MoimStatus.COMPLETED; + } + + public void cancel() { + this.moimStatus = MoimStatus.CANCELED; + } + + public void reopen() { + this.moimStatus = MoimStatus.MOIMING; + } +} diff --git a/backend/src/main/java/mouda/backend/moim/domain/MoimOverview.java b/backend/src/main/java/mouda/backend/moim/domain/MoimOverview.java new file mode 100644 index 000000000..655d272da --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/domain/MoimOverview.java @@ -0,0 +1,17 @@ +package mouda.backend.moim.domain; + +import lombok.Getter; + +@Getter +public class MoimOverview { + + private final Moim moim; + private final int currentPeople; + private final boolean isZzimed; + + public MoimOverview(Moim moim, int currentPeople, boolean isZzimed) { + this.moim = moim; + this.currentPeople = currentPeople; + this.isZzimed = isZzimed; + } +} diff --git a/backend/src/main/java/mouda/backend/moim/domain/MoimRole.java b/backend/src/main/java/mouda/backend/moim/domain/MoimRole.java new file mode 100644 index 000000000..262f1f494 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/domain/MoimRole.java @@ -0,0 +1,8 @@ +package mouda.backend.moim.domain; + +import lombok.Getter; + +@Getter +public enum MoimRole { + MOIMER, MOIMEE, NON_MOIMEE +} diff --git a/backend/src/main/java/mouda/backend/moim/domain/MoimStatus.java b/backend/src/main/java/mouda/backend/moim/domain/MoimStatus.java new file mode 100644 index 000000000..421456331 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/domain/MoimStatus.java @@ -0,0 +1,8 @@ +package mouda.backend.moim.domain; + +import lombok.Getter; + +@Getter +public enum MoimStatus { + MOIMING, COMPLETED, CANCELED +} diff --git a/backend/src/main/java/mouda/backend/moim/domain/ParentComment.java b/backend/src/main/java/mouda/backend/moim/domain/ParentComment.java new file mode 100644 index 000000000..dbd8e9ec0 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/domain/ParentComment.java @@ -0,0 +1,17 @@ +package mouda.backend.moim.domain; + +import java.util.List; + +import lombok.Getter; + +@Getter +public class ParentComment { + + private final Comment comment; + private final List children; + + public ParentComment(Comment comment, List children) { + this.comment = comment; + this.children = children; + } +} diff --git a/backend/src/main/java/mouda/backend/moim/domain/Zzim.java b/backend/src/main/java/mouda/backend/moim/domain/Zzim.java new file mode 100644 index 000000000..efddf0311 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/domain/Zzim.java @@ -0,0 +1,38 @@ +package mouda.backend.moim.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +@Entity +@Table(name = "zzim") +@Getter +@NoArgsConstructor +public class Zzim { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(nullable = false) + private Moim moim; + + @ManyToOne + @JoinColumn(nullable = false) + private DarakbangMember darakbangMember; + + @Builder + public Zzim(Moim moim, DarakbangMember darakbangMember) { + this.moim = moim; + this.darakbangMember = darakbangMember; + } +} diff --git a/backend/src/main/java/mouda/backend/moim/exception/ChamyoErrorMessage.java b/backend/src/main/java/mouda/backend/moim/exception/ChamyoErrorMessage.java new file mode 100644 index 000000000..12f94d594 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/exception/ChamyoErrorMessage.java @@ -0,0 +1,19 @@ +package mouda.backend.moim.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ChamyoErrorMessage { + + NOT_FOUND("참여 정보가 존재하지 않습니다."), + ALREADY_PARTICIPATED("이미 참여했어요!"), + MOIM_FULL("모임이 꽉 찼어요!"), + MOIMING_CANCLED("모임이 취소됐어요!"), + MOIMING_COMPLETE("모집이 완료됐어요!"), + CANNOT_CANCEL_CHAMYO("취소할 수 없어요!"), + NOT_MOIMER("모이머가 아닙니다."); + + private final String message; +} diff --git a/backend/src/main/java/mouda/backend/moim/exception/ChamyoException.java b/backend/src/main/java/mouda/backend/moim/exception/ChamyoException.java new file mode 100644 index 000000000..5836a2261 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/exception/ChamyoException.java @@ -0,0 +1,12 @@ +package mouda.backend.moim.exception; + +import org.springframework.http.HttpStatus; + +import mouda.backend.common.exception.MoudaException; + +public class ChamyoException extends MoudaException { + + public ChamyoException(HttpStatus httpStatus, ChamyoErrorMessage chamyoErrorMessage) { + super(httpStatus, chamyoErrorMessage.getMessage()); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/exception/CommentErrorMessage.java b/backend/src/main/java/mouda/backend/moim/exception/CommentErrorMessage.java new file mode 100644 index 000000000..2e6af08da --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/exception/CommentErrorMessage.java @@ -0,0 +1,16 @@ +package mouda.backend.moim.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum CommentErrorMessage { + + CONTENT_NOT_FOUND("댓글 내용이 존재하지 않습니다."), + MEMBER_NOT_FOUND("작성자가 존재하지 않습니다."), + PARENT_NOT_FOUND("부모 댓글이 존재하지 않습니다."), + MOIM_NOT_FOUND("모임이 존재하지 않습니다."); + + private final String message; +} diff --git a/backend/src/main/java/mouda/backend/moim/exception/CommentException.java b/backend/src/main/java/mouda/backend/moim/exception/CommentException.java new file mode 100644 index 000000000..47b3dd4a8 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/exception/CommentException.java @@ -0,0 +1,12 @@ +package mouda.backend.moim.exception; + +import org.springframework.http.HttpStatus; + +import mouda.backend.common.exception.MoudaException; + +public class CommentException extends MoudaException { + + public CommentException(HttpStatus httpStatus, CommentErrorMessage commentErrorMessage) { + super(httpStatus, commentErrorMessage.getMessage()); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/exception/MoimErrorMessage.java b/backend/src/main/java/mouda/backend/moim/exception/MoimErrorMessage.java new file mode 100644 index 000000000..0e86d97e4 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/exception/MoimErrorMessage.java @@ -0,0 +1,33 @@ +package mouda.backend.moim.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum MoimErrorMessage { + + NOT_FOUND("모임이 존재하지 않습니다."), + PAST_DATE_TIME("모임 날짜를 현재 시점 이후로 입력해주세요."), + TITLE_NOT_EXIST("모임 제목을 입력해주세요."), + TITLE_TOO_LONG("모임 제목을 조금 더 짧게 입력해주세요."), + PLACE_NOT_EXIST("모임 장소를 입력해주세요."), + PLACE_TOO_LONG("모임 장소를 조금 더 짧게 입력해주세요."), + MAX_PEOPLE_IS_POSITIVE("모임 최대 인원은 양수여야 합니다."), + MAX_PEOPLE_TOO_MANY("모임 최대 인원을 조금 더 적게 입력해주세요."), + DESCRIPTION_TOO_LONG("모임 설명을 조금 더 짧게 입력해주세요."), + NOT_ALLOWED_TO_COMPLETE("방장만 완료할 수 있어요."), + MOIM_CANCELED("이미 취소된 모임이에요."), + ALREADY_COMPLETED("이미 모집 완료된 모임이에요."), + NOT_ALLOWED_TO_CANCEL("방장만 취소할 수 있어요."), + NOT_ALLOWED_TO_REOPEN("방장만 다시 열 수 있어요."), + MOIM_FULL_FOR_REOPEN("모임이 꽉 차서 다시 열 수 없어요."), + ALREADY_MOIMING("이미 모집 중인 모임이에요."), + NOT_ALLOWED_TO_EDIT("방장만 수정할 수 있어요."), + MAX_PEOPLE_IS_LOWER_THAN_CURRENT_PEOPLE("모임 최대 인원을 현재 인원보다 작게 설정할 수 없어요."), + + DARAKBANG_NOT_FOUND("이 모임의 다락방 정보를 찾을 수 없어요."), + ; + + private final String message; +} diff --git a/backend/src/main/java/mouda/backend/moim/exception/MoimException.java b/backend/src/main/java/mouda/backend/moim/exception/MoimException.java new file mode 100644 index 000000000..9768edf1e --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/exception/MoimException.java @@ -0,0 +1,12 @@ +package mouda.backend.moim.exception; + +import org.springframework.http.HttpStatus; + +import mouda.backend.common.exception.MoudaException; + +public class MoimException extends MoudaException { + + public MoimException(HttpStatus httpStatus, MoimErrorMessage moimErrorMessage) { + super(httpStatus, moimErrorMessage.getMessage()); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/exception/ZzimErrorMessage.java b/backend/src/main/java/mouda/backend/moim/exception/ZzimErrorMessage.java new file mode 100644 index 000000000..c8cc00ca0 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/exception/ZzimErrorMessage.java @@ -0,0 +1,13 @@ +package mouda.backend.moim.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ZzimErrorMessage { + + MOIN_NOT_FOUND("모임이 존재하지 않습니다."); + + private final String message; +} diff --git a/backend/src/main/java/mouda/backend/moim/exception/ZzimException.java b/backend/src/main/java/mouda/backend/moim/exception/ZzimException.java new file mode 100644 index 000000000..b4cf86b00 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/exception/ZzimException.java @@ -0,0 +1,12 @@ +package mouda.backend.moim.exception; + +import org.springframework.http.HttpStatus; + +import mouda.backend.common.exception.MoudaException; + +public class ZzimException extends MoudaException { + + public ZzimException(HttpStatus httpStatus, ZzimErrorMessage zzimErrorMessage) { + super(httpStatus, zzimErrorMessage.getMessage()); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/finder/ChamyoFinder.java b/backend/src/main/java/mouda/backend/moim/implement/finder/ChamyoFinder.java new file mode 100644 index 000000000..89ba0773e --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/finder/ChamyoFinder.java @@ -0,0 +1,61 @@ +package mouda.backend.moim.implement.finder; + +import java.util.List; +import java.util.Optional; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.exception.ChamyoErrorMessage; +import mouda.backend.moim.exception.ChamyoException; +import mouda.backend.moim.infrastructure.ChamyoRepository; + +@Component +@RequiredArgsConstructor +public class ChamyoFinder { + + private final ChamyoRepository chamyoRepository; + + public Chamyo read(Moim moim, DarakbangMember darakbangMember) { + return read(moim.getId(), darakbangMember); + } + + public Chamyo read(long moimId, DarakbangMember darakbangMember) { + return find(moimId, darakbangMember) + .orElseThrow(() -> new ChamyoException(HttpStatus.NOT_FOUND, ChamyoErrorMessage.NOT_FOUND)); + } + + private Optional find(long moimId, DarakbangMember darakbangMember) { + return chamyoRepository.findByMoimIdAndDarakbangMemberId(moimId, darakbangMember.getId()); + } + + public boolean exists(long moimId, DarakbangMember darakbangMember) { + return chamyoRepository.existsByMoimIdAndDarakbangMemberId(moimId, darakbangMember.getId()); + } + + public MoimRole readMoimRole(Moim moim, DarakbangMember darakbangMember) { + Optional chamyoOptional = find(moim.getId(), darakbangMember); + if (chamyoOptional.isEmpty()) { + return MoimRole.NON_MOIMEE; + } + + Chamyo chamyo = chamyoOptional.get(); + return chamyo.getMoimRole(); + } + + public List readAll(Moim moim) { + return chamyoRepository.findAllByMoimId(moim.getId()); + } + + public List readAllChatOpened(long darakbangId, DarakbangMember darakbangMember) { + return chamyoRepository.findAllByDarakbangMemberIdAndMoim_DarakbangId(darakbangMember.getId(), darakbangId) + .stream() + .filter(chamyo -> chamyo.getMoim().isChatOpened()) + .toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/finder/CommentFinder.java b/backend/src/main/java/mouda/backend/moim/implement/finder/CommentFinder.java new file mode 100644 index 000000000..49003c94e --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/finder/CommentFinder.java @@ -0,0 +1,35 @@ +package mouda.backend.moim.implement.finder; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.ParentComment; +import mouda.backend.moim.infrastructure.CommentRepository; + +@Component +@RequiredArgsConstructor +public class CommentFinder { + + private final CommentRepository commentRepository; + + public List readAllParentComments(Moim moim) { + List comments = commentRepository.findAllByMoimOrderByCreatedAt(moim); + + return comments.stream() + .filter(Comment::isComment) + .map(parentComment -> new ParentComment(parentComment, getChildComments(parentComment, comments))) + .collect(Collectors.toList()); + } + + private List getChildComments(Comment parentComment, List comments) { + return comments.stream() + .filter(comment -> Objects.equals(comment.getParentId(), parentComment.getId())) + .toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/finder/MoimFinder.java b/backend/src/main/java/mouda/backend/moim/implement/finder/MoimFinder.java new file mode 100644 index 000000000..d3b91db65 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/finder/MoimFinder.java @@ -0,0 +1,87 @@ +package mouda.backend.moim.implement.finder; + +import java.util.List; +import java.util.function.Predicate; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.chat.implement.ChatRoomFinder; +import mouda.backend.chat.infrastructure.ChatRoomRepository; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.FilterType; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimOverview; +import mouda.backend.moim.exception.MoimErrorMessage; +import mouda.backend.moim.exception.MoimException; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.MoimRepository; +import mouda.backend.moim.infrastructure.ZzimRepository; + +@Component +@RequiredArgsConstructor +public class MoimFinder { + + private final MoimRepository moimRepository; + private final ChamyoRepository chamyoRepository; + private final ZzimFinder zzimFinder; + private final ZzimRepository zzimRepository; + private final ChatRoomRepository chatRoomRepository; + private final ChatRoomFinder chatRoomFinder; + private final ChamyoFinder chamyoFinder; + + public Moim read(long moimId, long currentDarakbangId) { + return moimRepository.findByIdAndDarakbangId(moimId, currentDarakbangId) + .orElseThrow(() -> new MoimException(HttpStatus.NOT_FOUND, MoimErrorMessage.NOT_FOUND)); + } + + public List readAll(long darakbangId, DarakbangMember darakbangMember) { + return moimRepository.findAllByDarakbangIdOrderByIdDesc(darakbangId).stream() + .map(moim -> createMoimOverview(moim, darakbangMember)) + .toList(); + } + + public List readAllMyMoim(DarakbangMember darakbangMember, FilterType filterType) { + return chamyoRepository.findAllByDarakbangMemberIdOrderByIdDesc(darakbangMember.getId()).stream() + .map(Chamyo::getMoim) + .filter(getFilter(filterType)) + .map(moim -> createMoimOverview(moim, darakbangMember)) + .toList(); + } + + private Predicate getFilter(FilterType filterType) { + if (filterType == FilterType.PAST) { + return Moim::isPastMoim; + } + if (filterType == FilterType.UPCOMING) { + return Moim::isUpcomingMoim; + } + return moim -> true; + } + + public List readAllZzimedMoim(DarakbangMember darakbangMember) { + return zzimRepository.findAllByDarakbangMemberIdOrderByIdDesc(darakbangMember.getId()).stream() + .map(zzim -> createMoimOverview(zzim.getMoim(), darakbangMember)) + .toList(); + } + + private MoimOverview createMoimOverview(Moim moim, DarakbangMember darakbangMember) { + int currentPeople = countCurrentPeople(moim); + boolean isZzimed = zzimFinder.isMoimZzimedByMember(moim.getId(), darakbangMember); + + return new MoimOverview(moim, currentPeople, isZzimed); + } + + public int countCurrentPeople(Moim moim) { + return chamyoRepository.countByMoim(moim); + } + + public List readAllMyMoims(DarakbangMember darakbangMember) { + return chamyoRepository.findAllByDarakbangMemberIdOrderByIdDesc(darakbangMember.getId()) + .stream() + .map(Chamyo::getMoim) + .toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/finder/ZzimFinder.java b/backend/src/main/java/mouda/backend/moim/implement/finder/ZzimFinder.java new file mode 100644 index 000000000..cb9961854 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/finder/ZzimFinder.java @@ -0,0 +1,25 @@ +package mouda.backend.moim.implement.finder; + +import java.util.Optional; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Zzim; +import mouda.backend.moim.infrastructure.ZzimRepository; + +@Component +@RequiredArgsConstructor +public class ZzimFinder { + + private final ZzimRepository zzimRepository; + + public Optional find(long moimId, DarakbangMember darakbangMember) { + return zzimRepository.findByMoimIdAndDarakbangMemberId(moimId, darakbangMember.getId()); + } + + public boolean isMoimZzimedByMember(long moimId, DarakbangMember darakbangMember) { + return zzimRepository.existsByMoimIdAndDarakbangMemberId(moimId, darakbangMember.getId()); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/AbstractMoimRelatedNotificationEventHandler.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/AbstractMoimRelatedNotificationEventHandler.java new file mode 100644 index 000000000..b5837da48 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/AbstractMoimRelatedNotificationEventHandler.java @@ -0,0 +1,21 @@ +package mouda.backend.moim.implement.notificiation; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.UrlConfig; +import mouda.backend.notification.implement.NotificationProcessor; + +@Component +@RequiredArgsConstructor +@EnableConfigurationProperties(UrlConfig.class) +public abstract class AbstractMoimRelatedNotificationEventHandler { + + protected final UrlConfig urlConfig; + protected final NotificationProcessor notificationProcessor; + + protected String getMoimUrl(long darakbangId, long moimId) { + return urlConfig.getMoimUrl(darakbangId, moimId); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/ChamyoNotificationEventHandler.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/ChamyoNotificationEventHandler.java new file mode 100644 index 000000000..545398a27 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/ChamyoNotificationEventHandler.java @@ -0,0 +1,71 @@ +package mouda.backend.moim.implement.notificiation; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import mouda.backend.common.config.UrlConfig; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.notificiation.event.ChamyoNotificationEvent; +import mouda.backend.notification.domain.NotificationPayload; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.exception.NotificationErrorMessage; +import mouda.backend.notification.exception.NotificationException; +import mouda.backend.notification.implement.NotificationProcessor; + +@Component +public class ChamyoNotificationEventHandler extends AbstractMoimRelatedNotificationEventHandler { + + private final ChamyoRecipientFinder chamyoRecipientFinder; + + public ChamyoNotificationEventHandler( + UrlConfig urlConfig, NotificationProcessor notificationProcessor, ChamyoRecipientFinder chamyoRecipientFinder + ) { + super(urlConfig, notificationProcessor); + this.chamyoRecipientFinder = chamyoRecipientFinder; + } + + @Async + @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(classes = ChamyoNotificationEvent.class, phase = TransactionPhase.AFTER_COMMIT) + public void handleChamyoNotificationEvent(ChamyoNotificationEvent event) { + Moim moim = event.getMoim(); + DarakbangMember updatedMember = event.getUpdatedMember(); + Darakbang darakbang = updatedMember.getDarakbang(); + + List recipients = chamyoRecipientFinder.getChamyoNotificationRecipients(moim, updatedMember); + + NotificationPayload payload = NotificationPayload.createNonChatPayload( + event.getNotificationType(), + darakbang.getName(), + ChamyoNotificationMessage.create(updatedMember.getNickname(), moim.getTitle(), event.getNotificationType()), + getMoimUrl(darakbang.getId(), moim.getId()), + recipients + ); + notificationProcessor.process(payload); + } + + static class ChamyoNotificationMessage { + + public static String create(String updatedMemberName, String moimTitle, NotificationType type) { + if (type == NotificationType.NEW_MOIMEE_JOINED) { + return updatedMemberName + "님이 " + moimTitle + " 모임에 참여했어요!"; + } + if (type == NotificationType.MOIMEE_LEFT) { + return updatedMemberName + "님이 " + moimTitle + " 모임 참여를 취소했어요!"; + } + throw new NotificationException( + HttpStatus.BAD_REQUEST, NotificationErrorMessage.NOT_ALLOWED_NOTIFICATION_TYPE + ); + } + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/ChamyoRecipientFinder.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/ChamyoRecipientFinder.java new file mode 100644 index 000000000..fe2448470 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/ChamyoRecipientFinder.java @@ -0,0 +1,32 @@ +package mouda.backend.moim.implement.notificiation; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.notification.domain.Recipient; + +@Component +@RequiredArgsConstructor +public class ChamyoRecipientFinder { + + private final ChamyoRepository chamyoRepository; + + public List getChamyoNotificationRecipients(Moim moim, DarakbangMember updatedMember) { + List chamyos = chamyoRepository.findAllByMoimId(moim.getId()); + return chamyos.stream() + .filter(chamyo -> chamyo.isNotSameMember(updatedMember)) + .map(Chamyo::getDarakbangMember) + .map(darakbangMember -> Recipient.builder() + .darakbangMemberId(darakbangMember.getId()) + .memberId(darakbangMember.getMemberId()) + .build() + ) + .toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentNotificationEventHandler.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentNotificationEventHandler.java new file mode 100644 index 000000000..f3c6b0362 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentNotificationEventHandler.java @@ -0,0 +1,78 @@ +package mouda.backend.moim.implement.notificiation; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import mouda.backend.common.config.UrlConfig; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.notificiation.event.CommentNotificationEvent; +import mouda.backend.notification.domain.NotificationPayload; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.exception.NotificationErrorMessage; +import mouda.backend.notification.exception.NotificationException; +import mouda.backend.notification.implement.NotificationProcessor; + +@Component +public class CommentNotificationEventHandler extends AbstractMoimRelatedNotificationEventHandler { + + private final CommentRecipientFinder commentRecipientFinder; + + public CommentNotificationEventHandler(UrlConfig urlConfig, NotificationProcessor notificationProcessor, + CommentRecipientFinder commentRecipientFinder) { + super(urlConfig, notificationProcessor); + this.commentRecipientFinder = commentRecipientFinder; + } + + @Async + @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(classes = CommentNotificationEvent.class, phase = TransactionPhase.AFTER_COMMIT) + public void handleCommentNotificationEvent(CommentNotificationEvent event) { + Comment comment = event.getComment(); + DarakbangMember author = event.getAuthor(); + + CommentRecipients commentRecipients = commentRecipientFinder.getAllRecipient(comment); + + commentRecipients.getRecipients() + .forEach((type, recipients) -> processNotification(type, recipients, comment, author)); + } + + private void processNotification( + NotificationType notificationType, List recipients, Comment comment, DarakbangMember author + ) { + Moim moim = comment.getMoim(); + NotificationPayload payload = NotificationPayload.createNonChatPayload( + notificationType, + moim.getTitle(), + CommentNotificationMessage.create(author.getNickname(), notificationType), + getMoimUrl(moim.getDarakbangId(), moim.getId()), + recipients + ); + + notificationProcessor.process(payload); + } + + static class CommentNotificationMessage { + + public static String create(String author, NotificationType type) { + if (type == NotificationType.NEW_COMMENT) { + return author + "님이 댓글을 남겼어요!"; + } + if (type == NotificationType.NEW_REPLY) { + return author + "님이 답글을 남겼어요!"; + } + throw new NotificationException( + HttpStatus.BAD_REQUEST, NotificationErrorMessage.NOT_ALLOWED_NOTIFICATION_TYPE + ); + } + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentRecipientFinder.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentRecipientFinder.java new file mode 100644 index 000000000..36a0f9f89 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentRecipientFinder.java @@ -0,0 +1,101 @@ +package mouda.backend.moim.implement.notificiation; + +import java.util.Optional; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.exception.ChamyoErrorMessage; +import mouda.backend.moim.exception.ChamyoException; +import mouda.backend.moim.exception.CommentErrorMessage; +import mouda.backend.moim.exception.CommentException; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.CommentRepository; +import mouda.backend.notification.domain.NotificationType; + +@Component +@RequiredArgsConstructor +public class CommentRecipientFinder { + + private final ChamyoRepository chamyoRepository; + private final CommentRepository commentRepository; + + public CommentRecipients getAllRecipient(Comment comment) { + if (comment.isComment()) { + return getCommentRecipientWhenComment(comment); + } + return getCommentRecipientWhenReply(comment); + } + + // 댓글 + // 작성자가 방장인 경우: 아무에게도 알림을 보내지 않음. + // 작성자가 방장이 아닌 경우: 방장에게 '댓글' 알림을 보냄. + private CommentRecipients getCommentRecipientWhenComment(Comment comment) { + CommentRecipients commentRecipients = new CommentRecipients(); + + Moim moim = comment.getMoim(); + DarakbangMember moimer = getMoimer(moim); + DarakbangMember author = comment.getDarakbangMember(); + + if (moimer.isNotSameMemberWith(author)) { + commentRecipients.addRecipient(NotificationType.NEW_COMMENT, moimer.getMemberId(), moimer.getId()); + } + + return commentRecipients; + } + + // 답글 + // 작성자가 방장인 경우: + // -> 원 댓글 작성자가 방장이면 아무에게도 알림 X + // -> 원 댓글 작성자가 방장이 아니면 원 댓글 작성자에게만 답글 알림 + // 작성자가 방장이 아닌 경우: + // -> 원 댓글 작성자가 방장인 경우: 방장에게만 답글 알림 + // -> 원 댓글 작성자가 자신인 경우: 방장에게만 댓글 알림 + // -> 원 댓글 작성자가 방장이 아닌 경우: 원 댓글 작성자에게는 답글 알림, 방장에게는 댓글 알림. + private CommentRecipients getCommentRecipientWhenReply(Comment comment) { + CommentRecipients commentRecipients = new CommentRecipients(); + + Moim moim = comment.getMoim(); + + DarakbangMember moimer = getMoimer(moim); + DarakbangMember author = comment.getDarakbangMember(); + DarakbangMember parentAuthor = commentRepository.findParentCommentByParentId(comment.getParentId()) + .map(Comment::getDarakbangMember) + .orElseThrow(() -> new CommentException(HttpStatus.NOT_FOUND, CommentErrorMessage.PARENT_NOT_FOUND)); + + // 작성자가 방장인 경우 + if (author.isSameMemberWith(moimer)) { + // 원 댓글 작성자가 방장이 아닌 경우 + if (parentAuthor.isNotSameMemberWith(moimer)) { + commentRecipients.addRecipient(NotificationType.NEW_REPLY, parentAuthor.getMemberId(), parentAuthor.getId()); + } + } else { + // 작성자가 방장이 아닌 경우 + if (parentAuthor.isSameMemberWith(moimer)) { + // 원 댓글 작성자가 방장인 경우 + commentRecipients.addRecipient(NotificationType.NEW_REPLY, moimer.getMemberId(), moimer.getId()); + } else { + // 원 댓글 작성자가 방장이 아닌 경우 + if (parentAuthor.isNotSameMemberWith(author)) { + commentRecipients.addRecipient(NotificationType.NEW_REPLY, parentAuthor.getMemberId(), parentAuthor.getId()); + } + commentRecipients.addRecipient(NotificationType.NEW_COMMENT, moimer.getMemberId(), moimer.getId()); + } + } + + return commentRecipients; + } + + private DarakbangMember getMoimer(Moim moim) { + Optional chamyoOptional = chamyoRepository.findMoimerByMoimId(moim.getId()); + if (chamyoOptional.isEmpty()) { + throw new ChamyoException(HttpStatus.NOT_FOUND, ChamyoErrorMessage.NOT_FOUND); + } + return chamyoOptional.get().getDarakbangMember(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentRecipients.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentRecipients.java new file mode 100644 index 000000000..c148e03a6 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/CommentRecipients.java @@ -0,0 +1,40 @@ +package mouda.backend.moim.implement.notificiation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; + +@RequiredArgsConstructor +@Getter +@Builder +public class CommentRecipients { + + private final Map> recipients; + + public CommentRecipients() { + this.recipients = new ConcurrentHashMap<>(); + } + + public void addRecipient(NotificationType notificationType, long memberId, long darakbangMemberId) { + Recipient recipient = Recipient.builder() + .memberId(memberId) + .darakbangMemberId(darakbangMemberId) + .build(); + + if (recipients.containsKey(notificationType)) { + recipients.get(notificationType).add(recipient); + return; + } + + List recipientList = new ArrayList<>(); + recipientList.add(recipient); + recipients.put(notificationType, recipientList); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimNotificationEventHandler.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimNotificationEventHandler.java new file mode 100644 index 000000000..73e179f59 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimNotificationEventHandler.java @@ -0,0 +1,124 @@ +package mouda.backend.moim.implement.notificiation; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionalEventListener; + +import mouda.backend.common.config.UrlConfig; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.exception.MoimErrorMessage; +import mouda.backend.moim.exception.MoimException; +import mouda.backend.moim.implement.notificiation.event.MoimCreateNotificationEvent; +import mouda.backend.moim.implement.notificiation.event.MoimEditedNotificationEvent; +import mouda.backend.moim.implement.notificiation.event.MoimStatusChangeNotificationEvent; +import mouda.backend.notification.domain.NotificationPayload; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.exception.NotificationErrorMessage; +import mouda.backend.notification.exception.NotificationException; +import mouda.backend.notification.implement.NotificationProcessor; + +@Component +public class MoimNotificationEventHandler extends AbstractMoimRelatedNotificationEventHandler { + + private final MoimRecipientFinder moimRecipientFinder; + private final DarakbangRepository darakbangRepository; + + public MoimNotificationEventHandler( + UrlConfig urlConfig, NotificationProcessor notificationProcessor, + MoimRecipientFinder moimRecipientFinder, DarakbangRepository darakbangRepository + ) { + super(urlConfig, notificationProcessor); + this.moimRecipientFinder = moimRecipientFinder; + this.darakbangRepository = darakbangRepository; + } + + @Async + @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(classes = MoimCreateNotificationEvent.class) + public void handleMoimCreateNotificationEvent(MoimCreateNotificationEvent event) { + Moim moim = event.getMoim(); + NotificationType notificationType = event.getNotificationType(); + + List recipients = moimRecipientFinder.getMoimCreatedNotificationRecipients(moim.getDarakbangId(), + event.getHost().getId()); + String message = MoimNotificationMessage.create(moim.getTitle(), notificationType); + + processNotification(notificationType, moim, message, recipients); + } + + @Async + @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(classes = MoimStatusChangeNotificationEvent.class) + public void handleMoimStatusChangeNotificationEvent(MoimStatusChangeNotificationEvent event) { + Moim moim = event.getMoim(); + NotificationType notificationType = event.getNotificationType(); + + String message = MoimNotificationMessage.create(moim.getTitle(), notificationType); + processMoimModifiedNotification(moim, notificationType, message); + } + + @Async + @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(classes = MoimEditedNotificationEvent.class) + public void handleMoimEditedNotificationEvent(MoimEditedNotificationEvent event) { + Moim moim = event.getMoim(); + NotificationType notificationType = event.getNotificationType(); + + String message = MoimNotificationMessage.create(event.getOldMoimTitle(), notificationType); + processMoimModifiedNotification(moim, notificationType, message); + } + + private void processMoimModifiedNotification(Moim moim, NotificationType notificationType, String message) { + List recipients = moimRecipientFinder.getMoimModifiedNotificationRecipients(moim.getId()); + processNotification(notificationType, moim, message, recipients); + } + + private void processNotification( + NotificationType notificationType, Moim moim, String message, List recipients + ) { + Darakbang darakbang = darakbangRepository.findById(moim.getDarakbangId()) + .orElseThrow(() -> new MoimException(HttpStatus.NOT_FOUND, MoimErrorMessage.DARAKBANG_NOT_FOUND)); + + NotificationPayload payload = NotificationPayload.createNonChatPayload( + notificationType, + darakbang.getName(), + message, + getMoimUrl(darakbang.getId(), moim.getId()), + recipients + ); + + notificationProcessor.process(payload); + } + + static class MoimNotificationMessage { + + public static String create(String moimTitle, NotificationType type) { + if (type == NotificationType.MOIM_CREATED) { + return moimTitle + " 모임이 만들어졌어요!"; + } + if (type == NotificationType.MOIMING_COMPLETED) { + return moimTitle + " 모집이 마감되었어요!"; + } + if (type == NotificationType.MOINING_REOPENED) { + return moimTitle + " 모집이 재개되었어요!"; + } + if (type == NotificationType.MOIM_CANCELLED) { + return moimTitle + " 모임이 취소되었어요!"; + } + if (type == NotificationType.MOIM_MODIFIED) { + return moimTitle + " 모임 정보가 변경되었어요!"; + } + throw new NotificationException( + HttpStatus.BAD_REQUEST, NotificationErrorMessage.NOT_ALLOWED_NOTIFICATION_TYPE + ); + } + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimRecipientFinder.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimRecipientFinder.java new file mode 100644 index 000000000..0abf38320 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimRecipientFinder.java @@ -0,0 +1,48 @@ +package mouda.backend.moim.implement.notificiation; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.notification.domain.Recipient; + +@Component +@RequiredArgsConstructor +public class MoimRecipientFinder { + + private final ChamyoRepository chamyoRepository; + private final DarakbangMemberRepository darakbangMemberRepository; + + public List getMoimCreatedNotificationRecipients(long darakbangId, long authorId) { + List darakbangMembers = darakbangMemberRepository.findAllByDarakbangId(darakbangId); + + return darakbangMembers.stream() + .filter(darakbangMember -> darakbangMember.getId() != authorId) + .map(darakbangMember -> Recipient.builder() + .memberId(darakbangMember.getMemberId()) + .darakbangMemberId(darakbangMember.getId()) + .build() + ) + .toList(); + } + + public List getMoimModifiedNotificationRecipients(long moimId) { + List chamyos = chamyoRepository.findAllByMoimId(moimId); + + return chamyos.stream() + .filter(chamyo -> chamyo.getMoimRole() != MoimRole.MOIMER) + .map(Chamyo::getDarakbangMember) + .map(darakbangMember -> Recipient.builder() + .memberId(darakbangMember.getMemberId()) + .darakbangMemberId(darakbangMember.getId()) + .build() + ) + .toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimRelatedNotificationSender.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimRelatedNotificationSender.java new file mode 100644 index 000000000..0061c379c --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/MoimRelatedNotificationSender.java @@ -0,0 +1,70 @@ +package mouda.backend.moim.implement.notificiation; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.notificiation.event.ChamyoNotificationEvent; +import mouda.backend.moim.implement.notificiation.event.CommentNotificationEvent; +import mouda.backend.moim.implement.notificiation.event.MoimCreateNotificationEvent; +import mouda.backend.moim.implement.notificiation.event.MoimEditedNotificationEvent; +import mouda.backend.moim.implement.notificiation.event.MoimStatusChangeNotificationEvent; +import mouda.backend.notification.domain.NotificationType; + +@Component +@RequiredArgsConstructor +public class MoimRelatedNotificationSender { + + private final ApplicationEventPublisher eventPublisher; + + public void sendMoimCreatedNotification(Moim moim, DarakbangMember host, NotificationType notificationType) { + MoimCreateNotificationEvent event = MoimCreateNotificationEvent.builder() + .moim(moim) + .host(host) + .notificationType(notificationType) + .build(); + + eventPublisher.publishEvent(event); + } + + public void sendMoimEditedNotification(Moim moim, String oldMoimTitle, NotificationType notificationType) { + MoimEditedNotificationEvent event = MoimEditedNotificationEvent.builder() + .moim(moim) + .oldMoimTitle(oldMoimTitle) + .notificationType(notificationType) + .build(); + + eventPublisher.publishEvent(event); + } + + public void sendMoimStatusChangeNotification(Moim moim, NotificationType notificationType) { + MoimStatusChangeNotificationEvent event = MoimStatusChangeNotificationEvent.builder() + .moim(moim) + .notificationType(notificationType) + .build(); + + eventPublisher.publishEvent(event); + } + + public void sendChamyoNotification(Chamyo chamyo, NotificationType notificationType) { + ChamyoNotificationEvent event = ChamyoNotificationEvent.builder() + .chamyo(chamyo) + .notificationType(notificationType) + .build(); + + eventPublisher.publishEvent(event); + } + + public void sendCommentNotification(Comment comment, DarakbangMember darakbangMember) { + CommentNotificationEvent event = CommentNotificationEvent.builder() + .comment(comment) + .author(darakbangMember) + .build(); + + eventPublisher.publishEvent(event); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/ChamyoNotificationEvent.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/ChamyoNotificationEvent.java new file mode 100644 index 000000000..6d0f1703e --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/ChamyoNotificationEvent.java @@ -0,0 +1,26 @@ +package mouda.backend.moim.implement.notificiation.event; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.notification.domain.NotificationType; + +@Getter +@RequiredArgsConstructor +@Builder +public class ChamyoNotificationEvent { + + private final Chamyo chamyo; + private final NotificationType notificationType; + + public Moim getMoim() { + return chamyo.getMoim(); + } + + public DarakbangMember getUpdatedMember() { + return chamyo.getDarakbangMember(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/CommentNotificationEvent.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/CommentNotificationEvent.java new file mode 100644 index 000000000..2c9e12a7f --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/CommentNotificationEvent.java @@ -0,0 +1,16 @@ +package mouda.backend.moim.implement.notificiation.event; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Comment; + +@Getter +@RequiredArgsConstructor +@Builder +public class CommentNotificationEvent { + + private final Comment comment; + private final DarakbangMember author; +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimCreateNotificationEvent.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimCreateNotificationEvent.java new file mode 100644 index 000000000..e85fd4a16 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimCreateNotificationEvent.java @@ -0,0 +1,18 @@ +package mouda.backend.moim.implement.notificiation.event; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Moim; +import mouda.backend.notification.domain.NotificationType; + +@Getter +@RequiredArgsConstructor +@Builder +public class MoimCreateNotificationEvent { + + private final Moim moim; + private final DarakbangMember host; + private final NotificationType notificationType; +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimEditedNotificationEvent.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimEditedNotificationEvent.java new file mode 100644 index 000000000..c7fd27a27 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimEditedNotificationEvent.java @@ -0,0 +1,17 @@ +package mouda.backend.moim.implement.notificiation.event; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.moim.domain.Moim; +import mouda.backend.notification.domain.NotificationType; + +@Getter +@RequiredArgsConstructor +@Builder +public class MoimEditedNotificationEvent { + + private final Moim moim; + private final String oldMoimTitle; + private final NotificationType notificationType; +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimStatusChangeNotificationEvent.java b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimStatusChangeNotificationEvent.java new file mode 100644 index 000000000..a6bedabab --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/notificiation/event/MoimStatusChangeNotificationEvent.java @@ -0,0 +1,16 @@ +package mouda.backend.moim.implement.notificiation.event; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.moim.domain.Moim; +import mouda.backend.notification.domain.NotificationType; + +@Getter +@RequiredArgsConstructor +@Builder +public class MoimStatusChangeNotificationEvent { + + private final Moim moim; + private final NotificationType notificationType; +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/validator/ChamyoValidator.java b/backend/src/main/java/mouda/backend/moim/implement/validator/ChamyoValidator.java new file mode 100644 index 000000000..289b51c13 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/validator/ChamyoValidator.java @@ -0,0 +1,70 @@ +package mouda.backend.moim.implement.validator; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.exception.ChamyoErrorMessage; +import mouda.backend.moim.exception.ChamyoException; +import mouda.backend.moim.implement.finder.ChamyoFinder; +import mouda.backend.moim.implement.finder.MoimFinder; +import mouda.backend.moim.infrastructure.ChamyoRepository; + +@Component +@RequiredArgsConstructor +public class ChamyoValidator { + + private final MoimFinder moimFinder; + private final ChamyoRepository chamyoRepository; + private final ChamyoFinder chamyoFinder; + + public void validateCanParticipate(Moim moim, DarakbangMember darakbangMember) { + validateMoimIsParticipable(moim); + validateAlreadyParticipated(moim, darakbangMember); + } + + private void validateMoimIsParticipable(Moim moim) { + int currentPeople = moimFinder.countCurrentPeople(moim); + if (moim.isFull(currentPeople)) { + throw new ChamyoException(HttpStatus.BAD_REQUEST, ChamyoErrorMessage.MOIM_FULL); + } + if (moim.isCanceled()) { + throw new ChamyoException(HttpStatus.BAD_REQUEST, ChamyoErrorMessage.MOIMING_CANCLED); + } + if (moim.isCompleted()) { + throw new ChamyoException(HttpStatus.BAD_REQUEST, ChamyoErrorMessage.MOIMING_COMPLETE); + } + } + + private void validateAlreadyParticipated(Moim moim, DarakbangMember darakbangMember) { + boolean isChamyoExists = chamyoRepository.existsByMoimIdAndDarakbangMemberId( + moim.getId(), darakbangMember.getId() + ); + if (isChamyoExists) { + throw new ChamyoException(HttpStatus.BAD_REQUEST, ChamyoErrorMessage.ALREADY_PARTICIPATED); + } + } + + public void validateCanCancel(Chamyo chamyo) { + if (chamyo.getMoimRole() != MoimRole.MOIMEE) { + throw new ChamyoException(HttpStatus.BAD_REQUEST, ChamyoErrorMessage.CANNOT_CANCEL_CHAMYO); + } + } + + public void validateMoimer(Moim moim, DarakbangMember darakbangMember) { + Chamyo chamyo = chamyoFinder.read(moim, darakbangMember); + if (chamyo.getMoimRole() != MoimRole.MOIMER) { + throw new ChamyoException(HttpStatus.BAD_REQUEST, ChamyoErrorMessage.NOT_MOIMER); + } + } + + public void validateMemberChamyoMoim(Moim moim, DarakbangMember darakbangMember) { + if (!chamyoFinder.exists(moim.getId(), darakbangMember)) { + throw new ChamyoException(HttpStatus.BAD_REQUEST, ChamyoErrorMessage.NOT_FOUND); + } + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/validator/CommentValidator.java b/backend/src/main/java/mouda/backend/moim/implement/validator/CommentValidator.java new file mode 100644 index 000000000..547b1e0fe --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/validator/CommentValidator.java @@ -0,0 +1,25 @@ +package mouda.backend.moim.implement.validator; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.moim.exception.CommentErrorMessage; +import mouda.backend.moim.exception.CommentException; +import mouda.backend.moim.infrastructure.CommentRepository; + +@Component +@RequiredArgsConstructor +public class CommentValidator { + + private final CommentRepository commentRepository; + + public void validateParentCommentExists(Long parentId) { + if (parentId == null) { + return; + } + if (!commentRepository.existsById(parentId)) { + throw new CommentException(HttpStatus.BAD_REQUEST, CommentErrorMessage.PARENT_NOT_FOUND); + } + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/validator/MoimValidator.java b/backend/src/main/java/mouda/backend/moim/implement/validator/MoimValidator.java new file mode 100644 index 000000000..b1421f1b9 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/validator/MoimValidator.java @@ -0,0 +1,86 @@ +package mouda.backend.moim.implement.validator; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.exception.MoimErrorMessage; +import mouda.backend.moim.exception.MoimException; +import mouda.backend.moim.implement.finder.ChamyoFinder; +import mouda.backend.moim.implement.finder.MoimFinder; +import mouda.backend.moim.infrastructure.MoimRepository; + +@Component +@RequiredArgsConstructor +public class MoimValidator { + + private final MoimRepository moimRepository; + private final ChamyoFinder chamyoFinder; + private final MoimFinder moimFinder; + + public void validateMoimExists(long moimId, long darakbangId) { + boolean isNotExists = !moimRepository.existsByIdAndDarakbangId(moimId, darakbangId); + if (isNotExists) { + throw new MoimException(HttpStatus.NOT_FOUND, MoimErrorMessage.NOT_FOUND); + } + } + + public void validateCanCompleteMoim(Moim moim, DarakbangMember darakbangMember) { + validateIsMoimer(moim, darakbangMember, MoimErrorMessage.NOT_ALLOWED_TO_COMPLETE); + validateMoimIsNotCompleted(moim); + validateMoimIsNotCanceled(moim); + } + + public void validateCanCancelMoim(Moim moim, DarakbangMember darakbangMember) { + validateIsMoimer(moim, darakbangMember, MoimErrorMessage.NOT_ALLOWED_TO_CANCEL); + validateMoimIsNotMoiming(moim); + } + + public void validateCanReopenMoim(Moim moim, DarakbangMember darakbangMember) { + validateIsMoimer(moim, darakbangMember, MoimErrorMessage.NOT_ALLOWED_TO_REOPEN); + validateMoimIsNotFull(moim); + validateMoimIsNotCanceled(moim); + validateMoimIsNotMoiming(moim); + } + + public void validateCanEditMoim(Moim moim, DarakbangMember darakbangMember) { + validateIsMoimer(moim, darakbangMember, MoimErrorMessage.NOT_ALLOWED_TO_EDIT); + validateMoimIsNotCompleted(moim); + validateMoimIsNotCanceled(moim); + } + + private void validateMoimIsNotFull(Moim moim) { + if (moim.isFull(moimFinder.countCurrentPeople(moim))) { + throw new MoimException(HttpStatus.BAD_REQUEST, MoimErrorMessage.MOIM_FULL_FOR_REOPEN); + } + } + + public void validateIsMoimer(Moim moim, DarakbangMember darakbangMember, MoimErrorMessage expectedErrorMessage) { + MoimRole moimRole = chamyoFinder.readMoimRole(moim, darakbangMember); + + if (moimRole != MoimRole.MOIMER) { + throw new MoimException(HttpStatus.FORBIDDEN, expectedErrorMessage); + } + } + + private void validateMoimIsNotCanceled(Moim moim) { + if (moim.isCanceled()) { + throw new MoimException(HttpStatus.BAD_REQUEST, MoimErrorMessage.MOIM_CANCELED); + } + } + + private void validateMoimIsNotCompleted(Moim moim) { + if (moim.isCompleted()) { + throw new MoimException(HttpStatus.BAD_REQUEST, MoimErrorMessage.ALREADY_COMPLETED); + } + } + + private void validateMoimIsNotMoiming(Moim moim) { + if (moim.isMoiming()) { + throw new MoimException(HttpStatus.BAD_REQUEST, MoimErrorMessage.ALREADY_MOIMING); + } + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/validator/ZzimValidator.java b/backend/src/main/java/mouda/backend/moim/implement/validator/ZzimValidator.java new file mode 100644 index 000000000..b69e15a77 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/validator/ZzimValidator.java @@ -0,0 +1,10 @@ +package mouda.backend.moim.implement.validator; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class ZzimValidator { +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/writer/ChamyoWriter.java b/backend/src/main/java/mouda/backend/moim/implement/writer/ChamyoWriter.java new file mode 100644 index 000000000..c43b62e32 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/writer/ChamyoWriter.java @@ -0,0 +1,58 @@ +package mouda.backend.moim.implement.writer; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.exception.ChamyoErrorMessage; +import mouda.backend.moim.exception.ChamyoException; +import mouda.backend.moim.implement.validator.ChamyoValidator; +import mouda.backend.moim.infrastructure.ChamyoRepository; + +@Component +@RequiredArgsConstructor +public class ChamyoWriter { + + private final ChamyoValidator chamyoValidator; + private final ChamyoRepository chamyoRepository; + + public Chamyo saveAsMoimer(Moim moim, DarakbangMember darakbangMember) { + return save(moim, darakbangMember, MoimRole.MOIMER); + } + + public Chamyo saveAsMoimee(Moim moim, DarakbangMember darakbangMember) { + return save(moim, darakbangMember, MoimRole.MOIMEE); + } + + private Chamyo save(Moim moim, DarakbangMember darakbangMember, MoimRole moimRole) { + chamyoValidator.validateCanParticipate(moim, darakbangMember); + + Chamyo chamyo = Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangMember) + .moimRole(moimRole) + .build(); + + try { + return chamyoRepository.save(chamyo); + } catch (DataIntegrityViolationException exception) { + throw new ChamyoException(HttpStatus.BAD_REQUEST, ChamyoErrorMessage.ALREADY_PARTICIPATED); + } + } + + public void delete(Chamyo chamyo) { + chamyoValidator.validateCanCancel(chamyo); + chamyoRepository.delete(chamyo); + } + + public void updateLastReadChat(long targetId, DarakbangMember darakbangMember, long lastReadChatId) { + Chamyo chamyo = chamyoRepository.findByMoimIdAndDarakbangMemberId(targetId, darakbangMember.getId()) + .orElseThrow(() -> new ChamyoException(HttpStatus.NOT_FOUND, ChamyoErrorMessage.NOT_FOUND)); + chamyo.updateLastChat(lastReadChatId); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/writer/CommentWriter.java b/backend/src/main/java/mouda/backend/moim/implement/writer/CommentWriter.java new file mode 100644 index 000000000..2cf971a49 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/writer/CommentWriter.java @@ -0,0 +1,34 @@ +package mouda.backend.moim.implement.writer; + +import java.time.LocalDateTime; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.validator.CommentValidator; +import mouda.backend.moim.infrastructure.CommentRepository; + +@Component +@RequiredArgsConstructor +public class CommentWriter { + + private final CommentRepository commentRepository; + private final CommentValidator commentValidator; + + public Comment saveComment(Moim moim, DarakbangMember darakbangMember, Long parentId, String content) { + commentValidator.validateParentCommentExists(parentId); + + Comment comment = Comment.builder() + .content(content) + .moim(moim) + .darakbangMember(darakbangMember) + .parentId(parentId) + .createdAt(LocalDateTime.now()) + .build(); + + return commentRepository.save(comment); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/writer/MoimWriter.java b/backend/src/main/java/mouda/backend/moim/implement/writer/MoimWriter.java new file mode 100644 index 000000000..863ebc53a --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/writer/MoimWriter.java @@ -0,0 +1,78 @@ +package mouda.backend.moim.implement.writer; + +import java.time.LocalDate; +import java.time.LocalTime; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.finder.MoimFinder; +import mouda.backend.moim.implement.validator.ChamyoValidator; +import mouda.backend.moim.implement.validator.MoimValidator; +import mouda.backend.moim.infrastructure.MoimRepository; + +@Component +@RequiredArgsConstructor +public class MoimWriter { + + private final MoimRepository moimRepository; + private final MoimValidator moimValidator; + private final ChamyoWriter chamyoWriter; + private final MoimFinder moimFinder; + private final ChamyoValidator chamyoValidator; + + public Moim save(Moim moim, DarakbangMember darakbangMember) { + Moim saved = moimRepository.save(moim); + chamyoWriter.saveAsMoimer(saved, darakbangMember); + + return saved; + } + + public void updateMoimStatusIfFull(Moim moim) { + if (moim.isFull(moimFinder.countCurrentPeople(moim))) { + moim.complete(); + } + } + + public void completeMoim(Moim moim, DarakbangMember darakbangMember) { + moimValidator.validateCanCompleteMoim(moim, darakbangMember); + moim.complete(); + } + + public void cancelMoim(Moim moim, DarakbangMember darakbangMember) { + moimValidator.validateCanCancelMoim(moim, darakbangMember); + moim.cancel(); + } + + public void reopenMoim(Moim moim, DarakbangMember darakbangMember) { + moimValidator.validateCanReopenMoim(moim, darakbangMember); + moim.reopen(); + } + + public void updateMoim( + Moim moim, DarakbangMember darakbangMember, + String newTitle, LocalDate newDate, LocalTime newTime, + String newPlace, int newMaxPeople, String newDescription + ) { + moimValidator.validateCanEditMoim(moim, darakbangMember); + moim.update(newTitle, newDate, newTime, newPlace, newMaxPeople, newDescription, + moimFinder.countCurrentPeople(moim)); + } + + public void confirmPlace(Moim moim, DarakbangMember darakbangMember, String place) { + chamyoValidator.validateMoimer(moim, darakbangMember); + moim.confirmPlace(place); + } + + public void confirmDateTime(Moim moim, DarakbangMember darakbangMember, LocalDate date, LocalTime time) { + chamyoValidator.validateMoimer(moim, darakbangMember); + moim.confirmDateTime(date, time); + } + + public void openChatByMoimer(Moim moim, DarakbangMember darakbangMember) { + chamyoValidator.validateMoimer(moim, darakbangMember); + moim.openChat(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/implement/writer/ZzimWriter.java b/backend/src/main/java/mouda/backend/moim/implement/writer/ZzimWriter.java new file mode 100644 index 000000000..bd08746fa --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/implement/writer/ZzimWriter.java @@ -0,0 +1,40 @@ +package mouda.backend.moim.implement.writer; + +import java.util.Optional; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.Zzim; +import mouda.backend.moim.implement.finder.MoimFinder; +import mouda.backend.moim.implement.finder.ZzimFinder; +import mouda.backend.moim.infrastructure.ZzimRepository; + +@Component +@RequiredArgsConstructor +public class ZzimWriter { + + private final ZzimRepository zzimRepository; + private final ZzimFinder zzimFinder; + + public void updateZzimStatus(Moim moim, DarakbangMember darakbangMember) { + zzimFinder.find(moim.getId(), darakbangMember).ifPresentOrElse( + this::delete, + () -> save(moim, darakbangMember) + ); + } + + public Zzim save(Moim moim, DarakbangMember darakbangMember) { + Zzim zzim = Zzim.builder() + .moim(moim) + .darakbangMember(darakbangMember) + .build(); + return zzimRepository.save(zzim); + } + + public void delete(Zzim zzim) { + zzimRepository.delete(zzim); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/infrastructure/ChamyoRepository.java b/backend/src/main/java/mouda/backend/moim/infrastructure/ChamyoRepository.java new file mode 100644 index 000000000..fcf345e8f --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/infrastructure/ChamyoRepository.java @@ -0,0 +1,39 @@ +package mouda.backend.moim.infrastructure; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; + +public interface ChamyoRepository extends JpaRepository { + + Optional findByMoimIdAndDarakbangMemberId(Long moimId, Long darakbangMemberId); + + List findAllByMoimId(Long moimId); + + int countByMoim(Moim moim); + + int countByMoimId(long moimId); + + boolean existsByMoimIdAndDarakbangMemberId(Long moimId, Long darakbangMemberId); + + List findAllByDarakbangMemberIdOrderByIdDesc(Long darakbangMemberId); + + void deleteByMoimIdAndDarakbangMemberId(Long moimId, Long darakbangMemberId); + + @Query("SELECT c.darakbangMember.memberId FROM Chamyo c WHERE c.moim.id = :moimId AND c.moimRole = 'MOIMER'") + Long findMoimerIdByMoimId(@Param("moimId") Long moimId); + + List findAllByDarakbangMemberIdAndMoim_DarakbangId(Long darakbangMemberId, Long darakbangId); + + @Query("SELECT c.lastReadChatId FROM Chamyo c WHERE c.moim.id = :moimId") + long findLastReadChatIdByMoimId(@Param("moimId") long moimId); + + @Query("SELECT c FROM Chamyo c WHERE c.moim.id = :moimId AND c.moimRole = 'MOIMER'") + Optional findMoimerByMoimId(@Param("moimId") Long moimId); +} diff --git a/backend/src/main/java/mouda/backend/moim/infrastructure/CommentRepository.java b/backend/src/main/java/mouda/backend/moim/infrastructure/CommentRepository.java new file mode 100644 index 000000000..2ef460f38 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/infrastructure/CommentRepository.java @@ -0,0 +1,22 @@ +package mouda.backend.moim.infrastructure; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; + +public interface CommentRepository extends JpaRepository { + + @Query("SELECT c.darakbangMember.memberId FROM Comment c WHERE c.id = :parentId") + Long findMemberIdByParentId(@Param("parentId") long parentId); + + List findAllByMoimOrderByCreatedAt(Moim moim); + + @Query("SELECT c FROM Comment c WHERE c.id = :parentId") + Optional findParentCommentByParentId(@Param("parentId") Long parentId); +} diff --git a/backend/src/main/java/mouda/backend/moim/infrastructure/MoimRepository.java b/backend/src/main/java/mouda/backend/moim/infrastructure/MoimRepository.java new file mode 100644 index 000000000..90898906e --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/infrastructure/MoimRepository.java @@ -0,0 +1,36 @@ +package mouda.backend.moim.infrastructure; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimStatus; + +public interface MoimRepository extends JpaRepository { + + @Query(""" + UPDATE Moim m + SET m.moimStatus = :status + WHERE m.id = :id + """) + @Modifying + int updateMoimStatusById(@Param("id") Long moimId, @Param("status") MoimStatus status); + + @Query(""" + SELECT m From Moim m + WHERE m.darakbangId = :darakbangId AND m.moimStatus = 'MOIMING' + ORDER BY m.id DESC + """) + List findAllByDarakbangIdOrderByIdDesc(@Param("darakbangId") Long darakbangId); + + Optional findByIdAndDarakbangId(Long moimId, Long darakbangId); + + boolean existsByIdAndDarakbangId(Long moimId, Long darakbangId); + +} diff --git a/backend/src/main/java/mouda/backend/moim/infrastructure/ZzimRepository.java b/backend/src/main/java/mouda/backend/moim/infrastructure/ZzimRepository.java new file mode 100644 index 000000000..018e8f6f5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/infrastructure/ZzimRepository.java @@ -0,0 +1,17 @@ +package mouda.backend.moim.infrastructure; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import mouda.backend.moim.domain.Zzim; + +public interface ZzimRepository extends JpaRepository { + + boolean existsByMoimIdAndDarakbangMemberId(Long moimId, Long darakbangMemberId); + + Optional findByMoimIdAndDarakbangMemberId(Long moimId, Long darakbangMemberId); + + List findAllByDarakbangMemberIdOrderByIdDesc(Long darakbangMemberId); +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/controller/ChamyoController.java b/backend/src/main/java/mouda/backend/moim/presentation/controller/ChamyoController.java new file mode 100644 index 000000000..3d4cdc86d --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/controller/ChamyoController.java @@ -0,0 +1,79 @@ +package mouda.backend.moim.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.business.ChamyoService; +import mouda.backend.moim.presentation.controller.swagger.ChamyoSwagger; +import mouda.backend.moim.presentation.request.chamyo.ChamyoCancelRequest; +import mouda.backend.moim.presentation.request.chamyo.MoimChamyoRequest; +import mouda.backend.moim.presentation.response.chamyo.ChamyoFindAllResponses; +import mouda.backend.moim.presentation.response.chamyo.MoimRoleFindResponse; + +@RestController +@RequestMapping("/v1/darakbang/{darakbangId}/chamyo") +@RequiredArgsConstructor +public class ChamyoController implements ChamyoSwagger { + + private final ChamyoService chamyoService; + + @Override + @GetMapping("/mine") + public ResponseEntity> findMoimRoleByMember( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @RequestParam Long moimId + ) { + MoimRoleFindResponse moimRoleFindResponse = chamyoService.findMoimRole(darakbangId, moimId, member); + + return ResponseEntity.ok().body(new RestResponse<>(moimRoleFindResponse)); + } + + @Override + @GetMapping("/all") + public ResponseEntity> findAllChamyoByMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @RequestParam Long moimId + ) { + ChamyoFindAllResponses chamyoFindAllResponses = chamyoService.findAllChamyo(darakbangId, moimId); + + return ResponseEntity.ok().body(new RestResponse<>(chamyoFindAllResponses)); + } + + @Override + @PostMapping + public ResponseEntity chamyoMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @Valid @RequestBody MoimChamyoRequest request + ) { + chamyoService.chamyoMoim(darakbangId, request.moimId(), member); + + return ResponseEntity.ok().build(); + } + + @Override + @DeleteMapping + public ResponseEntity cancelChamyo( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @Valid @RequestBody ChamyoCancelRequest request + ) { + chamyoService.cancelChamyo(darakbangId, request.moimId(), member); + + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/controller/MoimController.java b/backend/src/main/java/mouda/backend/moim/presentation/controller/MoimController.java new file mode 100644 index 000000000..f5c9209fc --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/controller/MoimController.java @@ -0,0 +1,155 @@ +package mouda.backend.moim.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.business.CommentService; +import mouda.backend.moim.business.MoimService; +import mouda.backend.moim.domain.FilterType; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.presentation.controller.swagger.MoimSwagger; +import mouda.backend.moim.presentation.request.comment.CommentCreateRequest; +import mouda.backend.moim.presentation.request.moim.MoimCreateRequest; +import mouda.backend.moim.presentation.request.moim.MoimEditRequest; +import mouda.backend.moim.presentation.response.moim.MoimDetailsFindResponse; +import mouda.backend.moim.presentation.response.moim.MoimFindAllResponses; + +@RestController +@RequestMapping("/v1/darakbang/{darakbangId}/moim") +@RequiredArgsConstructor +public class MoimController implements MoimSwagger { + + private final MoimService moimService; + private final CommentService commentService; + + @Override + @PostMapping + public ResponseEntity> createMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @Valid @RequestBody MoimCreateRequest moimCreateRequest + ) { + Moim moim = moimService.createMoim(darakbangId, member, moimCreateRequest); + + return ResponseEntity.ok().body(new RestResponse<>(moim.getId())); + } + + @Override + @GetMapping + public ResponseEntity> findAllMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member + ) { + MoimFindAllResponses moimFindAllResponses = moimService.findAllMoim(darakbangId, member); + + return ResponseEntity.ok().body(new RestResponse<>(moimFindAllResponses)); + } + + @Override + @GetMapping("/{moimId}") + public ResponseEntity> findMoimDetails( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @PathVariable("moimId") Long moimId + ) { + MoimDetailsFindResponse moimDetailsFindResponse = moimService.findMoimDetails(darakbangId, moimId); + + return ResponseEntity.ok().body(new RestResponse<>(moimDetailsFindResponse)); + } + + @Override + @PatchMapping("/{moimId}/complete") + public ResponseEntity completeMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @PathVariable Long moimId + ) { + moimService.completeMoim(darakbangId, moimId, member); + + return ResponseEntity.ok().build(); + } + + @Override + @PatchMapping("/{moimId}/cancel") + public ResponseEntity cancelMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @PathVariable Long moimId + ) { + moimService.cancelMoim(darakbangId, moimId, member); + + return ResponseEntity.ok().build(); + } + + @Override + @PatchMapping("/{moimId}/reopen") + public ResponseEntity reopenMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @PathVariable Long moimId + ) { + moimService.reopenMoim(darakbangId, moimId, member); + + return ResponseEntity.ok().build(); + } + + @Override + @PatchMapping + public ResponseEntity editMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @Valid @RequestBody MoimEditRequest request + ) { + moimService.editMoim(darakbangId, request, member); + + return ResponseEntity.ok().build(); + } + + @Override + @PostMapping("/{moimId}") + public ResponseEntity createComment( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @PathVariable Long moimId, + @RequestBody CommentCreateRequest commentCreateRequest + ) { + commentService.createComment(darakbangId, moimId, member, commentCreateRequest); + + return ResponseEntity.ok().build(); + } + + @Override + @GetMapping("/mine") + public ResponseEntity> findAllMyMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @RequestParam(value = "filter", defaultValue = "ALL") FilterType filter + ) { + MoimFindAllResponses moimFindAllResponses = moimService.findAllMyMoim(member, filter); + + return ResponseEntity.ok().body(new RestResponse<>(moimFindAllResponses)); + } + + @Override + @GetMapping("/zzim") + public ResponseEntity> findAllZzimedMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member + ) { + MoimFindAllResponses moimFindAllResponses = moimService.findZzimedMoim(member); + + return ResponseEntity.ok().body(new RestResponse<>(moimFindAllResponses)); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/controller/ZzimController.java b/backend/src/main/java/mouda/backend/moim/presentation/controller/ZzimController.java new file mode 100644 index 000000000..df7f3d067 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/controller/ZzimController.java @@ -0,0 +1,52 @@ +package mouda.backend.moim.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.business.ZzimService; +import mouda.backend.moim.presentation.controller.swagger.ZzimSwagger; +import mouda.backend.moim.presentation.request.zzim.ZzimUpdateRequest; +import mouda.backend.moim.presentation.response.zzim.ZzimCheckResponse; + +@RestController +@RequestMapping("/v1/darakbang/{darakbangId}/zzim") +@RequiredArgsConstructor +public class ZzimController implements ZzimSwagger { + + private final ZzimService zzimService; + + @Override + @GetMapping("/mine") + public ResponseEntity> checkZzimByMoimAndMember( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @RequestParam Long moimId + ) { + ZzimCheckResponse zzimCheckResponse = zzimService.checkZzimByMember(darakbangId, moimId, member); + + return ResponseEntity.ok().body(new RestResponse<>(zzimCheckResponse)); + } + + @Override + @PostMapping + public ResponseEntity updateZzim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @Valid @RequestBody ZzimUpdateRequest request + ) { + zzimService.updateZzim(darakbangId, request.moimId(), member); + + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/controller/swagger/ChamyoSwagger.java b/backend/src/main/java/mouda/backend/moim/presentation/controller/swagger/ChamyoSwagger.java new file mode 100644 index 000000000..8ef8c2199 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/controller/swagger/ChamyoSwagger.java @@ -0,0 +1,60 @@ +package mouda.backend.moim.presentation.controller.swagger; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.presentation.request.chamyo.ChamyoCancelRequest; +import mouda.backend.moim.presentation.request.chamyo.MoimChamyoRequest; +import mouda.backend.moim.presentation.response.chamyo.ChamyoFindAllResponses; +import mouda.backend.moim.presentation.response.chamyo.MoimRoleFindResponse; + +public interface ChamyoSwagger { + + @Operation(summary = "모임 참여 여부 조회", description = "현재 로그인된 회원의 모임 참여 여부를 조회합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모임 참여 여부 조회 성공") + }) + ResponseEntity> findMoimRoleByMember( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @RequestParam Long moimId + ); + + @Operation(summary = "모든 모임 참여자 조회", description = "모임에 참여한 모든 회원을 조회합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모든 모임 참여자 조회 성공") + }) + ResponseEntity> findAllChamyoByMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @RequestParam Long moimId + ); + + @Operation(summary = "모임 참여", description = "모임에 참여합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모임 참여 성공") + }) + ResponseEntity chamyoMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @Valid @RequestBody MoimChamyoRequest request + ); + + @Operation(summary = "모임 참여 취소", description = "모임 참여를 취소합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모임 참여 취소 성공") + }) + ResponseEntity cancelChamyo( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @Valid @RequestBody ChamyoCancelRequest request); +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/controller/swagger/MoimSwagger.java b/backend/src/main/java/mouda/backend/moim/presentation/controller/swagger/MoimSwagger.java new file mode 100644 index 000000000..49ce8b3d4 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/controller/swagger/MoimSwagger.java @@ -0,0 +1,121 @@ +package mouda.backend.moim.presentation.controller.swagger; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.FilterType; +import mouda.backend.moim.presentation.request.comment.CommentCreateRequest; +import mouda.backend.moim.presentation.request.moim.MoimCreateRequest; +import mouda.backend.moim.presentation.request.moim.MoimEditRequest; +import mouda.backend.moim.presentation.response.moim.MoimDetailsFindResponse; +import mouda.backend.moim.presentation.response.moim.MoimFindAllResponses; + +public interface MoimSwagger { + + @Operation(summary = "모임 생성", description = "모임을 생성한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모임 생성 성공!"), + }) + ResponseEntity> createMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @RequestBody MoimCreateRequest moimCreateRequest + ); + + @Operation(summary = "모임 전체 조회", description = "모든 모임을 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모임 조회 성공!"), + }) + ResponseEntity> findAllMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member + ); + + @Operation(summary = "모임 상세 조회", description = "모임 상세 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모임 상세 조회 성공!"), + }) + ResponseEntity> findMoimDetails( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @PathVariable Long moimId + ); + + @Operation(summary = "모집 완료", description = "방장이 모집을 완료합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모집 완료 성공!") + }) + ResponseEntity completeMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @PathVariable Long moimId + ); + + @Operation(summary = "모임 취소", description = "방장이 모임을 취소합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모임 취소 성공!") + }) + ResponseEntity cancelMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @PathVariable Long moimId + ); + + @Operation(summary = "모집 재개", description = "방장이 모집을 재개합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모집 재개 성공!") + }) + ResponseEntity reopenMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @PathVariable Long moimId + ); + + @Operation(summary = "모임 수정", description = "해당하는 id의 모임을 수정한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모임 수정 성공!") + }) + ResponseEntity editMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @Valid @RequestBody MoimEditRequest request + ); + + @Operation(summary = "댓글 작성", description = "해당하는 id의 모임에 댓글을 생성한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "댓글 생성 성공!") + }) + ResponseEntity createComment( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @PathVariable Long moimId, + @RequestBody CommentCreateRequest commentCreateRequest); + + @Operation(summary = "나의 모임 목록 조회", description = "내가 참여하는 모임의 목록을 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "나의 모임 목록 조회 성공!") + }) + ResponseEntity> findAllMyMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @RequestParam(value = "filter", defaultValue = "ALL") FilterType filter + ); + + @Operation(summary = "찜한 모임 목록 조회", description = "찜한 모임의 목록을 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "찜한 모임 조회 성공!") + }) + ResponseEntity> findAllZzimedMoim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member + ); +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/controller/swagger/ZzimSwagger.java b/backend/src/main/java/mouda/backend/moim/presentation/controller/swagger/ZzimSwagger.java new file mode 100644 index 000000000..cccd60d36 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/controller/swagger/ZzimSwagger.java @@ -0,0 +1,39 @@ +package mouda.backend.moim.presentation.controller.swagger; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.presentation.request.zzim.ZzimUpdateRequest; +import mouda.backend.moim.presentation.response.zzim.ZzimCheckResponse; + +public interface ZzimSwagger { + + @Operation(summary = "찜 여부 조회", description = "해당 모임에 대한 회원의 찜 여부를 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "찜 여부 조회 성공!") + }) + ResponseEntity> checkZzimByMoimAndMember( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @RequestParam Long moimId + ); + + @Operation(summary = "모임 찜하기", description = "해당 모임을 찜한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "모임 찜하기 성공!") + }) + ResponseEntity updateZzim( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @Valid @RequestBody ZzimUpdateRequest request + ); +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/request/chamyo/ChamyoCancelRequest.java b/backend/src/main/java/mouda/backend/moim/presentation/request/chamyo/ChamyoCancelRequest.java new file mode 100644 index 000000000..ce517cdf6 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/request/chamyo/ChamyoCancelRequest.java @@ -0,0 +1,10 @@ +package mouda.backend.moim.presentation.request.chamyo; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +public record ChamyoCancelRequest( + @NotNull @Positive + Long moimId +) { +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/request/chamyo/MoimChamyoRequest.java b/backend/src/main/java/mouda/backend/moim/presentation/request/chamyo/MoimChamyoRequest.java new file mode 100644 index 000000000..8de16a6d2 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/request/chamyo/MoimChamyoRequest.java @@ -0,0 +1,10 @@ +package mouda.backend.moim.presentation.request.chamyo; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +public record MoimChamyoRequest( + @NotNull @Positive + Long moimId +) { +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/request/comment/CommentCreateRequest.java b/backend/src/main/java/mouda/backend/moim/presentation/request/comment/CommentCreateRequest.java new file mode 100644 index 000000000..45c64d498 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/request/comment/CommentCreateRequest.java @@ -0,0 +1,25 @@ +package mouda.backend.moim.presentation.request.comment; + +import java.time.LocalDateTime; + +import jakarta.validation.constraints.NotNull; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; + +public record CommentCreateRequest( + Long parentId, + + @NotNull + String content +) { + public Comment toEntity(Moim moim, DarakbangMember darakbangMember) { + return Comment.builder() + .content(content) + .moim(moim) + .darakbangMember(darakbangMember) + .createdAt(LocalDateTime.now()) + .parentId(parentId) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/request/moim/MoimCreateRequest.java b/backend/src/main/java/mouda/backend/moim/presentation/request/moim/MoimCreateRequest.java new file mode 100644 index 000000000..dcdcfbc3c --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/request/moim/MoimCreateRequest.java @@ -0,0 +1,37 @@ +package mouda.backend.moim.presentation.request.moim; + +import java.time.LocalDate; +import java.time.LocalTime; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import mouda.backend.moim.domain.Moim; + +public record MoimCreateRequest( + @NotBlank + String title, + + LocalDate date, + + LocalTime time, + + String place, + + @NotNull + Integer maxPeople, + + String description +) { + + public Moim toEntity(Long darakbangId) { + return Moim.builder() + .title(title) + .date(date) + .time(time) + .place(place) + .maxPeople(maxPeople) + .description(description) + .darakbangId(darakbangId) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/request/moim/MoimEditRequest.java b/backend/src/main/java/mouda/backend/moim/presentation/request/moim/MoimEditRequest.java new file mode 100644 index 000000000..86beb3e94 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/request/moim/MoimEditRequest.java @@ -0,0 +1,28 @@ +package mouda.backend.moim.presentation.request.moim; + +import java.time.LocalDate; +import java.time.LocalTime; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +public record MoimEditRequest( + @NotNull @Positive + Long moimId, + + @NotBlank + String title, + + LocalDate date, + + LocalTime time, + + String place, + + @NotNull + Integer maxPeople, + + String description +) { +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/request/moim/MoimJoinRequest.java b/backend/src/main/java/mouda/backend/moim/presentation/request/moim/MoimJoinRequest.java new file mode 100644 index 000000000..5107ce18c --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/request/moim/MoimJoinRequest.java @@ -0,0 +1,15 @@ +package mouda.backend.moim.presentation.request.moim; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +public record MoimJoinRequest( + @NotNull + @Positive + Long moimId, + + @NotBlank + String nickname +) { +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/request/zzim/ZzimUpdateRequest.java b/backend/src/main/java/mouda/backend/moim/presentation/request/zzim/ZzimUpdateRequest.java new file mode 100644 index 000000000..3881187b6 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/request/zzim/ZzimUpdateRequest.java @@ -0,0 +1,10 @@ +package mouda.backend.moim.presentation.request.zzim; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +public record ZzimUpdateRequest( + @NotNull @Positive + Long moimId +) { +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/response/chamyo/ChamyoFindAllResponse.java b/backend/src/main/java/mouda/backend/moim/presentation/response/chamyo/ChamyoFindAllResponse.java new file mode 100644 index 000000000..a5e9fbaa2 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/response/chamyo/ChamyoFindAllResponse.java @@ -0,0 +1,22 @@ +package mouda.backend.moim.presentation.response.chamyo; + +import lombok.Builder; +import mouda.backend.moim.domain.Chamyo; + +@Builder +public record ChamyoFindAllResponse( + long darakbangMemberId, + String nickname, + String profile, + String role +) { + + public static ChamyoFindAllResponse toResponse(Chamyo chamyo) { + return ChamyoFindAllResponse.builder() + .darakbangMemberId(chamyo.getDarakbangMember().getId()) + .nickname(chamyo.getDarakbangMember().getNickname()) + .profile(chamyo.getDarakbangMember().getProfile()) + .role(chamyo.getMoimRole().name()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/response/chamyo/ChamyoFindAllResponses.java b/backend/src/main/java/mouda/backend/moim/presentation/response/chamyo/ChamyoFindAllResponses.java new file mode 100644 index 000000000..f1174a1f2 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/response/chamyo/ChamyoFindAllResponses.java @@ -0,0 +1,18 @@ +package mouda.backend.moim.presentation.response.chamyo; + +import java.util.List; + +import mouda.backend.moim.domain.Chamyo; + +public record ChamyoFindAllResponses( + List chamyos +) { + + public static ChamyoFindAllResponses toResponse(List chamyos) { + List responses = chamyos.stream() + .map(ChamyoFindAllResponse::toResponse) + .toList(); + + return new ChamyoFindAllResponses(responses); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/response/chamyo/MoimRoleFindResponse.java b/backend/src/main/java/mouda/backend/moim/presentation/response/chamyo/MoimRoleFindResponse.java new file mode 100644 index 000000000..bc857fe20 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/response/chamyo/MoimRoleFindResponse.java @@ -0,0 +1,16 @@ +package mouda.backend.moim.presentation.response.chamyo; + +import lombok.Builder; +import mouda.backend.moim.domain.MoimRole; + +@Builder +public record MoimRoleFindResponse( + String role +) { + + public static MoimRoleFindResponse toResponse(MoimRole moimRole) { + return MoimRoleFindResponse.builder() + .role(moimRole.name()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/response/comment/ChildCommentResponse.java b/backend/src/main/java/mouda/backend/moim/presentation/response/comment/ChildCommentResponse.java new file mode 100644 index 000000000..c48ef6809 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/response/comment/ChildCommentResponse.java @@ -0,0 +1,29 @@ +package mouda.backend.moim.presentation.response.comment; + +import java.time.LocalDateTime; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import lombok.Builder; +import mouda.backend.moim.domain.Comment; + +@Builder +public record ChildCommentResponse( + Long commentId, + + String nickname, + + String content, + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + LocalDateTime dateTime +) { + public static ChildCommentResponse toResponse(Comment comment) { + return ChildCommentResponse.builder() + .commentId(comment.getId()) + .nickname(comment.getAuthorNickname()) + .content(comment.getContent()) + .dateTime(comment.getCreatedAt()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/response/comment/CommentResponse.java b/backend/src/main/java/mouda/backend/moim/presentation/response/comment/CommentResponse.java new file mode 100644 index 000000000..d9a76dae5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/response/comment/CommentResponse.java @@ -0,0 +1,41 @@ +package mouda.backend.moim.presentation.response.comment; + +import java.time.LocalDateTime; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import lombok.Builder; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.ParentComment; + +@Builder +public record CommentResponse( + Long commentId, + + String nickname, + + String content, + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm") + LocalDateTime dateTime, + + List children +) { + + public static CommentResponse fromParentComment(ParentComment parentComment) { + List children = parentComment.getChildren().stream() + .map(ChildCommentResponse::toResponse) + .toList(); + + Comment comment = parentComment.getComment(); + + return CommentResponse.builder() + .commentId(comment.getId()) + .nickname(comment.getAuthorNickname()) + .content(comment.getContent()) + .dateTime(comment.getCreatedAt()) + .children(children) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/response/comment/CommentResponses.java b/backend/src/main/java/mouda/backend/moim/presentation/response/comment/CommentResponses.java new file mode 100644 index 000000000..a64ddab44 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/response/comment/CommentResponses.java @@ -0,0 +1,18 @@ +package mouda.backend.moim.presentation.response.comment; + +import java.util.List; + +import mouda.backend.moim.domain.ParentComment; + +public record CommentResponses( + List commentResponses +) { + + public static CommentResponses toResponse(List parentComments) { + List responses = parentComments.stream() + .map(CommentResponse::fromParentComment) + .toList(); + + return new CommentResponses(responses); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/response/moim/MoimDetailsFindResponse.java b/backend/src/main/java/mouda/backend/moim/presentation/response/moim/MoimDetailsFindResponse.java new file mode 100644 index 000000000..efc69de40 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/response/moim/MoimDetailsFindResponse.java @@ -0,0 +1,49 @@ +package mouda.backend.moim.presentation.response.moim; + +import static java.time.format.DateTimeFormatter.*; + +import java.util.List; + +import lombok.Builder; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.presentation.response.comment.CommentResponse; +import mouda.backend.moim.presentation.response.comment.CommentResponses; + +@Builder +public record MoimDetailsFindResponse( + String title, + String date, + String time, + String place, + int currentPeople, + int maxPeople, + String authorNickname, + String description, + String status, + List comments, + Long chatRoomId +) { + + public static MoimDetailsFindResponse toResponse( + Moim moim, + int currentPeople, + CommentResponses comments, + Long chatRoomId + ) { + String time = moim.getTime() == null ? "" : moim.getTime().format(ofPattern("HH:mm")); + String date = moim.getDate() == null ? "" : moim.getDate().format(ISO_LOCAL_DATE); + String place = moim.getPlace() == null ? "" : moim.getPlace(); + return MoimDetailsFindResponse.builder() + .title(moim.getTitle()) + .date(date) + .time(time) + .place(place) + .currentPeople(currentPeople) + .maxPeople(moim.getMaxPeople()) + .description(moim.getDescription()) + .status(moim.getMoimStatus().name()) + .comments(comments.commentResponses()) + .chatRoomId(chatRoomId) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/response/moim/MoimFindAllResponse.java b/backend/src/main/java/mouda/backend/moim/presentation/response/moim/MoimFindAllResponse.java new file mode 100644 index 000000000..10eb228a3 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/response/moim/MoimFindAllResponse.java @@ -0,0 +1,40 @@ +package mouda.backend.moim.presentation.response.moim; + +import static java.time.format.DateTimeFormatter.*; + +import lombok.Builder; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimOverview; + +@Builder +public record MoimFindAllResponse( + Long moimId, + String title, + String date, + String time, + String place, + int currentPeople, + int maxPeople, + String authorNickname, + String description, + boolean isZzimed +) { + + public static MoimFindAllResponse toResponse(MoimOverview moimOverview) { + Moim moim = moimOverview.getMoim(); + String time = moim.getTime() == null ? "" : moim.getTime().format(ofPattern("HH:mm")); + String date = moim.getDate() == null ? "" : moim.getDate().format(ISO_LOCAL_DATE); + String place = moim.getPlace() == null ? "" : moim.getPlace(); + return MoimFindAllResponse.builder() + .moimId(moim.getId()) + .title(moim.getTitle()) + .date(date) + .time(time) + .place(place) + .currentPeople(moimOverview.getCurrentPeople()) + .maxPeople(moim.getMaxPeople()) + .description(moim.getDescription()) + .isZzimed(moimOverview.isZzimed()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/response/moim/MoimFindAllResponses.java b/backend/src/main/java/mouda/backend/moim/presentation/response/moim/MoimFindAllResponses.java new file mode 100644 index 000000000..07c739241 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/response/moim/MoimFindAllResponses.java @@ -0,0 +1,18 @@ +package mouda.backend.moim.presentation.response.moim; + +import java.util.List; + +import mouda.backend.moim.domain.MoimOverview; + +public record MoimFindAllResponses( + List moims +) { + + public static MoimFindAllResponses toResponse(List moimOverviews) { + List responses = moimOverviews.stream() + .map(MoimFindAllResponse::toResponse) + .toList(); + + return new MoimFindAllResponses(responses); + } +} diff --git a/backend/src/main/java/mouda/backend/moim/presentation/response/zzim/ZzimCheckResponse.java b/backend/src/main/java/mouda/backend/moim/presentation/response/zzim/ZzimCheckResponse.java new file mode 100644 index 000000000..cb11c7f54 --- /dev/null +++ b/backend/src/main/java/mouda/backend/moim/presentation/response/zzim/ZzimCheckResponse.java @@ -0,0 +1,10 @@ +package mouda.backend.moim.presentation.response.zzim; + +public record ZzimCheckResponse( + boolean isZzimed +) { + + public static ZzimCheckResponse toResponse(boolean isZzimed) { + return new ZzimCheckResponse(isZzimed); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/business/FcmTokenService.java b/backend/src/main/java/mouda/backend/notification/business/FcmTokenService.java new file mode 100644 index 000000000..41a0e73b2 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/business/FcmTokenService.java @@ -0,0 +1,23 @@ +package mouda.backend.notification.business; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.implement.fcm.token.FcmTokenWriter; +import mouda.backend.notification.presentation.request.FcmTokenRequest; + +@Service +@RequiredArgsConstructor +@Slf4j +public class FcmTokenService { + + private final FcmTokenWriter fcmTokenWriter; + + @Transactional + public void saveOrRefreshToken(Member member, FcmTokenRequest tokenRequest) { + fcmTokenWriter.saveOrRefresh(member, tokenRequest.token()); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/business/MemberNotificationService.java b/backend/src/main/java/mouda/backend/notification/business/MemberNotificationService.java new file mode 100644 index 000000000..23bf34baf --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/business/MemberNotificationService.java @@ -0,0 +1,26 @@ +package mouda.backend.notification.business; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.notification.domain.MemberNotification; +import mouda.backend.notification.implement.NotificationFinder; +import mouda.backend.notification.presentation.response.MemberNotificationFindAllResponse; + +@Service +@RequiredArgsConstructor +public class MemberNotificationService { + + private final NotificationFinder notificationFinder; + + @Transactional(readOnly = true) + public MemberNotificationFindAllResponse findAllMemberNotification(DarakbangMember darakbangMember) { + List notifications = notificationFinder.findAllMemberNotification(darakbangMember); + + return MemberNotificationFindAllResponse.from(notifications); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/business/SubscriptionService.java b/backend/src/main/java/mouda/backend/notification/business/SubscriptionService.java new file mode 100644 index 000000000..920100bc7 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/business/SubscriptionService.java @@ -0,0 +1,49 @@ +package mouda.backend.notification.business; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.domain.Subscription; +import mouda.backend.notification.implement.subscription.SubscriptionFinder; +import mouda.backend.notification.implement.subscription.SubscriptionWriter; +import mouda.backend.notification.presentation.request.ChatSubscriptionRequest; +import mouda.backend.notification.presentation.response.SubscriptionResponse; + +@Service +@RequiredArgsConstructor +@Transactional +public class SubscriptionService { + + private final SubscriptionFinder subscriptionFinder; + private final SubscriptionWriter subscriptionWriter; + + public SubscriptionResponse readMoimCreateSubscription(Member member) { + Subscription subscription = subscriptionFinder.readSubscription(member); + boolean isSubscribed = subscription.isSubscribedMoimCreate(); + + return SubscriptionResponse.builder() + .isSubscribed(isSubscribed) + .build(); + } + + public SubscriptionResponse readChatRoomSubscription( + Member member, Long darakbangId, Long chatRoomId + ) { + Subscription subscription = subscriptionFinder.readSubscription(member); + boolean isSubscribed = subscription.isSubscribedChatRoom(darakbangId, chatRoomId); + + return SubscriptionResponse.builder() + .isSubscribed(isSubscribed) + .build(); + } + + public void changeMoimCreateSubscription(Member member) { + subscriptionWriter.changeMoimSubscription(member); + } + + public void changeChatRoomSubscription(Member member, ChatSubscriptionRequest request) { + subscriptionWriter.changeChatRoomSubscription(member, request.darakbangId(), request.chatRoomId()); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/domain/CommonNotification.java b/backend/src/main/java/mouda/backend/notification/domain/CommonNotification.java new file mode 100644 index 000000000..8848243b5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/domain/CommonNotification.java @@ -0,0 +1,35 @@ +package mouda.backend.notification.domain; + +import com.google.firebase.messaging.Notification; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class CommonNotification { + + private final NotificationType type; + private final String title; //-> 다락방 이름 + private final String body; + private final LocalDateTime createdAt; + private final String redirectUrl; + + @Builder + public CommonNotification(NotificationType type, String title, String body, String redirectUrl) { + this.type = type; + this.title = title; + this.body = body; + this.redirectUrl = redirectUrl; + this.createdAt = LocalDateTime.now(); + } + + public Notification toNotification() { + return Notification.builder() + .setTitle(title) + .setBody(body) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/domain/FcmFailedResponse.java b/backend/src/main/java/mouda/backend/notification/domain/FcmFailedResponse.java new file mode 100644 index 000000000..882cbd998 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/domain/FcmFailedResponse.java @@ -0,0 +1,105 @@ +package mouda.backend.notification.domain; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import com.google.firebase.messaging.BatchResponse; +import com.google.firebase.messaging.MessagingErrorCode; +import com.google.firebase.messaging.SendResponse; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.util.FcmRetryAfterExtractor; + +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class FcmFailedResponse { + + private final BatchResponse batchResponse; + private final Map> failedTokens; + + public static FcmFailedResponse from(BatchResponse response, List triedTokens) { + Map> result = new ConcurrentHashMap<>(); + List responses = response.getResponses(); + IntStream.range(0, responses.size()) + .forEach(i -> { + SendResponse sendResponse = responses.get(i); + if (sendResponse.isSuccessful()) { + return; + } + FcmToken token = triedTokens.get(i); + MessagingErrorCode errorCode = sendResponse.getException().getMessagingErrorCode(); + result.computeIfAbsent(errorCode, k -> new ArrayList<>()).add(token); + }); + + return new FcmFailedResponse(response, result); + } + + public List getFailedWith404Tokens() { + return getTokens(this::isFailedWith404); + } + + public List getFailedWith429Tokens() { + return getTokens(this::isFailedWith429); + } + + public List getFailedWith5xxTokens() { + return getTokens(this::isFailedWith5xx); + } + + public Map> getNonRetryableFailedTokens() { + return failedTokens.keySet().stream() + .filter(errorCode -> !isFailedWith429(errorCode) && !isFailedWith5xx(errorCode)) + .collect(Collectors.toMap(errorCode -> errorCode, failedTokens::get)); + } + + public int getRetryAfterSeconds() { + return FcmRetryAfterExtractor.getRetryAfterSeconds(batchResponse); + } + + public boolean hasNoRetryableTokens() { + return isTokenAbsent(MessagingErrorCode.QUOTA_EXCEEDED, MessagingErrorCode.INTERNAL, + MessagingErrorCode.UNAVAILABLE); + } + + private List getTokens(Predicate filter) { + return failedTokens.keySet().stream() + .filter(filter) + .flatMap(errorCode -> failedTokens.get(errorCode).stream()) + .toList(); + } + + private boolean isFailedWith404(MessagingErrorCode errorCode) { + return errorCode == MessagingErrorCode.UNREGISTERED; + } + + private boolean isFailedWith429(MessagingErrorCode errorCode) { + return errorCode == MessagingErrorCode.QUOTA_EXCEEDED; + } + + private boolean isFailedWith5xx(MessagingErrorCode errorCode) { + return errorCode == MessagingErrorCode.INTERNAL || + errorCode == MessagingErrorCode.UNAVAILABLE; + } + + private boolean isTokenAbsent(MessagingErrorCode... errorCodes) { + return Arrays.stream(errorCodes) + .map(failedTokens::get) + .allMatch(tokens -> tokens == null || tokens.isEmpty()); + } + + public boolean hasNoFailedTokens() { + return failedTokens.isEmpty(); + } + + public void removeFailedWith404Tokens() { + failedTokens.remove(MessagingErrorCode.UNREGISTERED); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/domain/FcmToken.java b/backend/src/main/java/mouda/backend/notification/domain/FcmToken.java new file mode 100644 index 000000000..46f32ee4a --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/domain/FcmToken.java @@ -0,0 +1,23 @@ +package mouda.backend.notification.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +@Builder +public class FcmToken { + + private final long memberId; + private final long tokenId; + private final String token; + + @Override + public String toString() { + return "FcmToken{" + + "memberId=" + memberId + + ", tokenId=" + tokenId + + '}'; + } +} diff --git a/backend/src/main/java/mouda/backend/notification/domain/MemberNotification.java b/backend/src/main/java/mouda/backend/notification/domain/MemberNotification.java new file mode 100644 index 000000000..391ac6ed9 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/domain/MemberNotification.java @@ -0,0 +1,20 @@ +package mouda.backend.notification.domain; + +import java.time.LocalDateTime; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +@Builder +public class MemberNotification { + + private final String title; + private final String message; + private final LocalDateTime createdAt; + private final String type; + private final String redirectUrl; + +} diff --git a/backend/src/main/java/mouda/backend/notification/domain/NotificationEvent.java b/backend/src/main/java/mouda/backend/notification/domain/NotificationEvent.java new file mode 100644 index 000000000..5aa43b743 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/domain/NotificationEvent.java @@ -0,0 +1,52 @@ +package mouda.backend.notification.domain; + +import java.util.List; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class NotificationEvent { + + private final NotificationType notificationType; + private final String title; + private final String body; + private final String redirectUrl; + private final List recipients; + private final Long darakbangId; + private final Long chatRoomId; + + public static NotificationEvent nonChatEvent( + NotificationType notificationType, + String title, + String body, + String redirectUrl, + List recipients + ) { + return new NotificationEvent( + notificationType, title, body, redirectUrl, recipients, null, null + ); + } + + public static NotificationEvent chatEvent( + NotificationType notificationType, + String title, + String body, + String redirectUrl, + List recipients, + Long darakbangId, + Long chatRoomId + ) { + return new NotificationEvent( + notificationType, title, body, redirectUrl, recipients, darakbangId, chatRoomId + ); + } + + public CommonNotification toCommonNotification() { + return new CommonNotification( + notificationType, title, body, redirectUrl + ); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/domain/NotificationPayload.java b/backend/src/main/java/mouda/backend/notification/domain/NotificationPayload.java new file mode 100644 index 000000000..2a2baaa63 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/domain/NotificationPayload.java @@ -0,0 +1,52 @@ +package mouda.backend.notification.domain; + +import java.util.List; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class NotificationPayload { + + private final NotificationType notificationType; + private final String title; + private final String body; + private final String redirectUrl; + private final List recipients; + private final Long darakbangId; + private final Long chatRoomId; + + public static NotificationPayload createNonChatPayload( + NotificationType notificationType, + String title, + String body, + String redirectUrl, + List recipients + ) { + return new NotificationPayload( + notificationType, title, body, redirectUrl, recipients, null, null + ); + } + + public static NotificationPayload createChatPayload( + NotificationType notificationType, + String title, + String body, + String redirectUrl, + List recipients, + Long darakbangId, + Long chatRoomId + ) { + return new NotificationPayload( + notificationType, title, body, redirectUrl, recipients, darakbangId, chatRoomId + ); + } + + public CommonNotification toCommonNotification() { + return new CommonNotification( + notificationType, title, body, redirectUrl + ); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/domain/NotificationSendEvent.java b/backend/src/main/java/mouda/backend/notification/domain/NotificationSendEvent.java new file mode 100644 index 000000000..c22d1d4b4 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/domain/NotificationSendEvent.java @@ -0,0 +1,44 @@ +package mouda.backend.notification.domain; + +import java.util.List; + +import org.springframework.http.HttpStatus; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.exception.NotificationErrorMessage; +import mouda.backend.notification.exception.NotificationException; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@Builder +public class NotificationSendEvent { + + private final CommonNotification notification; + private final List recipients; + private final Long darakbangId; + private final Long chatRoomId; + + public static NotificationSendEvent from(NotificationPayload payload) { + validate(payload); + return NotificationSendEvent.builder() + .notification(payload.toCommonNotification()) + .recipients(payload.getRecipients()) + .darakbangId(payload.getDarakbangId()) + .chatRoomId(payload.getChatRoomId()) + .build(); + } + + private static void validate(NotificationPayload payload) { + NotificationType notificationType = payload.getNotificationType(); + Long chatRoomId = payload.getChatRoomId(); + Long darakbangId = payload.getDarakbangId(); + + if (notificationType.isChatType() && (chatRoomId == null || darakbangId == null)) { + throw new NotificationException(HttpStatus.BAD_REQUEST, + NotificationErrorMessage.NULL_ID_VALUES_FOR_CHAT_NOTIFICATION); + } + } +} diff --git a/backend/src/main/java/mouda/backend/notification/domain/NotificationType.java b/backend/src/main/java/mouda/backend/notification/domain/NotificationType.java new file mode 100644 index 000000000..aacaa15d3 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/domain/NotificationType.java @@ -0,0 +1,30 @@ +package mouda.backend.notification.domain; + +public enum NotificationType { + + MOIM_CREATED, + + MOIMING_COMPLETED, + MOINING_REOPENED, + MOIM_CANCELLED, + MOIM_MODIFIED, + MOIM_PLACE_CONFIRMED, + MOIM_TIME_CONFIRMED, + + NEW_MOIMEE_JOINED, + MOIMEE_LEFT, + + NEW_COMMENT, + NEW_REPLY, + + NEW_CHAT, + ; + + public boolean isConfirmedType() { + return this == MOIM_PLACE_CONFIRMED || this == MOIM_TIME_CONFIRMED; + } + + public boolean isChatType() { + return this == NEW_CHAT || isConfirmedType(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/domain/Recipient.java b/backend/src/main/java/mouda/backend/notification/domain/Recipient.java new file mode 100644 index 000000000..ab2d4f0bc --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/domain/Recipient.java @@ -0,0 +1,14 @@ +package mouda.backend.notification.domain; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +@Builder +public class Recipient { + + private final long memberId; + private final long darakbangMemberId; +} diff --git a/backend/src/main/java/mouda/backend/notification/domain/Subscription.java b/backend/src/main/java/mouda/backend/notification/domain/Subscription.java new file mode 100644 index 000000000..8cde927c2 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/domain/Subscription.java @@ -0,0 +1,28 @@ +package mouda.backend.notification.domain; + +import java.util.List; +import java.util.Map; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +@Builder +public class Subscription { + + private final boolean isSubscribedMoimCreate; + private final Map> unsubscribedChatRooms; + + public boolean isSubscribedChatRoom(Long darakbangId, Long chatRoomId) { + if (unsubscribedChatRooms.containsKey(darakbangId)) { + return !unsubscribedChatRooms.get(darakbangId).contains(chatRoomId); + } + return true; + } + + public boolean isSubscribedMoimCreate() { + return isSubscribedMoimCreate; + } +} diff --git a/backend/src/main/java/mouda/backend/notification/exception/NotificationErrorMessage.java b/backend/src/main/java/mouda/backend/notification/exception/NotificationErrorMessage.java new file mode 100644 index 000000000..3a27a1b6a --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/exception/NotificationErrorMessage.java @@ -0,0 +1,17 @@ +package mouda.backend.notification.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum NotificationErrorMessage { + + NOT_ALLOWED_NOTIFICATION_TYPE("지원하지 않는 알림 타입이에요."), + FILTER_NOT_FOUND("입력된 알림 타입에 대한 구독 필터를 찾을 수 없어요."), + FILTER_NOT_UNIQUE("입력된 알림 타입에 대한 구독 필터가 유일하지 않아요."), + NULL_ID_VALUES_FOR_CHAT_NOTIFICATION("채팅 알림을 보내기 위해선 다락방과 채팅방 ID가 필요해요."), + ; + + private final String message; +} diff --git a/backend/src/main/java/mouda/backend/notification/exception/NotificationException.java b/backend/src/main/java/mouda/backend/notification/exception/NotificationException.java new file mode 100644 index 000000000..d9d937fe9 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/exception/NotificationException.java @@ -0,0 +1,12 @@ +package mouda.backend.notification.exception; + +import org.springframework.http.HttpStatus; + +import mouda.backend.common.exception.MoudaException; + +public class NotificationException extends MoudaException { + + public NotificationException(HttpStatus httpStatus, NotificationErrorMessage notificationErrorMessage) { + super(httpStatus, notificationErrorMessage.getMessage()); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/MessageFactory.java b/backend/src/main/java/mouda/backend/notification/implement/MessageFactory.java new file mode 100644 index 000000000..fc3ede523 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/MessageFactory.java @@ -0,0 +1,10 @@ +package mouda.backend.notification.implement; + +import java.util.List; + +import mouda.backend.notification.domain.CommonNotification; + +public interface MessageFactory { + + T createMessage(CommonNotification notification, List receivers); +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/NotificationFinder.java b/backend/src/main/java/mouda/backend/notification/implement/NotificationFinder.java new file mode 100644 index 000000000..91da690ac --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/NotificationFinder.java @@ -0,0 +1,35 @@ +package mouda.backend.notification.implement; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.notification.domain.MemberNotification; +import mouda.backend.notification.infrastructure.entity.MemberNotificationEntity; +import mouda.backend.notification.infrastructure.repository.MemberNotificationRepository; + +@Component +@RequiredArgsConstructor +public class NotificationFinder { + + private final MemberNotificationRepository memberNotificationRepository; + + public List findAllMemberNotification(DarakbangMember darakbangMember) { + return memberNotificationRepository.findAllByDarakbangMemberId(darakbangMember.getId()).stream() + .map(this::convertTo) + .sorted((n1, n2) -> n2.getCreatedAt().compareTo(n1.getCreatedAt())) + .toList(); + } + + private MemberNotification convertTo(MemberNotificationEntity entity) { + return MemberNotification.builder() + .title(entity.getTitle()) + .message(entity.getBody()) + .createdAt(entity.getCreatedAt()) + .type(entity.getType()) + .redirectUrl(entity.getTargetUrl()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/NotificationProcessor.java b/backend/src/main/java/mouda/backend/notification/implement/NotificationProcessor.java new file mode 100644 index 000000000..3d0c6d498 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/NotificationProcessor.java @@ -0,0 +1,26 @@ +package mouda.backend.notification.implement; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.NotificationPayload; +import mouda.backend.notification.domain.NotificationSendEvent; + +@Component +@RequiredArgsConstructor +public class NotificationProcessor { + + private final NotificationWriter notificationWriter; + private final ApplicationEventPublisher notificationSendEventPublisher; + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void process(NotificationPayload payload) { + notificationWriter.saveMemberNotification(payload); + + NotificationSendEvent event = NotificationSendEvent.from(payload); + notificationSendEventPublisher.publishEvent(event); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/NotificationSendEventHandler.java b/backend/src/main/java/mouda/backend/notification/implement/NotificationSendEventHandler.java new file mode 100644 index 000000000..8c7ed6fea --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/NotificationSendEventHandler.java @@ -0,0 +1,35 @@ +package mouda.backend.notification.implement; + +import java.util.List; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.NotificationSendEvent; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.implement.filter.SubscriptionFilterRegistry; + +@Component +@RequiredArgsConstructor +public class NotificationSendEventHandler { + + private final SubscriptionFilterRegistry subscriptionFilterRegistry; + private final NotificationSender notificationSender; + + @Async + @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) + @TransactionalEventListener(classes = NotificationSendEvent.class, phase = TransactionPhase.AFTER_COMMIT) + public void handle(NotificationSendEvent event) { + List filteredRecipients = filterRecipientsBySubscription(event); + notificationSender.sendNotification(event.getNotification(), filteredRecipients); + } + + private List filterRecipientsBySubscription(NotificationSendEvent event) { + return subscriptionFilterRegistry.getFilter(event.getNotification().getType()).filter(event); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/NotificationSender.java b/backend/src/main/java/mouda/backend/notification/implement/NotificationSender.java new file mode 100644 index 000000000..846146afb --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/NotificationSender.java @@ -0,0 +1,11 @@ +package mouda.backend.notification.implement; + +import java.util.List; + +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.domain.Recipient; + +public interface NotificationSender { + + void sendNotification(CommonNotification notification, List recipients); +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/NotificationWriter.java b/backend/src/main/java/mouda/backend/notification/implement/NotificationWriter.java new file mode 100644 index 000000000..e4f80cc7c --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/NotificationWriter.java @@ -0,0 +1,46 @@ +package mouda.backend.notification.implement; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.domain.NotificationPayload; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.infrastructure.entity.MemberNotificationEntity; +import mouda.backend.notification.infrastructure.repository.MemberNotificationRepository; + +@Component +@RequiredArgsConstructor +public class NotificationWriter { + + private final MemberNotificationRepository memberNotificationRepository; + + public void saveMemberNotification(NotificationPayload notificationPayload) { + CommonNotification notification = notificationPayload.toCommonNotification(); + List recipients = notificationPayload.getRecipients(); + + if (notification.getType() == NotificationType.NEW_CHAT) { + return; + } + + List memberNotifications = recipients.stream() + .map(recipient -> createEntity(notification, recipient)) + .toList(); + + memberNotificationRepository.saveAll(memberNotifications); + } + + private MemberNotificationEntity createEntity(CommonNotification notification, Recipient recipient) { + return MemberNotificationEntity.builder() + .darakbangMemberId(recipient.getDarakbangMemberId()) + .type(notification.getType().name()) + .title(notification.getTitle()) + .body(notification.getBody()) + .targeturl(notification.getRedirectUrl()) + .createdAt(notification.getCreatedAt()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/AsyncFcmNotificationSender.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/AsyncFcmNotificationSender.java new file mode 100644 index 000000000..aaa427a53 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/AsyncFcmNotificationSender.java @@ -0,0 +1,69 @@ +package mouda.backend.notification.implement.fcm; + +import java.util.List; +import java.util.concurrent.Executors; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutureCallback; +import com.google.api.core.ApiFutures; +import com.google.firebase.messaging.BatchResponse; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.MulticastMessage; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.domain.FcmToken; + +@Component +@Slf4j +@RequiredArgsConstructor +public class AsyncFcmNotificationSender { + + private static final int THREAD_POOL_SIZE_FOR_CALLBACK = 5; + + private final FcmResponseHandler fcmResponseHandler; + + @Async + public void sendAllMulticastMessage( + CommonNotification notification, List messages, List tokens + ) { + if (tokens.isEmpty()) { + return; + } + + messages.forEach(multicastMessage -> sendSingleMulticastMessage(notification, multicastMessage, tokens)); + } + + private void sendSingleMulticastMessage( + CommonNotification notification, MulticastMessage message, List initialTokens + ) { + ApiFuture future = FirebaseMessaging.getInstance().sendEachForMulticastAsync(message); + ApiFutures.addCallback(future, new ApiFutureCallback<>() { + @Override + public void onFailure(Throwable t) { + if (t instanceof FirebaseMessagingException exception) { + log.error( + "Error Sending Message. title: {}, body: {}, error code: {}, messaging error code: {}, error message: {}", + notification.getTitle(), notification.getBody(), exception.getErrorCode(), + exception.getMessagingErrorCode(), exception.getMessage() + ); + } + } + + @Override + public void onSuccess(BatchResponse result) { + if (result.getFailureCount() == 0) { + log.info("All messages were sent successfully. title: {}, body: {}", notification.getTitle(), + notification.getBody()); + return; + } + fcmResponseHandler.handleBatchResponse(result, notification, initialTokens); + } + }, Executors.newFixedThreadPool(THREAD_POOL_SIZE_FOR_CALLBACK)); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmConfigBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmConfigBuilder.java new file mode 100644 index 000000000..1295c981a --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmConfigBuilder.java @@ -0,0 +1,8 @@ +package mouda.backend.notification.implement.fcm; + +import com.google.firebase.messaging.MulticastMessage; + +public interface FcmConfigBuilder { + + MulticastMessage.Builder buildMulticastMessage(MulticastMessage.Builder builder); +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmMessageFactory.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmMessageFactory.java new file mode 100644 index 000000000..47cbdbc06 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmMessageFactory.java @@ -0,0 +1,50 @@ +package mouda.backend.notification.implement.fcm; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.google.firebase.messaging.MulticastMessage; +import com.google.firebase.messaging.MulticastMessage.Builder; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.implement.MessageFactory; + +@Component +@RequiredArgsConstructor +public class FcmMessageFactory implements MessageFactory> { + + private static final int TOKEN_BATCH_SIZE = 500; + private final List fcmConfigBuilders; + + @Override + public List createMessage(CommonNotification notification, List fcmTokens) { + List> partitionedTokens = partitionTokensByBatch(fcmTokens); + + return partitionedTokens.stream() + .map(tokens -> defaultMulticastMessageBuilder(notification).addAllTokens(tokens).build()) + .toList(); + } + + private Builder defaultMulticastMessageBuilder(CommonNotification notification) { + Builder builder = MulticastMessage.builder() + .setNotification(notification.toNotification()) + .putData("link", notification.getRedirectUrl()); + + for (FcmConfigBuilder configBuilder : fcmConfigBuilders) { + builder = configBuilder.buildMulticastMessage(builder); + } + + return builder; + } + + private List> partitionTokensByBatch(List tokens) { + List> result = new ArrayList<>(); + for (int i = 0; i < tokens.size(); i += TOKEN_BATCH_SIZE) { + result.add(tokens.subList(i, Math.min(i + TOKEN_BATCH_SIZE, tokens.size()))); + } + return result; + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmNotificationSender.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmNotificationSender.java new file mode 100644 index 000000000..13373857d --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmNotificationSender.java @@ -0,0 +1,34 @@ +package mouda.backend.notification.implement.fcm; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.google.firebase.messaging.MulticastMessage; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.domain.FcmToken; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.implement.NotificationSender; +import mouda.backend.notification.implement.fcm.token.FcmTokenFinder; + +@Component +@RequiredArgsConstructor +public class FcmNotificationSender implements NotificationSender { + + private final FcmMessageFactory fcmMessageFactory; + private final FcmTokenFinder fcmTokenFinder; + private final AsyncFcmNotificationSender asyncFcmNotificationSender; + + @Override + public void sendNotification(CommonNotification notification, List recipients) { + List tokens = fcmTokenFinder.findAllTokensByMemberIn(recipients); + List tokenStrings = tokens.stream() + .map(FcmToken::getToken) + .toList(); + + List messages = fcmMessageFactory.createMessage(notification, tokenStrings); + asyncFcmNotificationSender.sendAllMulticastMessage(notification, messages, tokens); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmResponseHandler.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmResponseHandler.java new file mode 100644 index 000000000..8afc5f257 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmResponseHandler.java @@ -0,0 +1,108 @@ +package mouda.backend.notification.implement.fcm; + +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.springframework.stereotype.Component; + +import com.google.firebase.messaging.BatchResponse; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.MulticastMessage; + +import jakarta.annotation.PreDestroy; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.domain.FcmFailedResponse; +import mouda.backend.notification.domain.FcmToken; + +@Component +@RequiredArgsConstructor +@Slf4j +public class FcmResponseHandler { + + private static final int BACKOFF_DELAY_FOR_SECONDS = 10; + private static final int BACKOFF_MULTIPLIER = 1; + + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(5); + private final FcmRetryableChecker fcmRetryableChecker; + private final FcmMessageFactory fcmMessageFactory; + + @PreDestroy + public void destroy() { + scheduler.shutdown(); + } + + public void handleBatchResponse( + BatchResponse batchResponse, CommonNotification notification, List initialTokens + ) { + FcmFailedResponse failedResponse = FcmFailedResponse.from(batchResponse, initialTokens); + + int attempt = 1; + retryAsync(notification, failedResponse, attempt, BACKOFF_DELAY_FOR_SECONDS); + } + + private void retryAsync( + CommonNotification notification, FcmFailedResponse failedResponse, int attempt, int backoffDelayForSeconds + ) { + boolean canRetry = fcmRetryableChecker.check(notification, failedResponse, attempt); + + if (canRetry) { + retryUsingRetryAfter(notification, failedResponse, attempt, backoffDelayForSeconds); + retryUsingBackoff(notification, failedResponse, attempt, backoffDelayForSeconds); + } + } + + private void retryUsingRetryAfter( + CommonNotification notification, FcmFailedResponse failedResponse, int attempt, int backOffDelayForSeconds + ) { + List failedWith429Tokens = failedResponse.getFailedWith429Tokens(); + if (failedWith429Tokens.isEmpty()) { + return; + } + + int retryAfterSeconds = failedResponse.getRetryAfterSeconds(); + scheduler.schedule(() -> { + log.info("Retrying 429 retry for title: {}, body: {}, tokens: {}.", notification.getTitle(), + notification.getBody(), failedWith429Tokens); + + FcmFailedResponse retryResponse = sendNotification(failedResponse, notification, failedWith429Tokens); + retryAsync(notification, retryResponse, attempt + 1, backOffDelayForSeconds * BACKOFF_MULTIPLIER); + }, retryAfterSeconds, TimeUnit.SECONDS); + } + + private void retryUsingBackoff( + CommonNotification notification, FcmFailedResponse failedResponse, int attempt, int backoffDelayForSeconds + ) { + List failedWith5xxTokens = failedResponse.getFailedWith5xxTokens(); + if (failedWith5xxTokens.isEmpty()) { + return; + } + + scheduler.schedule(() -> { + log.info("Retrying 5xx for title: {}, body: {}, tokens: {}.", notification.getTitle(), + notification.getBody(), failedWith5xxTokens); + FcmFailedResponse retryResponse = sendNotification(failedResponse, notification, failedWith5xxTokens); + retryAsync(notification, retryResponse, attempt + 1, backoffDelayForSeconds * BACKOFF_MULTIPLIER); + }, backoffDelayForSeconds, TimeUnit.SECONDS); + } + + private FcmFailedResponse sendNotification( + FcmFailedResponse origin, CommonNotification notification, List retryTokens + ) { + List tokens = retryTokens.stream().map(FcmToken::getToken).toList(); + MulticastMessage message = fcmMessageFactory.createMessage(notification, tokens).get(0); + + try { + BatchResponse response = FirebaseMessaging.getInstance().sendEachForMulticast(message); + return FcmFailedResponse.from(response, retryTokens); + } catch (FirebaseMessagingException e) { + log.error("Error Sending Message while retrying.. title: {}, body: {}, error message: {}", + notification.getTitle(), notification.getBody(), e.getMessage()); + return origin; + } + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmRetryableChecker.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmRetryableChecker.java new file mode 100644 index 000000000..ad8b856b4 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/FcmRetryableChecker.java @@ -0,0 +1,62 @@ +package mouda.backend.notification.implement.fcm; + +import java.util.List; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.domain.FcmFailedResponse; +import mouda.backend.notification.domain.FcmToken; +import mouda.backend.notification.implement.fcm.token.FcmTokenWriter; + +@Component +@RequiredArgsConstructor +@Slf4j +public class FcmRetryableChecker { + + private static final int MAX_ATTEMPT = 3; + + private final FcmTokenWriter fcmTokenWriter; + + @Transactional + public boolean check(CommonNotification notification, FcmFailedResponse failedResponse, int attempt) { + if (failedResponse.hasNoFailedTokens()) { + log.info("No failed tokens for title: {}, body: {}.", notification.getTitle(), notification.getBody()); + return false; + } + return checkWhenFailedTokensExist(notification, failedResponse, attempt); + } + + private boolean checkWhenFailedTokensExist( + CommonNotification notification, FcmFailedResponse failedResponse, int attempt + ) { + removeAllUnregisteredTokens(failedResponse); + if (attempt > MAX_ATTEMPT) { + log.info("Max attempt reached for title: {}, body: {}, tokens: {}", notification.getTitle(), + notification.getBody(), failedResponse.getFailedTokens()); + return false; + } + if (failedResponse.hasNoRetryableTokens()) { + log.info("No retryable tokens for title: {}, body: {}, tokens: {}", notification.getTitle(), notification.getBody(), + failedResponse.getNonRetryableFailedTokens()); + return false; + } + return true; + } + + private void removeAllUnregisteredTokens(FcmFailedResponse failedResponse) { + List failedWith404Tokens = failedResponse.getFailedWith404Tokens(); + if (failedWith404Tokens.isEmpty()) { + return; + } + + log.info("Removing all unregistered tokens: {}", failedWith404Tokens); + List tokens = failedWith404Tokens.stream().map(FcmToken::getToken).toList(); + + fcmTokenWriter.deleteAll(tokens); + failedResponse.removeFailedWith404Tokens(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/WebFcmConfigBuilder.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/WebFcmConfigBuilder.java new file mode 100644 index 000000000..533e62cbc --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/WebFcmConfigBuilder.java @@ -0,0 +1,32 @@ +package mouda.backend.notification.implement.fcm; + +import org.springframework.stereotype.Component; + +import com.google.firebase.messaging.MulticastMessage; +import com.google.firebase.messaging.WebpushConfig; +import com.google.firebase.messaging.WebpushFcmOptions; + +@Component +public class WebFcmConfigBuilder implements FcmConfigBuilder { + + private static final WebpushConfig webpushConfig; + + static { + webpushConfig = createWebpushConfig(); + } + + @Override + public MulticastMessage.Builder buildMulticastMessage(MulticastMessage.Builder builder) { + return builder.setWebpushConfig(webpushConfig); + } + + private static WebpushConfig createWebpushConfig() { + return WebpushConfig.builder() + .setFcmOptions(option()) + .build(); + } + + private static WebpushFcmOptions option() { + return WebpushFcmOptions.builder().build(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenFinder.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenFinder.java new file mode 100644 index 000000000..57b41782e --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenFinder.java @@ -0,0 +1,33 @@ +package mouda.backend.notification.implement.fcm.token; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.FcmToken; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.infrastructure.entity.FcmTokenEntity; +import mouda.backend.notification.infrastructure.repository.FcmTokenRepository; + +@Component +@RequiredArgsConstructor +public class FcmTokenFinder { + + private final FcmTokenRepository fcmTokenRepository; + + public List findAllTokensByMemberIn(List recipients) { + return recipients.stream() + .flatMap(recipient -> fcmTokenRepository.findAllByMemberId(recipient.getMemberId()).stream()) + .map(this::createByEntity) + .toList(); + } + + private FcmToken createByEntity(FcmTokenEntity entity) { + return FcmToken.builder() + .tokenId(entity.getId()) + .memberId(entity.getMemberId()) + .token(entity.getToken()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenScheduler.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenScheduler.java new file mode 100644 index 000000000..39ae4a006 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenScheduler.java @@ -0,0 +1,34 @@ +package mouda.backend.notification.implement.fcm.token; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.infrastructure.entity.FcmTokenEntity; +import mouda.backend.notification.infrastructure.repository.FcmTokenRepository; + +@Component +@RequiredArgsConstructor +public class FcmTokenScheduler { + + private final FcmTokenRepository fcmTokenRepository; + + @Scheduled(cron = "0 0 0 1 * ?") + @Transactional + public void checkAllTokensIfInactiveOrExpired() { + fcmTokenRepository.findAll().forEach(this::deactiveOrDelete); + } + + private void deactiveOrDelete(FcmTokenEntity tokenEntity) { + if (tokenEntity.isExpired()) { + fcmTokenRepository.delete(tokenEntity); + return; + } + if (tokenEntity.isInactive()) { + tokenEntity.refresh(); + tokenEntity.deactivate(); + fcmTokenRepository.save(tokenEntity); + } + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenWriter.java b/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenWriter.java new file mode 100644 index 000000000..93016a9ef --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/fcm/token/FcmTokenWriter.java @@ -0,0 +1,46 @@ +package mouda.backend.notification.implement.fcm.token; + +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.infrastructure.entity.FcmTokenEntity; +import mouda.backend.notification.infrastructure.repository.FcmTokenRepository; + +@Component +@RequiredArgsConstructor +public class FcmTokenWriter { + + private final FcmTokenRepository fcmTokenRepository; + + public void saveOrRefresh(Member member, String token) { + Optional tokenEntity = fcmTokenRepository.findByToken(token); + tokenEntity.ifPresentOrElse(this::refresh, () -> save(member, token)); + } + + private void refresh(FcmTokenEntity tokenEntity) { + if (tokenEntity.isInactive()) { + tokenEntity.activate(); + } + + tokenEntity.refresh(); + fcmTokenRepository.save(tokenEntity); + } + + private void save(Member member, String token) { + FcmTokenEntity tokenEntity = FcmTokenEntity.builder() + .memberId(member.getId()) + .token(token) + .build(); + fcmTokenRepository.save(tokenEntity); + } + + @Transactional + public void deleteAll(List unregisteredTokens) { + fcmTokenRepository.deleteAllByTokenIn(unregisteredTokens); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilter.java b/backend/src/main/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilter.java new file mode 100644 index 000000000..1144bdcdd --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilter.java @@ -0,0 +1,39 @@ +package mouda.backend.notification.implement.filter; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.NotificationSendEvent; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.domain.Subscription; +import mouda.backend.notification.implement.subscription.SubscriptionFinder; + +@Component +@RequiredArgsConstructor +public class ChatRoomSubscriptionFilter implements SubscriptionFilter { + + private final SubscriptionFinder subscriptionFinder; + + @Override + public boolean support(NotificationType notificationType) { + return notificationType == NotificationType.NEW_CHAT || + notificationType.isConfirmedType(); + } + + @Override + public List filter(NotificationSendEvent notificationSendEvent) { + return notificationSendEvent.getRecipients().stream() + .filter(recipient -> { + NotificationType type = notificationSendEvent.getNotification().getType(); + if (type.isConfirmedType()) { + return true; + } + Subscription subscription = subscriptionFinder.readSubscription(recipient.getMemberId()); + return subscription.isSubscribedChatRoom(notificationSendEvent.getDarakbangId(), notificationSendEvent.getChatRoomId()); + }) + .toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/filter/MoimCreatedSubscriptionFilter.java b/backend/src/main/java/mouda/backend/notification/implement/filter/MoimCreatedSubscriptionFilter.java new file mode 100644 index 000000000..9543ed01b --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/filter/MoimCreatedSubscriptionFilter.java @@ -0,0 +1,34 @@ +package mouda.backend.notification.implement.filter; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.NotificationSendEvent; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.domain.Subscription; +import mouda.backend.notification.implement.subscription.SubscriptionFinder; + +@Component +@RequiredArgsConstructor +public class MoimCreatedSubscriptionFilter implements SubscriptionFilter { + + private final SubscriptionFinder subscriptionFinder; + + @Override + public boolean support(NotificationType notificationType) { + return notificationType == NotificationType.MOIM_CREATED; + } + + @Override + public List filter(NotificationSendEvent notificationSendEvent) { + return notificationSendEvent.getRecipients().stream() + .filter(recipient -> { + Subscription subscription = subscriptionFinder.readSubscription(recipient.getMemberId()); + return subscription.isSubscribedMoimCreate(); + }) + .toList(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/filter/NonSubscriptionFilter.java b/backend/src/main/java/mouda/backend/notification/implement/filter/NonSubscriptionFilter.java new file mode 100644 index 000000000..6b8e846f7 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/filter/NonSubscriptionFilter.java @@ -0,0 +1,25 @@ +package mouda.backend.notification.implement.filter; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import mouda.backend.notification.domain.NotificationSendEvent; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; + +@Component +public class NonSubscriptionFilter implements SubscriptionFilter { + + @Override + public boolean support(NotificationType notificationType) { + return notificationType != NotificationType.NEW_CHAT && + !notificationType.isConfirmedType() && + notificationType != NotificationType.MOIM_CREATED; + } + + @Override + public List filter(NotificationSendEvent notificationSendEvent) { + return notificationSendEvent.getRecipients(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilter.java b/backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilter.java new file mode 100644 index 000000000..64593eb25 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilter.java @@ -0,0 +1,14 @@ +package mouda.backend.notification.implement.filter; + +import java.util.List; + +import mouda.backend.notification.domain.NotificationSendEvent; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; + +public interface SubscriptionFilter { + + boolean support(NotificationType notificationType); + + List filter(NotificationSendEvent notificationSendEvent); +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilterRegistry.java b/backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilterRegistry.java new file mode 100644 index 000000000..5f6d742cb --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/filter/SubscriptionFilterRegistry.java @@ -0,0 +1,32 @@ +package mouda.backend.notification.implement.filter; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.exception.NotificationErrorMessage; +import mouda.backend.notification.exception.NotificationException; + +@Component +@RequiredArgsConstructor +public class SubscriptionFilterRegistry { + + private final List subscriptionFilters; + + public SubscriptionFilter getFilter(NotificationType notificationType) { + List filters = subscriptionFilters.stream() + .filter(subscriptionFilter -> subscriptionFilter.support(notificationType)) + .toList(); + + if (filters.isEmpty()) { + throw new NotificationException(HttpStatus.NOT_FOUND, NotificationErrorMessage.FILTER_NOT_FOUND); + } + if (filters.size() > 1) { + throw new NotificationException(HttpStatus.INTERNAL_SERVER_ERROR, NotificationErrorMessage.FILTER_NOT_UNIQUE); + } + return filters.get(0); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/subscription/SubscriptionFinder.java b/backend/src/main/java/mouda/backend/notification/implement/subscription/SubscriptionFinder.java new file mode 100644 index 000000000..ee6b8f941 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/subscription/SubscriptionFinder.java @@ -0,0 +1,54 @@ +package mouda.backend.notification.implement.subscription; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.domain.Subscription; +import mouda.backend.notification.infrastructure.entity.SubscriptionEntity; +import mouda.backend.notification.infrastructure.entity.UnsubscribedChatRooms; +import mouda.backend.notification.infrastructure.repository.SubscriptionRepository; + +@Component +@RequiredArgsConstructor +public class SubscriptionFinder { + + private final SubscriptionRepository subscriptionRepository; + + public Subscription readSubscription(Member member) { + return readSubscription(member.getId()); + } + + public Subscription readSubscription(long memberId) { + Optional subscriptionEntityOptional = subscriptionRepository.findByMemberId(memberId); + + if (subscriptionEntityOptional.isPresent()) { + return convertToSubscription(subscriptionEntityOptional.get()); + } + + SubscriptionEntity createdSubscriptionEntity = subscriptionRepository.save(SubscriptionEntity.builder() + .memberId(memberId) + .unsubscribedChats(new ArrayList<>()) + .build()); + return convertToSubscription(createdSubscriptionEntity); + } + + private Subscription convertToSubscription(SubscriptionEntity subscriptionEntity) { + Map> unsubscribedChatRooms = subscriptionEntity.getUnsubscribedChats().stream() + .collect(Collectors.toConcurrentMap( + UnsubscribedChatRooms::getDarakbangId, + UnsubscribedChatRooms::getChatRoomIds + )); + + return Subscription.builder() + .isSubscribedMoimCreate(subscriptionEntity.isSubscribedMoimCreate()) + .unsubscribedChatRooms(unsubscribedChatRooms) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/implement/subscription/SubscriptionWriter.java b/backend/src/main/java/mouda/backend/notification/implement/subscription/SubscriptionWriter.java new file mode 100644 index 000000000..d397e2126 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/implement/subscription/SubscriptionWriter.java @@ -0,0 +1,50 @@ +package mouda.backend.notification.implement.subscription; + +import java.util.ArrayList; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.infrastructure.entity.SubscriptionEntity; +import mouda.backend.notification.infrastructure.repository.SubscriptionRepository; + +@Component +@RequiredArgsConstructor +public class SubscriptionWriter { + + private final SubscriptionRepository subscriptionRepository; + + public void changeMoimSubscription(Member member) { + subscriptionRepository.findByMemberId(member.getId()) + .ifPresentOrElse( + this::changeMoimSubscription, + () -> changeMoimSubscription(create(member)) + ); + } + + public void changeChatRoomSubscription(Member member, long darakbangId, long chatRoomId) { + subscriptionRepository.findByMemberId(member.getId()) + .ifPresentOrElse( + subscription -> changeChatRoomSubscription(subscription, darakbangId, chatRoomId), + () -> changeChatRoomSubscription(create(member), darakbangId, chatRoomId) + ); + } + + private void changeMoimSubscription(SubscriptionEntity subscriptionEntity) { + subscriptionEntity.changeMoimCreateSubscription(); + subscriptionRepository.save(subscriptionEntity); + } + + private void changeChatRoomSubscription(SubscriptionEntity subscriptionEntity, long darakbangId, long chatRoomId) { + subscriptionEntity.changeChatRoomSubscription(darakbangId, chatRoomId); + subscriptionRepository.save(subscriptionEntity); + } + + private SubscriptionEntity create(Member member) { + return subscriptionRepository.save(SubscriptionEntity.builder() + .memberId(member.getId()) + .unsubscribedChats(new ArrayList<>()) + .build()); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/entity/FcmTokenEntity.java b/backend/src/main/java/mouda/backend/notification/infrastructure/entity/FcmTokenEntity.java new file mode 100644 index 000000000..6f75065e9 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/infrastructure/entity/FcmTokenEntity.java @@ -0,0 +1,62 @@ +package mouda.backend.notification.infrastructure.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "fcm_token") +@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED) +@Getter +public class FcmTokenEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long memberId; + + private String token; + + private boolean isActive; + + private LocalDateTime lastUpdated; + + @Builder + public FcmTokenEntity(Long memberId, String token) { + this.memberId = memberId; + this.token = token; + this.isActive = true; + this.lastUpdated = LocalDateTime.now(); + } + + public void refresh() { + this.lastUpdated = LocalDateTime.now(); + } + + public void activate() { + this.isActive = true; + } + + public void deactivate() { + this.isActive = false; + } + + public boolean isActive(LocalDateTime threshold) { + return isActive && lastUpdated.isBefore(LocalDateTime.now().minusMonths(1L)); + } + + public boolean isInactive() { + return !isActive(); + } + + public boolean isExpired() { + return isInactive() && lastUpdated.isBefore(LocalDateTime.now().minusDays(270L)); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/entity/MemberNotificationEntity.java b/backend/src/main/java/mouda/backend/notification/infrastructure/entity/MemberNotificationEntity.java new file mode 100644 index 000000000..88f0d5f39 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/infrastructure/entity/MemberNotificationEntity.java @@ -0,0 +1,46 @@ +package mouda.backend.notification.infrastructure.entity; + +import java.time.LocalDateTime; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "member_notification") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MemberNotificationEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private long darakbangMemberId; + + private String title; + + private String body; + + private String type; + + private String targetUrl; + + private LocalDateTime createdAt; + + @Builder + public MemberNotificationEntity(long darakbangMemberId, String title, String body, String type, String targeturl, LocalDateTime createdAt) { + this.darakbangMemberId = darakbangMemberId; + this.title = title; + this.body = body; + this.type = type; + this.targetUrl = targeturl; + this.createdAt = createdAt; + } +} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/entity/SubscriptionEntity.java b/backend/src/main/java/mouda/backend/notification/infrastructure/entity/SubscriptionEntity.java new file mode 100644 index 000000000..0cd9ef897 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/infrastructure/entity/SubscriptionEntity.java @@ -0,0 +1,61 @@ +package mouda.backend.notification.infrastructure.entity; + +import java.util.List; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "subscription") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SubscriptionEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private long memberId; + + private boolean moimCreate; + + @JdbcTypeCode(SqlTypes.JSON) + @Column(name = "unsubscribed_chats", columnDefinition = "json") + private List unsubscribedChats; + + @Builder + public SubscriptionEntity(long memberId, List unsubscribedChats) { + this.memberId = memberId; + this.unsubscribedChats = unsubscribedChats; + this.moimCreate = true; + } + + public boolean isSubscribedMoimCreate() { + return moimCreate; + } + + public void changeMoimCreateSubscription() { + this.moimCreate = !this.moimCreate; + } + + public void changeChatRoomSubscription(long darakbangId, long chatRoomId) { + for (UnsubscribedChatRooms unsubscribedChatRoom : unsubscribedChats) { + if (unsubscribedChatRoom.getDarakbangId() == darakbangId) { + unsubscribedChatRoom.changeChatRoomSubscription(chatRoomId); + return; + } + } + unsubscribedChats.add(UnsubscribedChatRooms.create(darakbangId, chatRoomId)); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/entity/UnsubscribedChatRooms.java b/backend/src/main/java/mouda/backend/notification/infrastructure/entity/UnsubscribedChatRooms.java new file mode 100644 index 000000000..bfe812ad9 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/infrastructure/entity/UnsubscribedChatRooms.java @@ -0,0 +1,31 @@ +package mouda.backend.notification.infrastructure.entity; + +import java.util.ArrayList; +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class UnsubscribedChatRooms { + + private long darakbangId; + private List chatRoomIds; + + public static UnsubscribedChatRooms create(long darakbangId, long chatRoomId) { + List chatRoomIds = new ArrayList<>(); + chatRoomIds.add(chatRoomId); + return new UnsubscribedChatRooms(darakbangId, chatRoomIds); + } + + public void changeChatRoomSubscription(long chatRoomId) { + if (chatRoomIds.contains(chatRoomId)) { + chatRoomIds.remove(chatRoomId); + return; + } + chatRoomIds.add(chatRoomId); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/repository/FcmTokenRepository.java b/backend/src/main/java/mouda/backend/notification/infrastructure/repository/FcmTokenRepository.java new file mode 100644 index 000000000..1450cd0b0 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/infrastructure/repository/FcmTokenRepository.java @@ -0,0 +1,19 @@ +package mouda.backend.notification.infrastructure.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import mouda.backend.notification.infrastructure.entity.FcmTokenEntity; + +public interface FcmTokenRepository extends JpaRepository { + + List findAllByMemberId(long memberId); + + Optional findByToken(String token); + + void deleteAllByTokenIn(List unregisteredTokens); + + List findAllByTokenIn(List tokens); +} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/repository/MemberNotificationRepository.java b/backend/src/main/java/mouda/backend/notification/infrastructure/repository/MemberNotificationRepository.java new file mode 100644 index 000000000..d3a771689 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/infrastructure/repository/MemberNotificationRepository.java @@ -0,0 +1,12 @@ +package mouda.backend.notification.infrastructure.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import mouda.backend.notification.infrastructure.entity.MemberNotificationEntity; + +public interface MemberNotificationRepository extends JpaRepository { + + List findAllByDarakbangMemberId(Long darakbangMemberId); +} diff --git a/backend/src/main/java/mouda/backend/notification/infrastructure/repository/SubscriptionRepository.java b/backend/src/main/java/mouda/backend/notification/infrastructure/repository/SubscriptionRepository.java new file mode 100644 index 000000000..1ac089fcc --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/infrastructure/repository/SubscriptionRepository.java @@ -0,0 +1,12 @@ +package mouda.backend.notification.infrastructure.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import mouda.backend.notification.infrastructure.entity.SubscriptionEntity; + +public interface SubscriptionRepository extends JpaRepository { + + Optional findByMemberId(long memberId); +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/controller/MemberNotificationController.java b/backend/src/main/java/mouda/backend/notification/presentation/controller/MemberNotificationController.java new file mode 100644 index 000000000..81acaa582 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/controller/MemberNotificationController.java @@ -0,0 +1,32 @@ +package mouda.backend.notification.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.notification.business.MemberNotificationService; +import mouda.backend.notification.presentation.response.MemberNotificationFindAllResponse; + +@RestController +@RequiredArgsConstructor +public class MemberNotificationController implements MemberNotificationSwagger { + + private final MemberNotificationService memberNotificationService; + + // todo: 이제 darakbangId로 조회할 필요가 없음. DarakbangMember의 id로 조회. -> API 명세 수정 필요 + @GetMapping("/v1/darakbang/{darakbangId}/notification/mine") + @Override + public ResponseEntity> findAllMemberNotification( + @LoginDarakbangMember DarakbangMember darakbangMember, + @PathVariable Long darakbangId + ) { + MemberNotificationFindAllResponse response = memberNotificationService.findAllMemberNotification( + darakbangMember); + return ResponseEntity.ok(new RestResponse<>(response)); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/controller/MemberNotificationSwagger.java b/backend/src/main/java/mouda/backend/notification/presentation/controller/MemberNotificationSwagger.java new file mode 100644 index 000000000..7d6b7d51f --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/controller/MemberNotificationSwagger.java @@ -0,0 +1,24 @@ +package mouda.backend.notification.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.notification.presentation.response.MemberNotificationFindAllResponse; + +public interface MemberNotificationSwagger { + + @Operation(summary = "회원의 개별 알림 조회", description = "알림 센터에 표시되는 회원의 개별 알림을 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "조회 성공!"), + }) + ResponseEntity> findAllMemberNotification( + @LoginDarakbangMember DarakbangMember darakbangMember, + @PathVariable Long darakbangId + ); +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationTokenController.java b/backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationTokenController.java new file mode 100644 index 000000000..656fb5003 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationTokenController.java @@ -0,0 +1,29 @@ +package mouda.backend.notification.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.argumentresolver.LoginMember; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.business.FcmTokenService; +import mouda.backend.notification.presentation.request.FcmTokenRequest; + +@RestController +@RequiredArgsConstructor +public class NotificationTokenController implements NotificationTokenSwagger { + + private final FcmTokenService fcmTokenService; + + @PostMapping("/v1/notification/register") + public ResponseEntity saveOrRefreshToken( + @LoginMember Member member, + @RequestBody FcmTokenRequest tokenRequest + ) { + fcmTokenService.saveOrRefreshToken(member, tokenRequest); + + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationTokenSwagger.java b/backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationTokenSwagger.java new file mode 100644 index 000000000..398670c0d --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/controller/NotificationTokenSwagger.java @@ -0,0 +1,23 @@ +package mouda.backend.notification.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import mouda.backend.common.config.argumentresolver.LoginMember; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.presentation.request.FcmTokenRequest; + +public interface NotificationTokenSwagger { + + @Operation(summary = "FCM 토큰 등록", description = "알림 허용시 FCM 토큰을 등록하고, 이미 등록된 토큰이면 갱신한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "등록(갱신) 성공!"), + }) + ResponseEntity saveOrRefreshToken( + @LoginMember Member member, + @RequestBody FcmTokenRequest tokenRequest + ); +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/controller/SubscriptionController.java b/backend/src/main/java/mouda/backend/notification/presentation/controller/SubscriptionController.java new file mode 100644 index 000000000..d4772890e --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/controller/SubscriptionController.java @@ -0,0 +1,68 @@ +package mouda.backend.notification.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.argumentresolver.LoginMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.business.SubscriptionService; +import mouda.backend.notification.presentation.request.ChatSubscriptionRequest; +import mouda.backend.notification.presentation.response.SubscriptionResponse; + +@RestController +@RequiredArgsConstructor +public class SubscriptionController implements SubscriptionSwagger { + + private final SubscriptionService subscriptionService; + + @GetMapping("/v1/subscription/moim") + @Override + public ResponseEntity> readMoimCreateSubscription( + @LoginMember Member member + ) { + SubscriptionResponse response = subscriptionService.readMoimCreateSubscription(member); + + return ResponseEntity.ok(new RestResponse<>(response)); + } + + @PostMapping("/v1/subscription/moim") + @Override + public ResponseEntity changeMoimCreateSubscription( + @LoginMember Member member + ) { + subscriptionService.changeMoimCreateSubscription(member); + + return ResponseEntity.ok().build(); + } + + @GetMapping("/v1/subscription/chat") + @Override + public ResponseEntity> readSpecificChatRoomSubscription( + @LoginMember Member member, + @RequestParam("darakbangId") Long darakbangId, + @RequestParam("chatRoomId") Long chatRoomId + ) { + SubscriptionResponse response = subscriptionService.readChatRoomSubscription(member, + darakbangId, chatRoomId); + + return ResponseEntity.ok(new RestResponse<>(response)); + } + + @PostMapping("/v1/subscription/chat") + @Override + public ResponseEntity changeChatRoomSubscription( + @LoginMember Member member, + @Valid @RequestBody ChatSubscriptionRequest chatSubscriptionRequest + ) { + subscriptionService.changeChatRoomSubscription(member, chatSubscriptionRequest); + + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/controller/SubscriptionSwagger.java b/backend/src/main/java/mouda/backend/notification/presentation/controller/SubscriptionSwagger.java new file mode 100644 index 000000000..947d6af9a --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/controller/SubscriptionSwagger.java @@ -0,0 +1,53 @@ +package mouda.backend.notification.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import mouda.backend.common.config.argumentresolver.LoginMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.member.domain.Member; +import mouda.backend.notification.presentation.request.ChatSubscriptionRequest; +import mouda.backend.notification.presentation.response.SubscriptionResponse; + +public interface SubscriptionSwagger { + + @Operation(summary = "모임 생성시 알림 여부 조회", description = "모임 생성에 대한 알림 허용 여부를 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "조회 성공"), + }) + ResponseEntity> readMoimCreateSubscription( + @LoginMember Member member + ); + + @Operation(summary = "모임 생성 알림 여부 수정", description = "알림 허용상태면 비허용으로, 비허용 상태면 허용 상태로 변경한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "변경 성공"), + }) + ResponseEntity changeMoimCreateSubscription( + @LoginMember Member member + ); + + @Operation(summary = "특정 채팅방에 대한 알림 여부 조회", description = "특정 채팅방에 대한 알림 허용 여부를 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "조회 성공"), + }) + ResponseEntity> readSpecificChatRoomSubscription( + @LoginMember Member member, + @RequestParam("darakbangId") Long darakbangId, + @RequestParam("chatRoomId") Long chatRoomId + ); + + @Operation(summary = "특정 채팅방에 대한 알림 여부 수정", description = "알림 허용상태면 비허용으로, 비허용 상태면 허용 상태로 변경한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "변경 성공"), + }) + ResponseEntity changeChatRoomSubscription( + @LoginMember Member member, + @Valid @RequestBody ChatSubscriptionRequest chatSubscriptionRequest + ); +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/request/ChatSubscriptionRequest.java b/backend/src/main/java/mouda/backend/notification/presentation/request/ChatSubscriptionRequest.java new file mode 100644 index 000000000..934f1c06c --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/request/ChatSubscriptionRequest.java @@ -0,0 +1,12 @@ +package mouda.backend.notification.presentation.request; + +import jakarta.validation.constraints.NotNull; + +public record ChatSubscriptionRequest( + @NotNull + Long darakbangId, + + @NotNull + Long chatRoomId +) { +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/request/FcmTokenRequest.java b/backend/src/main/java/mouda/backend/notification/presentation/request/FcmTokenRequest.java new file mode 100644 index 000000000..d7d135e98 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/request/FcmTokenRequest.java @@ -0,0 +1,6 @@ +package mouda.backend.notification.presentation.request; + +public record FcmTokenRequest( + String token +) { +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/response/MemberNotificationFindAllResponse.java b/backend/src/main/java/mouda/backend/notification/presentation/response/MemberNotificationFindAllResponse.java new file mode 100644 index 000000000..faa4f67b1 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/response/MemberNotificationFindAllResponse.java @@ -0,0 +1,18 @@ +package mouda.backend.notification.presentation.response; + +import java.util.List; + +import mouda.backend.notification.domain.MemberNotification; + +public record MemberNotificationFindAllResponse( + List notifications +) { + + public static MemberNotificationFindAllResponse from(List notifications) { + List responses = notifications.stream() + .map(MemberNotificationFindResponse::from) + .toList(); + + return new MemberNotificationFindAllResponse(responses); + } +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/response/MemberNotificationFindResponse.java b/backend/src/main/java/mouda/backend/notification/presentation/response/MemberNotificationFindResponse.java new file mode 100644 index 000000000..e0de0ab8f --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/response/MemberNotificationFindResponse.java @@ -0,0 +1,42 @@ +package mouda.backend.notification.presentation.response; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +import mouda.backend.notification.domain.MemberNotification; + +public record MemberNotificationFindResponse( + String message, + String createdAt, + String type, + String redirectUrl +) { + + public static MemberNotificationFindResponse from(MemberNotification notification) { + return new MemberNotificationFindResponse( + notification.getMessage(), + parseTime(notification.getCreatedAt()), + notification.getType(), + notification.getRedirectUrl() + ); + } + + + private static String parseTime(LocalDateTime notificationCreatedAt) { + LocalDateTime now = LocalDateTime.now(); + long minutes = notificationCreatedAt.until(now, ChronoUnit.MINUTES); + long hours = notificationCreatedAt.until(now, ChronoUnit.HOURS); + long days = notificationCreatedAt.until(now, ChronoUnit.DAYS); + + if (minutes == 0) { + return "방금 전"; + } + if (minutes < 60) { + return minutes + "분 전"; + } + if (hours < 24) { + return hours + "시간 전"; + } + return days + "일 전"; + } +} diff --git a/backend/src/main/java/mouda/backend/notification/presentation/response/SubscriptionResponse.java b/backend/src/main/java/mouda/backend/notification/presentation/response/SubscriptionResponse.java new file mode 100644 index 000000000..80ab8b2f5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/presentation/response/SubscriptionResponse.java @@ -0,0 +1,9 @@ +package mouda.backend.notification.presentation.response; + +import lombok.Builder; + +@Builder +public record SubscriptionResponse( + boolean isSubscribed +) { +} diff --git a/backend/src/main/java/mouda/backend/notification/util/FcmRetryAfterExtractor.java b/backend/src/main/java/mouda/backend/notification/util/FcmRetryAfterExtractor.java new file mode 100644 index 000000000..1934f45b3 --- /dev/null +++ b/backend/src/main/java/mouda/backend/notification/util/FcmRetryAfterExtractor.java @@ -0,0 +1,39 @@ +package mouda.backend.notification.util; + +import java.util.List; + +import com.google.firebase.IncomingHttpResponse; +import com.google.firebase.messaging.BatchResponse; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.MessagingErrorCode; +import com.google.firebase.messaging.SendResponse; + +public class FcmRetryAfterExtractor { + + private static final int DEFAULT_RETRY_AFTER_SECONDS = 60; + + public static int getRetryAfterSeconds(BatchResponse batchResponse) { + List responses = batchResponse.getResponses(); + return responses.stream() + .filter(FcmRetryAfterExtractor::isFailedWith429) + .map(FcmRetryAfterExtractor::parseRetryAfterSeconds) + .findAny() + .orElse(DEFAULT_RETRY_AFTER_SECONDS); + } + + private static boolean isFailedWith429(SendResponse response) { + return response.getException().getMessagingErrorCode() == MessagingErrorCode.QUOTA_EXCEEDED; + } + + private static int parseRetryAfterSeconds(SendResponse response) { + FirebaseMessagingException exception = response.getException(); + IncomingHttpResponse httpResponse = exception.getHttpResponse(); + Object retryAfterHeader = httpResponse.getHeaders().get("Retry-After"); + + try { + return Integer.parseInt(retryAfterHeader.toString()); + } catch (Exception e) { + return DEFAULT_RETRY_AFTER_SECONDS; + } + } +} diff --git a/backend/src/main/java/mouda/backend/please/business/InterestService.java b/backend/src/main/java/mouda/backend/please/business/InterestService.java new file mode 100644 index 000000000..072f3cd8b --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/business/InterestService.java @@ -0,0 +1,25 @@ +package mouda.backend.please.business; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.please.domain.Please; +import mouda.backend.please.implement.InterestWriter; +import mouda.backend.please.implement.PleaseFinder; +import mouda.backend.please.presentation.request.InterestUpdateRequest; + +@Service +@Transactional +@RequiredArgsConstructor +public class InterestService { + + private final PleaseFinder pleaseFinder; + private final InterestWriter interestWriter; + + public void updateInterest(Long darakbangId, DarakbangMember darakbangMember, InterestUpdateRequest request) { + Please please = pleaseFinder.find(request.pleaseId(), darakbangId); + interestWriter.changeInterest(please, request.isInterested(), darakbangMember); + } +} diff --git a/backend/src/main/java/mouda/backend/please/business/PleaseService.java b/backend/src/main/java/mouda/backend/please/business/PleaseService.java new file mode 100644 index 000000000..00d774105 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/business/PleaseService.java @@ -0,0 +1,43 @@ +package mouda.backend.please.business; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.please.domain.Please; +import mouda.backend.please.domain.PleaseWithInterests; +import mouda.backend.please.implement.PleaseFinder; +import mouda.backend.please.implement.PleaseValidator; +import mouda.backend.please.implement.PleaseWriter; +import mouda.backend.please.presentation.request.PleaseCreateRequest; +import mouda.backend.please.presentation.response.PleaseFindAllResponses; + +@Service +@Transactional +@RequiredArgsConstructor +public class PleaseService { + + private final PleaseWriter pleaseWriter; + private final PleaseFinder pleaseFinder; + private final PleaseValidator pleaseValidator; + + public Please createPlease(Long darakbangId, DarakbangMember darakbangMember, + PleaseCreateRequest pleaseCreateRequest) { + Please please = pleaseCreateRequest.toEntity(darakbangMember.getId(), darakbangId); + + return pleaseWriter.savePlease(please); + } + + public void deletePlease(Long darakbangId, Long pleaseId, DarakbangMember darakbangMember) { + Please please = pleaseFinder.find(pleaseId, darakbangId); + pleaseValidator.validate(please, darakbangId, darakbangMember); + pleaseWriter.delete(pleaseId); + } + + public PleaseFindAllResponses findAllPlease(Long darakbangId, DarakbangMember darakbangMember) { + PleaseWithInterests pleaseWithInterests = pleaseFinder.findPleasesDesc(darakbangId, darakbangMember); + + return PleaseFindAllResponses.toResponse(pleaseWithInterests); + } +} diff --git a/backend/src/main/java/mouda/backend/please/domain/Interest.java b/backend/src/main/java/mouda/backend/please/domain/Interest.java new file mode 100644 index 000000000..24b9f018e --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/domain/Interest.java @@ -0,0 +1,56 @@ +package mouda.backend.please.domain; + +import org.springframework.http.HttpStatus; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.please.exception.PleaseErrorMessage; +import mouda.backend.please.exception.PleaseException; + +@Entity +@Getter +@Table(name = "interest") +@NoArgsConstructor +public class Interest { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(nullable = false) + private DarakbangMember darakbangMember; + + @ManyToOne + @JoinColumn(nullable = false) + private Please please; + + @Builder + public Interest(DarakbangMember darakbangMember, Please please) { + validateMember(darakbangMember); + validatePlease(please); + this.darakbangMember = darakbangMember; + this.please = please; + } + + private void validateMember(DarakbangMember darakbangMember) { + if (darakbangMember == null) { + throw new PleaseException(HttpStatus.NOT_FOUND, PleaseErrorMessage.MEMBER_NOT_FOUND); + } + } + + private void validatePlease(Please please) { + if (please == null) { + throw new PleaseException(HttpStatus.NOT_FOUND, PleaseErrorMessage.NOT_FOUND); + } + } +} diff --git a/backend/src/main/java/mouda/backend/please/domain/Please.java b/backend/src/main/java/mouda/backend/please/domain/Please.java new file mode 100644 index 000000000..1197e30e9 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/domain/Please.java @@ -0,0 +1,68 @@ +package mouda.backend.please.domain; + +import org.springframework.http.HttpStatus; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import mouda.backend.please.exception.PleaseErrorMessage; +import mouda.backend.please.exception.PleaseException; + +@Entity +@Getter +@Table(name = "please") +@NoArgsConstructor +public class Please { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String description; + + @Column(nullable = false) + private long authorId; + + @Column(nullable = false) + private long darakbangId; + + @Builder + public Please(String title, String description, long authorId, long darakbangId) { + validateTitle(title); + validateDescription(description); + this.title = title; + this.description = description; + this.authorId = authorId; + this.darakbangId = darakbangId; + } + + private void validateTitle(String title) { + if (title == null || title.isEmpty()) { + throw new PleaseException(HttpStatus.BAD_REQUEST, PleaseErrorMessage.TITLE_NOT_EXIST); + } + } + + private void validateDescription(String description) { + if (description == null || description.isEmpty()) { + throw new PleaseException(HttpStatus.BAD_REQUEST, PleaseErrorMessage.DESCRIPTION_NOT_EXIST); + } + } + + public boolean isNotAuthor(long memberId) { + return authorId != memberId; + } + + public boolean isNotInDarakbang(long darakbangId) { + return this.darakbangId != darakbangId; + } +} diff --git a/backend/src/main/java/mouda/backend/please/domain/PleaseWithInterest.java b/backend/src/main/java/mouda/backend/please/domain/PleaseWithInterest.java new file mode 100644 index 000000000..207547c48 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/domain/PleaseWithInterest.java @@ -0,0 +1,23 @@ +package mouda.backend.please.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class PleaseWithInterest implements Comparable { + + private final Please please; + private final boolean isInterested; + private final long interestCount; + + @Override + public int compareTo(PleaseWithInterest other) { + int interestCompare = Long.compare(other.interestCount, this.interestCount); + + if (interestCompare != 0) { + return interestCompare; + } + return Long.compare(this.please.getId(), other.please.getId()); + } +} diff --git a/backend/src/main/java/mouda/backend/please/domain/PleaseWithInterests.java b/backend/src/main/java/mouda/backend/please/domain/PleaseWithInterests.java new file mode 100644 index 000000000..5f65ced74 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/domain/PleaseWithInterests.java @@ -0,0 +1,13 @@ +package mouda.backend.please.domain; + +import java.util.List; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class PleaseWithInterests { + + private final List pleaseWithInterests; +} diff --git a/backend/src/main/java/mouda/backend/please/exception/PleaseErrorMessage.java b/backend/src/main/java/mouda/backend/please/exception/PleaseErrorMessage.java new file mode 100644 index 000000000..9de5a68b5 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/exception/PleaseErrorMessage.java @@ -0,0 +1,18 @@ +package mouda.backend.please.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum PleaseErrorMessage { + + NOT_FOUND("해주세요가 존재하지 않습니다."), + TITLE_NOT_EXIST("제목이 존재하지 않습니다."), + DESCRIPTION_NOT_EXIST("내용이 존재하지 않습니다."), + NOT_ALLOWED_TO_DELETE("삭제 권한이 없습니다."), + MEMBER_NOT_FOUND("멤버가 없습니다."), + PLEASE_NOT_IN_DARAKBANG("다락방에 존재하는 해주세요가 아닙니다."); + + private final String message; +} diff --git a/backend/src/main/java/mouda/backend/please/exception/PleaseException.java b/backend/src/main/java/mouda/backend/please/exception/PleaseException.java new file mode 100644 index 000000000..4767a1afb --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/exception/PleaseException.java @@ -0,0 +1,12 @@ +package mouda.backend.please.exception; + +import org.springframework.http.HttpStatus; + +import mouda.backend.common.exception.MoudaException; + +public class PleaseException extends MoudaException { + + public PleaseException(HttpStatus httpStatus, PleaseErrorMessage message) { + super(httpStatus, message.getMessage()); + } +} diff --git a/backend/src/main/java/mouda/backend/please/implement/InterestFinder.java b/backend/src/main/java/mouda/backend/please/implement/InterestFinder.java new file mode 100644 index 000000000..d0166490f --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/implement/InterestFinder.java @@ -0,0 +1,25 @@ +package mouda.backend.please.implement; + +import java.util.Optional; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.please.domain.Interest; +import mouda.backend.please.domain.Please; +import mouda.backend.please.infrastructure.InterestRepository; + +@Component +@RequiredArgsConstructor +public class InterestFinder { + + private final InterestRepository interestRepository; + + public Optional findInterest(Long darakbangMemberId, Long pleaseId) { + return interestRepository.findByDarakbangMemberIdAndPleaseId(darakbangMemberId, pleaseId); + } + + public long countInterest(Please please) { + return interestRepository.countByPleaseId(please.getId()); + } +} diff --git a/backend/src/main/java/mouda/backend/please/implement/InterestValidator.java b/backend/src/main/java/mouda/backend/please/implement/InterestValidator.java new file mode 100644 index 000000000..f0f55a298 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/implement/InterestValidator.java @@ -0,0 +1,21 @@ +package mouda.backend.please.implement; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.please.domain.Please; +import mouda.backend.please.infrastructure.InterestRepository; +import mouda.backend.please.infrastructure.PleaseRepository; + +@Component +@RequiredArgsConstructor +public class InterestValidator { + + private final InterestRepository interestRepository; + private final PleaseRepository pleaseRepository; + private final PleaseValidator pleaseValidator; + + public void validate(Please please, Long darakbangId) { + pleaseValidator.validateNotInDarakbang(please, darakbangId); + } +} diff --git a/backend/src/main/java/mouda/backend/please/implement/InterestWriter.java b/backend/src/main/java/mouda/backend/please/implement/InterestWriter.java new file mode 100644 index 000000000..6dc254076 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/implement/InterestWriter.java @@ -0,0 +1,36 @@ +package mouda.backend.please.implement; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.please.domain.Interest; +import mouda.backend.please.domain.Please; +import mouda.backend.please.infrastructure.InterestRepository; + +@Component +@RequiredArgsConstructor +public class InterestWriter { + + private final InterestRepository interestRepository; + private final InterestFinder interestFinder; + private final PleaseFinder pleaseFinder; + private final InterestValidator interestValidator; + + public void changeInterest(Please please, boolean isInterested, DarakbangMember darakbangMember) { + if (isInterested) { + Interest newInterest = Interest.builder() + .darakbangMember(darakbangMember) + .please(please) + .build(); + interestRepository.save(newInterest); + return; + } + removeInterest(darakbangMember.getMemberId(), please.getId()); + } + + private void removeInterest(Long darakbangMemberId, Long pleaseId) { + interestFinder.findInterest(darakbangMemberId, pleaseId) + .ifPresent(interestRepository::delete); + } +} diff --git a/backend/src/main/java/mouda/backend/please/implement/PleaseFinder.java b/backend/src/main/java/mouda/backend/please/implement/PleaseFinder.java new file mode 100644 index 000000000..f16229c32 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/implement/PleaseFinder.java @@ -0,0 +1,50 @@ +package mouda.backend.please.implement; + +import java.util.List; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.please.domain.Please; +import mouda.backend.please.domain.PleaseWithInterest; +import mouda.backend.please.domain.PleaseWithInterests; +import mouda.backend.please.exception.PleaseErrorMessage; +import mouda.backend.please.exception.PleaseException; +import mouda.backend.please.infrastructure.PleaseRepository; + +@Component +@RequiredArgsConstructor +public class PleaseFinder { + + private final PleaseRepository pleaseRepository; + private final InterestFinder interestFinder; + private final InterestValidator interestValidator; + + public Please find(Long pleaseId, long darakbangId) { + Please please = pleaseRepository.findById(pleaseId) + .orElseThrow(() -> new PleaseException(HttpStatus.NOT_FOUND, PleaseErrorMessage.NOT_FOUND)); + + interestValidator.validate(please, darakbangId); + return please; + } + + public PleaseWithInterests findPleasesDesc(Long darakbangId, DarakbangMember darakbangMember) { + List pleases = pleaseRepository.findAllByDarakbangIdOrderByIdDesc(darakbangId); + + List pleaseWithInterests = pleases.stream() + .map(please -> getPleaseWithInterest(darakbangMember, please)) + .sorted() + .toList(); + + return new PleaseWithInterests(pleaseWithInterests); + } + + private PleaseWithInterest getPleaseWithInterest(DarakbangMember darakbangMember, Please please) { + boolean isInterested = interestFinder.findInterest(darakbangMember.getId(), please.getId()) + .isPresent(); + long interestCount = interestFinder.countInterest(please); + return new PleaseWithInterest(please, isInterested, interestCount); + } +} diff --git a/backend/src/main/java/mouda/backend/please/implement/PleaseValidator.java b/backend/src/main/java/mouda/backend/please/implement/PleaseValidator.java new file mode 100644 index 000000000..5db28cd26 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/implement/PleaseValidator.java @@ -0,0 +1,32 @@ +package mouda.backend.please.implement; + +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.please.domain.Please; +import mouda.backend.please.exception.PleaseErrorMessage; +import mouda.backend.please.exception.PleaseException; + +@Component +@RequiredArgsConstructor +public class PleaseValidator { + + public void validate(Please please, Long darakbangId, DarakbangMember darakbangMember) { + validateNotInDarakbang(please, darakbangId); + validateAuthorize(please, darakbangMember); + } + + public void validateNotInDarakbang(Please please, Long darakbangId) { + if (please.isNotInDarakbang(darakbangId)) { + throw new PleaseException(HttpStatus.BAD_REQUEST, PleaseErrorMessage.PLEASE_NOT_IN_DARAKBANG); + } + } + + private void validateAuthorize(Please please, DarakbangMember darakbangMember) { + if (please.isNotAuthor(darakbangMember.getId())) { + throw new PleaseException(HttpStatus.FORBIDDEN, PleaseErrorMessage.NOT_ALLOWED_TO_DELETE); + } + } +} diff --git a/backend/src/main/java/mouda/backend/please/implement/PleaseWriter.java b/backend/src/main/java/mouda/backend/please/implement/PleaseWriter.java new file mode 100644 index 000000000..c43026581 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/implement/PleaseWriter.java @@ -0,0 +1,22 @@ +package mouda.backend.please.implement; + +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; +import mouda.backend.please.domain.Please; +import mouda.backend.please.infrastructure.PleaseRepository; + +@Component +@RequiredArgsConstructor +public class PleaseWriter { + + private final PleaseRepository pleaseRepository; + + public Please savePlease(Please please) { + return pleaseRepository.save(please); + } + + public void delete(Long pleaseId) { + pleaseRepository.deleteById(pleaseId); + } +} diff --git a/backend/src/main/java/mouda/backend/please/infrastructure/InterestRepository.java b/backend/src/main/java/mouda/backend/please/infrastructure/InterestRepository.java new file mode 100644 index 000000000..7b1ae4d82 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/infrastructure/InterestRepository.java @@ -0,0 +1,18 @@ +package mouda.backend.please.infrastructure; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import mouda.backend.please.domain.Interest; + +public interface InterestRepository extends JpaRepository { + + boolean existsByDarakbangMemberId(Long id); + + long countByPleaseId(Long pleaseId); + + Optional findByDarakbangMemberIdAndPleaseId(long memberId, long pleasedId); + + boolean existsByDarakbangMemberIdAndPleaseId(long memberId, long pleaseId); +} diff --git a/backend/src/main/java/mouda/backend/please/infrastructure/PleaseRepository.java b/backend/src/main/java/mouda/backend/please/infrastructure/PleaseRepository.java new file mode 100644 index 000000000..91353c063 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/infrastructure/PleaseRepository.java @@ -0,0 +1,12 @@ +package mouda.backend.please.infrastructure; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import mouda.backend.please.domain.Please; + +public interface PleaseRepository extends JpaRepository { + + List findAllByDarakbangIdOrderByIdDesc(Long darakbangId); +} diff --git a/backend/src/main/java/mouda/backend/please/presentation/controller/InterestController.java b/backend/src/main/java/mouda/backend/please/presentation/controller/InterestController.java new file mode 100644 index 000000000..2f4df8270 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/presentation/controller/InterestController.java @@ -0,0 +1,35 @@ +package mouda.backend.please.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.please.business.InterestService; +import mouda.backend.please.presentation.controller.swagger.InterestSwagger; +import mouda.backend.please.presentation.request.InterestUpdateRequest; + +@RestController +@RequestMapping("/v1/darakbang/{darakbangId}/interest") +@RequiredArgsConstructor +public class InterestController implements InterestSwagger { + + private final InterestService interestService; + + @Override + @PostMapping + public ResponseEntity updateInterest( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @RequestBody InterestUpdateRequest request + ) { + interestService.updateInterest(darakbangId, member, request); + + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/mouda/backend/please/presentation/controller/PleaseController.java b/backend/src/main/java/mouda/backend/please/presentation/controller/PleaseController.java new file mode 100644 index 000000000..d0f0cb0b6 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/presentation/controller/PleaseController.java @@ -0,0 +1,64 @@ +package mouda.backend.please.presentation.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.please.business.PleaseService; +import mouda.backend.please.domain.Please; +import mouda.backend.please.presentation.controller.swagger.PleaseSwagger; +import mouda.backend.please.presentation.request.PleaseCreateRequest; +import mouda.backend.please.presentation.response.PleaseFindAllResponses; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/darakbang/{darakbangId}/please") +public class PleaseController implements PleaseSwagger { + + private final PleaseService pleaseService; + + @Override + @PostMapping + public ResponseEntity> createPlease( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @Valid @RequestBody PleaseCreateRequest pleaseCreateRequest + ) { + Please please = pleaseService.createPlease(darakbangId, member, pleaseCreateRequest); + + return ResponseEntity.ok().body(new RestResponse<>(please.getId())); + } + + @Override + @DeleteMapping("/{pleaseId}") + public ResponseEntity deletePlease( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @PathVariable Long pleaseId + ) { + pleaseService.deletePlease(darakbangId, pleaseId, member); + + return ResponseEntity.ok().build(); + } + + @Override + @GetMapping + public ResponseEntity> findAllPlease( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member + ) { + PleaseFindAllResponses responses = pleaseService.findAllPlease(darakbangId, member); + + return ResponseEntity.ok().body(new RestResponse<>(responses)); + } +} diff --git a/backend/src/main/java/mouda/backend/please/presentation/controller/swagger/InterestSwagger.java b/backend/src/main/java/mouda/backend/please/presentation/controller/swagger/InterestSwagger.java new file mode 100644 index 000000000..ae4ecd9a3 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/presentation/controller/swagger/InterestSwagger.java @@ -0,0 +1,25 @@ +package mouda.backend.please.presentation.controller.swagger; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.please.presentation.request.InterestUpdateRequest; + +public interface InterestSwagger { + + @Operation(summary = "관심 상태 변경", description = "관심 상태를 변경한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "관심 상태 변경 성공!"), + }) + ResponseEntity updateInterest( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @RequestBody InterestUpdateRequest request + ); +} diff --git a/backend/src/main/java/mouda/backend/please/presentation/controller/swagger/PleaseSwagger.java b/backend/src/main/java/mouda/backend/please/presentation/controller/swagger/PleaseSwagger.java new file mode 100644 index 000000000..a687562bd --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/presentation/controller/swagger/PleaseSwagger.java @@ -0,0 +1,47 @@ +package mouda.backend.please.presentation.controller.swagger; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import mouda.backend.common.config.argumentresolver.LoginDarakbangMember; +import mouda.backend.common.response.RestResponse; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.please.presentation.request.PleaseCreateRequest; +import mouda.backend.please.presentation.response.PleaseFindAllResponses; + +public interface PleaseSwagger { + + @Operation(summary = "해주세요 생성", description = "해주세요를 생성한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "해주세요 생성 성공!"), + }) + ResponseEntity> createPlease( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @Valid @RequestBody PleaseCreateRequest pleaseCreateRequest + ); + + @Operation(summary = "해주세요 삭제", description = "해주세요를 삭제한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "해주세요 삭제 성공!"), + }) + ResponseEntity deletePlease( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member, + @PathVariable Long pleaseId + ); + + @Operation(summary = "해주세요 목록 조회", description = "해주세요 목록을 조회한다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "해주세요 목록 조회 성공!"), + }) + ResponseEntity> findAllPlease( + @PathVariable Long darakbangId, + @LoginDarakbangMember DarakbangMember member + ); +} diff --git a/backend/src/main/java/mouda/backend/please/presentation/request/InterestUpdateRequest.java b/backend/src/main/java/mouda/backend/please/presentation/request/InterestUpdateRequest.java new file mode 100644 index 000000000..702109fe7 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/presentation/request/InterestUpdateRequest.java @@ -0,0 +1,7 @@ +package mouda.backend.please.presentation.request; + +public record InterestUpdateRequest( + long pleaseId, + boolean isInterested +) { +} diff --git a/backend/src/main/java/mouda/backend/please/presentation/request/PleaseCreateRequest.java b/backend/src/main/java/mouda/backend/please/presentation/request/PleaseCreateRequest.java new file mode 100644 index 000000000..2661dea5d --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/presentation/request/PleaseCreateRequest.java @@ -0,0 +1,23 @@ +package mouda.backend.please.presentation.request; + +import jakarta.validation.constraints.NotNull; +import mouda.backend.please.domain.Please; + +public record PleaseCreateRequest( + + @NotNull + String title, + + @NotNull + String description +) { + + public Please toEntity(long darakbangMemberId, long darakbangId) { + return Please.builder() + .title(title) + .description(description) + .authorId(darakbangMemberId) + .darakbangId(darakbangId) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/please/presentation/response/PleaseFindAllResponse.java b/backend/src/main/java/mouda/backend/please/presentation/response/PleaseFindAllResponse.java new file mode 100644 index 000000000..aeb1a9471 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/presentation/response/PleaseFindAllResponse.java @@ -0,0 +1,23 @@ +package mouda.backend.please.presentation.response; + +import lombok.Builder; +import mouda.backend.please.domain.PleaseWithInterest; + +@Builder +public record PleaseFindAllResponse( + long pleaseId, + String title, + String description, + boolean isInterested, + long interestCount +) { + public static PleaseFindAllResponse toResponse(PleaseWithInterest pleaseWithInterest) { + return PleaseFindAllResponse.builder() + .pleaseId(pleaseWithInterest.getPlease().getId()) + .title(pleaseWithInterest.getPlease().getTitle()) + .description(pleaseWithInterest.getPlease().getDescription()) + .isInterested(pleaseWithInterest.isInterested()) + .interestCount(pleaseWithInterest.getInterestCount()) + .build(); + } +} diff --git a/backend/src/main/java/mouda/backend/please/presentation/response/PleaseFindAllResponses.java b/backend/src/main/java/mouda/backend/please/presentation/response/PleaseFindAllResponses.java new file mode 100644 index 000000000..22b67e7c4 --- /dev/null +++ b/backend/src/main/java/mouda/backend/please/presentation/response/PleaseFindAllResponses.java @@ -0,0 +1,18 @@ +package mouda.backend.please.presentation.response; + +import java.util.List; + +import mouda.backend.please.domain.PleaseWithInterests; + +public record PleaseFindAllResponses( + List pleases +) { + + public static PleaseFindAllResponses toResponse(PleaseWithInterests pleaseWithInterests) { + List pleaseFindAllResponses = pleaseWithInterests.getPleaseWithInterests().stream() + .map(PleaseFindAllResponse::toResponse) + .toList(); + + return new PleaseFindAllResponses(pleaseFindAllResponses); + } +} diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml new file mode 100644 index 000000000..eb15ca11d --- /dev/null +++ b/backend/src/main/resources/application-dev.yml @@ -0,0 +1,52 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + maximum-pool-size: 20 + jpa: + hibernate: + ddl-auto: update + show-sql: false + +url: + base: https://dev.mouda.site + +oauth: + kakao: + redirect-uri: https://dev.mouda.site/oauth/kakao + google: + client-secret: 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 + redirect-uri: https://dev.mouda.site/oauth/google + apple: + redirect-uri: https://api.dev.mouda.site/v1/auth/apple + redirection: https://dev.mouda.site/oauth/apple?token=%s&isConverted=%s + +aws: + region: + static: ap-northeast-2 + s3: + bucket: techcourse-project-2024 + key-prefix: mouda/dev/asset/profile/ + prefix: https://dev.mouda.site/profile/ + +management: + metrics: + enable: + tomcat: true + endpoints: + web: + exposure: + include: prometheus + endpoint: + prometheus: + enabled: true + metrics: + enabled: true + health: + show-details: always +server: + tomcat: + mbeanregistry: + enabled: true + threads: + max: 100 diff --git a/backend/src/main/resources/application-local.yml b/backend/src/main/resources/application-local.yml new file mode 100644 index 000000000..4b6465323 --- /dev/null +++ b/backend/src/main/resources/application-local.yml @@ -0,0 +1,62 @@ +spring: + application: + name: backend + datasource: + url: jdbc:h2:mem:test + username: sa + password: + driverClassName: org.h2.Driver + jpa: + database-platform: org.hibernate.dialect.H2Dialect + show-sql: true + properties: + hibernate: + format_sql: true + hibernate: + ddl-auto: create-drop + defer-datasource-initialization: true + h2: + console: + enabled: true + path: /h2-console + +oauth: + kakao: + redirect-uri: http://localhost:8081/oauth/kakao + google: + client-secret: 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 + redirect-uri: http://localhost:8081/oauth/google + apple: + redirect-uri: https://api.dev.mouda.site/v1/auth/apple + redirection: https://dev.mouda.site/oauth/apple?token=%s&isConverted=%s + +aws: + region: + static: ap-northeast-2 + s3: + bucket: techcourse-project-2024 + key-prefix: mouda/dev/asset/profile/ + prefix: https://dev.mouda.site/profile/ + +url: + base: http://localhost:8081 + +management: + metrics: + enable: + tomcat: true + endpoints: + web: + exposure: + include: prometheus + endpoint: + prometheus: + enabled: true + metrics: + enabled: true + health: + show-details: always +server: + tomcat: + mbeanregistry: + enabled: true diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml new file mode 100644 index 000000000..8ac4cbe48 --- /dev/null +++ b/backend/src/main/resources/application-prod.yml @@ -0,0 +1,66 @@ +spring: + datasource: + master: + hikari: + username: ${MASTER_MYSQL_USERNAME} + password: ${MASTER_MYSQL_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + jdbc-url: ${MASTER_MYSQL_URL} + slave: + hikari: + username: ${SLAVE_MYSQL_USERNAME} + password: ${SLAVE_MYSQL_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + jdbc-url: ${SLAVE_MYSQL_URL} + + jpa: + hibernate: + ddl-auto: none + show-sql: false + +security: + jwt: + token: + secret-key: ${JWT_SECRET_KEY} + expire-length: ${JWT_EXPIRE_LENGTH} + +url: + base: https://mouda.site + +oauth: + kakao: + redirect-uri: https://mouda.site/oauth/kakao + google: + client-secret: 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 + redirect-uri: https://mouda.site/oauth/google + apple: + redirect-uri: https://mouda.site/v1/auth/apple + redirection: https://test.mouda.site/oauth/apple?token=%s&isConverted=%s + +aws: + region: + static: ap-northeast-2 + s3: + bucket: techcourse-project-2024 + key-prefix: mouda/dev/asset/profile/ + prefix: https://dev.mouda.site/profile/ + +management: + metrics: + enable: + tomcat: true + endpoints: + web: + exposure: + include: prometheus + endpoint: + prometheus: + enabled: true + metrics: + enabled: true + health: + show-details: always +server: + tomcat: + mbeanregistry: + enabled: true diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 000000000..b0bfffd94 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,27 @@ +server: + forward-headers-strategy: native + +spring: + servlet: + multipart: + max-file-size: 10MB + max-request-size: 10MB + profiles: + active: + - local + jpa: + open-in-view: false + +security: + jwt: + token: + secret-key: kksangdolbabokksangdolbabokksangdolbabokksangdolbabokksangdolbabokksangdolbabo + expire-length: 3600000 + +url: + moim: /darakbang/%d/moim/%d + chat: /darakbang/%d/chat/%d + chatroom: /darakbang/%d/chatting-room/%d + +bet: + schedule: "0 * * * * *" # 매분마다 diff --git a/backend/src/main/resources/data.sql b/backend/src/main/resources/data.sql new file mode 100644 index 000000000..ec8e18581 --- /dev/null +++ b/backend/src/main/resources/data.sql @@ -0,0 +1,14 @@ +INSERT INTO darakbang (name, code) +VALUES ('테스트용 다락방', 'MOUDA'); + +INSERT INTO member (name, oauth_type, identifier, is_converted) +VALUES ('김민겸', 'KAKAO', 'identifier', true); + +INSERT INTO darakbang_member (darakbang_id, member_id, nickname, role) +VALUES (1, 1, '안나', 'MANAGER'); + +INSERT INTO moim(title, date, time, place, max_people, description, moim_status, is_chat_opened, darakbang_id) +VALUES ('제목', '2024-10-01', '11:11', '장소', 2, 'description', 'MOIMING', true, 1); + +INSERT INTO chamyo(moim_id, darakbang_member_id, moim_role, last_read_chat_id) +VALUES (1, 1, 'MOIMER', 1); diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml new file mode 100644 index 000000000..b34efac8b --- /dev/null +++ b/backend/src/main/resources/logback-spring.xml @@ -0,0 +1,42 @@ + + + + + + + + + ${logPattern} + + + + + ${logPath}/${serviceName}-${NOW}.log + + ${logPath}/${serviceName}-%d{yyyy-MM-dd}-%i.log.gz + + 5MB + + 15 + + + ${logPattern} + + + info + + + + + + + + + + + + + + + + diff --git a/backend/src/test/java/mouda/backend/auth/Infrastructure/AppleOauthClientTest.java b/backend/src/test/java/mouda/backend/auth/Infrastructure/AppleOauthClientTest.java new file mode 100644 index 000000000..ae7db1cac --- /dev/null +++ b/backend/src/test/java/mouda/backend/auth/Infrastructure/AppleOauthClientTest.java @@ -0,0 +1,26 @@ +package mouda.backend.auth.Infrastructure; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AppleOauthClientTest { + + @Autowired + private AppleOauthClient appleOauthClient; + + @DisplayName("애플 로그인 시 애플 서버에게 토큰을 요청한다.") + @Test + @Disabled("실제 애플 서버에 요청을 보내는 테스트이다. 프론트 서버를 켜서 코드르 발급 받아 필요할 때만 테스트한다.") + void getIdToken() { + String code = ""; + + String idToken = appleOauthClient.getIdToken(code); + assertThat(idToken).isNotNull(); + } +} diff --git a/backend/src/test/java/mouda/backend/auth/business/AppleAuthServiceTest.java b/backend/src/test/java/mouda/backend/auth/business/AppleAuthServiceTest.java new file mode 100644 index 000000000..398f3af39 --- /dev/null +++ b/backend/src/test/java/mouda/backend/auth/business/AppleAuthServiceTest.java @@ -0,0 +1,107 @@ +package mouda.backend.auth.business; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import mouda.backend.auth.implement.AppleUserInfoProvider; +import mouda.backend.auth.presentation.response.LoginResponse; +import mouda.backend.common.fixture.MemberFixture; +import mouda.backend.member.domain.Member; +import mouda.backend.member.domain.MemberStatus; +import mouda.backend.member.infrastructure.MemberRepository; + +@SpringBootTest +class AppleAuthServiceTest { + + @Autowired + private AppleAuthService appleAuthService; + + @Autowired + private MemberRepository memberRepository; + + @MockBean + private AppleUserInfoProvider userInfoProvider; + + private final String identifier = "123"; + private final String name = "김민겸"; + + @BeforeEach + void setUp() { + when(userInfoProvider.getName(anyString())).thenReturn(name); + when(userInfoProvider.getIdentifier(anyString())).thenReturn(identifier); + } + + @DisplayName("최초 로그인인 경우 회원 가입과 로그인을 진행한다.") + @Test + void joinAndLogin() { + // when + LoginResponse response = appleAuthService.login("idToken", "user"); + + // then + assertThat(response.accessToken()).isNotNull(); + Optional member = memberRepository.findActiveOrDeletedByIdentifier(identifier); + assertThat(member.isPresent()).isTrue(); + assertThat(member.get().getName()).isEqualTo(name); + } + + @DisplayName("회원가입 이력이 있는 경우 회원가입 없이 로그인을 진행한다.") + @Test + void login() { + // given + Member anna = MemberFixture.getAnna(identifier); + memberRepository.save(anna); + + // when + LoginResponse response = appleAuthService.login("idToken", null); + + // then + assertThat(response.accessToken()).isNotNull(); + Optional member = memberRepository.findActiveOrDeletedByIdentifier(identifier); + assertThat(member.isPresent()).isTrue(); + } + + @DisplayName("회원 탈퇴 이력이 있는 경우 재가입 후 로그인을 진행한다.") + @Test + void rejoinAndLogin() { + // given + Member anna = MemberFixture.getAnna(identifier); + anna.withdraw(); + memberRepository.save(anna); + + // when + LoginResponse response = appleAuthService.login("idToken", null); + + // then + assertThat(response.accessToken()).isNotNull(); + Optional member = memberRepository.findActiveOrDeletedByIdentifier(identifier); + assertThat(member.isPresent()).isTrue(); + assertThat(member.get().getMemberStatus()).isEqualTo(MemberStatus.ACTIVE); + } + + @DisplayName("최초 애플 로그인인 경우에 DB에 이미 로그인 이력이 있다면 바로 로그인한다.") + @Test + void loginIfExistsMember() { + // given + Member anna = MemberFixture.getAnna(identifier); + memberRepository.save(anna); + + // when + LoginResponse response = appleAuthService.login("idToken", "user"); + + // then + assertThat(response.accessToken()).isNotNull(); + Optional member = memberRepository.findByLoginDetail_Identifier(identifier); + assertThat(member.isPresent()).isTrue(); + assertThat(member.get().getMemberStatus()).isEqualTo(MemberStatus.ACTIVE); + assertThat(member.get().getIdentifier()).isEqualTo(identifier); + } +} diff --git a/backend/src/test/java/mouda/backend/auth/business/GoogleAuthServiceTest.java b/backend/src/test/java/mouda/backend/auth/business/GoogleAuthServiceTest.java new file mode 100644 index 000000000..639d200e3 --- /dev/null +++ b/backend/src/test/java/mouda/backend/auth/business/GoogleAuthServiceTest.java @@ -0,0 +1,55 @@ +package mouda.backend.auth.business; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import mouda.backend.auth.implement.GoogleUserInfoProvider; +import mouda.backend.auth.presentation.request.GoogleLoginRequest; +import mouda.backend.auth.presentation.response.LoginResponse; +import mouda.backend.common.fixture.MemberFixture; +import mouda.backend.member.domain.Member; +import mouda.backend.member.domain.MemberStatus; +import mouda.backend.member.infrastructure.MemberRepository; + +@SpringBootTest +class GoogleAuthServiceTest { + + @Autowired + private GoogleAuthService googleAuthService; + + @Autowired + private MemberRepository memberRepository; + + @MockBean + private GoogleUserInfoProvider googleUserInfoProvider; + + @DisplayName("회원 탈퇴한 사용자가 재가입하는 경우 상태 정보를 변경한다.") + @Test + void processSocialLoginWhoDeletedBefore() { + // given + Member anna = MemberFixture.getAnna(); + anna.withdraw(); + memberRepository.save(anna); + + when(googleUserInfoProvider.getName(anyString())).thenReturn("anna"); + when(googleUserInfoProvider.getIdentifier(anyString())).thenReturn(anna.getIdentifier()); + + // when + LoginResponse loginResponse = googleAuthService.login(new GoogleLoginRequest("IdToken")); + + // then + assertThat(loginResponse.accessToken()).isNotNull(); + Optional member = memberRepository.findActiveOrDeletedByIdentifier("1234"); + assertThat(member.isPresent()).isTrue(); + assertThat(member.get().getMemberStatus()).isEqualTo(MemberStatus.ACTIVE); + } +} diff --git a/backend/src/test/java/mouda/backend/auth/business/KakaoAuthServiceTest.java b/backend/src/test/java/mouda/backend/auth/business/KakaoAuthServiceTest.java new file mode 100644 index 000000000..44c90d850 --- /dev/null +++ b/backend/src/test/java/mouda/backend/auth/business/KakaoAuthServiceTest.java @@ -0,0 +1,65 @@ +package mouda.backend.auth.business; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import mouda.backend.auth.implement.KakaoUserInfoProvider; +import mouda.backend.auth.presentation.request.KakaoConvertRequest; +import mouda.backend.common.fixture.MemberFixture; +import mouda.backend.member.domain.Member; +import mouda.backend.member.domain.MemberStatus; +import mouda.backend.member.domain.OauthType; +import mouda.backend.member.infrastructure.MemberRepository; + +@SpringBootTest +class KakaoAuthServiceTest { + + @Autowired + private KakaoAuthService kakaoAuthService; + + @Autowired + private MemberRepository memberRepository; + + @MockBean + private KakaoUserInfoProvider userInfoProvider; + + @DisplayName("카카오 회원 정보를 애플 회원으로 변경한다.") + @Test + void convert() { + // given + String kakaoIdentifier = "kakaoIdentifier"; + Member kakao = MemberFixture.getAnna(kakaoIdentifier); + memberRepository.save(kakao); + + String googleIdentifier = "googleIdentifier"; + Member google = MemberFixture.getAnna(OauthType.APPLE, googleIdentifier); + memberRepository.save(google); + when(userInfoProvider.getIdentifier(anyString())).thenReturn(kakaoIdentifier); + + // when + kakaoAuthService.convert(google, new KakaoConvertRequest("code")); + + // then + Optional kakaoMember = memberRepository.findActiveOrDeletedByIdentifier(kakaoIdentifier); + assertThat(kakaoMember.isEmpty()).isTrue(); + + Optional convertedMember = memberRepository.findActiveOrDeletedByIdentifier(googleIdentifier); + assertThat(convertedMember.isPresent()).isTrue(); + assertThat(convertedMember.get().getMemberStatus()).isEqualTo(MemberStatus.ACTIVE); + assertThat(convertedMember.get().isConverted()).isTrue(); + + Optional googleMember = memberRepository.findDeprecatedByIdentifier(googleIdentifier); + assertThat(googleMember.isPresent()).isTrue(); + assertThat(googleMember.get().getMemberStatus()).isEqualTo(MemberStatus.DEPRECATED); + assertThat(googleMember.get().getLoginDetail()).isEqualTo(google.getLoginDetail()); + } +} + diff --git a/backend/src/test/java/mouda/backend/auth/implement/AppleUserInfoProviderTest.java b/backend/src/test/java/mouda/backend/auth/implement/AppleUserInfoProviderTest.java new file mode 100644 index 000000000..152678cc8 --- /dev/null +++ b/backend/src/test/java/mouda/backend/auth/implement/AppleUserInfoProviderTest.java @@ -0,0 +1,26 @@ +package mouda.backend.auth.implement; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AppleUserInfoProviderTest { + + @Autowired + private AppleUserInfoProvider userInfoProvider; + + @DisplayName("Resource Server받아온 Identity Token으로 사용자 정보를 추출한다.") + @Test + @Disabled("실제 Resource Server에게 요청을 보내는 테스트이다. 프론트 서버를 켜서 코드르 발급 받아 필요할 때만 테스트한다.") + void getUserInfo() { + String code = ""; + + String identifier = userInfoProvider.getIdentifier(code); + assertThat(identifier).isNotNull(); + } +} diff --git a/backend/src/test/java/mouda/backend/auth/implement/MemberFinderTest.java b/backend/src/test/java/mouda/backend/auth/implement/MemberFinderTest.java new file mode 100644 index 000000000..98304f30d --- /dev/null +++ b/backend/src/test/java/mouda/backend/auth/implement/MemberFinderTest.java @@ -0,0 +1,50 @@ +package mouda.backend.auth.implement; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.auth.exception.AuthException; +import mouda.backend.common.fixture.MemberFixture; +import mouda.backend.member.domain.Member; +import mouda.backend.member.implement.MemberFinder; +import mouda.backend.member.infrastructure.MemberRepository; + +@SpringBootTest +class MemberFinderTest { + + @Autowired + MemberFinder memberFinder; + + @Autowired + MemberRepository memberRepository; + + @DisplayName("멤버 id로 멤버를 찾는다") + @Test + void findByMemberId() { + // given + Member tebah = MemberFixture.getTebah(); + memberRepository.save(tebah); + + // when + Member member = memberFinder.findByMemberId(1L); + + // then + assertThat(member.getIdentifier()).isEqualTo(tebah.getIdentifier()); + } + + @DisplayName("멤버가 존재하지 않으면 예외가 발생한다.") + @Test + void findByInvalidMemberId() { + // given + Member tebah = MemberFixture.getTebah(); + memberRepository.save(tebah); + + // when & than + assertThatThrownBy(() -> memberFinder.findByMemberId(2L)) + .isInstanceOf(AuthException.class); + } +} diff --git a/backend/src/test/java/mouda/backend/auth/implement/MemberWriterTest.java b/backend/src/test/java/mouda/backend/auth/implement/MemberWriterTest.java new file mode 100644 index 000000000..99c663e85 --- /dev/null +++ b/backend/src/test/java/mouda/backend/auth/implement/MemberWriterTest.java @@ -0,0 +1,56 @@ +package mouda.backend.auth.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.MemberFixture; +import mouda.backend.member.domain.Member; +import mouda.backend.member.implement.MemberWriter; +import mouda.backend.member.infrastructure.MemberRepository; + +@SpringBootTest +class MemberWriterTest { + + @Autowired + MemberWriter memberWriter; + + @Autowired + MemberRepository memberRepository; + + @DisplayName("새로운 멤버를 추가한다.") + @Test + void append() { + // given + Member tebah = MemberFixture.getTebah(); + + // when + memberWriter.append(tebah); + + // then + Member savedMember = memberRepository.save(tebah); + assertThat(savedMember.getId()).isEqualTo(1L); + assertThat(memberRepository.findAll()).hasSize(1); + } + + @DisplayName("회원을 삭제 시 상태가 변경된다.") + @Test + void withdraw() { + // given + Member tebah = MemberFixture.getTebah(); + memberWriter.append(tebah); + + // when + memberWriter.withdraw(tebah); + + // then + List members = memberRepository.findAll(); + assertThat(members).hasSize(1); + assertThat(members.get(0)).isEqualTo(tebah); + } +} diff --git a/backend/src/test/java/mouda/backend/auth/presentation/controller/AuthControllerTest.java b/backend/src/test/java/mouda/backend/auth/presentation/controller/AuthControllerTest.java new file mode 100644 index 000000000..6b5cdfca0 --- /dev/null +++ b/backend/src/test/java/mouda/backend/auth/presentation/controller/AuthControllerTest.java @@ -0,0 +1,47 @@ +package mouda.backend.auth.presentation.controller; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; + +import mouda.backend.auth.business.AppleAuthService; +import mouda.backend.auth.presentation.response.LoginResponse; + +@SpringBootTest +class AuthControllerTest { + + @Autowired + private AuthController authController; + + @Value("${oauth.apple.redirection}") + private String redirection; + + @MockBean + private AppleAuthService appleAuthService; + + @DisplayName("애플 로그인 후 토큰과 전환 여부를 반환한다.") + @Test + void loginApple() { + when(appleAuthService.login(anyString(), anyString())).thenReturn( + new LoginResponse("token", true) + ); + + ResponseEntity responseEntity = authController.loginApple("idtoken", "user"); + HttpHeaders headers = responseEntity.getHeaders(); + assertThat(headers.containsKey("Location")).isTrue(); + List location = headers.get("Location"); + assertThat(location).hasSize(1); + String format = String.format(redirection, "token", "true"); + assertThat(location.get(0)).isEqualTo(format); + } +} diff --git a/backend/src/test/java/mouda/backend/bet/business/BetSchedulerTest.java b/backend/src/test/java/mouda/backend/bet/business/BetSchedulerTest.java new file mode 100644 index 000000000..838400701 --- /dev/null +++ b/backend/src/test/java/mouda/backend/bet/business/BetSchedulerTest.java @@ -0,0 +1,62 @@ +package mouda.backend.bet.business; + +import java.time.LocalDateTime; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.bet.entity.BetDarakbangMemberEntity; +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.bet.infrastructure.BetRepository; +import mouda.backend.common.fixture.DarakbangSetUp; + +@SpringBootTest +class BetSchedulerTest extends DarakbangSetUp { + + @Autowired + BetScheduler betScheduler; + + @Autowired + BetRepository betRepository; + + @Autowired + BetDarakbangMemberRepository betDarakbangMemberRepository; + + @DisplayName("스케줄러에 의해 추첨을 실행한다.") + @Test + void performScheduledTask() { + // given + BetEntity betEntity = BetEntity.builder() + .title("testBet") + .bettingTime(LocalDateTime.now() + .withSecond(0) + .withNano(0)) + .darakbangId(1L) + .moimerId(1L) + .build(); + + betRepository.save(betEntity); + + betDarakbangMemberRepository.save(new BetDarakbangMemberEntity(darakbangHogee, betEntity)); + betDarakbangMemberRepository.save(new BetDarakbangMemberEntity(darakbangAnna, betEntity)); + + // when & then + // await() + // .atMost(1, MINUTES) + // .untilAsserted(() -> assertThat(hasLoser()).isTrue()); + // + // Optional savedBet = betRepository.findById(1L); + // assertThat(savedBet).isPresent(); + // assertThat(savedBet.get().getLoserDarakbangMemberId()).isNotNull(); + // assertThat(savedBet.get().getDarakbangId()).isEqualTo(1L); + } + + private boolean hasLoser() { + Optional savedBetEntity = betRepository.findById(1L); + return savedBetEntity.isPresent() && savedBetEntity.get().getLoserDarakbangMemberId() != null; + } +} diff --git a/backend/src/test/java/mouda/backend/bet/business/BetServiceTest.java b/backend/src/test/java/mouda/backend/bet/business/BetServiceTest.java new file mode 100644 index 000000000..36744e3da --- /dev/null +++ b/backend/src/test/java/mouda/backend/bet/business/BetServiceTest.java @@ -0,0 +1,153 @@ +package mouda.backend.bet.business; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.bet.domain.Bet; +import mouda.backend.bet.entity.BetDarakbangMemberEntity; +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.implement.BetWriter; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.bet.infrastructure.BetRepository; +import mouda.backend.bet.presentation.request.BetCreateRequest; +import mouda.backend.bet.presentation.response.BetFindAllResponse; +import mouda.backend.bet.presentation.response.BetFindAllResponses; +import mouda.backend.bet.presentation.response.BetFindResponse; +import mouda.backend.common.fixture.BetEntityFixture; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; + +@SpringBootTest +class BetServiceTest extends DarakbangSetUp { + + @Autowired + BetService betService; + + @Autowired + BetWriter betWriter; + + @Autowired + BetRepository betRepository; + + @Autowired + BetDarakbangMemberRepository betDarakbangMemberRepository; + + @Autowired + DarakbangMemberRepository darakbangMemberRepository; + + @DisplayName("조회된 안내면진다는 정렬되어있다.") + @Test + void findAll() { + // given + long darakbangId = darakbang.getId(); + BetEntity betEntity1 = BetEntityFixture.getBetEntity(darakbangId, darakbangAnna.getId(), + LocalDateTime.now().plusMinutes(5)); + BetEntity betEntity2 = BetEntityFixture.getBetEntity(darakbangId, darakbangAnna.getId(), + LocalDateTime.now().minusMinutes(3)); + BetEntity betEntity3 = BetEntityFixture.getBetEntity(darakbangId, darakbangAnna.getId(), + LocalDateTime.now().plusMinutes(10)); + + BetEntity savedBetEntity1 = betRepository.save(betEntity1); + BetEntity savedBetEntity2 = betRepository.save(betEntity2); + BetEntity savedBetEntity3 = betRepository.save(betEntity3); + + // when + BetFindAllResponses findAllResponses = betService.findAllBets(darakbangId); + + // then + List expectedOrder = Arrays.asList(savedBetEntity2.getId(), savedBetEntity1.getId(), + savedBetEntity3.getId()); + List actualOrder = findAllResponses.bets().stream() + .map(BetFindAllResponse::id) + .toList(); + + assertThat(actualOrder).isEqualTo(expectedOrder); + } + + @DisplayName("추첨하지 않은 안내면진다를 상세 조회한다.") + @Test + void findNotDrawnBet() { + // given + long darakbangId = darakbang.getId(); + BetEntity drawnBetEntity = BetEntityFixture.getFutureBetEntity(darakbangId, darakbangAnna.getId()); + BetEntity savedBetEntity = betRepository.save(drawnBetEntity); + + // when + BetFindResponse bet = betService.findBet(darakbangId, savedBetEntity.getId(), darakbangAnna); + + // then + assertThat(bet.isAnnounced()).isFalse(); + } + + @DisplayName("추첨한 안내면진다를 상세 조회한다.") + @Test + void findDrawnBet() { + // given + long darakbangId = darakbang.getId(); + BetEntity drawnBetEntity = BetEntityFixture.getDrawedBetEntity(darakbangId, darakbangAnna.getId()); + BetEntity savedBetEntity = betRepository.save(drawnBetEntity); + + // when + BetFindResponse bet = betService.findBet(darakbangId, savedBetEntity.getId(), darakbangAnna); + + // then + assertThat(bet.isAnnounced()).isTrue(); + } + + @DisplayName("새로운 안내면진다를 생성한다.") + @Test + void createBet() { + // given + BetCreateRequest betRequest = new BetCreateRequest("테니바보", 10); + // when + long createdBetId = betService.createBet(1L, betRequest, darakbangHogee); + //then + assertThat(createdBetId).isEqualTo(1L); + } + + @DisplayName("안내면진다에 참여한다.") + @Test + void participateBet() { + // given + long darakbangId = 1L; + BetCreateRequest betRequest = new BetCreateRequest("테니바보", 10); + Bet bet = betRequest.toBet(2L); + BetEntity betEntity = betRepository.save(BetEntity.create(bet, darakbangId)); + + long betId = betEntity.getId(); + + // when + betService.participateBet(darakbangId, betId, darakbangHogee); + + //then + List entities = betDarakbangMemberRepository.findAll(); + assertThat(entities).hasSize(1); + } + + @DisplayName("당첨자를 강제추첨한다.") + @Test + void drawBet() { + // given + Long darakbangId = darakbang.getId(); + BetEntity betEntity = BetEntityFixture.getFutureBetEntity(darakbangId, darakbangAnna.getId()); + BetEntity savedBetEntity = betRepository.save(betEntity); + betWriter.participate(darakbangId, savedBetEntity.getId(), darakbangAnna); + + // when + betService.drawBet(darakbangId, savedBetEntity.getId()); + + //then + Optional drawnBetEntity = betRepository.findById(savedBetEntity.getId()); + assertThat(drawnBetEntity).isPresent(); + assertThat(drawnBetEntity.get().getLoserDarakbangMemberId()).isNotNull(); + } +} diff --git a/backend/src/test/java/mouda/backend/bet/domain/BetTest.java b/backend/src/test/java/mouda/backend/bet/domain/BetTest.java new file mode 100644 index 000000000..e592755c8 --- /dev/null +++ b/backend/src/test/java/mouda/backend/bet/domain/BetTest.java @@ -0,0 +1,59 @@ +package mouda.backend.bet.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class BetTest { + + @DisplayName("당첨자를 결정한다.") + @Test + void draw() { + // given + List participants = List.of(new Participant(1L, "테바", "profile"), new Participant(2L, "테니", "profile")); + BetDetails betDetails = BetDetails.builder() + .id(1L) + .title("테바 미안") + .bettingTime(LocalDateTime.now().plusMinutes(5L).withSecond(0).withNano(0)) + .build(); + + Bet bet = Bet.builder() + .betDetails(betDetails) + .participants(participants) + .build(); + + // when + bet.draw(); + + //then + assertThat(bet.hasLoser()).isTrue(); + } + + @DisplayName("모이머의 id를 반환한다.") + @Test + void getMoimerId() { + List participants = List.of(new Participant(1L, "테바", "profile"), new Participant(2L, "테니", "profile")); + BetDetails betDetails = BetDetails.builder() + .id(1L) + .title("테바 미안") + .bettingTime(LocalDateTime.now().plusMinutes(5L).withSecond(0).withNano(0)) + .build(); + + long expected = 1L; + Bet bet = Bet.builder() + .betDetails(betDetails) + .participants(participants) + .moimerId(expected) + .build(); + + // when + long actual = bet.getMoimerId(); + + //then + assertThat(expected).isEqualTo(actual); + } +} diff --git a/backend/src/test/java/mouda/backend/bet/domain/BettingTimeTest.java b/backend/src/test/java/mouda/backend/bet/domain/BettingTimeTest.java new file mode 100644 index 000000000..4581a3a3e --- /dev/null +++ b/backend/src/test/java/mouda/backend/bet/domain/BettingTimeTest.java @@ -0,0 +1,25 @@ +package mouda.backend.bet.domain; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class BettingTimeTest { + + @DisplayName("배팅시간은 분까지만 저장한다.") + @Test + void create() { + // given + LocalDateTime givenTime = LocalDateTime.of(2024, 10, 1, 12, 30, 45, 123456789); + + // when + BettingTime bettingTime = new BettingTime(givenTime); + + // then + LocalDateTime expectedTime = LocalDateTime.of(2024, 10, 1, 12, 30, 0, 0); + assertEquals(expectedTime, bettingTime.getBettingTime()); + } +} diff --git a/backend/src/test/java/mouda/backend/bet/implement/BetFinderTest.java b/backend/src/test/java/mouda/backend/bet/implement/BetFinderTest.java new file mode 100644 index 000000000..232e1f85a --- /dev/null +++ b/backend/src/test/java/mouda/backend/bet/implement/BetFinderTest.java @@ -0,0 +1,121 @@ +package mouda.backend.bet.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import mouda.backend.bet.domain.Bet; +import mouda.backend.bet.domain.Loser; +import mouda.backend.bet.entity.BetDarakbangMemberEntity; +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.exception.BetException; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.bet.infrastructure.BetRepository; +import mouda.backend.common.fixture.BetEntityFixture; +import mouda.backend.common.fixture.DarakbangSetUp; + +class BetFinderTest extends DarakbangSetUp { + + @Autowired + BetFinder betFinder; + + @Autowired + BetWriter betWriter; + + @Autowired + BetRepository betRepository; + + @Autowired + BetDarakbangMemberRepository betDarakbangMemberRepository; + + @DisplayName("다락방에 존재하는 안내면진다를 조회한다.") + @Test + void find() { + // given + long darakbangId = darakbang.getId(); + String title = "테니바보"; + BetEntity betEntity = BetEntityFixture.getBetEntity(title, darakbangId, darakbangAnna.getId()); + BetEntity savedBetEntity = betRepository.save(betEntity); + + // when + Bet bet = betFinder.find(darakbangId, savedBetEntity.getId()); + + //then + assertThat(bet.getId()).isEqualTo(savedBetEntity.getId()); + assertThat(bet.getBetDetails().getTitle()).isEqualTo(title); + } + + @DisplayName("다락방에 존재하는 모든 안내면진다를 조회한다.") + @Test + void findAllDetails() { + // given + long darakbangId = darakbang.getId(); + BetEntity betEntity = BetEntityFixture.getFutureBetEntity(darakbangId, darakbangAnna.getId()); + betRepository.save(betEntity); + + // when + List betDetails = betFinder.findAllByDarakbangId(darakbangId); + List emptyBetDetails = betFinder.findAllByDarakbangId(123L); + + //then + assertThat(betDetails).hasSize(1); + assertThat(emptyBetDetails).hasSize(0); + } + + @DisplayName("추첨 가능한 모든 안내면진다를 조회한다.") + @Test + void findAllDrawableBets() { + // given + long darakbangId = darakbang.getId(); + BetEntity betEntity = BetEntityFixture.getBetEntity(darakbangId, darakbangAnna.getId()); + betRepository.save(betEntity); + + long moudaDarakbangId = mouda.getId(); + BetEntity betEntity2 = BetEntityFixture.getBetEntity(moudaDarakbangId, darakbangAnna.getId()); + betRepository.save(betEntity2); + + BetEntity betEntity3 = BetEntityFixture.getBetEntity(moudaDarakbangId, darakbangAnna.getId(), + LocalDateTime.now().plusMinutes(10)); + betRepository.save(betEntity3); + + // when + List bets = betFinder.findAllDrawableBet(); + + //then + assertThat(bets).hasSize(2); + } + + @DisplayName("추첨 결과를 가져온다.") + @Test + void findBetResult() { + // given + long darakbangId = darakbang.getId(); + BetEntity betEntity = BetEntityFixture.getDrawedBetEntity(darakbangId, darakbangAnna.getId()); + BetEntity savedBetEntity = betRepository.save(betEntity); + betDarakbangMemberRepository.save(new BetDarakbangMemberEntity(darakbangAnna, savedBetEntity)); + + // when + Loser loser = betFinder.findResult(darakbangId, savedBetEntity.getId()); + + //then + assertThat(loser.getName()).isEqualTo(darakbangAnna.getNickname()); + } + + @DisplayName("추첨이 진행되지 않은 결과를 조회할 경우 예외가 발생한다.") + @Test + void findBetResult_beforeDraw() { + // given + long darakbangId = darakbang.getId(); + BetEntity betEntity = BetEntityFixture.getBetEntity(darakbangId, darakbangAnna.getId()); + BetEntity savedBetEntity = betRepository.save(betEntity); + + // when & then + assertThatThrownBy(() -> betFinder.findResult(darakbangId, savedBetEntity.getId())) + .isInstanceOf(BetException.class); + } +} diff --git a/backend/src/test/java/mouda/backend/bet/implement/BetSorterTest.java b/backend/src/test/java/mouda/backend/bet/implement/BetSorterTest.java new file mode 100644 index 000000000..35cb1b395 --- /dev/null +++ b/backend/src/test/java/mouda/backend/bet/implement/BetSorterTest.java @@ -0,0 +1,77 @@ +package mouda.backend.bet.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import mouda.backend.bet.domain.Bet; +import mouda.backend.common.fixture.BetFixture; + +public class BetSorterTest { + + private BetSorter betSorter; + + @BeforeEach + public void setUp() { + betSorter = new BetSorter(); + } + + @DisplayName("현재 시간과의 차이가 적은 순서대로 정렬한다.") + @Test + public void sortByTimeDifference() { + // given + LocalDateTime now = LocalDateTime.now(); + Bet bet1 = BetFixture.createBet(1L, now.plusMinutes(5), null); + Bet bet2 = BetFixture.createBet(2L, now.plusMinutes(1), null); + Bet bet3 = BetFixture.createBet(3L, now.plusMinutes(10), null); + Bet bet4 = BetFixture.createBet(4L, now.minusMinutes(3), null); + + // when + List bets = Arrays.asList(bet1, bet2, bet3, bet4); + List sortedBets = betSorter.sort(bets); + + // then + assertThat(sortedBets).containsExactly(bet2, bet4, bet1, bet3); + } + + @DisplayName("모든 베팅이 추첨되었다면 추첨시간과 가까운 순으로 배치한다.") + @Test + public void sortByLoserIdWhenTimeIsEqual() { + // given + LocalDateTime now = LocalDateTime.now(); + Bet bet1 = BetFixture.createBet(1L, now.plusMinutes(5), 100L); + Bet bet2 = BetFixture.createBet(2L, now.plusMinutes(1), 100L); + Bet bet3 = BetFixture.createBet(3L, now.plusMinutes(3), 100L); + + // when + List bets = Arrays.asList(bet1, bet2, bet3); + List sortedBets = betSorter.sort(bets); + + // then + assertThat(sortedBets).containsExactly(bet2, bet3, bet1); + } + + @DisplayName("추첨이 완료되지 않은 배팅이 추첨시간과 가까운 순으로 먼저 배치된다. 추첨된 베팅은 추첨시간과 가까운 순으로 뒤에 배치된다.") + @Test + public void sortByHasLoser() { + // given + LocalDateTime now = LocalDateTime.now(); + Bet bet1 = BetFixture.createBet(1L, now.plusMinutes(5), null); + Bet bet2 = BetFixture.createBet(2L, now.plusMinutes(1), 100L); + Bet bet3 = BetFixture.createBet(3L, now.plusMinutes(1), null); + Bet bet4 = BetFixture.createBet(4L, now.plusMinutes(5), 100L); + + // when + List bets = Arrays.asList(bet1, bet2, bet3, bet4); + List sortedBets = betSorter.sort(bets); + + // then + assertThat(sortedBets).containsExactly(bet3, bet1, bet2, bet4); + } +} diff --git a/backend/src/test/java/mouda/backend/bet/implement/BetWriterTest.java b/backend/src/test/java/mouda/backend/bet/implement/BetWriterTest.java new file mode 100644 index 000000000..586159514 --- /dev/null +++ b/backend/src/test/java/mouda/backend/bet/implement/BetWriterTest.java @@ -0,0 +1,86 @@ +package mouda.backend.bet.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.bet.business.BetService; +import mouda.backend.bet.entity.BetDarakbangMemberEntity; +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.exception.BetException; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.bet.infrastructure.BetRepository; +import mouda.backend.common.fixture.BetEntityFixture; +import mouda.backend.common.fixture.DarakbangSetUp; + +@SpringBootTest +class BetWriterTest extends DarakbangSetUp { + + @Autowired + BetService betService; + + @Autowired + BetRepository betRepository; + + @Autowired + BetDarakbangMemberRepository betDarakbangMemberRepository; + + @DisplayName("추첨 시간 이후 참여할 수 없다.") + @Test + void failToParticipate_pastBettingTIme() { + // given + LocalDateTime pastBettingTime = LocalDateTime.now().minusSeconds(1); + BetEntity bet = betRepository.save( + BetEntityFixture.getBetEntity(darakbang.getId(), darakbangAnna.getId(), pastBettingTime)); + + // when & then + assertThatThrownBy(() -> betService.participateBet(darakbang.getId(), bet.getId(), darakbangHogee)) + .isInstanceOf(BetException.class); + } + + @DisplayName("이미 당첨자가 존재하면 참여할 수 없다.") + @Test + void failToParticipate_loserExists() { + // given + BetEntity bet = betRepository.save( + BetEntityFixture.getDrawedBetEntity(darakbang.getId(), darakbangAnna.getId())); + + // when & then + assertThatThrownBy(() -> betService.participateBet(darakbang.getId(), bet.getId(), darakbangHogee)) + .isInstanceOf(BetException.class); + } + + @DisplayName("중복 참여할 수 없다.") + @Test + void failToParticipate_alreadyParticipate() { + // given + BetEntity bet = betRepository.save( + BetEntityFixture.getFutureBetEntity(darakbang.getId(), darakbangAnna.getId())); + betDarakbangMemberRepository.save(new BetDarakbangMemberEntity(darakbangAnna, bet)); + + // when & then + assertThatThrownBy(() -> betService.participateBet(darakbang.getId(), bet.getId(), darakbangAnna)) + .isInstanceOf(BetException.class); + } + + @DisplayName("참여에 성공한다.") + @Test + void participate() { + // given + BetEntity bet = betRepository.save( + BetEntityFixture.getFutureBetEntity(darakbang.getId(), darakbangAnna.getId())); + + // when + betService.participateBet(darakbang.getId(), bet.getId(), darakbangHogee); + + // then + List participants = betDarakbangMemberRepository.findAllByBetId(bet.getId()); + assertThat(participants).hasSize(1); + } +} diff --git a/backend/src/test/java/mouda/backend/bet/implement/ParticipantFinderTest.java b/backend/src/test/java/mouda/backend/bet/implement/ParticipantFinderTest.java new file mode 100644 index 000000000..14d815dd8 --- /dev/null +++ b/backend/src/test/java/mouda/backend/bet/implement/ParticipantFinderTest.java @@ -0,0 +1,50 @@ +package mouda.backend.bet.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.bet.domain.Participant; +import mouda.backend.bet.entity.BetDarakbangMemberEntity; +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.bet.infrastructure.BetRepository; +import mouda.backend.common.fixture.BetEntityFixture; +import mouda.backend.common.fixture.DarakbangSetUp; + +@SpringBootTest +class ParticipantFinderTest extends DarakbangSetUp { + + @Autowired + ParticipantFinder participantFinder; + + @Autowired + BetRepository betRepository; + + @Autowired + BetDarakbangMemberRepository betDarakbangMemberRepository; + + @DisplayName("안내면진다에 참여한 참여자 전원을 조회한다.") + @Test + void findAllByBetEntity() { + // given + BetEntity betEntity = BetEntityFixture.getBetEntity(darakbang.getId(), darakbangAnna.getId()); + betRepository.save(betEntity); + + BetDarakbangMemberEntity betDarakbangMemberEntity1 = new BetDarakbangMemberEntity(darakbangAnna, betEntity); + betDarakbangMemberRepository.save(betDarakbangMemberEntity1); + BetDarakbangMemberEntity betDarakbangMemberEntity2 = new BetDarakbangMemberEntity(darakbangHogee, betEntity); + betDarakbangMemberRepository.save(betDarakbangMemberEntity2); + + // when + List participants = participantFinder.findAllByBetEntity(betEntity); + + //then + assertThat(participants).hasSize(2); + } +} diff --git a/backend/src/test/java/mouda/backend/bet/infrastructure/BetRepositoryTest.java b/backend/src/test/java/mouda/backend/bet/infrastructure/BetRepositoryTest.java new file mode 100644 index 000000000..aa4c984df --- /dev/null +++ b/backend/src/test/java/mouda/backend/bet/infrastructure/BetRepositoryTest.java @@ -0,0 +1,39 @@ +package mouda.backend.bet.infrastructure; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.common.fixture.BetEntityFixture; + +@SpringBootTest +class BetRepositoryTest { + + @Autowired + BetRepository betRepository; + + @DisplayName("현재 시각과 동일하고 추첨하지 않은 배팅 목록을 가져온다.") + @Test + void findAllByBettingTimeAndLoserDarakbangMemberIdIsNull() { + // given + BetEntity betEntity1 = BetEntityFixture.getBetEntity(1L, 1L, LocalDateTime.now()); + BetEntity betEntity2 = BetEntityFixture.getDrawedBetEntity(1L, 2L); + + betRepository.save(betEntity1); + betRepository.save(betEntity2); + + // when + List betEntities = betRepository.findAllByBettingTimeAndLoserDarakbangMemberIdIsNull( + LocalDateTime.now().withSecond(0).withNano(0)); + + //then + assertThat(betEntities).hasSize(1); + } +} diff --git a/backend/src/test/java/mouda/backend/chamyo/service/ChamyoServiceTest.java b/backend/src/test/java/mouda/backend/chamyo/service/ChamyoServiceTest.java new file mode 100644 index 000000000..5ef7932a9 --- /dev/null +++ b/backend/src/test/java/mouda/backend/chamyo/service/ChamyoServiceTest.java @@ -0,0 +1,113 @@ +package mouda.backend.chamyo.service; + +import static org.assertj.core.api.Assertions.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangFixture; +import mouda.backend.common.fixture.DarakbangMemberFixture; +import mouda.backend.common.fixture.MemberFixture; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.member.domain.Member; +import mouda.backend.member.infrastructure.MemberRepository; +import mouda.backend.moim.business.ChamyoService; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.exception.ChamyoException; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +class ChamyoServiceTest { + + @Autowired + private ChamyoService chamyoService; + + @Autowired + private DarakbangRepository darakbangRepository; + + @Autowired + private DarakbangMemberRepository darakbangMemberRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MoimRepository moimRepository; + + @DisplayName("같은 회원이 따로 따로 여러 번 참여 요청을 하면 두 번째 요청 시 참여에 실패한다.") + @Test + void chamyoMoim() throws InterruptedException { + Darakbang darakbang = DarakbangFixture.getDarakbangWithMouda(); + darakbangRepository.save(darakbang); + + Member member = MemberFixture.getAnna(); + memberRepository.save(member); + + DarakbangMember darakbangMember = DarakbangMemberFixture + .getDarakbangMemberWithWooteco(darakbang, member); + darakbangMemberRepository.save(darakbangMember); + + Moim moim = MoimFixture.getBasketballMoim(darakbang.getId()); + moimRepository.save(moim); + + chamyoService.chamyoMoim(darakbang.getId(), moim.getId(), darakbangMember); + assertThatThrownBy(() -> chamyoService.chamyoMoim(darakbang.getId(), moim.getId(), darakbangMember)) + .isInstanceOf(ChamyoException.class); + assertThat(chamyoService.findAllChamyo(darakbang.getId(), moim.getId()).chamyos()).hasSize(1); + } + + @DisplayName("동시 참여 테스트") + @Nested + class ConcurrencyTest { + + @DisplayName("같은 회원이 동시에 여러 번 참여 요청을 하면 동시 참여가 불가능하다.") + @Test + void chamyoMoimConcurrently() throws InterruptedException { + int threadCount = 2; + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + Darakbang darakbang = DarakbangFixture.getDarakbangWithMouda(); + darakbangRepository.save(darakbang); + + Moim moim = MoimFixture.getBasketballMoim(darakbang.getId()); + moimRepository.save(moim); + + Member member = MemberFixture.getAnna(); + memberRepository.save(member); + + DarakbangMember darakbangMember = DarakbangMemberFixture + .getDarakbangMemberWithWooteco(darakbang, member); + darakbangMemberRepository.save(darakbangMember); + + long startTime = System.currentTimeMillis(); + for (int i = 0; i < threadCount; i++) { + executorService.execute(() -> { + try { + chamyoService.chamyoMoim(darakbang.getId(), moim.getId(), darakbangMember); + } finally { + latch.countDown(); + } + }); + } + latch.await(); + executorService.shutdown(); + + long endTime = System.currentTimeMillis(); + System.out.printf("Test time : %d ms\n", endTime - startTime); + + assertThat(chamyoService.findAllChamyo(darakbang.getId(), moim.getId()).chamyos()).hasSize(1); + } + } +} diff --git a/backend/src/test/java/mouda/backend/chat/ChatAsyncTest.java b/backend/src/test/java/mouda/backend/chat/ChatAsyncTest.java new file mode 100644 index 000000000..9774223a4 --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/ChatAsyncTest.java @@ -0,0 +1,91 @@ +package mouda.backend.chat; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import mouda.backend.auth.presentation.controller.AuthController; +import mouda.backend.chat.business.ChatService; +import mouda.backend.chat.domain.Author; +import mouda.backend.chat.domain.Chat; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.entity.ChatEntity; +import mouda.backend.chat.entity.ChatRoomEntity; +import mouda.backend.chat.implement.notification.ChatRecipientFinder; +import mouda.backend.chat.infrastructure.ChatRepository; +import mouda.backend.chat.infrastructure.ChatRoomRepository; +import mouda.backend.chat.presentation.request.ChatCreateRequest; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +public class ChatAsyncTest extends DarakbangSetUp { + + @Autowired + private ChatService chatService; + + @Autowired + private ChatRoomRepository chatRoomRepository; + + @MockBean + private ChatRecipientFinder chatRecipientFinder; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private ChamyoRepository chamyoRepository; + + @Autowired + private ChatRepository chatRepository; + + private Moim moim; + + @BeforeEach + void init() { + moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + chamyoRepository.save(Chamyo.builder() + .moimRole(MoimRole.MOIMER) + .darakbangMember(darakbangAnna) + .moim(moim) + .build()); + } + + @DisplayName("채팅 알림 전송 과정에서 예외가 발생해도 채팅은 정상적으로 저장된다.") + @Test + void createChatAsync() { + // given + String content = "비동기 확인 채팅~"; + ChatCreateRequest chatCreateRequest = new ChatCreateRequest(content); + ChatRoomEntity chatRoom = chatRoomRepository.save( + new ChatRoomEntity(moim.getId(), darakbang.getId(), ChatRoomType.MOIM)); + + // when + when(chatRecipientFinder.getMoimChatNotificationRecipients(anyLong(), any(Author.class))) + .thenThrow(new RuntimeException("삐용12")); + + chatService.createChat(darakbang.getId(), chatRoom.getId(), chatCreateRequest, darakbangAnna); + + // then + List chats = chatRepository.findAll(); + assertThat(chats).hasSize(1); + + ChatEntity chat = chats.get(0); + assertThat(chat.getContent()).isEqualTo(content); + assertThat(chat.getChatRoomId()).isEqualTo(chatRoom.getId()); + } +} diff --git a/backend/src/test/java/mouda/backend/chat/business/ChatRoomServiceTest.java b/backend/src/test/java/mouda/backend/chat/business/ChatRoomServiceTest.java new file mode 100644 index 000000000..acdfe6290 --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/business/ChatRoomServiceTest.java @@ -0,0 +1,163 @@ +package mouda.backend.chat.business; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.bet.entity.BetDarakbangMemberEntity; +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.bet.infrastructure.BetRepository; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.entity.ChatEntity; +import mouda.backend.chat.entity.ChatRoomEntity; +import mouda.backend.chat.entity.ChatType; +import mouda.backend.chat.infrastructure.ChatRepository; +import mouda.backend.chat.infrastructure.ChatRoomRepository; +import mouda.backend.chat.presentation.request.ChatCreateRequest; +import mouda.backend.chat.presentation.response.ChatPreviewResponse; +import mouda.backend.chat.presentation.response.ChatPreviewResponses; +import mouda.backend.common.fixture.BetEntityFixture; +import mouda.backend.common.fixture.ChatRoomEntityFixture; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +public class ChatRoomServiceTest extends DarakbangSetUp { + + @Autowired + ChatService chatService; + + @Autowired + ChatRoomService chatRoomService; + + @Autowired + MoimRepository moimRepository; + + @Autowired + ChamyoRepository chamyoRepository; + + @Autowired + ChatRoomRepository chatRoomRepository; + + @Autowired + ChatRepository chatRepository; + + @Autowired + BetRepository betRepository; + + @Autowired + BetDarakbangMemberRepository betDarakbangMemberRepository; + + @DisplayName("모임 채팅 미리보기를 조회한다.") + @Test + void findMoimChatPreview() { + // given + Moim moim = MoimFixture.getBasketballMoim(darakbang.getId()); + moim.openChat(); + Moim savedMoim = moimRepository.save(moim); + + Chamyo chamyo = chamyoRepository.save(new Chamyo(moim, darakbangHogee, MoimRole.MOIMEE)); + chamyoRepository.save(chamyo); + + ChatRoomEntity chatRoomEntity = ChatRoomEntityFixture.getChatRoomEntityOfMoim(savedMoim.getId(), + darakbang.getId()); + ChatRoomEntity chatRoom = chatRoomRepository.save(chatRoomEntity); + + sendChat(chatRoom); + + // when + ChatPreviewResponses moimChatPreviews = chatRoomService.findChatPreview(darakbangHogee, ChatRoomType.MOIM); + + // then + assertThat(moimChatPreviews.previews()).hasSize(1); + assertThat(moimChatPreviews.previews().get(0).lastContent()).isEqualTo("안녕하세요"); + assertThat(moimChatPreviews.previews().get(0).lastReadChatId()).isEqualTo(0); + assertThat(moimChatPreviews.previews().get(0).participations()).hasSize(1); + } + + @DisplayName("안내면진다 채팅 미리보기를 조회한다.") + @Test + void findBetChatPreview() { + // given + BetEntity betEntity = BetEntityFixture.getDrawedBetEntity(darakbang.getId(), darakbangHogee.getId()); + BetEntity savedBetEntity = betRepository.save(betEntity); + + BetDarakbangMemberEntity betDarakbangMemberEntity = new BetDarakbangMemberEntity(darakbangHogee, + savedBetEntity); + betDarakbangMemberRepository.save(betDarakbangMemberEntity); + + ChatRoomEntity chatRoomEntity = ChatRoomEntityFixture.getChatRoomEntityOfBet(savedBetEntity.getId(), + darakbang.getId()); + ChatRoomEntity chatRoom = chatRoomRepository.save(chatRoomEntity); + + sendChat(chatRoom); + + // when + ChatPreviewResponses betChatPreviews = chatRoomService.findChatPreview(darakbangHogee, ChatRoomType.BET); + + // then + assertThat(betChatPreviews.previews().size()).isEqualTo(1); + assertThat(betChatPreviews.previews().get(0).lastContent()).isEqualTo("안녕하세요"); + assertThat(betChatPreviews.previews().get(0).lastReadChatId()).isEqualTo(0); + assertThat(betChatPreviews.previews().get(0).participations()).hasSize(1); + } + + private void sendChat(ChatRoomEntity savedChatRoom) { + ChatEntity chatEntity = new ChatEntity("안녕하세요", savedChatRoom.getId(), darakbangHogee, LocalDate.now(), + LocalTime.now(), ChatType.BASIC); + chatRepository.save(chatEntity); + } + + @DisplayName("가장 최근에 생성된 채팅을 기준으로 채팅방 목록을 조회하고, 채팅이 없는 채팅방은 가장 아래에 위치한다.") + @Test + void findChatPreview_sortedByLastChatCreatedAt() { + Moim soccerMoim = moimRepository.save(MoimFixture.getSoccerMoim(darakbang.getId())); + Moim coffeeMoim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + Moim basketballMoim = moimRepository.save(MoimFixture.getBasketballMoim(darakbang.getId())); + + chamyoRepository.save(Chamyo.builder() + .moim(soccerMoim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMER) + .build()); + + chamyoRepository.save(Chamyo.builder() + .moim(coffeeMoim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMER) + .build()); + + chamyoRepository.save(Chamyo.builder() + .moim(basketballMoim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMER) + .build()); + + chatRoomService.openChatRoom(darakbang.getId(), soccerMoim.getId(), darakbangAnna); + chatRoomService.openChatRoom(darakbang.getId(), coffeeMoim.getId(), darakbangAnna); + chatRoomService.openChatRoom(darakbang.getId(), basketballMoim.getId(), darakbangAnna); + + chatService.createChat(darakbang.getId(), 1L, new ChatCreateRequest("1번 채팅"), darakbangAnna); + chatService.createChat(darakbang.getId(), 2L, new ChatCreateRequest("2번 채팅"), darakbangAnna); + + List chatPreviewResponses = chatRoomService.findChatPreview(darakbangAnna, + ChatRoomType.MOIM) + .previews(); + + assertThat(chatPreviewResponses).extracting(ChatPreviewResponse::lastContent) + .containsExactly("2번 채팅", "1번 채팅", ""); + } +} diff --git a/backend/src/test/java/mouda/backend/chat/business/ChatServiceTest.java b/backend/src/test/java/mouda/backend/chat/business/ChatServiceTest.java new file mode 100644 index 000000000..fa27b5548 --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/business/ChatServiceTest.java @@ -0,0 +1,313 @@ +package mouda.backend.chat.business; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.bet.infrastructure.BetRepository; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.entity.ChatEntity; +import mouda.backend.chat.entity.ChatRoomEntity; +import mouda.backend.chat.exception.ChatException; +import mouda.backend.chat.infrastructure.ChatRepository; +import mouda.backend.chat.infrastructure.ChatRoomRepository; +import mouda.backend.chat.presentation.request.ChatCreateRequest; +import mouda.backend.chat.presentation.request.DateTimeConfirmRequest; +import mouda.backend.chat.presentation.request.LastReadChatRequest; +import mouda.backend.chat.presentation.request.PlaceConfirmRequest; +import mouda.backend.chat.presentation.response.ChatFindUnloadedResponse; +import mouda.backend.chat.presentation.response.ChatPreviewResponses; +import mouda.backend.common.fixture.BetEntityFixture; +import mouda.backend.common.fixture.ChatEntityFixture; +import mouda.backend.common.fixture.ChatRoomEntityFixture; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.exception.ChamyoException; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +class ChatServiceTest extends DarakbangSetUp { + + @Autowired + ChatService chatService; + + @Autowired + ChatRoomService chatRoomService; + + @Autowired + MoimRepository moimRepository; + + @Autowired + ChamyoRepository chamyoRepository; + + @Autowired + ChatRoomRepository chatRoomRepository; + + @Autowired + ChatRepository chatRepository; + + @Autowired + BetRepository betRepository; + + @Autowired + BetDarakbangMemberRepository betDarakbangMemberRepository; + + @DisplayName("채팅을 생성한다.") + @Test + void createChat() { + // given + Moim moim = MoimFixture.getSoccerMoim(darakbang.getId()); + Moim savedMoim = moimRepository.save(moim); + + chamyoRepository.save(new Chamyo(moim, darakbangHogee, MoimRole.MOIMER)); + + ChatRoomEntity chatRoom = chatRoomRepository.save( + ChatRoomEntityFixture.getChatRoomEntityOfMoim(savedMoim.getId(), darakbang.getId())); + + String content = "아 배고파. 오늘 뭐먹지?"; + ChatCreateRequest chatCreateRequest = new ChatCreateRequest(content); + + // when + chatService.createChat(darakbang.getId(), chatRoom.getId(), chatCreateRequest, darakbangHogee); + + // then + Optional chatOptional = chatRepository.findById(1L); + assertThat(chatOptional.isPresent()).isTrue(); + assertThat(chatOptional.get().getContent()).isEqualTo(content); + } + + @DisplayName("아무런 채팅이 없는 모임인 경우 빈 값을 반환한다.") + @Test + void findUnloadedChatsForFirstTime() { + // given + Moim moim = MoimFixture.getSoccerMoim(darakbang.getId()); + Moim savedMoim = moimRepository.save(moim); + + chamyoRepository.save(new Chamyo(moim, darakbangHogee, MoimRole.MOIMER)); + + ChatRoomEntity chatRoom = chatRoomRepository.save( + ChatRoomEntityFixture.getChatRoomEntityOfMoim(savedMoim.getId(), darakbang.getId())); + + // when + ChatFindUnloadedResponse unloadedChats = chatService.findUnloadedChats(1L, 0L, chatRoom.getId(), + darakbangHogee); + + // then + assertThat(unloadedChats.chats()).hasSize(0); + } + + @DisplayName("장소 확정 채팅에 성공한다.") + @Test + void confirmPlace() { + Moim moim = moimRepository.save(MoimFixture.getSoccerMoim(darakbang.getId())); + chamyoRepository.save(new Chamyo(moim, darakbangHogee, MoimRole.MOIMER)); + ChatRoomEntity chatRoomEntity = chatRoomRepository.save( + ChatRoomEntityFixture.getChatRoomEntityOfMoim(moim.getId(), darakbang.getId())); + + String place = "서울시 용산구 강원대로 127-10"; + PlaceConfirmRequest request = new PlaceConfirmRequest(place); + + // when + chatService.confirmPlace(darakbang.getId(), chatRoomEntity.getId(), request, darakbangHogee); + + // then + Optional chatOptional = chatRepository.findById(1L); + assertThat(chatOptional.isPresent()).isTrue(); + assertThat(chatOptional.get().getContent()).isEqualTo(place); + + Optional moimOptional = moimRepository.findById(moim.getId()); + assertThat(moimOptional.isPresent()).isTrue(); + assertThat(moimOptional.get().getPlace()).isEqualTo(place); + } + + @DisplayName("모임 채팅이 아닌 경우 장소 확정에 실패한다.") + @Test + void failToConfirmPlaceWithBetChat() { + BetEntity betEntity = betRepository.save( + BetEntityFixture.getBetEntity(darakbang.getId(), darakbangHogee.getId())); + + ChatRoomEntity chatRoomEntity = chatRoomRepository.save( + ChatRoomEntityFixture.getChatRoomEntityOfBet(betEntity.getId(), darakbang.getId())); + + String place = "서울시 용산구 강원대로 127-10"; + PlaceConfirmRequest request = new PlaceConfirmRequest(place); + + // when & then + assertThatThrownBy( + () -> chatService.confirmPlace(darakbang.getId(), chatRoomEntity.getId(), request, darakbangHogee)) + .isInstanceOf(ChatException.class) + .hasMessage("잘못된 채팅 방 타입입니다."); + } + + @DisplayName("날짜 확정 채팅에 성공한다.") + @Test + void confirmDateTime() { + Moim moim = moimRepository.save(MoimFixture.getSoccerMoim(darakbang.getId())); + chamyoRepository.save(new Chamyo(moim, darakbangHogee, MoimRole.MOIMER)); + ChatRoomEntity chatRoomEntity = chatRoomRepository.save( + ChatRoomEntityFixture.getChatRoomEntityOfMoim(moim.getId(), darakbang.getId())); + + LocalDate date = LocalDate.now().plusDays(1); + LocalTime time = LocalTime.now(); + DateTimeConfirmRequest request = new DateTimeConfirmRequest(date, time); + + // when + chatService.confirmDateTime(darakbang.getId(), chatRoomEntity.getId(), request, darakbangHogee); + + // then + Optional chatOptional = chatRepository.findById(1L); + assertThat(chatOptional.isPresent()).isTrue(); + assertThat(chatOptional.get().getContent()).isEqualTo(date + " " + time); + + Optional moimOptional = moimRepository.findById(moim.getId()); + assertThat(moimOptional.isPresent()).isTrue(); + assertThat(moimOptional.get().getDate()).isEqualTo(date); + assertThat(moimOptional.get().getTime().getHour()).isEqualTo(time.getHour()); + assertThat(moimOptional.get().getTime().getMinute()).isEqualTo(time.getMinute()); + } + + @DisplayName("모임 채팅이 아닌 경우 날짜 확정에 실패한다.") + @Test + void failToConfirmDateTimeWithBetChat() { + BetEntity betEntity = betRepository.save( + BetEntityFixture.getBetEntity(darakbang.getId(), darakbangHogee.getId())); + + ChatRoomEntity chatRoomEntity = chatRoomRepository.save( + ChatRoomEntityFixture.getChatRoomEntityOfBet(betEntity.getId(), darakbang.getId())); + + LocalDate date = LocalDate.now().plusDays(1); + LocalTime time = LocalTime.now(); + DateTimeConfirmRequest request = new DateTimeConfirmRequest(date, time); + + // when & then + assertThatThrownBy( + () -> chatService.confirmDateTime(darakbang.getId(), chatRoomEntity.getId(), request, darakbangHogee)) + .isInstanceOf(ChatException.class) + .hasMessage("잘못된 채팅 방 타입입니다."); + } + + @DisplayName("작성자가 아닌 회원이 장소를 확정을 요청하면 실패한다.") + @Test + void cannotConfirmPlaceWhenMemberIsNotMoimer() { + // given + Moim moim = MoimFixture.getSoccerMoim(darakbang.getId()); + moimRepository.save(moim); + + chamyoRepository.save(new Chamyo(moim, darakbangHogee, MoimRole.MOIMEE)); + + ChatRoomEntity chatRoomEntity = chatRoomRepository.save( + ChatRoomEntityFixture.getChatRoomEntityOfMoim(moim.getId(), darakbang.getId())); + + String place = "서울시 용산구 강원대로 127-10"; + PlaceConfirmRequest request = new PlaceConfirmRequest(place); + + // when & then + assertThatThrownBy( + () -> chatService.confirmPlace(darakbang.getId(), chatRoomEntity.getId(), request, darakbangHogee)) + .isInstanceOf(ChamyoException.class) + .hasMessage("모이머가 아닙니다."); + } + + @DisplayName("마지막 채팅을 저장한다.") + @Test + void updateLastReadChat() { + // given + Moim moim = MoimFixture.getSoccerMoim(darakbang.getId()); + Moim savedMoim = moimRepository.save(moim); + + Chamyo chamyo = chamyoRepository.save(new Chamyo(moim, darakbangHogee, MoimRole.MOIMEE)); + + LastReadChatRequest request = new LastReadChatRequest(1L); + + ChatRoomEntity chatRoomEntity = new ChatRoomEntity(savedMoim.getId(), darakbang.getId(), ChatRoomType.MOIM); + ChatRoomEntity savedChatRoom = chatRoomRepository.save(chatRoomEntity); + + // when + chatService.updateLastReadChat(darakbang.getId(), savedChatRoom.getId(), request, darakbangHogee); + + // then + Optional optionalChamyo = chamyoRepository.findById(chamyo.getId()); + assertThat(optionalChamyo.isPresent()).isTrue(); + assertThat(optionalChamyo.get().getLastReadChatId()).isEqualTo(1L); + } + + @DisplayName("최근에 조회한 채팅에서 새롭게 생성된 채팅 내역을 반환한다.") + @Test + void findUnloadedChats() { + // given + Moim moim = MoimFixture.getSoccerMoim(darakbang.getId()); + Moim savedMoim = moimRepository.save(moim); + + chamyoRepository.save(new Chamyo(moim, darakbangHogee, MoimRole.MOIMER)); + + ChatRoomEntity chatRoom = chatRoomRepository.save( + ChatRoomEntityFixture.getChatRoomEntityOfMoim(savedMoim.getId(), darakbang.getId())); + + chatRepository.save(ChatEntityFixture.getChatEntity(darakbangHogee)); + chatRepository.save(ChatEntityFixture.getChatEntity(darakbangAnna)); + chatRepository.save(ChatEntityFixture.getChatEntity(darakbangHogee)); + + // when + ChatFindUnloadedResponse unloadedChats = chatService.findUnloadedChats(1L, 1L, chatRoom.getId(), + darakbangHogee); + + // then + assertThat(unloadedChats.chats()).hasSize(2); + assertThat(unloadedChats.chats().get(0).isMyMessage()).isFalse(); + assertThat(unloadedChats.chats().get(0).participation().nickname()).isEqualTo(darakbangAnna.getNickname()); + assertThat(unloadedChats.chats().get(1).isMyMessage()).isTrue(); + assertThat(unloadedChats.chats().get(1).participation().nickname()).isEqualTo(darakbangHogee.getNickname()); + } + + @DisplayName("열린 채팅방이 없다면 빈 리스트를 반환한다.") + @Test + void findChatPreview() { + Moim basketballMoim = MoimFixture.getBasketballMoim(darakbang.getId()); + moimRepository.save(basketballMoim); + + Chamyo chamyo = Chamyo.builder() + .darakbangMember(darakbangHogee) + .moim(basketballMoim) + .moimRole(MoimRole.MOIMEE) + .build(); + chamyoRepository.save(chamyo); + + ChatPreviewResponses chatPreview = chatRoomService.findChatPreview(darakbangHogee, ChatRoomType.MOIM); + assertThat(chatPreview.previews()).isEmpty(); + } + + @DisplayName("다락방별 채팅을 조회한다.") + @Test + void readDarakbangChatPreview() { + Moim darakbangMoim = MoimFixture.getSoccerMoim(darakbang.getId()); + moimRepository.save(darakbangMoim); + chamyoRepository.save(new Chamyo(darakbangMoim, darakbangHogee, MoimRole.MOIMER)); + chatRoomService.openChatRoom(darakbang.getId(), darakbangMoim.getId(), darakbangHogee); + chatRepository.save(ChatEntityFixture.getChatEntity(darakbangHogee)); + + ChatPreviewResponses chatPreview = chatRoomService.findChatPreview(darakbangHogee, ChatRoomType.MOIM); + assertThat(chatPreview.previews()) + .hasSize(1); + + Moim moudaMoim = MoimFixture.getSoccerMoim(mouda.getId()); + moimRepository.save(moudaMoim); + chamyoRepository.save(new Chamyo(moudaMoim, moudaHogee, MoimRole.MOIMER)); + + ChatPreviewResponses emptyChatPreview = chatRoomService.findChatPreview(moudaHogee, ChatRoomType.MOIM); + assertThat(emptyChatPreview.previews()) + .hasSize(0); + } +} diff --git a/backend/src/test/java/mouda/backend/chat/entity/ChatRoomEntityTest.java b/backend/src/test/java/mouda/backend/chat/entity/ChatRoomEntityTest.java new file mode 100644 index 000000000..a3f5cd765 --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/entity/ChatRoomEntityTest.java @@ -0,0 +1,55 @@ +package mouda.backend.chat.entity; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; + +import mouda.backend.chat.infrastructure.ChatRoomRepository; +import mouda.backend.common.fixture.ChatRoomEntityFixture; + +@SpringBootTest +class ChatRoomEntityTest { + + @Autowired + ChatRoomRepository chatRoomRepository; + + @DisplayName("targetId와 type이 같은 ChatRoomEntity는 저장할 수 없다.") + @Test + void failToCreateDuplicatedUnique() { + ChatRoomEntity chatRoom = ChatRoomEntityFixture.getChatRoomEntityOfBet(1L, 1L); + chatRoomRepository.save(chatRoom); + + ChatRoomEntity duplicated = ChatRoomEntityFixture.getChatRoomEntityOfBet(1L, 2L); + + assertThatThrownBy(() -> chatRoomRepository.save(duplicated)) + .isInstanceOf(DataIntegrityViolationException.class); + } + + @DisplayName("targetId이 같아도 type이 다르다면 ChatRoomEntity는 저장할 수 있다.") + @Test + void createWithDifferentType() { + ChatRoomEntity chatRoom1 = ChatRoomEntityFixture.getChatRoomEntityOfBet(1L, 1L); + chatRoomRepository.save(chatRoom1); + + ChatRoomEntity chatRoom2 = ChatRoomEntityFixture.getChatRoomEntityOfMoim(1L, 2L); + chatRoomRepository.save(chatRoom2); + + assertThat(chatRoomRepository.findAll()).hasSize(2); + } + + @DisplayName("type이 같아도 targetId이 다르다면 ChatRoomEntity는 저장할 수 있다.") + @Test + void createWithDifferentTargetId() { + ChatRoomEntity chatRoom1 = ChatRoomEntityFixture.getChatRoomEntityOfBet(1L, 1L); + chatRoomRepository.save(chatRoom1); + + ChatRoomEntity chatRoom2 = ChatRoomEntityFixture.getChatRoomEntityOfBet(2L, 2L); + chatRoomRepository.save(chatRoom2); + + assertThat(chatRoomRepository.findAll()).hasSize(2); + } +} diff --git a/backend/src/test/java/mouda/backend/chat/implement/AttributeManagerRegistryTest.java b/backend/src/test/java/mouda/backend/chat/implement/AttributeManagerRegistryTest.java new file mode 100644 index 000000000..b92e12b0e --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/implement/AttributeManagerRegistryTest.java @@ -0,0 +1,40 @@ +package mouda.backend.chat.implement; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.chat.domain.ChatRoomType; + +@SpringBootTest +class AttributeManagerRegistryTest { + + @Autowired + AttributeManagerRegistry attributeManagerRegistry; + + @Autowired + MoimAttributeManager moimAttributeManager; + + @Autowired + BetAttributeManager betAttributeManager; + + @DisplayName("채팅방 타입에 맞는 매니저를 가져온다.") + @ParameterizedTest + @CsvSource({ + "MOIM", + "BET" + }) + void getManager(ChatRoomType chatRoomType) { + // given + // when + AttributeManager result = attributeManagerRegistry.getManager(chatRoomType); + + // then + assertThat(result.support(chatRoomType)).isTrue(); + } + +} diff --git a/backend/src/test/java/mouda/backend/chat/implement/BetAttributeManagerTest.java b/backend/src/test/java/mouda/backend/chat/implement/BetAttributeManagerTest.java new file mode 100644 index 000000000..b0a3b9041 --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/implement/BetAttributeManagerTest.java @@ -0,0 +1,142 @@ +package mouda.backend.chat.implement; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.LocalDateTime; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import mouda.backend.bet.domain.Bet; +import mouda.backend.bet.domain.BetDetails; +import mouda.backend.bet.entity.BetDarakbangMemberEntity; +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.implement.BetFinder; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.chat.domain.Attributes; +import mouda.backend.chat.domain.BetAttributes; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.exception.ChatException; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +class BetAttributeManagerTest { + + @Mock + private BetFinder betFinder; + + @Mock + private BetDarakbangMemberRepository betDarakbangMemberRepository; + + @InjectMocks + private BetAttributeManager betAttributeManager; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @DisplayName("안내면진다 채팅방을 지원한다.") + @Test + void support_shouldReturnTrueForBetType() { + // given + ChatRoomType chatRoomType = ChatRoomType.BET; + + // when + boolean result = betAttributeManager.support(chatRoomType); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("안내면진다 채팅방이 아닌 채팅방은 지원하지 않는다.") + @Test + void support_shouldReturnFalseForNonBetType() { + // given + ChatRoomType chatRoomType = ChatRoomType.MOIM; + + // when + boolean result = betAttributeManager.support(chatRoomType); + + // then + assertThat(result).isFalse(); + } + + @DisplayName("안내면진다 어트리뷰트를 반환한다.") + @Test + void create_shouldReturnBetAttributes() { + // given + ChatRoom chatRoom = mock(ChatRoom.class); + DarakbangMember darakbangMember = mock(DarakbangMember.class); + Darakbang darakbang = mock(Darakbang.class); + Bet bet = mock(Bet.class); + + when(chatRoom.getTargetId()).thenReturn(1L); + when(darakbangMember.getDarakbang()).thenReturn(darakbang); + when(darakbang.getId()).thenReturn(1L); + + when(betFinder.find(1L, 1L)).thenReturn(bet); + when(bet.getBetDetails()).thenReturn( + new BetDetails(1L, "test bet", LocalDateTime.of(2024, 6, 5, 12, 3), 1L, null)); + when(bet.isLoser(darakbangMember.getId())).thenReturn(true); + when(bet.getId()).thenReturn(1L); + when(bet.getLoserId()).thenReturn(2L); + when(bet.getMoimerId()).thenReturn(1L); + + DarakbangMember loserMember = mock(DarakbangMember.class); + when(loserMember.getNickname()).thenReturn("loserNick"); + when(loserMember.getProfile()).thenReturn("profilePic"); + BetEntity betEntity = BetEntity.builder() + .id(1L) + .bettingTime(LocalDateTime.now()) + .title("test bet") + .moimerId(1L) + .loserDarakbangMemberId(2L) + .darakbangId(2L) + .build(); + when(betDarakbangMemberRepository.findByBetIdAndDarakbangMemberId(1L, 2L)) + .thenReturn(Optional.of(new BetDarakbangMemberEntity(loserMember, betEntity))); + + // when + Attributes attributes = betAttributeManager.create(chatRoom, darakbangMember); + + // then + assertThat(attributes).isInstanceOf(BetAttributes.class); + BetAttributes betAttributes = (BetAttributes)attributes; + assertThat(betAttributes.isLoser()).isTrue(); + assertThat(betAttributes.getBetId()).isEqualTo(1L); + assertThat(betAttributes.getLoser().getNickname()).isEqualTo("loserNick"); + assertThat(betAttributes.getLoser().getProfile()).isEqualTo("profilePic"); + } + + @DisplayName("참여하지 않는 안내면진다의 어트리뷰트를 요청하면 예외를 발생한다.") + @Test + void create_shouldThrowExceptionWhenLoserNotFound() { + // given + ChatRoom chatRoom = mock(ChatRoom.class); + DarakbangMember darakbangMember = mock(DarakbangMember.class); + Darakbang darakbang = mock(Darakbang.class); + Bet bet = mock(Bet.class); + + when(chatRoom.getTargetId()).thenReturn(1L); + when(darakbangMember.getDarakbang()).thenReturn(darakbang); + when(darakbang.getId()).thenReturn(1L); + + when(betFinder.find(1L, 1L)).thenReturn(bet); + when(bet.getLoserId()).thenReturn(2L); + + when(betDarakbangMemberRepository.findByBetIdAndDarakbangMemberId(1L, 2L)) + .thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> betAttributeManager.create(chatRoom, darakbangMember)) + .isInstanceOf(ChatException.class); + } +} diff --git a/backend/src/test/java/mouda/backend/chat/implement/BetChatPreviewManagerTest.java b/backend/src/test/java/mouda/backend/chat/implement/BetChatPreviewManagerTest.java new file mode 100644 index 000000000..2f44c6dd6 --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/implement/BetChatPreviewManagerTest.java @@ -0,0 +1,68 @@ +package mouda.backend.chat.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import mouda.backend.bet.entity.BetDarakbangMemberEntity; +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.bet.infrastructure.BetRepository; +import mouda.backend.chat.domain.ChatPreview; +import mouda.backend.chat.entity.ChatRoomEntity; +import mouda.backend.chat.infrastructure.ChatRoomRepository; +import mouda.backend.common.fixture.BetEntityFixture; +import mouda.backend.common.fixture.ChatRoomEntityFixture; +import mouda.backend.common.fixture.DarakbangSetUp; + +class BetChatPreviewManagerTest extends DarakbangSetUp { + + @Autowired + BetChatPreviewManager betChatPreviewManager; + + @Autowired + BetRepository betRepository; + + @Autowired + BetDarakbangMemberRepository betDarakbangMemberRepository; + + @Autowired + ChatRoomRepository chatRoomRepository; + + @DisplayName("참여한 안내면 진다가 없는 경우 빈 리스트를 반환한다.") + @Test + void createWithoutBet() { + // when + List chatPreviews = betChatPreviewManager.create(darakbangAnna); + + // then + assertThat(chatPreviews).isEmpty(); + } + + @DisplayName("채팅방 목록에서 추첨 후 채팅방이 열린 안내면진다만 조회한다.") + @Test + void create() { + // given + BetEntity betEntity1 = BetEntityFixture.getDrawedBetEntity(darakbangAnna.getId(), 1L); + BetEntity betEntity2 = BetEntityFixture.getBetEntity(darakbangAnna.getId(), 1L); + BetEntity savedBet1 = betRepository.save(betEntity1); + BetEntity savedBet2 = betRepository.save(betEntity2); + betDarakbangMemberRepository.save(new BetDarakbangMemberEntity(darakbangHogee, savedBet1)); + betDarakbangMemberRepository.save(new BetDarakbangMemberEntity(darakbangHogee, savedBet2)); + + ChatRoomEntity chatRoom1 = ChatRoomEntityFixture.getChatRoomEntityOfBet(savedBet1.getId(), darakbang.getId()); + chatRoomRepository.save(chatRoom1); + + // when + List chatPreviews = betChatPreviewManager.create(darakbangHogee); + + // then + assertThat(chatPreviews) + .isNotEmpty() + .hasSize(1); + } +} diff --git a/backend/src/test/java/mouda/backend/chat/implement/BetParticipantResolverTest.java b/backend/src/test/java/mouda/backend/chat/implement/BetParticipantResolverTest.java new file mode 100644 index 000000000..d737bb6a2 --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/implement/BetParticipantResolverTest.java @@ -0,0 +1,64 @@ +package mouda.backend.chat.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.bet.entity.BetDarakbangMemberEntity; +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.bet.infrastructure.BetRepository; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.Participant; +import mouda.backend.chat.entity.ChatRoomEntity; +import mouda.backend.chat.infrastructure.ChatRoomRepository; +import mouda.backend.common.fixture.BetEntityFixture; +import mouda.backend.common.fixture.ChatRoomEntityFixture; +import mouda.backend.common.fixture.DarakbangSetUp; + +@SpringBootTest +class BetParticipantResolverTest extends DarakbangSetUp { + + @Autowired + private BetDarakbangMemberRepository betDarakbangMemberRepository; + + @Autowired + private BetRepository betRepository; + + @Autowired + private BetParticipantResolver betParticipantResolver; + + @Autowired + private ChatRoomRepository chatRoomRepository; + + @DisplayName("모임 참여자 목록을 반환한다.") + @Test + void resolve_shouldReturnParticipantsForGivenChatRoom() { + // given + BetEntity betEntity = BetEntityFixture.getBetEntity(darakbang.getId(), darakbangAnna.getId()); + BetEntity savedBetEntity = betRepository.save(betEntity); + + BetDarakbangMemberEntity annaEntity = new BetDarakbangMemberEntity(darakbangAnna, savedBetEntity); + BetDarakbangMemberEntity hogeeEntity = new BetDarakbangMemberEntity(darakbangHogee, savedBetEntity); + betDarakbangMemberRepository.save(annaEntity); + betDarakbangMemberRepository.save(hogeeEntity); + + ChatRoomEntity chatRoomEntity = ChatRoomEntityFixture.getChatRoomEntityOfBet(betEntity.getId(), + darakbang.getId()); + ChatRoomEntity savedChatRoom = chatRoomRepository.save(chatRoomEntity); + ChatRoom chatRoom = new ChatRoom(savedChatRoom.getId(), savedChatRoom.getTargetId(), savedChatRoom.getType()); + + // when + List participants = betParticipantResolver.resolve(chatRoom); + + // then + assertThat(participants).hasSize(2); + assertThat(participants).extracting("nickname").containsExactlyInAnyOrder("anna", "hogee"); + assertThat(participants).extracting("role").containsExactlyInAnyOrder("MOIMER", "MOIMEE"); + } +} diff --git a/backend/src/test/java/mouda/backend/chat/implement/ChatFinderTest.java b/backend/src/test/java/mouda/backend/chat/implement/ChatFinderTest.java new file mode 100644 index 000000000..b5f439ee3 --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/implement/ChatFinderTest.java @@ -0,0 +1,102 @@ +package mouda.backend.chat.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.domain.Chats; +import mouda.backend.chat.entity.ChatEntity; +import mouda.backend.chat.infrastructure.ChatRepository; +import mouda.backend.chat.infrastructure.ChatRoomRepository; +import mouda.backend.common.fixture.ChatEntityFixture; +import mouda.backend.common.fixture.ChatRoomEntityFixture; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +class ChatFinderTest extends DarakbangSetUp { + + @Autowired + private ChatRoomFinder chatFinder; + + @Autowired + private ChatRepository chatRepository; + + @Autowired + MoimRepository moimRepository; + + @Autowired + private ChatRoomRepository chatRoomRepository; + + @DisplayName("조회하지 않은 채팅을 조회한다.") + @Test + void readAllUnloadedChats() { + Moim moim = MoimFixture.getCoffeeMoim(darakbang.getId()); + moimRepository.save(moim); + + chatRoomRepository.save(ChatRoomEntityFixture.getChatRoomEntityOfMoim(1L, 1L)); + + ChatEntity loadedChat = ChatEntityFixture.getChatEntity(darakbangAnna); + ChatEntity unloadedChat = ChatEntityFixture.getChatEntity(darakbangHogee); + chatRepository.save(loadedChat); + chatRepository.save(unloadedChat); + + Chats chats = chatFinder.findAllUnloadedChats(1L, 1L); + + assertThat(chats.getChats()).isNotEmpty(); + } + + @DisplayName("채팅이 있다면 마지막 채팅의 내용을 조회한다.") + @Test + void readLastChatContent() { + Moim moim = MoimFixture.getCoffeeMoim(darakbang.getId()); + moimRepository.save(moim); + + chatRoomRepository.save(ChatRoomEntityFixture.getChatRoomEntityOfMoim(1L, 1L)); + + ChatEntity chatEntity = ChatEntityFixture.getChatEntity(darakbangHogee); + chatRepository.save(chatEntity); + + ChatRoom chatRoom = chatFinder.readChatRoomByTargetId(moim.getId(), ChatRoomType.MOIM); + + assertThat(chatRoom.getLastChat().getContent()).isEqualTo(chatEntity.getContent()); + } + + @DisplayName("채팅이 없다면 EmptyChat을 반환한다.") + @Test + void readLastChatContentWhenChatAbsent() { + chatRoomRepository.save(ChatRoomEntityFixture.getChatRoomEntityOfMoim(1L, 1L)); + + ChatRoom chatRoom = chatFinder.readChatRoomByTargetId(1L, ChatRoomType.MOIM); + + assertThat(chatRoom.getLastChat().getContent()).isEqualTo(""); + } + + @DisplayName("채팅이 있다면 마지막 채팅의 날짜 시간을 조회한다.") + @Test + void readLastChatDateTime() { + Moim moim = MoimFixture.getCoffeeMoim(darakbang.getId()); + moimRepository.save(moim); + + chatRoomRepository.save(ChatRoomEntityFixture.getChatRoomEntityOfMoim(1L, 1L)); + + ChatEntity chat1 = ChatEntityFixture.getChatEntity(darakbangAnna); + ChatEntity chat2 = ChatEntityFixture.getChatEntity(darakbangHogee); + chatRepository.save(chat1); + chatRepository.save(chat2); + + ChatRoom chatRoom = chatFinder.readChatRoomByTargetId(moim.getId(), ChatRoomType.MOIM); + + assertThat(chatRoom.getLastChatDateTime().withSecond(0).withNano(0)).isEqualTo( + LocalDateTime.of(chat2.getDate(), chat2.getTime()).withSecond(0).withNano(0)); + } +} diff --git a/backend/src/test/java/mouda/backend/chat/implement/ChatRecipientFinderTest.java b/backend/src/test/java/mouda/backend/chat/implement/ChatRecipientFinderTest.java new file mode 100644 index 000000000..979186800 --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/implement/ChatRecipientFinderTest.java @@ -0,0 +1,49 @@ +package mouda.backend.chat.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.chat.domain.Author; +import mouda.backend.chat.implement.notification.ChatRecipientFinder; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.writer.ChamyoWriter; +import mouda.backend.moim.implement.writer.MoimWriter; +import mouda.backend.notification.domain.Recipient; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ChatRecipientFinderTest extends DarakbangSetUp { + + @Autowired + private ChatRecipientFinder chatRecipientFinder; + + @Autowired + private ChamyoWriter chamyoWriter; + + @Autowired + private MoimWriter moimWriter; + + @DisplayName("채팅 알림은 메시지를 보낸 사람을 제외한 모든 참여자를 대상으로 한다.") + @Test + void getMoimChatNotificationRecipients() { + // given + Moim moim = moimWriter.save(MoimFixture.getCoffeeMoim(darakbang.getId()), darakbangAnna); + chamyoWriter.saveAsMoimee(moim, darakbangHogee); + + // when + Author author = Author.builder().memberId(darakbangAnna.getMemberId()).darakbangMemberId(darakbangAnna.getId()) + .profile("").nickname("안나").build(); + List result = chatRecipientFinder.getMoimChatNotificationRecipients(moim.getId(), author); + + // then + assertThat(result).hasSize(1); + assertThat(result).extracting("memberId").containsExactly(darakbangHogee.getMemberId()); + } +} diff --git a/backend/src/test/java/mouda/backend/chat/implement/ChatRoomDetailsFinderTest.java b/backend/src/test/java/mouda/backend/chat/implement/ChatRoomDetailsFinderTest.java new file mode 100644 index 000000000..4db2a2158 --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/implement/ChatRoomDetailsFinderTest.java @@ -0,0 +1,153 @@ +package mouda.backend.chat.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.bet.entity.BetDarakbangMemberEntity; +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.infrastructure.BetDarakbangMemberRepository; +import mouda.backend.bet.infrastructure.BetRepository; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomDetails; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.domain.Participant; +import mouda.backend.chat.entity.ChatRoomEntity; +import mouda.backend.chat.infrastructure.ChatRoomRepository; +import mouda.backend.common.fixture.BetEntityFixture; +import mouda.backend.common.fixture.ChatRoomEntityFixture; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +class ChatRoomDetailsFinderTest extends DarakbangSetUp { + + @Autowired + private ChatRoomDetailsFinder chatRoomDetailsFinder; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private ChamyoRepository chamyoRepository; + + @Autowired + private BetRepository betRepository; + + @Autowired + private BetDarakbangMemberRepository betDarakbangMemberRepository; + + @Autowired + private ChatRoomRepository chatRoomRepository; + + @DisplayName("모임 타입 채팅룸 디테일을 반환한다.") + @Test + void find_moimChatRoomType() { + // given + Moim moim = MoimFixture.getCoffeeMoim(darakbang.getId()); + Moim savedMoim = moimRepository.save(moim); + + Chamyo annaChamyo = Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMER) + .build(); + Chamyo hogeeChamyo = Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangHogee) + .moimRole(MoimRole.MOIMEE) + .build(); + chamyoRepository.save(annaChamyo); + chamyoRepository.save(hogeeChamyo); + + ChatRoomEntity chatRoomEntity = ChatRoomEntityFixture.getChatRoomEntityOfMoim(moim.getId(), darakbang.getId()); + ChatRoomEntity savedChatRoomEntity = chatRoomRepository.save(chatRoomEntity); + ChatRoom chatRoom = new ChatRoom(savedChatRoomEntity.getId(), savedChatRoomEntity.getTargetId(), + savedChatRoomEntity.getType()); + + // when + ChatRoomDetails chatRoomDetails = chatRoomDetailsFinder.find(darakbang.getId(), chatRoom.getId(), + darakbangAnna); + + // then + assertThat(chatRoomDetails.getChatRoomType()).isEqualTo(ChatRoomType.MOIM); + assertThat(chatRoomDetails.getTitle()).isEqualTo("커피 마실 사람?"); + assertThat(chatRoomDetails.getId()).isEqualTo(chatRoom.getId()); + assertThat(chatRoomDetails.getParticipants()).containsExactly( + new Participant(darakbangAnna.getId(), "anna", "profile", "MOIMER"), + new Participant(darakbangHogee.getId(), "hogee", "profile", "MOIMEE")); + assertThat(chatRoomDetails.getAttributes()) + .containsExactlyInAnyOrderEntriesOf(getExpectedMoimAttributes(savedMoim)); + } + + private Map getExpectedMoimAttributes(Moim moim) { + Map attributes = new HashMap<>(); + attributes.put("title", moim.getTitle()); + attributes.put("place", moim.getPlace()); + attributes.put("isMoimer", true); + attributes.put("isStarted", false); + attributes.put("description", moim.getDescription()); + attributes.put("date", moim.getDate()); + attributes.put("time", moim.getTime().withNano(0)); + attributes.put("moimId", 1L); + return attributes; + } + + @DisplayName("안내면진다 채팅룸 디테일을 반환한다.") + @Test + void find_betChatRoomType() { + // given + BetEntity betEntity = BetEntityFixture.getDrawedBetEntity(darakbang.getId(), darakbangAnna.getId()); + BetEntity savedBetEntity = betRepository.save(betEntity); + + BetDarakbangMemberEntity annaEntity = new BetDarakbangMemberEntity(darakbangAnna, savedBetEntity); + BetDarakbangMemberEntity hogeeEntity = new BetDarakbangMemberEntity(darakbangHogee, savedBetEntity); + betDarakbangMemberRepository.save(annaEntity); + betDarakbangMemberRepository.save(hogeeEntity); + + ChatRoomEntity chatRoomEntity = ChatRoomEntityFixture.getChatRoomEntityOfBet(betEntity.getId(), + darakbang.getId()); + ChatRoomEntity savedChatRoomEntity = chatRoomRepository.save(chatRoomEntity); + ChatRoom chatRoom = new ChatRoom(savedChatRoomEntity.getId(), savedChatRoomEntity.getTargetId(), + savedChatRoomEntity.getType()); + + // when + ChatRoomDetails chatRoomDetails = chatRoomDetailsFinder.find(darakbang.getId(), chatRoom.getId(), + darakbangAnna); + + // then + assertThat(chatRoomDetails.getChatRoomType()).isEqualTo(ChatRoomType.BET); + assertThat(chatRoomDetails.getTitle()).isEqualTo("테바바보"); + assertThat(chatRoomDetails.getId()).isEqualTo(chatRoom.getId()); + assertThat(chatRoomDetails.getParticipants()).containsExactly( + new Participant(darakbangAnna.getId(), "anna", "profile", "MOIMER"), + new Participant(darakbangHogee.getId(), "hogee", "profile", "MOIMEE")); + assertThat(chatRoomDetails.getAttributes()) + .containsExactlyInAnyOrderEntriesOf(getExpectedBetAttributes(savedBetEntity)); + } + + private Map getExpectedBetAttributes(BetEntity bet) { + Map attributes = new HashMap<>(); + attributes.put("title", bet.getTitle()); + attributes.put("isLoser", true); + attributes.put("betId", bet.getId()); + attributes.put("loser", new Participant( + darakbangAnna.getId(), + "anna", + "profile", + "MOIMER" + )); + return attributes; + } +} diff --git a/backend/src/test/java/mouda/backend/chat/implement/ChatRoomValidatorTest.java b/backend/src/test/java/mouda/backend/chat/implement/ChatRoomValidatorTest.java new file mode 100644 index 000000000..4e9f2700c --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/implement/ChatRoomValidatorTest.java @@ -0,0 +1,67 @@ +package mouda.backend.chat.implement; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.infrastructure.BetRepository; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.entity.ChatRoomEntity; +import mouda.backend.chat.exception.ChatException; +import mouda.backend.chat.infrastructure.ChatRoomRepository; +import mouda.backend.common.fixture.BetEntityFixture; +import mouda.backend.common.fixture.ChatRoomEntityFixture; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +class ChatRoomValidatorTest extends DarakbangSetUp { + + @Autowired + ChatRoomValidator chatRoomValidator; + + @Autowired + MoimRepository moimRepository; + + @Autowired + BetRepository betRepository; + + @Autowired + ChatRoomRepository chatRoomRepository; + + @DisplayName("이미 존재하는 채팅방인지 검증한다. - ChatRoomType.MOIM") + @Test + void existedChatRoom_typeMoim() { + // given + Moim moim = MoimFixture.getCoffeeMoim(darakbang.getId()); + Moim savedMoim = moimRepository.save(moim); + + ChatRoomEntity chatRoomEntityOfMoim = ChatRoomEntityFixture.getChatRoomEntityOfMoim(savedMoim.getId(), darakbang.getId()); + chatRoomRepository.save(chatRoomEntityOfMoim); + + // when & then + assertThatThrownBy(() -> chatRoomValidator.validateAlreadyExists(savedMoim.getId(), ChatRoomType.MOIM)) + .isInstanceOf(ChatException.class); + } + + @DisplayName("이미 존재하는 채팅방인지 검증한다. - ChatRoomType.BET") + @Test + void existedChatRoom_typeBet() { + // given + BetEntity betEntity = BetEntityFixture.getBetEntity(darakbang.getId(), darakbangAnna.getId()); + BetEntity savedBet = betRepository.save(betEntity); + + ChatRoomEntity chatRoomEntityOfBet = ChatRoomEntityFixture.getChatRoomEntityOfBet(savedBet.getId(), darakbang.getId()); + chatRoomRepository.save(chatRoomEntityOfBet); + + // when & then + assertThatThrownBy(() -> chatRoomValidator.validateAlreadyExists(savedBet.getId(), ChatRoomType.BET)) + .isInstanceOf(ChatException.class); + } +} diff --git a/backend/src/test/java/mouda/backend/chat/implement/ChatRoomWriterTest.java b/backend/src/test/java/mouda/backend/chat/implement/ChatRoomWriterTest.java new file mode 100644 index 000000000..893683574 --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/implement/ChatRoomWriterTest.java @@ -0,0 +1,79 @@ +package mouda.backend.chat.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import mouda.backend.bet.entity.BetEntity; +import mouda.backend.bet.infrastructure.BetRepository; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.entity.ChatRoomEntity; +import mouda.backend.chat.exception.ChatException; +import mouda.backend.chat.infrastructure.ChatRoomRepository; +import mouda.backend.common.fixture.BetEntityFixture; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.infrastructure.MoimRepository; + +class ChatRoomWriterTest extends DarakbangSetUp { + + @Autowired + ChatRoomWriter chatRoomWriter; + + @Autowired + MoimRepository moimRepository; + + @Autowired + BetRepository betRepository; + + @Autowired + ChatRoomRepository chatRoomRepository; + + @DisplayName("새로운 채팅방을 생성한다.") + @Test + void append() { + // given + Moim moim = MoimFixture.getCoffeeMoim(darakbang.getId()); + Moim savedMoim = moimRepository.save(moim); + + // when + chatRoomWriter.append(savedMoim.getId(), darakbang.getId(), ChatRoomType.MOIM); + + //then + Optional chatRoomEntity = chatRoomRepository.findByTargetIdAndType(savedMoim.getId(), ChatRoomType.MOIM); + assertThat(chatRoomEntity.isPresent()).isTrue(); + assertThat(chatRoomEntity.get().getTargetId()).isEqualTo(savedMoim.getId()); + assertThat(chatRoomEntity.get().getType()).isEqualTo(ChatRoomType.MOIM); + } + + @DisplayName("이미 존재하는 모임 채팅방일 땐 예외를 발생한다.") + @Test + void append_alreadyExistedMoimChatRoom() { + // given + Moim moim = MoimFixture.getCoffeeMoim(darakbang.getId()); + Moim savedMoim = moimRepository.save(moim); + chatRoomWriter.append(savedMoim.getId(), darakbang.getId(), ChatRoomType.MOIM); + + // when & then + assertThatThrownBy(() -> chatRoomWriter.append(savedMoim.getId(), darakbang.getId(), ChatRoomType.MOIM)) + .isInstanceOf(ChatException.class); + } + + @DisplayName("이미 존재하는 배팅 채팅방일 땐 예외를 발생한다.") + @Test + void append_alreadyExistedBetChatRoom() { + // given + BetEntity betEntity = BetEntityFixture.getBetEntity(darakbang.getId(), darakbangAnna.getId()); + BetEntity savedBet = betRepository.save(betEntity); + chatRoomWriter.append(savedBet.getId(), darakbang.getId(), ChatRoomType.BET); + + // when & then + assertThatThrownBy(() -> chatRoomWriter.append(savedBet.getId(), darakbang.getId(), ChatRoomType.BET)) + .isInstanceOf(ChatException.class); + } +} diff --git a/backend/src/test/java/mouda/backend/chat/implement/MoimAttributeManagerTest.java b/backend/src/test/java/mouda/backend/chat/implement/MoimAttributeManagerTest.java new file mode 100644 index 000000000..3a8f6ca78 --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/implement/MoimAttributeManagerTest.java @@ -0,0 +1,107 @@ +package mouda.backend.chat.implement; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.time.LocalDate; +import java.time.LocalTime; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import mouda.backend.chat.domain.Attributes; +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.domain.MoimAttributes; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.implement.finder.ChamyoFinder; +import mouda.backend.moim.implement.finder.MoimFinder; + +class MoimAttributeManagerTest { + + @Mock + private MoimFinder moimFinder; + + @Mock + private ChamyoFinder chamyoFinder; + + @InjectMocks + private MoimAttributeManager moimAttributeManager; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @DisplayName("모임 타입 채팅방을 지원한다.") + @Test + void support_shouldReturnTrueForMoimType() { + // given + ChatRoomType chatRoomType = ChatRoomType.MOIM; + + // when + boolean result = moimAttributeManager.support(chatRoomType); + + // then + assertThat(result).isTrue(); + } + + @DisplayName("모임 타입 이외의 채팅방을 지원하지 않는다.") + @Test + void support_shouldReturnFalseForNonMoimType() { + // given + ChatRoomType chatRoomType = ChatRoomType.BET; + + // when + boolean result = moimAttributeManager.support(chatRoomType); + + // then + assertThat(result).isFalse(); + } + + @DisplayName("모임 어트리뷰트를 생성한다가.") + @Test + void create_shouldReturnMoimAttributes() { + // given + ChatRoom chatRoom = mock(ChatRoom.class); + DarakbangMember darakbangMember = mock(DarakbangMember.class); + Moim moim = mock(Moim.class); + Chamyo chamyo = mock(Chamyo.class); + + when(chatRoom.getTargetId()).thenReturn(1L); + when(darakbangMember.getDarakbang()).thenReturn(mock(Darakbang.class)); + when(darakbangMember.getDarakbang().getId()).thenReturn(1L); + + when(moimFinder.read(1L, 1L)).thenReturn(moim); + when(chamyoFinder.read(moim, darakbangMember)).thenReturn(chamyo); + when(moim.getPlace()).thenReturn("우테코 잠실캠"); + when(moim.isPastMoim()).thenReturn(false); + when(moim.getDescription()).thenReturn("설명"); + when(moim.getDate()).thenReturn(LocalDate.of(3333, 1, 1)); + when(moim.getTime()).thenReturn(LocalTime.of(12, 12, 7, 13)); + when(moim.getId()).thenReturn(12L); + when(chamyo.getMoimRole()).thenReturn(MoimRole.MOIMER); + + // when + Attributes attributes = moimAttributeManager.create(chatRoom, darakbangMember); + + // then + assertThat(attributes).isInstanceOf(MoimAttributes.class); + MoimAttributes moimAttributes = (MoimAttributes)attributes; + assertThat(moimAttributes.getPlace()).isEqualTo("우테코 잠실캠"); + assertThat(moimAttributes.getIsMoimer()).isTrue(); + assertThat(moimAttributes.getIsStarted()).isFalse(); + assertThat(moimAttributes.getDescription()).isEqualTo("설명"); + assertThat(moimAttributes.getDate()).isEqualTo("3333-01-01"); + assertThat(moimAttributes.getTime()).isEqualTo("12:12:07"); + assertThat(moimAttributes.getMoimId()).isEqualTo(12L); + } +} diff --git a/backend/src/test/java/mouda/backend/chat/implement/MoimChatPreviewManagerTest.java b/backend/src/test/java/mouda/backend/chat/implement/MoimChatPreviewManagerTest.java new file mode 100644 index 000000000..a532c94e3 --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/implement/MoimChatPreviewManagerTest.java @@ -0,0 +1,72 @@ +package mouda.backend.chat.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import mouda.backend.chat.domain.ChatPreview; +import mouda.backend.chat.entity.ChatRoomEntity; +import mouda.backend.chat.infrastructure.ChatRoomRepository; +import mouda.backend.common.fixture.ChatRoomEntityFixture; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.MoimRepository; + +class MoimChatPreviewManagerTest extends DarakbangSetUp { + + @Autowired + MoimChatPreviewManager moimChatPreviewManager; + + @Autowired + MoimRepository moimRepository; + + @Autowired + ChamyoRepository chamyoRepository; + + @Autowired + ChatRoomRepository chatRoomRepository; + + @DisplayName("채팅방이 생성된 모임이 없는 경우 빈 리스트를 반환한다.") + @Test + void createWithoutBet() { + // when + List chatPreviews = moimChatPreviewManager.create(darakbangAnna); + + // then + assertThat(chatPreviews).isEmpty(); + } + + @DisplayName("모임 채팅방 목록을 조회한다.") + @Test + void create() { + // given + Moim moim1 = MoimFixture.getBasketballMoim(darakbang.getId()); + moim1.openChat(); + Moim moim2 = MoimFixture.getCoffeeMoim(darakbang.getId()); + moimRepository.save(moim1); + moimRepository.save(moim2); + chamyoRepository.save(new Chamyo(moim1, darakbangAnna, MoimRole.MOIMER)); + chamyoRepository.save(new Chamyo(moim2, darakbangAnna, MoimRole.MOIMER)); + + ChatRoomEntity chatRoom1 = ChatRoomEntityFixture.getChatRoomEntityOfMoim(moim1.getId(), darakbang.getId()); + ChatRoomEntity chatRoom2 = ChatRoomEntityFixture.getChatRoomEntityOfMoim(moim2.getId(), darakbang.getId()); + chatRoomRepository.save(chatRoom1); + chatRoomRepository.save(chatRoom2); + + // when + List chatPreviews = moimChatPreviewManager.create(darakbangAnna); + + // then + assertThat(chatPreviews) + .isNotEmpty() + .hasSize(1); + } +} diff --git a/backend/src/test/java/mouda/backend/chat/implement/MoimParticipantResolverTest.java b/backend/src/test/java/mouda/backend/chat/implement/MoimParticipantResolverTest.java new file mode 100644 index 000000000..2833892fe --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/implement/MoimParticipantResolverTest.java @@ -0,0 +1,73 @@ +package mouda.backend.chat.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.chat.domain.ChatRoom; +import mouda.backend.chat.domain.Participant; +import mouda.backend.chat.entity.ChatRoomEntity; +import mouda.backend.chat.infrastructure.ChatRoomRepository; +import mouda.backend.common.fixture.ChatRoomEntityFixture; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +class MoimParticipantResolverTest extends DarakbangSetUp { + + @Autowired + private ChamyoRepository chamyoRepository; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private MoimParticipantResolver moimParticipantResolver; + + @Autowired + private ChatRoomRepository chatRoomRepository; + + @DisplayName("모임 참여자 목록을 반환한다.") + @Test + void resolve_shouldReturnParticipantsForGivenChatRoom() { + // given + Moim moim = MoimFixture.getCoffeeMoim(darakbang.getId()); + Moim savedMoim = moimRepository.save(moim); + + Chamyo annaChamyo = Chamyo.builder() + .moim(savedMoim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMER) + .build(); + Chamyo hogeeChamyo = Chamyo.builder() + .moim(savedMoim) + .darakbangMember(darakbangHogee) + .moimRole(MoimRole.MOIMEE) + .build(); + chamyoRepository.save(annaChamyo); + chamyoRepository.save(hogeeChamyo); + + ChatRoomEntity chatRoomEntity = ChatRoomEntityFixture.getChatRoomEntityOfMoim(moim.getId(), darakbang.getId()); + ChatRoomEntity savedChatRoomEntity = chatRoomRepository.save(chatRoomEntity); + ChatRoom chatRoom = new ChatRoom(savedChatRoomEntity.getId(), savedChatRoomEntity.getTargetId(), + savedChatRoomEntity.getType()); + + // when + List participants = moimParticipantResolver.resolve(chatRoom); + + // then + assertThat(participants).hasSize(2); + assertThat(participants).extracting("nickname").containsExactlyInAnyOrder("anna", "hogee"); + assertThat(participants).extracting("role").containsExactlyInAnyOrder("MOIMER", "MOIMEE"); + } +} diff --git a/backend/src/test/java/mouda/backend/chat/implement/ParticipantResolverRegistryTest.java b/backend/src/test/java/mouda/backend/chat/implement/ParticipantResolverRegistryTest.java new file mode 100644 index 000000000..d30077a47 --- /dev/null +++ b/backend/src/test/java/mouda/backend/chat/implement/ParticipantResolverRegistryTest.java @@ -0,0 +1,72 @@ +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.exception.ChatException; +import mouda.backend.chat.implement.ParticipantResolverRegistry; +import mouda.backend.chat.implement.ParticipantsResolver; + +class ParticipantResolverRegistryTest { + + @Mock + private ParticipantsResolver mockResolver1; + + @Mock + private ParticipantsResolver mockResolver2; + + @InjectMocks + private ParticipantResolverRegistry participantResolverRegistry; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @DisplayName("MOIM 타입 리졸버를 반환한다.") + @Test + void getResolver_shouldReturnCorrectResolverForSupportedChatRoomType() { + // given + ChatRoomType chatRoomType = ChatRoomType.MOIM; + + // when + when(mockResolver1.support(chatRoomType)).thenReturn(false); + when(mockResolver2.support(chatRoomType)).thenReturn(true); + + List resolvers = Arrays.asList(mockResolver1, mockResolver2); + participantResolverRegistry = new ParticipantResolverRegistry(resolvers); + + // when + ParticipantsResolver resolver = participantResolverRegistry.getResolver(chatRoomType); + + // then + assertThat(resolver).isEqualTo(mockResolver2); + } + + @DisplayName("BET 타입 리졸버를 반환한다.") + @Test + void getResolver_shouldThrowExceptionForUnsupportedChatRoomType() { + // given + ChatRoomType chatRoomType = ChatRoomType.BET; + + // when + when(mockResolver1.support(chatRoomType)).thenReturn(false); + when(mockResolver2.support(chatRoomType)).thenReturn(false); + + List resolvers = Arrays.asList(mockResolver1, mockResolver2); + participantResolverRegistry = new ParticipantResolverRegistry(resolvers); + + // then + assertThatThrownBy(() -> participantResolverRegistry.getResolver(chatRoomType)) + .isInstanceOf(ChatException.class); + } +} diff --git a/backend/src/test/java/mouda/backend/common/config/ChamyoCreator.java b/backend/src/test/java/mouda/backend/common/config/ChamyoCreator.java new file mode 100644 index 000000000..fcab37619 --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/config/ChamyoCreator.java @@ -0,0 +1,46 @@ +package mouda.backend.common.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import jakarta.persistence.EntityManager; + +@Component +public class ChamyoCreator { + + @Autowired + private EntityManager entityManager; + + @Transactional + public void setUpTwoPastThreeUpcomingMoim() { + entityManager.createNativeQuery( + "INSERT INTO Moim (title, date, time, place, max_people, description, moim_status, is_chat_opened) VALUES " + + "('과거 축구 모임', DATEADD('DAY', -1, CURRENT_DATE), CURRENT_TIME(), '과거 축구 장소', 10, '축구추구', 'COMPLETED', false), " + + "('과거 농구 모임', DATEADD('DAY', -5, CURRENT_DATE), CURRENT_TIME(), '과거 농구 장소', 15, '농구농구', 'CANCELED', true);" + ).executeUpdate(); + + entityManager.createNativeQuery( + "INSERT INTO Moim (title, date, time, place, max_people, description, moim_status, is_chat_opened) VALUES " + + "('미래 축구 모임', DATEADD('DAY', 5, CURRENT_DATE), CURRENT_TIME(), '축구 장소', 20, '많이 오세요', 'MOIMING', true), " + + "('미래 농구 모임', DATEADD('DAY', 10, CURRENT_DATE), CURRENT_TIME(), '농구 장소', 25, '많이 오세요', 'MOIMING', false), " + + "('미래 커피 모임', DATEADD('DAY', 15, CURRENT_DATE), CURRENT_TIME(), '커피 장소', 30, '많이 오세요', 'MOIMING', true);" + ).executeUpdate(); + + entityManager.createNativeQuery("INSERT INTO MEMBER (nickname) VALUES ('테바')").executeUpdate(); + + entityManager.createNativeQuery("INSERT INTO CHAMYO (member_id, moim_id, moim_role, last_read_chat_id) VALUES " + + "(1, 1, 'MOIMEE', 0), " + + "(1, 2, 'MOIMEE', 0), " + + "(1, 3, 'MOIMEE', 0), " + + "(1, 4, 'MOIMEE', 0), " + + "(1, 5, 'MOIMEE', 0)").executeUpdate(); + + entityManager.createNativeQuery("INSERT INTO ZZIM (member_id, moim_id) VALUES " + + "(1, 1), " + + "(1, 3), " + + "(1, 5)").executeUpdate(); + + entityManager.clear(); + } +} diff --git a/backend/src/test/java/mouda/backend/common/config/DatabaseCleaner.java b/backend/src/test/java/mouda/backend/common/config/DatabaseCleaner.java new file mode 100644 index 000000000..78d5d838b --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/config/DatabaseCleaner.java @@ -0,0 +1,39 @@ +package mouda.backend.common.config; + +import java.util.List; + +import org.springframework.context.ApplicationContext; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.transaction.support.TransactionTemplate; + +import jakarta.persistence.EntityManager; + +public abstract class DatabaseCleaner { + public static void clear(ApplicationContext applicationContext) { + var entityManager = applicationContext.getBean(EntityManager.class); + var jdbcTemplate = applicationContext.getBean(JdbcTemplate.class); + var transactionTemplate = applicationContext.getBean(TransactionTemplate.class); + + transactionTemplate.execute(status -> { + entityManager.clear(); + deleteAll(jdbcTemplate, entityManager); + return null; + }); + } + + private static void deleteAll(JdbcTemplate jdbcTemplate, EntityManager entityManager) { + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); + for (String tableName : findDatabaseTableNames(jdbcTemplate)) { + entityManager.createNativeQuery("DELETE FROM %s".formatted(tableName)).executeUpdate(); + entityManager.createNativeQuery("ALTER TABLE %s alter column id restart with 1".formatted(tableName)) + .executeUpdate(); + } + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); + } + + private static List findDatabaseTableNames(JdbcTemplate jdbcTemplate) { + return jdbcTemplate + .query("SHOW TABLES", (rs, rowNum) -> rs.getString(1)) + .stream().toList(); + } +} diff --git a/backend/src/test/java/mouda/backend/common/config/NoTransactionExtension.java b/backend/src/test/java/mouda/backend/common/config/NoTransactionExtension.java new file mode 100644 index 000000000..3741e42b7 --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/config/NoTransactionExtension.java @@ -0,0 +1,51 @@ +package mouda.backend.common.config; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.test.context.TestContextAnnotationUtils; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class NoTransactionExtension implements BeforeEachCallback { + + @Override + public void beforeEach(ExtensionContext extensionContext) { + var applicationContext = SpringExtension.getApplicationContext(extensionContext); + + validateTransactionalAnnotationExists(extensionContext); + cleanDatabase(applicationContext); + } + + private static void validateTransactionalAnnotationExists(ExtensionContext extensionContext) { + if (TestContextAnnotationUtils.hasAnnotation(extensionContext.getRequiredTestClass(), WithTransactionalTest.class)) { + return; + } + + if (TestContextAnnotationUtils.hasAnnotation(extensionContext.getRequiredTestClass(), Transactional.class) || + TestContextAnnotationUtils.hasAnnotation(extensionContext.getRequiredTestClass(), + jakarta.transaction.Transactional.class)) { + Assertions.fail("테스트 클래스에 @Transactional 또는 @jakarta.transaction.Transactional 어노테이션이 존재합니다."); + } + + if (AnnotatedElementUtils.hasAnnotation(extensionContext.getRequiredTestMethod(), Transactional.class) || + AnnotatedElementUtils.hasAnnotation(extensionContext.getRequiredTestMethod(), + jakarta.transaction.Transactional.class)) { + Assertions.fail("테스트 메서드에 @Transactional 또는 @jakarta.transaction.Transactional 어노테이션이 존재합니다."); + } + } + + private static void cleanDatabase(ApplicationContext applicationContext) { + try { + DatabaseCleaner.clear(applicationContext); + } catch (NoSuchBeanDefinitionException e) { + log.debug("Database Cleaning not supported."); + } + } +} diff --git a/backend/src/test/java/mouda/backend/common/config/WithTransactionalTest.java b/backend/src/test/java/mouda/backend/common/config/WithTransactionalTest.java new file mode 100644 index 000000000..fc5ebfdea --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/config/WithTransactionalTest.java @@ -0,0 +1,11 @@ +package mouda.backend.common.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface WithTransactionalTest { +} diff --git a/backend/src/test/java/mouda/backend/common/config/data/RoutingDataSourceTest.java b/backend/src/test/java/mouda/backend/common/config/data/RoutingDataSourceTest.java new file mode 100644 index 000000000..21f07d6b5 --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/config/data/RoutingDataSourceTest.java @@ -0,0 +1,58 @@ +package mouda.backend.common.config.data; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import java.util.HashMap; +import java.util.Map; +import javax.sql.DataSource; +import mouda.backend.common.config.WithTransactionalTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@WithTransactionalTest +class RoutingDataSourceTest { + + private RoutingDataSource routingDataSource; + + @BeforeEach + void setUp() { + DataSource masterDataSource = mock(DataSource.class); + DataSource slaveDataSource = mock(DataSource.class); + + Map dataSources = new HashMap<>(); + dataSources.put(DataSourceConfiguration.MASTER_DATA_SOURCE, masterDataSource); + dataSources.put(DataSourceConfiguration.SLAVE_DATA_SOURCE, slaveDataSource); + + routingDataSource = new RoutingDataSource(); + routingDataSource.setTargetDataSources(dataSources); + routingDataSource.setDefaultTargetDataSource(masterDataSource); + } + + @DisplayName("@Transactional이 readOnly가 아니면 Master DataSource로 라우팅한다.") + @Test + @Transactional + void routeToMasterDataSource() { + Object lookupKey = routingDataSource.determineCurrentLookupKey(); + assertThat(lookupKey).isEqualTo(DataSourceConfiguration.MASTER_DATA_SOURCE); + } + + @DisplayName("@Transactional이 readOnly 이면 Slave DataSource로 라우팅한다.") + @Test + @Transactional(readOnly = true) + void routeToSlaveDataSource() { + Object lookupKey = routingDataSource.determineCurrentLookupKey(); + assertThat(lookupKey).isEqualTo(DataSourceConfiguration.SLAVE_DATA_SOURCE); + } + + @DisplayName("@Transactional이 없는 경우 기본적으로 Master DataSource로 라우팅한다.") + @Test + void routeToMasterDataSource_WhenNoTransactionalExist() { + Object lookupKey = routingDataSource.determineCurrentLookupKey(); + assertThat(lookupKey).isEqualTo(DataSourceConfiguration.MASTER_DATA_SOURCE); + } +} diff --git a/backend/src/test/java/mouda/backend/common/fixture/BetEntityFixture.java b/backend/src/test/java/mouda/backend/common/fixture/BetEntityFixture.java new file mode 100644 index 000000000..a91ba7cfd --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/fixture/BetEntityFixture.java @@ -0,0 +1,63 @@ +package mouda.backend.common.fixture; + +import java.time.LocalDateTime; + +import mouda.backend.bet.entity.BetEntity; + +public class BetEntityFixture { + + public static BetEntity getBetEntity(long darakbangId, long moimerId) { + return BetEntity.builder() + .title("testBet") + .bettingTime(LocalDateTime.now() + .withSecond(0) + .withNano(0)) + .darakbangId(darakbangId) + .moimerId(moimerId) + .build(); + } + + public static BetEntity getFutureBetEntity(long darakbangId, long moimerId) { + return BetEntity.builder() + .title("testBet") + .bettingTime(LocalDateTime.now() + .plusMinutes(10) + .withSecond(0) + .withNano(0)) + .darakbangId(darakbangId) + .moimerId(moimerId) + .build(); + } + + public static BetEntity getBetEntity(String title, long darakbangId, long moimerId) { + return BetEntity.builder() + .title(title) + .bettingTime(LocalDateTime.now() + .withSecond(0) + .withNano(0)) + .darakbangId(darakbangId) + .moimerId(moimerId) + .build(); + } + + public static BetEntity getBetEntity(long darakbangId, long moimerId, LocalDateTime bettingTime) { + return BetEntity.builder() + .title("테바바보") + .bettingTime(bettingTime.withSecond(0).withNano(0)) + .darakbangId(darakbangId) + .moimerId(moimerId) + .build(); + } + + public static BetEntity getDrawedBetEntity(long darakbangId, long moimerId) { + return BetEntity.builder() + .title("테바바보") + .bettingTime(LocalDateTime.now() + .withSecond(0) + .withNano(0)) + .darakbangId(darakbangId) + .moimerId(moimerId) + .loserDarakbangMemberId(moimerId) + .build(); + } +} diff --git a/backend/src/test/java/mouda/backend/common/fixture/BetFixture.java b/backend/src/test/java/mouda/backend/common/fixture/BetFixture.java new file mode 100644 index 000000000..7444527e5 --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/fixture/BetFixture.java @@ -0,0 +1,26 @@ +package mouda.backend.common.fixture; + +import java.time.LocalDateTime; +import java.util.Arrays; + +import mouda.backend.bet.domain.Bet; +import mouda.backend.bet.domain.BetDetails; +import mouda.backend.bet.domain.Participant; + +public class BetFixture { + + public static Bet createBet(Long id, LocalDateTime bettingTime, Long loserId) { + BetDetails betDetails = BetDetails.builder() + .id(id) + .title("Bet " + id) + .bettingTime(bettingTime) + .build(); + + return Bet.builder() + .betDetails(betDetails) + .participants(Arrays.asList(new Participant(1L, "테바", "profile"), new Participant(2L, "테니", "profile"))) + .moimerId(1L) + .loserId(loserId) + .build(); + } +} diff --git a/backend/src/test/java/mouda/backend/common/fixture/ChatEntityFixture.java b/backend/src/test/java/mouda/backend/common/fixture/ChatEntityFixture.java new file mode 100644 index 000000000..a034a611a --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/fixture/ChatEntityFixture.java @@ -0,0 +1,22 @@ +package mouda.backend.common.fixture; + +import java.time.LocalDate; +import java.time.LocalTime; + +import mouda.backend.chat.entity.ChatEntity; +import mouda.backend.chat.entity.ChatType; +import mouda.backend.darakbangmember.domain.DarakbangMember; + +public class ChatEntityFixture { + + public static ChatEntity getChatEntity(DarakbangMember darakbangMember) { + return ChatEntity.builder() + .content("배고파요") + .chatRoomId(1L) + .darakbangMember(darakbangMember) + .date(LocalDate.now()) + .time(LocalTime.now()) + .chatType(ChatType.BASIC) + .build(); + } +} diff --git a/backend/src/test/java/mouda/backend/common/fixture/ChatRoomEntityFixture.java b/backend/src/test/java/mouda/backend/common/fixture/ChatRoomEntityFixture.java new file mode 100644 index 000000000..415b5b218 --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/fixture/ChatRoomEntityFixture.java @@ -0,0 +1,23 @@ +package mouda.backend.common.fixture; + +import mouda.backend.chat.domain.ChatRoomType; +import mouda.backend.chat.entity.ChatRoomEntity; + +public class ChatRoomEntityFixture { + + public static ChatRoomEntity getChatRoomEntityOfMoim(long moimId, long darakbangId) { + return ChatRoomEntity.builder() + .targetId(moimId) + .darakbangId(darakbangId) + .type(ChatRoomType.MOIM) + .build(); + } + + public static ChatRoomEntity getChatRoomEntityOfBet(long betId, long darakbangId) { + return ChatRoomEntity.builder() + .targetId(betId) + .darakbangId(darakbangId) + .type(ChatRoomType.BET) + .build(); + } +} diff --git a/backend/src/test/java/mouda/backend/common/fixture/CommentFixture.java b/backend/src/test/java/mouda/backend/common/fixture/CommentFixture.java new file mode 100644 index 000000000..6526a9b25 --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/fixture/CommentFixture.java @@ -0,0 +1,30 @@ +package mouda.backend.common.fixture; + +import java.time.LocalDateTime; + +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; + +public class CommentFixture { + + public static Comment getCommentWithAnnaAtSoccerMoim(DarakbangMember darakbangMember, Moim moim) { + return Comment.builder() + .content("그냥 댓글") + .moim(moim) + .darakbangMember(darakbangMember) + .createdAt(LocalDateTime.now()) + .parentId(null) + .build(); + } + + public static Comment getChildCommentWithAnnaAtSoccerMoim(DarakbangMember darakbangMember, Moim moim) { + return Comment.builder() + .content("그냥 자식 댓글") + .moim(moim) + .darakbangMember(darakbangMember) + .createdAt(LocalDateTime.now()) + .parentId(1L) + .build(); + } +} diff --git a/backend/src/test/java/mouda/backend/common/fixture/DarakbangFixture.java b/backend/src/test/java/mouda/backend/common/fixture/DarakbangFixture.java new file mode 100644 index 000000000..299e6fd60 --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/fixture/DarakbangFixture.java @@ -0,0 +1,20 @@ +package mouda.backend.common.fixture; + +import mouda.backend.darakbang.domain.Darakbang; + +public class DarakbangFixture { + + public static Darakbang getDarakbangWithWooteco() { + return Darakbang.builder() + .name("우아한테크코스") + .code("SOFABAC") + .build(); + } + + public static Darakbang getDarakbangWithMouda() { + return Darakbang.builder() + .name("모우다") + .code("SOFABAB") + .build(); + } +} diff --git a/backend/src/test/java/mouda/backend/common/fixture/DarakbangMemberFixture.java b/backend/src/test/java/mouda/backend/common/fixture/DarakbangMemberFixture.java new file mode 100644 index 000000000..f85033939 --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/fixture/DarakbangMemberFixture.java @@ -0,0 +1,53 @@ +package mouda.backend.common.fixture; + +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbangmember.domain.DarakBangMemberRole; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.member.domain.Member; + +public class DarakbangMemberFixture { + + public static DarakbangMember getDarakbangManagerWithWooteco(Darakbang darakbang, Member member) { + return DarakbangMember.builder() + .darakbang(darakbang) + .memberId(member.getId()) + .nickname("호호기기") + .profile("profile") + .description("description") + .role(DarakBangMemberRole.MANAGER) + .build(); + } + + public static DarakbangMember getDarakbangMemberWithWooteco(Darakbang darakbang, Member member) { + return DarakbangMember.builder() + .darakbang(darakbang) + .memberId(member.getId()) + .nickname(member.getName()) + .profile("profile") + .description("description") + .role(DarakBangMemberRole.MEMBER) + .build(); + } + + public static DarakbangMember getDarakbangMemberWithWooteco(Member member) { + return DarakbangMember.builder() + .darakbang(DarakbangFixture.getDarakbangWithMouda()) + .memberId(member.getId()) + .nickname(member.getName()) + .profile("profile") + .description("description") + .role(DarakBangMemberRole.MEMBER) + .build(); + } + + public static DarakbangMember getDarakbangOutsiderWithWooteco(Darakbang darakbang, Member member) { + return DarakbangMember.builder() + .darakbang(darakbang) + .memberId(member.getId()) + .nickname("치코치코니") + .profile("profile") + .description("description") + .role(DarakBangMemberRole.OUTSIDER) + .build(); + } +} diff --git a/backend/src/test/java/mouda/backend/common/fixture/DarakbangSetUp.java b/backend/src/test/java/mouda/backend/common/fixture/DarakbangSetUp.java new file mode 100644 index 000000000..bfe51989a --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/fixture/DarakbangSetUp.java @@ -0,0 +1,55 @@ +package mouda.backend.common.fixture; + +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.member.domain.Member; +import mouda.backend.member.infrastructure.MemberRepository; + +@SpringBootTest +public class DarakbangSetUp { + + @Autowired + private DarakbangRepository darakbangRepository; + + @Autowired + private DarakbangMemberRepository darakbangMemberRepository; + + @Autowired + private MemberRepository memberRepository; + + protected Darakbang darakbang; + protected Darakbang mouda; + protected Member hogee; + protected Member anna; + protected Member tebah; + protected DarakbangMember darakbangHogee; + protected DarakbangMember moudaHogee; + protected DarakbangMember darakbangAnna; + protected DarakbangMember darakbangManager; + + @BeforeEach + void setUp() { + darakbang = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + mouda = darakbangRepository.save(DarakbangFixture.getDarakbangWithMouda()); + + hogee = memberRepository.save(MemberFixture.getHogee()); + darakbangHogee = darakbangMemberRepository.save( + DarakbangMemberFixture.getDarakbangMemberWithWooteco(darakbang, hogee)); + moudaHogee = darakbangMemberRepository.save( + DarakbangMemberFixture.getDarakbangMemberWithWooteco(mouda, hogee)); + + anna = memberRepository.save(MemberFixture.getAnna()); + darakbangAnna = darakbangMemberRepository.save( + DarakbangMemberFixture.getDarakbangMemberWithWooteco(darakbang, anna)); + + tebah = memberRepository.save(MemberFixture.getTebah()); + darakbangManager = darakbangMemberRepository.save( + DarakbangMemberFixture.getDarakbangManagerWithWooteco(darakbang, tebah)); + } +} diff --git a/backend/src/test/java/mouda/backend/common/fixture/MemberFixture.java b/backend/src/test/java/mouda/backend/common/fixture/MemberFixture.java new file mode 100644 index 000000000..4e98d769b --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/fixture/MemberFixture.java @@ -0,0 +1,50 @@ +package mouda.backend.common.fixture; + +import mouda.backend.member.domain.LoginDetail; +import mouda.backend.member.domain.Member; +import mouda.backend.member.domain.OauthType; + +public class MemberFixture { + + public static Member getHogee() { + return Member.builder() + .name("hogee") + .loginDetail(new LoginDetail(OauthType.KAKAO, "1234")) + .build(); + } + + public static Member getAnna() { + return Member.builder() + .name("anna") + .loginDetail(new LoginDetail(OauthType.KAKAO, "1234")) + .build(); + } + + public static Member getAnna(String identifier) { + return Member.builder() + .name("anna") + .loginDetail(new LoginDetail(OauthType.KAKAO, identifier)) + .build(); + } + + public static Member getAnna(OauthType oauthType, String identifier) { + return Member.builder() + .name("anna") + .loginDetail(new LoginDetail(oauthType, identifier)) + .build(); + } + + public static Member getChico() { + return Member.builder() + .name("chico") + .loginDetail(new LoginDetail(OauthType.KAKAO, "identifier")) + .build(); + } + + public static Member getTebah() { + return Member.builder() + .name("tebah") + .loginDetail(new LoginDetail(OauthType.KAKAO, "123")) + .build(); + } +} diff --git a/backend/src/test/java/mouda/backend/common/fixture/MoimFixture.java b/backend/src/test/java/mouda/backend/common/fixture/MoimFixture.java new file mode 100644 index 000000000..897bf4dfc --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/fixture/MoimFixture.java @@ -0,0 +1,57 @@ +package mouda.backend.common.fixture; + +import java.time.LocalDate; +import java.time.LocalTime; + +import mouda.backend.moim.domain.Moim; + +public class MoimFixture { + + public static Moim getSoccerMoim(long darakbangId) { + return Moim.builder() + .title("풋살할 사람?") + .time(LocalTime.now().plusHours(1)) + .date(LocalDate.now().plusDays(1)) + .place("잠실 종합운동장") + .description("잘하는 사람만 와라") + .maxPeople(22) + .darakbangId(darakbangId) + .build(); + } + + public static Moim getBasketballMoim(long darakbangId) { + return Moim.builder() + .title("농구할 사람?") + .time(LocalTime.now().plusHours(1)) + .date(LocalDate.now().plusDays(1)) + .place("테바 집") + .description("파주로 와라") + .maxPeople(10) + .darakbangId(darakbangId) + .build(); + } + + public static Moim getAloneMoim(long darakbangId) { + return Moim.builder() + .title("혼자 놀기") + .time(LocalTime.now().plusHours(1)) + .date(LocalDate.now().plusDays(1)) + .place("상돌 집") + .description("혼자가 좋아") + .maxPeople(1) + .darakbangId(darakbangId) + .build(); + } + + public static Moim getCoffeeMoim(long darakbangId) { + return Moim.builder() + .title("커피 마실 사람?") + .time(LocalTime.now().plusHours(1)) + .date(LocalDate.now().plusDays(1)) + .place("안나 집") + .description("커피 머신 들고와라") + .maxPeople(60) + .darakbangId(darakbangId) + .build(); + } +} diff --git a/backend/src/test/java/mouda/backend/common/fixture/PleaseFixture.java b/backend/src/test/java/mouda/backend/common/fixture/PleaseFixture.java new file mode 100644 index 000000000..73559bbe8 --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/fixture/PleaseFixture.java @@ -0,0 +1,42 @@ +package mouda.backend.common.fixture; + +import mouda.backend.please.domain.Please; + +public class PleaseFixture { + + public static Please getPleaseChicken() { + return Please.builder() + .title("치킨 사주세요") + .description("제발요") + .authorId(1L) + .darakbangId(1L) + .build(); + } + + public static Please getPleasePizza() { + return Please.builder() + .title("피자 사주세요") + .description("제발요") + .authorId(2L) + .darakbangId(1L) + .build(); + } + + public static Please getPleaseHogee() { + return Please.builder() + .title("호기 사주세요") + .description("제발요") + .authorId(2L) + .darakbangId(1L) + .build(); + } + + public static Please getPlease(long id, long darakbangId) { + return Please.builder() + .title("라라스윗 사주세요") + .description("제발요") + .authorId(id) + .darakbangId(darakbangId) + .build(); + } +} diff --git a/backend/src/test/java/mouda/backend/common/global/IgnoreNotificationTest.java b/backend/src/test/java/mouda/backend/common/global/IgnoreNotificationTest.java new file mode 100644 index 000000000..bcff42696 --- /dev/null +++ b/backend/src/test/java/mouda/backend/common/global/IgnoreNotificationTest.java @@ -0,0 +1,23 @@ +// package mouda.backend.common.global; +// +// import static org.mockito.ArgumentMatchers.*; +// import static org.mockito.Mockito.*; +// +// import org.junit.jupiter.api.BeforeEach; +// import org.springframework.boot.test.context.SpringBootTest; +// import org.springframework.boot.test.mock.mockito.MockBean; +// +// import mouda.backend.notification.business.NotificationService; +// +// @SpringBootTest +// public class IgnoreNotificationTest { +// +// @MockBean +// private NotificationService notificationService; +// +// @BeforeEach +// void setUp() { +// doNothing().when(notificationService).notifyToMember(any(), anyLong(), any(), any(), anyLong()); +// doNothing().when(notificationService).notifyToMembers(any(), anyLong(), any(), any()); +// } +// } diff --git a/backend/src/test/java/mouda/backend/darakbang/business/DarakbangServiceTest.java b/backend/src/test/java/mouda/backend/darakbang/business/DarakbangServiceTest.java new file mode 100644 index 000000000..4d1d42b5c --- /dev/null +++ b/backend/src/test/java/mouda/backend/darakbang/business/DarakbangServiceTest.java @@ -0,0 +1,72 @@ +package mouda.backend.darakbang.business; + +import static org.assertj.core.api.Assertions.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangFixture; +import mouda.backend.common.fixture.DarakbangMemberFixture; +import mouda.backend.common.fixture.MemberFixture; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; +import mouda.backend.darakbang.presentation.request.DarakbangEnterRequest; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.member.domain.Member; +import mouda.backend.member.infrastructure.MemberRepository; + +@SpringBootTest +class DarakbangServiceTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private DarakbangRepository darakbangRepository; + + @Autowired + private DarakbangMemberRepository darakbangMemberRepository; + + @Autowired + private DarakbangService darakbangService; + + @DisplayName("다락방 참여 동시성 테스트") + @Nested + class DarakbangEnterConcurrencyTest { + + @DisplayName("한 명의 회원이 다락방에 여러 번 참여 시도하면 실패한다.") + @Test + void failToEnterDarakbangTwoTimes() throws InterruptedException { + Member hogee = memberRepository.save(MemberFixture.getHogee()); + Darakbang darakbang = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + darakbangMemberRepository.save(DarakbangMemberFixture.getDarakbangManagerWithWooteco(darakbang, hogee)); + + int threadCount = 2; + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + DarakbangEnterRequest request = new DarakbangEnterRequest("안나"); + Member member = memberRepository.save(MemberFixture.getAnna()); + for (int i = 0; i < threadCount; i++) { + executorService.execute(() -> { + try { + darakbangService.enter(darakbang.getCode(), request, member); + } finally { + latch.countDown(); + } + }); + } + latch.await(); + executorService.shutdown(); + + assertThat(darakbangMemberRepository.findAllByDarakbangId(darakbang.getId())).hasSize(2); + } + } +} diff --git a/backend/src/test/java/mouda/backend/darakbang/implement/DarakbangFinderTest.java b/backend/src/test/java/mouda/backend/darakbang/implement/DarakbangFinderTest.java new file mode 100644 index 000000000..a63789304 --- /dev/null +++ b/backend/src/test/java/mouda/backend/darakbang/implement/DarakbangFinderTest.java @@ -0,0 +1,94 @@ +package mouda.backend.darakbang.implement; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; + +import mouda.backend.common.fixture.DarakbangFixture; +import mouda.backend.common.fixture.DarakbangMemberFixture; +import mouda.backend.common.fixture.MemberFixture; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.domain.Darakbangs; +import mouda.backend.darakbang.exception.DarakbangErrorMessage; +import mouda.backend.darakbang.exception.DarakbangException; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.member.domain.Member; +import mouda.backend.member.infrastructure.MemberRepository; + +@SpringBootTest +class DarakbangFinderTest { + + @Autowired + private DarakbangFinder darakbangFinder; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private DarakbangRepository darakbangRepository; + + @Autowired + private DarakbangMemberRepository darakbangMemberRepository; + + @DisplayName("내가 참여한 다락방을 전체 조회한다.") + @Test + void findAllMyDarakbangs() { + Member hogee = memberRepository.save(MemberFixture.getHogee()); + + Darakbang wooteco = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + darakbangMemberRepository.save(DarakbangMemberFixture.getDarakbangMemberWithWooteco(wooteco, hogee)); + + Darakbang mouda = darakbangRepository.save(DarakbangFixture.getDarakbangWithMouda()); + darakbangMemberRepository.save(DarakbangMemberFixture.getDarakbangMemberWithWooteco(mouda, hogee)); + + Darakbangs result = darakbangFinder.findAllMyDarakbangs(hogee); + + assertThat(result.getDarakbangs()).hasSize(2); + } + + @DisplayName("다락방 아이디에 해당하는 다락방을 조회한다.") + @Test + void findById() { + Member hogee = memberRepository.save(MemberFixture.getHogee()); + + Darakbang wooteco = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + darakbangMemberRepository.save(DarakbangMemberFixture.getDarakbangMemberWithWooteco(wooteco, hogee)); + + Darakbang mouda = darakbangRepository.save(DarakbangFixture.getDarakbangWithMouda()); + darakbangMemberRepository.save(DarakbangMemberFixture.getDarakbangMemberWithWooteco(mouda, hogee)); + + Darakbang darakbang = darakbangFinder.findById(wooteco.getId()); + + assertThat(darakbang).isEqualTo(wooteco); + } + + @Test + @DisplayName("Darakbang을 찾지 못한 경우 예외가 발생한다") + void findDarakbang_notFound() { + assertThatThrownBy(() -> darakbangFinder.findById(1L)) + .isInstanceOf(DarakbangException.class) + .hasMessage(DarakbangErrorMessage.DARAKBANG_NOT_FOUND.getMessage()) + .hasFieldOrPropertyWithValue("httpStatus", HttpStatus.NOT_FOUND); + } + + @DisplayName("다락방 참여 코드 해당하는 다락방을 조회한다.") + @Test + void findByCode() { + Member hogee = memberRepository.save(MemberFixture.getHogee()); + + Darakbang wooteco = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + darakbangMemberRepository.save(DarakbangMemberFixture.getDarakbangMemberWithWooteco(wooteco, hogee)); + + Darakbang mouda = darakbangRepository.save(DarakbangFixture.getDarakbangWithMouda()); + darakbangMemberRepository.save(DarakbangMemberFixture.getDarakbangMemberWithWooteco(mouda, hogee)); + + Darakbang darakbang = darakbangFinder.findByCode(wooteco.getCode()); + + assertThat(darakbang).isEqualTo(wooteco); + } +} diff --git a/backend/src/test/java/mouda/backend/darakbang/implement/DarakbangValidatorTest.java b/backend/src/test/java/mouda/backend/darakbang/implement/DarakbangValidatorTest.java new file mode 100644 index 000000000..2d48122bb --- /dev/null +++ b/backend/src/test/java/mouda/backend/darakbang/implement/DarakbangValidatorTest.java @@ -0,0 +1,80 @@ +package mouda.backend.darakbang.implement; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangFixture; +import mouda.backend.common.fixture.DarakbangMemberFixture; +import mouda.backend.common.fixture.MemberFixture; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.exception.DarakbangException; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; +import mouda.backend.darakbangmember.exception.DarakbangMemberException; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.member.domain.Member; +import mouda.backend.member.infrastructure.MemberRepository; + +@SpringBootTest +class DarakbangValidatorTest { + + @Autowired + private DarakbangValidator darakbangValidator; + + @Autowired + private DarakbangRepository darakbangRepository; + + @Autowired + private DarakbangMemberRepository darakbangMemberRepository; + + @Autowired + private MemberRepository memberRepository; + + @DisplayName("다락방 이름이 중복되면 생성에 실패한다.") + @Test + void validateDuplicatedName() { + Darakbang darakbang = DarakbangFixture.getDarakbangWithWooteco(); + darakbangRepository.save(darakbang); + + assertThatThrownBy(() -> darakbangValidator.validateAlreadyExistsName(darakbang.getName())) + .isInstanceOf(DarakbangException.class); + } + + @DisplayName("다락방에 이미 가입한 멤버라면 가입에 실패한다.") + @Test + void validateDuplicatedNickname() { + Member hogee = memberRepository.save(MemberFixture.getHogee()); + Darakbang darakbang = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + darakbangMemberRepository.save(DarakbangMemberFixture.getDarakbangMemberWithWooteco(darakbang, hogee)); + + assertThatThrownBy(() -> darakbangValidator.validateCanEnterDarakbang(darakbang, "안나", hogee)) + .isInstanceOf(DarakbangMemberException.class); + } + + @DisplayName("다락방에 해당 닉네임이 이미 존재한다면 가입에 실패한다.") + @Test + void validateAlreadyEnteredMember() { + Member hogee = memberRepository.save(MemberFixture.getHogee()); + Darakbang darakbang = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + darakbangMemberRepository.save(DarakbangMemberFixture.getDarakbangMemberWithWooteco(darakbang, hogee)); + + Member anna = memberRepository.save(MemberFixture.getAnna()); + + assertThatThrownBy(() -> darakbangValidator.validateCanEnterDarakbang(darakbang, "hogee", anna)) + .isInstanceOf(DarakbangMemberException.class); + } + + @DisplayName("이미 사용 중인 참여 코드를 발급하면 다락방 생성에 실패한다. ") + @Test + void validateAlreadyExistsCode() { + Member hogee = memberRepository.save(MemberFixture.getHogee()); + Darakbang wooteco = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + darakbangMemberRepository.save(DarakbangMemberFixture.getDarakbangMemberWithWooteco(wooteco, hogee)); + + assertThatThrownBy(() -> darakbangValidator.validateAlreadyExistsCode(wooteco.getCode())) + .isInstanceOf(DarakbangException.class); + } +} diff --git a/backend/src/test/java/mouda/backend/darakbang/implement/DarakbangWriterTest.java b/backend/src/test/java/mouda/backend/darakbang/implement/DarakbangWriterTest.java new file mode 100644 index 000000000..e900d0888 --- /dev/null +++ b/backend/src/test/java/mouda/backend/darakbang/implement/DarakbangWriterTest.java @@ -0,0 +1,36 @@ +package mouda.backend.darakbang.implement; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.exception.DarakbangException; + +@SpringBootTest +class DarakbangWriterTest { + + @Autowired + private DarakbangWriter darakbangWriter; + + @DisplayName("다락방을 성공적으로 생성한다.") + @Test + void success() { + Darakbang darakbang = darakbangWriter.save("우아한테크코스"); + + assertThat(darakbang.getId()).isEqualTo(1L); + } + + @DisplayName("다락방 이름이 존재하지 않으면 생성에 실패한다.") + @NullAndEmptySource + @ParameterizedTest + void failToCreateDarakbangWithoutName(String name) { + assertThatThrownBy(() -> darakbangWriter.save(name)) + .isInstanceOf(DarakbangException.class); + } +} \ No newline at end of file diff --git a/backend/src/test/java/mouda/backend/darakbang/implement/InvitationCodeGeneratorTest.java b/backend/src/test/java/mouda/backend/darakbang/implement/InvitationCodeGeneratorTest.java new file mode 100644 index 000000000..faa87cad7 --- /dev/null +++ b/backend/src/test/java/mouda/backend/darakbang/implement/InvitationCodeGeneratorTest.java @@ -0,0 +1,23 @@ +package mouda.backend.darakbang.implement; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; + +@SpringBootTest +class InvitationCodeGeneratorTest { + + @SpyBean + private InvitationCodeGenerator invitationCodeGenerator; + + @DisplayName("7자리 랜덤코드를 생성한다.") + @Test + void success() { + String invitationCode = invitationCodeGenerator.generate(); + + assertThat(invitationCode).hasSize(7); + } +} diff --git a/backend/src/test/java/mouda/backend/darakbangmember/business/DarakbangMemberServiceTest.java b/backend/src/test/java/mouda/backend/darakbangmember/business/DarakbangMemberServiceTest.java new file mode 100644 index 000000000..a65a22bd3 --- /dev/null +++ b/backend/src/test/java/mouda/backend/darakbangmember/business/DarakbangMemberServiceTest.java @@ -0,0 +1,163 @@ +package mouda.backend.darakbangmember.business; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; + +import mouda.backend.common.fixture.DarakbangMemberFixture; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MemberFixture; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.implement.S3Client; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.darakbangmember.presentation.response.DarakbangMemberResponses; +import mouda.backend.darakbangmember.presentation.response.DarakbangMemberRoleResponse; +import mouda.backend.member.domain.Member; +import mouda.backend.member.infrastructure.MemberRepository; +import mouda.backend.member.presentation.response.DarakbangMemberInfoResponse; + +@SpringBootTest +class DarakbangMemberServiceTest extends DarakbangSetUp { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private DarakbangMemberRepository darakbangMemberRepository; + + @Autowired + private DarakbangMemberService darakbangMemberService; + + @MockBean + private S3Client s3Client; + + @Value("${aws.s3.prefix}") + private String prefix; + + @DisplayName("다락방 멤버 권한 조회 테스트") + @Nested + class DarakbangMemberRoleReadTest { + + @DisplayName("다락방 멤버가 아니라면 OUTSIDER를 반환한다.") + @Test + void success() { + Member chico = MemberFixture.getChico(); + memberRepository.save(chico); + DarakbangMember darakbangMember = DarakbangMemberFixture.getDarakbangOutsiderWithWooteco(darakbang, chico); + darakbangMemberRepository.save(darakbangMember); + DarakbangMemberRoleResponse response = darakbangMemberService.findDarakbangMemberRole( + darakbang.getId(), chico); + + assertThat(response.role()).isEqualTo("OUTSIDER"); + } + } + + @DisplayName("다락방 멤버 조회 테스트") + @Nested + class DarakbangMemberReadTest { + + @DisplayName("매니저는 다락방 멤버 목록을 조회할 수 있다.") + @Test + void managerCanReadDarakbangMembers() { + DarakbangMemberResponses responses = darakbangMemberService.findAllDarakbangMembers( + darakbang.getId(), darakbangManager); + + assertThat(responses.responses()).hasSize(3); + } + + @DisplayName("다락방 멤버는 다락방 멤버 목록을 조회할 수 있다.") + @Test + void memberCanReadDarakbangMembers() { + DarakbangMemberResponses responses = darakbangMemberService.findAllDarakbangMembers( + darakbang.getId(), darakbangAnna); + + assertThat(responses.responses()).hasSize(3); + } + } + + @DisplayName("내 정보를 조회한다.") + @Test + void findMyInfo() { + DarakbangMemberInfoResponse response = darakbangMemberService.findMyInfo(darakbangHogee); + + assertThat(response.name()).isEqualTo("hogee"); + assertThat(response.nickname()).isEqualTo("hogee"); + assertThat(response.profile()).isEqualTo("profile"); + assertThat(response.description()).isEqualTo("description"); + } + + @DisplayName("마이페이지 수정 테스트") + @Nested + class UpdateMyInfoTest { + + private String nickname = "수정된 닉네임"; + private String description = "수정된 소개"; + + @DisplayName("이미지 추가 없이 닉네임, 소개만 변경하는 경우 S3 통신 없이 DB값을 변경한다.") + @Test + void updateMyInfoWhenNoProfileUpdate() { + // when + darakbangMemberService.updateMyInfo(darakbangAnna, "false", null, nickname, description); + + // then + Optional annaOptional = darakbangMemberRepository.findById(darakbangAnna.getId()); + assertThat(annaOptional.isPresent()).isTrue(); + assertThat(annaOptional.get().getNickname()).isEqualTo(nickname); + assertThat(annaOptional.get().getDescription()).isEqualTo(description); + + verify(s3Client, times(0)).uploadFile(any()); + } + + @DisplayName("이미지 추가와 더불어 닉네임, 소개를 변경하는 경우 S3 통신과 함께 DB 값을 변경한다.") + @Test + void updateMyInfoWithProfileUpdate() { + // given + MockMultipartFile file = new MockMultipartFile("file", "test-file.txt", + MediaType.TEXT_PLAIN_VALUE, "Hello, World!".getBytes()); + when(s3Client.uploadFile(any())).thenReturn("https://s3url/uuid.png"); + + // when + darakbangMemberService.updateMyInfo(darakbangAnna, "false", file, nickname, description); + + // then + Optional annaOptional = darakbangMemberRepository.findById(darakbangAnna.getId()); + assertThat(annaOptional.isPresent()).isTrue(); + assertThat(annaOptional.get().getNickname()).isEqualTo(nickname); + assertThat(annaOptional.get().getDescription()).isEqualTo(description); + String expectedUrl = prefix + "uuid.png"; + assertThat(annaOptional.get().getProfile()).isEqualTo(expectedUrl); + + verify(s3Client, times(1)).uploadFile(any()); + } + + @DisplayName("기존 이미지로 변경하는 경우 S3 통신 없이 DB의 프로필 이미지 URL 값을 제거한다.") + @Test + void updateMyInfoWithBasicProfileUpdate() { + // given + doNothing().when(s3Client).deleteFile(anyString()); + + // when + darakbangMemberService.updateMyInfo(darakbangAnna, "true", null, nickname, description); + + // then + Optional annaOptional = darakbangMemberRepository.findById(darakbangAnna.getId()); + assertThat(annaOptional.isPresent()).isTrue(); + assertThat(annaOptional.get().getNickname()).isEqualTo(nickname); + assertThat(annaOptional.get().getDescription()).isEqualTo(description); + assertThat(annaOptional.get().getProfile()).isNull(); + + verify(s3Client, times(0)).uploadFile(any()); + } + } +} diff --git a/backend/src/test/java/mouda/backend/darakbangmember/domain/DarakbangMemberTest.java b/backend/src/test/java/mouda/backend/darakbangmember/domain/DarakbangMemberTest.java new file mode 100644 index 000000000..3efd8aeb8 --- /dev/null +++ b/backend/src/test/java/mouda/backend/darakbangmember/domain/DarakbangMemberTest.java @@ -0,0 +1,24 @@ +package mouda.backend.darakbangmember.domain; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.darakbangmember.exception.DarakbangMemberException; + +class DarakbangMemberTest extends DarakbangSetUp { + + @DisplayName("닉네임은 10글자 미만 이여야 한다.") + @Test + void invalidNickName() { + assertThatThrownBy(() -> new DarakbangMember( + darakbang, 10L, + "hogeehogeehgoee", " ", + " ", DarakBangMemberRole.MEMBER)) + .isInstanceOf(DarakbangMemberException.class) + .hasMessage("닉네임은 9글자 이하로만 가능합니다."); + } + +} diff --git a/backend/src/test/java/mouda/backend/darakbangmember/implement/DarakbangMemberFinderTest.java b/backend/src/test/java/mouda/backend/darakbangmember/implement/DarakbangMemberFinderTest.java new file mode 100644 index 000000000..fdb8a19c5 --- /dev/null +++ b/backend/src/test/java/mouda/backend/darakbangmember/implement/DarakbangMemberFinderTest.java @@ -0,0 +1,130 @@ +package mouda.backend.darakbangmember.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; + +import mouda.backend.common.fixture.DarakbangFixture; +import mouda.backend.common.fixture.DarakbangMemberFixture; +import mouda.backend.common.fixture.MemberFixture; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; +import mouda.backend.darakbangmember.domain.DarakBangMemberRole; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.domain.DarakbangMembers; +import mouda.backend.darakbangmember.exception.DarakbangMemberErrorMessage; +import mouda.backend.darakbangmember.exception.DarakbangMemberException; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.member.domain.Member; +import mouda.backend.member.infrastructure.MemberRepository; + +@SpringBootTest +class DarakbangMemberFinderTest { + + @Autowired + private DarakbangMemberFinder darakbangMemberFinder; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private DarakbangRepository darakbangRepository; + + @Autowired + private DarakbangMemberRepository darakbangMemberRepository; + + @DisplayName("회원이 속한 다락방 목록을 조회한다.") + @Test + void findAllByMember() { + Member hogee = memberRepository.save(MemberFixture.getHogee()); + + Darakbang wooteco = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + darakbangMemberRepository.save(DarakbangMemberFixture.getDarakbangMemberWithWooteco(wooteco, hogee)); + + Darakbang mouda = darakbangRepository.save(DarakbangFixture.getDarakbangWithMouda()); + darakbangMemberRepository.save(DarakbangMemberFixture.getDarakbangMemberWithWooteco(mouda, hogee)); + + List darakbangMembers = darakbangMemberFinder.findAllByMember(hogee); + + assertThat(darakbangMembers).hasSize(2); + } + + @DisplayName("다락방에 속한 전체 다락방 멤버를 조회한다.") + @Test + void findAllDarakbangMembers() { + Member hogee = memberRepository.save(MemberFixture.getHogee()); + Member anna = memberRepository.save(MemberFixture.getAnna()); + + Darakbang wooteco = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + DarakbangMember hogee1 = DarakbangMemberFixture.getDarakbangMemberWithWooteco(wooteco, + hogee); + darakbangMemberRepository.save(hogee1); + DarakbangMember anna1 = DarakbangMemberFixture.getDarakbangMemberWithWooteco(wooteco, + anna); + darakbangMemberRepository.save(anna1); + + DarakbangMembers darakbangMembers = darakbangMemberFinder.findAllDarakbangMembers(wooteco.getId()); + + assertThat(darakbangMembers.getDarakbangMembers()).hasSize(2); + assertThat(darakbangMembers.getDarakbangMembers().get(0)).isEqualTo(hogee1); + assertThat(darakbangMembers.getDarakbangMembers().get(1)).isEqualTo(anna1); + } + + @DisplayName("다락방 회원의 권한을 조회한다.") + @Test + void findDarakbangMemberRole() { + Member hogee = memberRepository.save(MemberFixture.getHogee()); + + Darakbang wooteco = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + darakbangMemberRepository.save(DarakbangMemberFixture.getDarakbangMemberWithWooteco(wooteco, hogee)); + + DarakBangMemberRole darakbangMemberRole = darakbangMemberFinder.findDarakbangMemberRole(wooteco.getId(), + hogee.getId()); + + assertThat(darakbangMemberRole).isEqualTo(DarakBangMemberRole.MEMBER); + } + + @Test + @DisplayName("DarakbangMember를 정상적으로 찾는 경우") + void findDarakbangMember_success() { + Member hogee = memberRepository.save(MemberFixture.getHogee()); + Darakbang wooteco = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + DarakbangMember darakbangMember = darakbangMemberRepository.save( + DarakbangMemberFixture.getDarakbangMemberWithWooteco(wooteco, hogee)); + + DarakbangMember foundDarakbangMember = darakbangMemberFinder.find(wooteco, hogee); + + assertThat(foundDarakbangMember).isEqualTo(darakbangMember); + } + + @Test + @DisplayName("DarakbangMember를 찾지 못한 경우 예외가 발생한다") + void findDarakbangMember_notFound() { + Member hogee = memberRepository.save(MemberFixture.getHogee()); + Darakbang wooteco = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + + assertThatThrownBy(() -> darakbangMemberFinder.find(wooteco, hogee)) + .isInstanceOf(DarakbangMemberException.class) + .hasMessage(DarakbangMemberErrorMessage.MEMBER_NOT_EXIST.getMessage()) + .hasFieldOrPropertyWithValue("httpStatus", HttpStatus.UNAUTHORIZED); + } + + @Test + @DisplayName("다락방멤버 Id로 다락방 멤버를 조회한다.") + void findDarakbangMember() { + Member hogee = memberRepository.save(MemberFixture.getHogee()); + Darakbang wooteco = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + DarakbangMember darakbangMember = darakbangMemberRepository.save( + DarakbangMemberFixture.getDarakbangMemberWithWooteco(wooteco, hogee)); + + DarakbangMember foundDarakbangMember = darakbangMemberFinder.find(darakbangMember.getId()); + + assertThat(foundDarakbangMember.getNickname()).isEqualTo(darakbangMember.getNickname()); + } +} diff --git a/backend/src/test/java/mouda/backend/darakbangmember/implement/DarakbangMemberWriterTest.java b/backend/src/test/java/mouda/backend/darakbangmember/implement/DarakbangMemberWriterTest.java new file mode 100644 index 000000000..fd0d728ba --- /dev/null +++ b/backend/src/test/java/mouda/backend/darakbangmember/implement/DarakbangMemberWriterTest.java @@ -0,0 +1,74 @@ +package mouda.backend.darakbangmember.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MemberFixture; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.exception.DarakbangMemberException; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.member.domain.Member; +import mouda.backend.member.infrastructure.MemberRepository; + +@SpringBootTest +class DarakbangMemberWriterTest extends DarakbangSetUp { + + @Autowired + private DarakbangMemberWriter darakbangMemberWriter; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private DarakbangRepository darakbangRepository; + @Autowired + private DarakbangMemberRepository darakbangMemberRepository; + + @DisplayName("정상적으로 매니저 신분으로 다락방 가입에 성공한다.") + @Test + void saveManager() { + Member chico = memberRepository.save(MemberFixture.getChico()); + DarakbangMember darakbangMember = darakbangMemberWriter.saveManager(darakbang, "치코치코니", chico); + + assertThat(darakbangMember).isNotNull(); + } + + @DisplayName("닉네임이 존재하지 않으면 다락방 참여에 실패한다.") + @NullAndEmptySource + @ParameterizedTest + void validateWithoutNickname(String nickname) { + assertThatThrownBy(() -> darakbangMemberWriter.saveManager(darakbang, nickname, hogee)) + .isInstanceOf(DarakbangMemberException.class); + } + + @DisplayName("정상적으로 회원 신분으로 다락방 가입에 성공한다.") + @Test + void saveMember() { + Member chico = MemberFixture.getChico(); + memberRepository.save(chico); + DarakbangMember darakbangMember = darakbangMemberWriter.saveMember(darakbang, "치코치코니", chico); + + assertThat(darakbangMember).isNotNull(); + } + + @DisplayName("내 정보를 수정한다.") + @Test + void updateMyInfo() { + darakbangMemberWriter.updateMyInfo(darakbangAnna, + "안나", "updatedProfile", "updatedDescription"); + + Optional annaOptional = darakbangMemberRepository.findById(darakbangAnna.getId()); + assertThat(annaOptional.isPresent()).isTrue(); + assertThat(annaOptional.get().getNickname()).isEqualTo("안나"); + } +} diff --git a/backend/src/test/java/mouda/backend/darakbangmember/implement/ImageParserTest.java b/backend/src/test/java/mouda/backend/darakbangmember/implement/ImageParserTest.java new file mode 100644 index 000000000..c0a1a8267 --- /dev/null +++ b/backend/src/test/java/mouda/backend/darakbangmember/implement/ImageParserTest.java @@ -0,0 +1,26 @@ +package mouda.backend.darakbangmember.implement; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ImageParserTest { + + @Autowired + private ImageParser imageParser; + + @DisplayName("이미지 URL이 입력되면 이를 파싱하여 다운로드 가능한 URL로 변경한다.") + @Test + void parse() { + String url = "https://techcourse-project-2024.s3.ap-northeast-2.amazonaws.com/7c60f9f0-a8ef-4209-8768-9c9a010bbc24.png"; + + String profile = imageParser.parse(url); + + String expected = "https://dev.mouda.site/profile/7c60f9f0-a8ef-4209-8768-9c9a010bbc24.png"; + assertThat(profile).isEqualTo(expected); + } +} diff --git a/backend/src/test/java/mouda/backend/member/implement/MemberValidatorTest.java b/backend/src/test/java/mouda/backend/member/implement/MemberValidatorTest.java new file mode 100644 index 000000000..ffc6249d7 --- /dev/null +++ b/backend/src/test/java/mouda/backend/member/implement/MemberValidatorTest.java @@ -0,0 +1,47 @@ +package mouda.backend.member.implement; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangFixture; +import mouda.backend.common.fixture.DarakbangMemberFixture; +import mouda.backend.common.fixture.MemberFixture; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.exception.DarakbangMemberException; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.member.domain.Member; +import mouda.backend.member.infrastructure.MemberRepository; + +@SpringBootTest +class MemberValidatorTest { + + @Autowired + private MemberValidator memberValidator; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private DarakbangRepository darakbangRepository; + + @Autowired + private DarakbangMemberRepository darakbangMemberRepository; + + @DisplayName("관리자가 아니라면 예외를 던진다.") + @Test + void validateNotManager() { + Member hogee = memberRepository.save(MemberFixture.getHogee()); + Darakbang darakbang = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + DarakbangMember darakbangHogee = darakbangMemberRepository.save( + DarakbangMemberFixture.getDarakbangMemberWithWooteco(darakbang, hogee)); + + assertThatThrownBy(() -> memberValidator.validateNotManager(darakbangHogee)) + .isInstanceOf(DarakbangMemberException.class); + } +} \ No newline at end of file diff --git a/backend/src/test/java/mouda/backend/moim/business/ChamyoAsyncTest.java b/backend/src/test/java/mouda/backend/moim/business/ChamyoAsyncTest.java new file mode 100644 index 000000000..b6b4767e6 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/business/ChamyoAsyncTest.java @@ -0,0 +1,84 @@ +package mouda.backend.moim.business; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.implement.notificiation.ChamyoRecipientFinder; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +public class ChamyoAsyncTest extends DarakbangSetUp { + + @Autowired + private ChamyoService chamyoService; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private ChamyoRepository chamyoRepository; + + @MockBean + private ChamyoRecipientFinder chamyoRecipientFinder; + + private Moim moim; + + @BeforeEach + void init() { + moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + chamyoRepository.save(Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMER) + .build()); + } + + @DisplayName("회원 참여시 알림 전송 과정에서 예외가 발생해도 참여는 정상적으로 처리된다.") + @Test + void asyncWhenChamyoCreate() { + // when + when(chamyoRecipientFinder.getChamyoNotificationRecipients(any(Moim.class), any(DarakbangMember.class))) + .thenThrow(new RuntimeException("삐용12")); + + chamyoService.chamyoMoim(darakbang.getId(), moim.getId(), darakbangHogee); + + // then + List chamyos = chamyoRepository.findAllByMoimId(moim.getId()); + assertThat(chamyos).hasSize(2); + assertThat(chamyos).extracting(chamyo -> chamyo.getDarakbangMember().getNickname()) + .contains(darakbangHogee.getNickname()); + } + + @DisplayName("회원 참여 취소시 알림 전송 과정에서 예외가 발생해도 취소는 정상적으로 처리된다.") + @Test + void asyncWhenChamyoCancel() { + // when + when(chamyoRecipientFinder.getChamyoNotificationRecipients(any(Moim.class), any(DarakbangMember.class))) + .thenThrow(new RuntimeException("삐용12")); + + chamyoService.chamyoMoim(darakbang.getId(), moim.getId(), darakbangHogee); + chamyoService.cancelChamyo(darakbang.getId(), moim.getId(), darakbangHogee); + + // then + List chamyos = chamyoRepository.findAllByMoimId(moim.getId()); + assertThat(chamyos).hasSize(1); + assertThat(chamyos).extracting(chamyo -> chamyo.getDarakbangMember().getNickname()) + .doesNotContain(darakbangHogee.getNickname()); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/business/CommentAsyncTest.java b/backend/src/test/java/mouda/backend/moim/business/CommentAsyncTest.java new file mode 100644 index 000000000..6b4dc3486 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/business/CommentAsyncTest.java @@ -0,0 +1,113 @@ +package mouda.backend.moim.business; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import mouda.backend.common.fixture.CommentFixture; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.implement.notificiation.CommentRecipientFinder; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.CommentRepository; +import mouda.backend.moim.infrastructure.MoimRepository; +import mouda.backend.moim.presentation.request.comment.CommentCreateRequest; + +@SpringBootTest +public class CommentAsyncTest extends DarakbangSetUp { + + @Autowired + private CommentService commentService; + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private ChamyoRepository chamyoRepository; + + @MockBean + private CommentRecipientFinder commentRecipientFinder; + + private Moim moim; + private Comment parentComment; + + @BeforeEach + void init() { + moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + chamyoRepository.save(Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMER) + .build()); + parentComment = commentRepository.save(CommentFixture.getCommentWithAnnaAtSoccerMoim(darakbangAnna, moim)); + } + + @DisplayName("댓글 생성시 알림 전송 과정에서 예외가 발생해도 댓글은 생성된다.") + @Test + void asyncWhenCommentCreate() { + // given + String content = "비동기 확인 댓글 ~"; + + // when + when(commentRecipientFinder.getAllRecipient(any(Comment.class))) + .thenThrow(new RuntimeException("삐용12")); + + commentService.createComment(darakbang.getId(), moim.getId(), darakbangHogee, new CommentCreateRequest( + null, + content + )); + + // then + Optional commentOptional = commentRepository.findById(getCommentId(moim, content)); + assertThat(commentOptional).isNotEmpty(); + + Comment comment = commentOptional.get(); + assertThat(comment.getContent()).isEqualTo(content); + } + + @DisplayName("답글 작성시 알림 전송 과정에서 예외가 발생해도 답글은 생성된다.") + @Test + void asyncWhenReplyCreate() { + // given + String content = "비동기 확인 답글 ~"; + + // when + when(commentRecipientFinder.getAllRecipient(any(Comment.class))) + .thenThrow(new RuntimeException("삐용12")); + + commentService.createComment(darakbang.getId(), moim.getId(), darakbangHogee, new CommentCreateRequest( + parentComment.getId(), + content + )); + + // then + Optional commentOptional = commentRepository.findById(getCommentId(moim, content)); + assertThat(commentOptional).isNotEmpty(); + + Comment comment = commentOptional.get(); + assertThat(comment.getContent()).isEqualTo(content); + } + + private long getCommentId(Moim moim, String content) { + return commentRepository.findAllByMoimOrderByCreatedAt(moim).stream() + .filter(comment -> comment.getContent().equals(content)) + .findFirst() + .orElseThrow(RuntimeException::new) + .getId(); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/business/MoimAsyncTest.java b/backend/src/test/java/mouda/backend/moim/business/MoimAsyncTest.java new file mode 100644 index 000000000..fc6263cd0 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/business/MoimAsyncTest.java @@ -0,0 +1,146 @@ +package mouda.backend.moim.business; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.notificiation.MoimRecipientFinder; +import mouda.backend.moim.infrastructure.MoimRepository; +import mouda.backend.moim.presentation.request.moim.MoimCreateRequest; +import mouda.backend.moim.presentation.request.moim.MoimEditRequest; +import mouda.backend.moim.presentation.response.moim.MoimFindAllResponse; + +@SpringBootTest +public class MoimAsyncTest extends DarakbangSetUp { + + @Autowired + private MoimService moimService; + + @Autowired + private MoimRepository moimRepository; + + @MockBean + private MoimRecipientFinder moimRecipientFinder; + + @DisplayName("모임 생성시 알림 전송 과정에서 예외가 발생해도 모임은 생성된다.") + @Test + void asyncWhenMoimCreate() { + // given + String title = "비동기 확인 ~"; + String description = "비동기동비"; + + MoimCreateRequest moimCreateRequest = new MoimCreateRequest( + title, + null, + null, + null, + 10, + description + ); + + // when + when(moimRecipientFinder.getMoimCreatedNotificationRecipients(anyLong(), anyLong())) + .thenThrow(new RuntimeException("삐용12")); + + moimService.createMoim(darakbang.getId(), darakbangAnna, moimCreateRequest); + long moimId = getMoimId(title, description); + + // then + Optional moimOptional = moimRepository.findById(moimId); + assertThat(moimOptional).isNotEmpty(); + + Moim moim = moimOptional.get(); + assertThat(moim.getTitle()).isEqualTo(title); + assertThat(moim.getDescription()).isEqualTo(description); + } + + @DisplayName("모임 정보 수정시 알림 전송 과정에서 예외가 발생해도 수정은 반영된다.") + @Test + void asyncWhenMoimEdit() { + // given + String title = "비동기 확인 ~"; + String description = "비동기동비"; + MoimCreateRequest moimCreateRequest = new MoimCreateRequest( + title, + null, + null, + null, + 10, + description + ); + moimService.createMoim(darakbang.getId(), darakbangAnna, moimCreateRequest); + long moimId = getMoimId(title, description); + + // when + when(moimRecipientFinder.getMoimCreatedNotificationRecipients(anyLong(), anyLong())) + .thenThrow(new RuntimeException("삐용12")); + String editedTitle = "수정된 비동기 ~"; + String editedDescription = "수정된 비동기동비"; + + moimService.editMoim(darakbang.getId(), new MoimEditRequest( + moimId, + editedTitle, + null, + null, + null, + 10, + editedDescription + ), darakbangAnna); + + // then + Optional moimOptional = moimRepository.findById(moimId); + assertThat(moimOptional).isNotEmpty(); + Moim moim = moimOptional.get(); + assertThat(moim.getTitle()).isEqualTo(editedTitle); + assertThat(moim.getDescription()).isEqualTo(editedDescription); + } + + @DisplayName("모임 상태 변경시 알림 전송 과정에서 예외가 발생해도 상태 변경은 반영된다.") + @Test + void asyncWhenMoimStatusChange() { + // given + String title = "비동기 확인 ~"; + String description = "비동기동비"; + MoimCreateRequest moimCreateRequest = new MoimCreateRequest( + title, + null, + null, + null, + 10, + description + ); + moimService.createMoim(darakbang.getId(), darakbangAnna, moimCreateRequest); + long moimId = getMoimId(title, description); + + // when + when(moimRecipientFinder.getMoimCreatedNotificationRecipients(anyLong(), anyLong())) + .thenThrow(new RuntimeException("삐용12")); + + moimService.completeMoim(darakbang.getId(), moimId, darakbangAnna); + + // then + Optional moimOptional = moimRepository.findById(moimId); + assertThat(moimOptional).isNotEmpty(); + + Moim moim = moimOptional.get(); + assertThat(moim.isCompleted()).isTrue(); + } + + private Long getMoimId(String title, String description) { + return moimService.findAllMoim(darakbang.getId(), darakbangAnna).moims().stream() + .filter(moim -> moim.title().equals(title) && moim.description().equals(description)) + .map(MoimFindAllResponse::moimId) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/chamyo/business/ChamyoServiceTest.java b/backend/src/test/java/mouda/backend/moim/chamyo/business/ChamyoServiceTest.java new file mode 100644 index 000000000..abdfadb3f --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/chamyo/business/ChamyoServiceTest.java @@ -0,0 +1,108 @@ +package mouda.backend.moim.chamyo.business; + +import static org.assertj.core.api.Assertions.*; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangFixture; +import mouda.backend.common.fixture.DarakbangMemberFixture; +import mouda.backend.common.fixture.MemberFixture; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.member.domain.Member; +import mouda.backend.member.infrastructure.MemberRepository; +import mouda.backend.moim.business.ChamyoService; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.exception.ChamyoException; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +class ChamyoServiceTest { + + @Autowired + private ChamyoService chamyoService; + + @Autowired + private DarakbangRepository darakbangRepository; + + @Autowired + private DarakbangMemberRepository darakbangMemberRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private MoimRepository moimRepository; + + @DisplayName("같은 회원이 따로 따로 여러 번 참여 요청을 하면 두 번째 요청 시 참여에 실패한다.") + @Test + void chamyoMoim() throws InterruptedException { + Darakbang darakbang = DarakbangFixture.getDarakbangWithMouda(); + darakbangRepository.save(darakbang); + + Member member = MemberFixture.getAnna(); + memberRepository.save(member); + + DarakbangMember darakbangMember = DarakbangMemberFixture + .getDarakbangMemberWithWooteco(darakbang, member); + darakbangMemberRepository.save(darakbangMember); + + Moim moim = MoimFixture.getBasketballMoim(darakbang.getId()); + moimRepository.save(moim); + + chamyoService.chamyoMoim(darakbang.getId(), moim.getId(), darakbangMember); + assertThatThrownBy(() -> chamyoService.chamyoMoim(darakbang.getId(), moim.getId(), darakbangMember)) + .isInstanceOf(ChamyoException.class); + assertThat(chamyoService.findAllChamyo(darakbang.getId(), moim.getId()).chamyos()).hasSize(1); + } + + @DisplayName("동시 참여 테스트") + @Nested + class ConcurrencyTest { + + @DisplayName("같은 회원이 동시에 여러 번 참여 요청을 하면 동시 참여가 불가능하다.") + @Test + void chamyoMoimConcurrently() throws InterruptedException { + int threadCount = 2; + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + Darakbang darakbang = DarakbangFixture.getDarakbangWithMouda(); + darakbangRepository.save(darakbang); + + Moim moim = MoimFixture.getBasketballMoim(darakbang.getId()); + moimRepository.save(moim); + + Member member = MemberFixture.getAnna(); + memberRepository.save(member); + + DarakbangMember darakbangMember = DarakbangMemberFixture + .getDarakbangMemberWithWooteco(darakbang, member); + darakbangMemberRepository.save(darakbangMember); + + for (int i = 0; i < threadCount; i++) { + executorService.execute(() -> { + try { + chamyoService.chamyoMoim(darakbang.getId(), moim.getId(), darakbangMember); + } finally { + latch.countDown(); + } + }); + } + latch.await(); + executorService.shutdown(); + assertThat(chamyoService.findAllChamyo(darakbang.getId(), moim.getId()).chamyos()).hasSize(1); + } + } +} diff --git a/backend/src/test/java/mouda/backend/moim/chat/infrastructure/ChatRepositoryTest.java b/backend/src/test/java/mouda/backend/moim/chat/infrastructure/ChatRepositoryTest.java new file mode 100644 index 000000000..73cbdd0cb --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/chat/infrastructure/ChatRepositoryTest.java @@ -0,0 +1,70 @@ +package mouda.backend.moim.chat.infrastructure; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.chat.entity.ChatEntity; +import mouda.backend.chat.entity.ChatRoomEntity; +import mouda.backend.chat.infrastructure.ChatRepository; +import mouda.backend.chat.infrastructure.ChatRoomRepository; +import mouda.backend.common.fixture.ChatEntityFixture; +import mouda.backend.common.fixture.ChatRoomEntityFixture; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +class ChatRepositoryTest extends DarakbangSetUp { + + @Autowired + private ChatRepository chatRepository; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private ChatRoomRepository chatRoomRepository; + + @DisplayName("모임 아이디가 동일하고 채팅 아이디가 더 큰 채팅 리스트가 조회된다.") + @Test + void test() { + Moim moim = MoimFixture.getCoffeeMoim(darakbang.getId()); + moimRepository.save(moim); + + ChatEntity anna = ChatEntityFixture.getChatEntity(darakbangAnna); + chatRepository.save(anna); + ChatEntity hogee = ChatEntityFixture.getChatEntity(darakbangHogee); + chatRepository.save(hogee); + + List allUnloadedChats = chatRepository.findAllUnloadedChats(1L, 1L); + + assertThat(allUnloadedChats).hasSize(1); + } + + @DisplayName("해당하는 모임의 가장 최근의 채팅을 보여준다.") + @Test + void findFirstByMoimIdOrderByIdDesc() { + Moim moim = MoimFixture.getCoffeeMoim(darakbang.getId()); + Moim savedMoim = moimRepository.save(moim); + + ChatEntity hogee1 = ChatEntityFixture.getChatEntity(darakbangHogee); + chatRepository.save(hogee1); + ChatEntity hogee2 = ChatEntityFixture.getChatEntity(darakbangHogee); + ChatEntity lastChat = chatRepository.save(hogee2); + + ChatRoomEntity chatRoomEntity = chatRoomRepository.save(ChatRoomEntityFixture.getChatRoomEntityOfMoim(1L, 1L)); + + Optional optionalChat = chatRepository.findFirstByChatRoomIdOrderByIdDesc( + chatRoomEntity.getId()); + assertThat(optionalChat).isPresent(); + assertThat(optionalChat.get().getId()).isEqualTo(lastChat.getId()); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/comment/domain/CommentTest.java b/backend/src/test/java/mouda/backend/moim/comment/domain/CommentTest.java new file mode 100644 index 000000000..e05e6b4a3 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/comment/domain/CommentTest.java @@ -0,0 +1,74 @@ +package mouda.backend.moim.comment.domain; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDateTime; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import mouda.backend.common.fixture.CommentFixture; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.exception.CommentException; + +class CommentTest extends DarakbangSetUp { + + @DisplayName("댓글 객체를 정상적으로 생성한다.") + @Test + void createMoim() { + Assertions.assertDoesNotThrow(() -> CommentFixture.getCommentWithAnnaAtSoccerMoim( + darakbangAnna, MoimFixture.getSoccerMoim(darakbang.getId())) + ); + } + + @DisplayName("내용이 null이면 댓글 객체 생성에 실패한다.") + @Test + void failToCreateCommentWhenContentIsNull() { + assertThrows(CommentException.class, () -> Comment.builder() + .content(null) + .moim(MoimFixture.getBasketballMoim(darakbang.getId())) + .darakbangMember(darakbangAnna) + .createdAt(LocalDateTime.now()) + .parentId(null) + .build()); + } + + @DisplayName("내용이 빈 문자열이면 댓글 객체 생성에 실패한다.") + @Test + void failToCreateCommentWhenContentDoesNotExist() { + assertThrows(CommentException.class, () -> Comment.builder() + .content("") + .moim(MoimFixture.getBasketballMoim(darakbang.getId())) + .darakbangMember(darakbangAnna) + .createdAt(LocalDateTime.now()) + .parentId(null) + .build()); + } + + @DisplayName("모임이 존재하지 않으면 댓글 객체 생성에 실패한다.") + @Test + void failToCreateCommentWhenMoimDoesNotExist() { + assertThrows(CommentException.class, () -> Comment.builder() + .content("댓글댓글") + .moim(null) + .darakbangMember(darakbangAnna) + .createdAt(LocalDateTime.now()) + .parentId(null) + .build()); + } + + @DisplayName("멤버가 존재하지 않으면 댓글 객체 생성에 실패한다.") + @Test + void failToCreateCommentWhenMemberDoesNotExist() { + assertThrows(CommentException.class, () -> Comment.builder() + .content("댓글댓글") + .moim(MoimFixture.getBasketballMoim(darakbang.getId())) + .darakbangMember(null) + .createdAt(LocalDateTime.now()) + .parentId(null) + .build()); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/implement/finder/ChamyoFinderTest.java b/backend/src/test/java/mouda/backend/moim/implement/finder/ChamyoFinderTest.java new file mode 100644 index 000000000..d8bf4b497 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/implement/finder/ChamyoFinderTest.java @@ -0,0 +1,157 @@ +package mouda.backend.moim.implement.finder; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.exception.ChamyoException; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +class ChamyoFinderTest extends DarakbangSetUp { + + @Autowired + private ChamyoFinder chamyoFinder; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private ChamyoRepository chamyoRepository; + + @DisplayName("모임 참여 여부를 조회한다.") + @Nested + class ReadTest { + + @DisplayName("모임 참여 여부를 조회한다.") + @Test + void read() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + Chamyo expected = chamyoRepository.save(Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangHogee) + .moimRole(MoimRole.MOIMEE) + .build()); + + Chamyo actual = chamyoFinder.read(moim, darakbangHogee); + + assertThat(actual.getId()).isEqualTo(expected.getId()); + } + + @DisplayName("참여 정보가 존재하지 않으면 예외가 발생한다.") + @Test + void read_() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + chamyoRepository.save(Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangHogee) + .moimRole(MoimRole.MOIMEE) + .build()); + + assertThatThrownBy(() -> chamyoFinder.read(moim, darakbangAnna)) + .isInstanceOf(ChamyoException.class); + } + } + + @Test + void exists() { + } + + @DisplayName("모임 참여자의 역할을 조회한다.") + @Nested + class ReadMoimRoleTest { + + @DisplayName("현재 회원은 모이머(방장) 이다.") + @Test + void readMoimRole_whenMemberIsMoimer() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + Chamyo chamyo = chamyoRepository.save(Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangHogee) + .moimRole(MoimRole.MOIMER) + .build()); + + MoimRole moimRole = chamyoFinder.readMoimRole(moim, darakbangHogee); + assertThat(moimRole).isEqualTo(MoimRole.MOIMER); + } + + @DisplayName("현재 회원은 모이미(참여자) 이다.") + @Test + void readMoimRole_whenMemberIsMoimee() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + chamyoRepository.save(Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangHogee) + .moimRole(MoimRole.MOIMEE) + .build()); + + MoimRole moimRole = chamyoFinder.readMoimRole(moim, darakbangHogee); + assertThat(moimRole).isEqualTo(MoimRole.MOIMEE); + } + + @DisplayName("현재 회원은 모임에 참여하지 않았다.") + @Test + void readMoimRole_whenMemberIsNonMoimee() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + + MoimRole moimRole = chamyoFinder.readMoimRole(moim, darakbangHogee); + assertThat(moimRole).isEqualTo(MoimRole.NON_MOIMEE); + } + } + + @DisplayName("모임 참여 정보를 조회한다.") + @Test + void readAll() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + Chamyo chamyo1 = chamyoRepository.save(Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangHogee) + .moimRole(MoimRole.MOIMEE) + .build()); + + Chamyo chamyo2 = chamyoRepository.save(Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMER) + .build()); + + List chamyos = chamyoFinder.readAll(moim); + assertThat(chamyos).extracting("id") + .containsExactly(chamyo1.getId(), chamyo2.getId()); + } + + @DisplayName("채팅방이 열린 모든 참여 정보를 조회한다.") + @Test + void readAllChatOpened() { + Moim coffeeMoim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + Chamyo coffeeMoimChamyo = chamyoRepository.save(Chamyo.builder() + .moim(coffeeMoim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMEE) + .build()); + coffeeMoim.openChat(); + moimRepository.save(coffeeMoim); + + Moim basketballMoim = moimRepository.save(MoimFixture.getBasketballMoim(darakbang.getId())); + Chamyo basketballMoimChamyo = chamyoRepository.save(Chamyo.builder() + .moim(basketballMoim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMEE) + .build()); + + List chamyos = chamyoFinder.readAllChatOpened(darakbang.getId(), darakbangAnna); + assertThat(chamyos).extracting("id").containsExactly(coffeeMoimChamyo.getId()); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/implement/finder/ChamyoRecipientFinderTest.java b/backend/src/test/java/mouda/backend/moim/implement/finder/ChamyoRecipientFinderTest.java new file mode 100644 index 000000000..8bebb0e1b --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/implement/finder/ChamyoRecipientFinderTest.java @@ -0,0 +1,53 @@ +package mouda.backend.moim.implement.finder; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.implement.notificiation.ChamyoRecipientFinder; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.MoimRepository; +import mouda.backend.notification.domain.Recipient; + +@SpringBootTest +class ChamyoRecipientFinderTest extends DarakbangSetUp { + + @Autowired + ChamyoRecipientFinder chamyoRecipientFinder; + + @Autowired + MoimRepository moimRepository; + + @Autowired + ChamyoRepository chamyoRepository; + + @DisplayName("모임 참여/참여 취소는 참여자/참여취소자를 제외한 모든 인원에게 전송한다.") + @Test + void getChamyoNotificationRecipients() { + // given + Moim moim = MoimFixture.getCoffeeMoim(darakbang.getId()); + Moim savedMoim = moimRepository.save(moim); + Chamyo chamyoWithMoimerAnna = Chamyo.builder() + .moim(savedMoim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMER) + .build(); + chamyoRepository.save(chamyoWithMoimerAnna); + + // when + List recipients = chamyoRecipientFinder.getChamyoNotificationRecipients(savedMoim, darakbangHogee); + + //then + assertThat(recipients).hasSize(1); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/implement/finder/CommentFinderTest.java b/backend/src/test/java/mouda/backend/moim/implement/finder/CommentFinderTest.java new file mode 100644 index 000000000..d82af4ba9 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/implement/finder/CommentFinderTest.java @@ -0,0 +1,50 @@ +package mouda.backend.moim.implement.finder; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.CommentFixture; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.ParentComment; +import mouda.backend.moim.infrastructure.CommentRepository; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +class CommentFinderTest extends DarakbangSetUp { + + @Autowired + private CommentFinder commentFinder; + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private MoimRepository moimRepository; + + @DisplayName("모든 댓글을 조회한다.") + @Test + void readAllParentComments() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + Comment parentComment = commentRepository.save( + CommentFixture.getCommentWithAnnaAtSoccerMoim(darakbangHogee, moim)); + Comment childComment = commentRepository.save( + CommentFixture.getChildCommentWithAnnaAtSoccerMoim(darakbangAnna, moim)); + + List parentComments = commentFinder.readAllParentComments(moim); + + assertThat(parentComments).hasSize(1); + ParentComment actual = parentComments.get(0); + assertThat(actual.getComment().getId()).isEqualTo(parentComment.getId()); + assertThat(actual.getChildren()).hasSize(1) + .extracting(Comment::getId).containsOnly(childComment.getId()); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/implement/finder/CommentRecipientsFinderTest.java b/backend/src/test/java/mouda/backend/moim/implement/finder/CommentRecipientsFinderTest.java new file mode 100644 index 000000000..0a54bca2d --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/implement/finder/CommentRecipientsFinderTest.java @@ -0,0 +1,182 @@ +package mouda.backend.moim.implement.finder; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.implement.notificiation.CommentRecipientFinder; +import mouda.backend.moim.implement.notificiation.CommentRecipients; +import mouda.backend.moim.implement.writer.MoimWriter; +import mouda.backend.moim.infrastructure.CommentRepository; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class CommentRecipientsFinderTest extends DarakbangSetUp { + + @Autowired + private CommentRecipientFinder commentRecipientFinder; + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private MoimWriter moimWriter; + + private Moim moim; + + @BeforeEach + void init() { + moim = moimWriter.save(MoimFixture.getCoffeeMoim(darakbang.getId()), darakbangAnna); + } + + @DisplayName("댓글인 경우") + @Nested + class CommentTest { + + @DisplayName("댓글 작성자가 방장이면 아무에게도 알림을 보내지 않는다.") + @Test + void getNewCommentNotificationRecipients_WhenAuthorMoimer() { + Comment comment = createComment(darakbangAnna, null); + + assertThat(commentRecipientFinder.getAllRecipient(comment).getRecipients()).isEmpty(); + } + + @DisplayName("댓글 작성자가 방장이 아니면 방장에게 알림을 보낸다.") + @Test + void getNewCommentNotificationRecipients_WhenAuthorNotMoimer() { + Comment comment = createComment(darakbangHogee, null); + + CommentRecipients commentRecipients = commentRecipientFinder.getAllRecipient(comment); + Map> results = commentRecipients.getRecipients(); + + assertThat(results).hasSize(1); + assertThat(results.keySet()).containsExactly(NotificationType.NEW_COMMENT); + assertThat(results.get(NotificationType.NEW_COMMENT)).extracting("memberId") + .containsExactly(darakbangAnna.getMemberId()); + } + } + + @DisplayName("답글인 경우") + @Nested + class ReplyTest { + + @DisplayName("방장의 댓글에 답글을 남기는 경우") + @Nested + class ReplyToMoimer { + + private Comment parentComment; + + @BeforeEach + void setUp() { + parentComment = createComment(darakbangAnna, null); + } + + @DisplayName("방장 자신이 답글을 남기는 경우 아무에게도 알림을 보내지 않는다.") + @Test + void getReplyNotificationRecipients_WhenAuthorMoimer() { + Comment childComment = createComment(darakbangAnna, parentComment.getId()); + + CommentRecipients commentRecipients = commentRecipientFinder.getAllRecipient(childComment); + Map> results = commentRecipients.getRecipients(); + + assertThat(results).isEmpty(); + } + + @DisplayName("방장 이외의 사람이 답글을 남기는 경우 방장에게 '답글' 알림을 보낸다.") + @Test + void getReplyNotificationRecipients_WhenAuthorNotMoimer() { + Comment childComment = createComment(darakbangHogee, parentComment.getId()); + + CommentRecipients commentRecipients = commentRecipientFinder.getAllRecipient(childComment); + Map> results = commentRecipients.getRecipients(); + + assertThat(results).hasSize(1); + assertThat(results.keySet()).containsExactly(NotificationType.NEW_REPLY); + assertThat(results.get(NotificationType.NEW_REPLY)).extracting("memberId") + .containsExactly(darakbangAnna.getMemberId()); + } + } + + @DisplayName("방장이 아닌 사람의 댓글에 답글을 남기는 경우") + @Nested + class ReplyToNotMoimer { + + private Comment parentComment; + + @BeforeEach + void setUp() { + parentComment = createComment(darakbangHogee, null); + } + + @DisplayName("방장이 답글을 남기면 댓글 작성자에게만 알림을 보낸다.") + @Test + void getReplyNotificationRecipients_WhenAuthorMoimer() { + Comment childComment = createComment(darakbangAnna, parentComment.getId()); + + CommentRecipients commentRecipients = commentRecipientFinder.getAllRecipient(childComment); + Map> results = commentRecipients.getRecipients(); + + assertThat(results).hasSize(1); + assertThat(results.keySet()).containsExactly(NotificationType.NEW_REPLY); + assertThat(results.get(NotificationType.NEW_REPLY)).extracting("memberId") + .containsExactly(darakbangHogee.getMemberId()); + + } + + @DisplayName("방장 이외의 사람이 답글을 남기는 경우 방장에겐 '댓글' 알림을 보내고, 원 댓글 작성자에겐 '답글' 알림을 보낸다.") + @Test + void getReplyNotificationRecipients_WhenAuthorNotMoimer() { + Comment childComment = createComment(darakbangManager, parentComment.getId()); + + CommentRecipients commentRecipients = commentRecipientFinder.getAllRecipient(childComment); + Map> results = commentRecipients.getRecipients(); + + assertThat(results).hasSize(2); + assertThat(results.keySet()).containsExactlyInAnyOrder(NotificationType.NEW_COMMENT, NotificationType.NEW_REPLY); + assertThat(results.get(NotificationType.NEW_COMMENT)).extracting("memberId") + .containsExactly(darakbangAnna.getMemberId()); + assertThat(results.get(NotificationType.NEW_REPLY)).extracting("memberId") + .containsExactly(darakbangHogee.getMemberId()); + } + + @DisplayName("댓글 작성자가 자신에게 답글을 남기면 방장에게만 '댓글' 알림을 보낸다.") + @Test + void getReplyNotificationRecipients_WhenAuthorIsParentAuthor() { + Comment childComment = createComment(darakbangHogee, parentComment.getId()); + + CommentRecipients commentRecipients = commentRecipientFinder.getAllRecipient(childComment); + Map> results = commentRecipients.getRecipients(); + + assertThat(results).hasSize(1); + assertThat(results.keySet()).containsExactly(NotificationType.NEW_COMMENT); + assertThat(results.get(NotificationType.NEW_COMMENT)).extracting("memberId") + .containsExactly(darakbangAnna.getMemberId()); + } + } + } + + private Comment createComment(DarakbangMember author, Long parentId) { + return commentRepository.save(Comment.builder() + .content("내용") + .moim(moim) + .darakbangMember(author) + .createdAt(LocalDateTime.now()) + .parentId(parentId) + .build()); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/implement/finder/MoimFinderTest.java b/backend/src/test/java/mouda/backend/moim/implement/finder/MoimFinderTest.java new file mode 100644 index 000000000..c55948b70 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/implement/finder/MoimFinderTest.java @@ -0,0 +1,181 @@ +package mouda.backend.moim.implement.finder; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.FilterType; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimOverview; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.domain.Zzim; +import mouda.backend.moim.exception.MoimErrorMessage; +import mouda.backend.moim.exception.MoimException; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.MoimRepository; +import mouda.backend.moim.infrastructure.ZzimRepository; + +@SpringBootTest +class MoimFinderTest extends DarakbangSetUp { + + @Autowired + private MoimFinder moimFinder; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private ChamyoRepository chamyoRepository; + + @Autowired + private ZzimRepository zzimRepository; + + @DisplayName("모임을 조회한다.") + @Nested + class ReadTest { + + @DisplayName("모임을 조회한다.") + @Test + void read() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + Moim actual = moimFinder.read(moim.getId(), darakbang.getId()); + + assertThat(moim.getId()).isEqualTo(actual.getId()); + } + + @DisplayName("모임이 존재하지 않으면 예외가 발생한다.") + @Test + void read_notExist() { + assertThatThrownBy(() -> moimFinder.read(1L, darakbang.getId())) + .isInstanceOf(MoimException.class) + .hasMessage(MoimErrorMessage.NOT_FOUND.getMessage()); + } + } + + @DisplayName("모든 모임을 조회한다.") + @Test + void readAll() { + Moim coffeeMoim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + Moim soccerMoim = moimRepository.save(MoimFixture.getSoccerMoim(darakbang.getId())); + + zzimRepository.save(Zzim.builder().moim(coffeeMoim).darakbangMember(darakbangHogee).build()); + + List moimOverviews = moimFinder.readAll(darakbang.getId(), darakbangHogee); + + assertThat(moimOverviews).extracting(moimOverview -> moimOverview.getMoim().getId()) + .containsExactly(soccerMoim.getId(), coffeeMoim.getId()); + assertThat(moimOverviews).extracting(MoimOverview::isZzimed) + .containsExactly(false, true); + } + + @DisplayName("내가 참여한 모임을 조회한다.") + @Nested + class ReadAllMyMoimTest { + + @DisplayName("내가 참여한 모임을 조회한다.") + @Test + void readAllMyMoim() { + Moim coffeeMoim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + chamyoRepository.save(Chamyo.builder() + .moim(coffeeMoim) + .darakbangMember(darakbangHogee) + .moimRole(MoimRole.MOIMEE) + .build() + ); + + Moim soccerMoim = moimRepository.save(MoimFixture.getSoccerMoim(darakbang.getId())); + + List moimOverviews = moimFinder.readAllMyMoim(darakbangHogee, FilterType.ALL); + + assertThat(moimOverviews).hasSize(1) + .extracting(MoimOverview::getMoim) + .extracting(Moim::getId) + .containsExactly(coffeeMoim.getId()); + } + + @DisplayName("내가 참여한 모임을 조회한다. (예정 모임)") + @Test + void readAllMyMoim_upcoming() { + Moim coffeeMoim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + chamyoRepository.save(Chamyo.builder() + .moim(coffeeMoim) + .darakbangMember(darakbangHogee) + .moimRole(MoimRole.MOIMEE) + .build() + ); + + Moim soccerMoim = moimRepository.save(MoimFixture.getSoccerMoim(darakbang.getId())); + chamyoRepository.save(Chamyo.builder() + .moim(soccerMoim) + .darakbangMember(darakbangHogee) + .moimRole(MoimRole.MOIMEE) + .build() + ); + + List moimOverviews = moimFinder.readAllMyMoim(darakbangHogee, FilterType.UPCOMING); + + assertThat(moimOverviews).hasSize(2) + .extracting(MoimOverview::getMoim) + .extracting(Moim::getId) + .containsExactly(soccerMoim.getId(), coffeeMoim.getId()); + } + } + + + @DisplayName("내가 찜한 모임을 조회한다.") + @Test + void readAllZzimedMoim() { + Moim coffeeMoim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + Moim soccerMoim = moimRepository.save(MoimFixture.getSoccerMoim(darakbang.getId())); + + zzimRepository.save(Zzim.builder() + .moim(coffeeMoim) + .darakbangMember(darakbangHogee) + .build() + ); + + zzimRepository.save(Zzim.builder() + .moim(soccerMoim) + .darakbangMember(darakbangHogee) + .build() + ); + + List moimOverviews = moimFinder.readAllZzimedMoim(darakbangHogee); + + assertThat(moimOverviews).hasSize(2) + .extracting(MoimOverview::getMoim) + .extracting(Moim::getId) + .containsExactly(soccerMoim.getId(), coffeeMoim.getId()); + } + + @DisplayName("모임의 현재 참여 인원을 조회한다.") + @Test + void countCurrentPeople() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + chamyoRepository.save(Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangHogee) + .moimRole(MoimRole.MOIMEE) + .build() + ); + chamyoRepository.save(Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMEE) + .build() + ); + + int currentPeople = moimFinder.countCurrentPeople(moim); + + assertThat(currentPeople).isEqualTo(2); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/implement/finder/MoimRecipientFinderTest.java b/backend/src/test/java/mouda/backend/moim/implement/finder/MoimRecipientFinderTest.java new file mode 100644 index 000000000..6bd08c70d --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/implement/finder/MoimRecipientFinderTest.java @@ -0,0 +1,73 @@ +package mouda.backend.moim.implement.finder; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.implement.notificiation.MoimRecipientFinder; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.MoimRepository; +import mouda.backend.notification.domain.Recipient; + +@SpringBootTest +class MoimRecipientFinderTest extends DarakbangSetUp { + + @Autowired + MoimRecipientFinder moimRecipientFinder; + + @Autowired + MoimRepository moimRepository; + + @Autowired + ChamyoRepository chamyoRepository; + + @DisplayName("모임 생성 알림은 다락방 참여 전원에게 알린다.") + @Test + void getMoimCreatedNotificationRecipients() { + // given + Long darakbangId = darakbang.getId(); + + // when + List recipients = moimRecipientFinder.getMoimCreatedNotificationRecipients(darakbangId, darakbangHogee.getId()); + + //then + assertThat(recipients).hasSize(2); + } + + @DisplayName("모임 상태 변화(모집마감, 모집재개, 모임정보변경, 모임장소/시간 확정)는 방장 제외 모임참여자 전원에게 알린다.") + @Test + void getMoimModifiedNotificationRecipients() { + // given + Moim moim = MoimFixture.getCoffeeMoim(darakbang.getId()); + Moim savedMoim = moimRepository.save(moim); + Chamyo chamyoWithMoimerAnna = Chamyo.builder() + .moim(savedMoim) + .darakbangMember(darakbangAnna) + .moimRole(MoimRole.MOIMER) + .build(); + + Chamyo chamyoWithMoimeeHogee = Chamyo.builder() + .moim(savedMoim) + .darakbangMember(darakbangHogee) + .moimRole(MoimRole.MOIMEE) + .build(); + chamyoRepository.save(chamyoWithMoimerAnna); + chamyoRepository.save(chamyoWithMoimeeHogee); + + // when + List recipients = moimRecipientFinder.getMoimModifiedNotificationRecipients(savedMoim.getId()); + + //then + assertThat(recipients).hasSize(1); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/implement/finder/ZzimFinderTest.java b/backend/src/test/java/mouda/backend/moim/implement/finder/ZzimFinderTest.java new file mode 100644 index 000000000..b9b0e5234 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/implement/finder/ZzimFinderTest.java @@ -0,0 +1,53 @@ +package mouda.backend.moim.implement.finder; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.Zzim; +import mouda.backend.moim.infrastructure.MoimRepository; +import mouda.backend.moim.infrastructure.ZzimRepository; + +@SpringBootTest +class ZzimFinderTest extends DarakbangSetUp { + + @Autowired + private ZzimFinder zzimFinder; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private ZzimRepository zzimRepository; + + @DisplayName("찜한 모임의 경우 참을 반환한다.") + @Test + void isTrueWhenMoimZzimedByMember() { + Moim moim = moimRepository.save(MoimFixture.getSoccerMoim(darakbang.getId())); + + boolean isZzimed = zzimFinder.isMoimZzimedByMember(moim.getId(), darakbangAnna); + + assertThat(isZzimed).isFalse(); + } + + @DisplayName("찜하지 않은 모임의 경우 거짓을 반환한다.") + @Test + void isFalseWhenMoimNotZzimedByMember() { + Moim moim = moimRepository.save(MoimFixture.getSoccerMoim(darakbang.getId())); + Zzim zzim = Zzim.builder() + .darakbangMember(darakbangAnna) + .moim(moim) + .build(); + zzimRepository.save(zzim); + + boolean isZzimed = zzimFinder.isMoimZzimedByMember(moim.getId(), darakbangAnna); + + assertThat(isZzimed).isTrue(); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/implement/writer/ChamyoWriterTest.java b/backend/src/test/java/mouda/backend/moim/implement/writer/ChamyoWriterTest.java new file mode 100644 index 000000000..b4e8dd294 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/implement/writer/ChamyoWriterTest.java @@ -0,0 +1,103 @@ +package mouda.backend.moim.implement.writer; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.exception.ChamyoException; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +class ChamyoWriterTest extends DarakbangSetUp { + + @Autowired + private ChamyoWriter chamyoWriter; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private ChamyoRepository chamyoRepository; + + @DisplayName("모이머로 참여한다.") + @Test + void saveAsMoimer() { + Moim moim = moimRepository.save(MoimFixture.getBasketballMoim(darakbang.getId())); + Chamyo chamyo = chamyoWriter.saveAsMoimer(moim, darakbangHogee); + + assertThat(chamyo.getMoimRole()).isEqualTo(MoimRole.MOIMER); + } + + @DisplayName("모이미로 참여한다.") + @Test + void saveAsMoimee() { + Moim moim = moimRepository.save(MoimFixture.getBasketballMoim(darakbang.getId())); + Chamyo chamyo = chamyoWriter.saveAsMoimee(moim, darakbangHogee); + + assertThat(chamyo.getMoimRole()).isEqualTo(MoimRole.MOIMEE); + } + + @DisplayName("참여 인원이 만원인 경우 예외가 발생한다.") + @Test + void failToChamyoWhenMoimIsFull() { + Moim moim = moimRepository.save(MoimFixture.getAloneMoim(darakbang.getId())); + chamyoRepository.save(new Chamyo(moim, darakbangAnna, MoimRole.MOIMER)); + + assertThatThrownBy(() -> chamyoWriter.saveAsMoimee(moim, darakbangHogee)) + .isInstanceOf(ChamyoException.class); + } + + @DisplayName("참여가 취소된 경우 예외가 발생한다.") + @Test + void failToChamyoWhenMoimIsCancel() { + Moim moim = moimRepository.save(MoimFixture.getAloneMoim(darakbang.getId())); + moim.cancel(); + + assertThatThrownBy(() -> chamyoWriter.saveAsMoimee(moim, darakbangHogee)) + .isInstanceOf(ChamyoException.class); + } + + @DisplayName("참여가 모집 완료된 경우 예외가 발생한다.") + @Test + void failToChamyoWhenMoimIsCompleted() { + Moim moim = moimRepository.save(MoimFixture.getAloneMoim(darakbang.getId())); + moim.complete(); + + assertThatThrownBy(() -> chamyoWriter.saveAsMoimee(moim, darakbangHogee)) + .isInstanceOf(ChamyoException.class); + } + + @DisplayName("이미 참여한 모임인 경우 예외가 발생한다.") + @Test + void failToChamyoWhenAlreadyChamyo() { + Moim moim = moimRepository.save(MoimFixture.getAloneMoim(darakbang.getId())); + chamyoRepository.save(new Chamyo(moim, darakbangHogee, MoimRole.MOIMER)); + + assertThatThrownBy(() -> chamyoWriter.saveAsMoimer(moim, darakbangHogee)) + .isInstanceOf(ChamyoException.class); + } + + @DisplayName("참여한 모임을 취소한다.") + @Test + void delete() { + Moim moim = moimRepository.save(MoimFixture.getAloneMoim(darakbang.getId())); + Chamyo chamyo = chamyoRepository.save(new Chamyo(moim, darakbangHogee, MoimRole.MOIMEE)); + + chamyoWriter.delete(chamyo); + + Optional optionalChamyo = chamyoRepository.findByMoimIdAndDarakbangMemberId(moim.getId(), + darakbangHogee.getId()); + assertThat(optionalChamyo).isEmpty(); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/implement/writer/CommentWriterTest.java b/backend/src/test/java/mouda/backend/moim/implement/writer/CommentWriterTest.java new file mode 100644 index 000000000..e596d0470 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/implement/writer/CommentWriterTest.java @@ -0,0 +1,59 @@ +package mouda.backend.moim.implement.writer; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.CommentFixture; +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.exception.CommentErrorMessage; +import mouda.backend.moim.exception.CommentException; +import mouda.backend.moim.infrastructure.CommentRepository; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +class CommentWriterTest extends DarakbangSetUp { + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private CommentWriter commentWriter; + + @Autowired + private CommentRepository commentRepository; + + @DisplayName("댓글을 추가한다.") + @Test + void saveComment() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + assertThatCode(() -> commentWriter.saveComment(moim, darakbangHogee, null, "댓글")) + .doesNotThrowAnyException(); + } + + @DisplayName("답글을 추가한다.") + @Test + void saveReply() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + Comment parentComment = commentRepository.save(CommentFixture.getCommentWithAnnaAtSoccerMoim(darakbangHogee, moim)); + + assertThatCode(() -> commentWriter.saveComment(moim, darakbangHogee, parentComment.getId(), "답글")) + .doesNotThrowAnyException(); + } + + @DisplayName("답글을 추가할때 부모 댓글이 없으면 예외가 발생한다.") + @Test + void saveReply_whenParentCommentNotExist() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + + assertThatThrownBy(() -> commentWriter.saveComment(moim, darakbangHogee, 1L, "답글")) + .isInstanceOf(CommentException.class) + .hasMessage(CommentErrorMessage.PARENT_NOT_FOUND.getMessage()); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/implement/writer/MoimWriterTest.java b/backend/src/test/java/mouda/backend/moim/implement/writer/MoimWriterTest.java new file mode 100644 index 000000000..81f9c926b --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/implement/writer/MoimWriterTest.java @@ -0,0 +1,125 @@ +package mouda.backend.moim.implement.writer; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDate; +import java.time.LocalTime; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.domain.MoimStatus; +import mouda.backend.moim.exception.ChamyoException; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.MoimRepository; + +@SpringBootTest +class MoimWriterTest extends DarakbangSetUp { + + @Autowired + private MoimWriter moimWriter; + + @Autowired + private ChamyoRepository chamyoRepository; + + @Autowired + private MoimRepository moimRepository; + + @DisplayName("모집 인원이 다 차면 모집을 완료한다.") + @Test + void updateMoimStatusIfFull() { + Moim moim = moimRepository.save(MoimFixture.getAloneMoim(darakbang.getId())); + Chamyo chamyo = new Chamyo(moim, darakbangHogee, MoimRole.MOIMER); + chamyoRepository.save(chamyo); + + moimWriter.updateMoimStatusIfFull(moim); + + assertThat(moim.getMoimStatus()).isEqualTo(MoimStatus.COMPLETED); + } + + @DisplayName("모이머가 장소를 변경하면 성공한다.") + @Test + void confirmPlace() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + Chamyo chamyo = new Chamyo(moim, darakbangHogee, MoimRole.MOIMER); + chamyoRepository.save(chamyo); + + String place = "호기네 카페"; + moimWriter.confirmPlace(moim, darakbangHogee, place); + + assertThat(moim.getPlace()).isEqualTo(place); + } + + @DisplayName("모이머가 아닌 사람이 장소를 변경하면 실패한다.") + @Test + void failToConfirmPlaceWithoutMoimer() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + Chamyo chamyo = new Chamyo(moim, darakbangHogee, MoimRole.MOIMEE); + chamyoRepository.save(chamyo); + + assertThatThrownBy(() -> moimWriter.confirmPlace(moim, darakbangHogee, "호기네 카페")) + .isInstanceOf(ChamyoException.class); + } + + @DisplayName("모이머가 일정을 변경하면 성공한다.") + @Test + void confirmDateTime() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + Chamyo chamyo = new Chamyo(moim, darakbangHogee, MoimRole.MOIMER); + chamyoRepository.save(chamyo); + + LocalDate nowDate = LocalDate.now().plusDays(1); + LocalTime nowTime = LocalTime.now().plusHours(1); + moimWriter.confirmDateTime(moim, darakbangHogee, nowDate, nowTime); + + assertAll( + () -> assertThat(moim.getDate()).isEqualTo(nowDate), + () -> assertThat(moim.getTime()).isEqualTo(nowTime) + ); + } + + @DisplayName("모이머가 아닌 사람이 일정을 변경하면 실패한다.") + @Test + void failToConfirmDateTimeWithoutMoimer() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + Chamyo chamyo = new Chamyo(moim, darakbangHogee, MoimRole.MOIMEE); + chamyoRepository.save(chamyo); + + LocalDate nowDate = LocalDate.now().plusDays(1); + LocalTime nowTime = LocalTime.now().plusHours(1); + + assertThatThrownBy(() -> moimWriter.confirmDateTime(moim, darakbangHogee, nowDate, nowTime)) + .isInstanceOf(ChamyoException.class); + } + + @DisplayName("모이머는 채팅을 열 수 있다.") + @Test + void openChatRoom() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + Chamyo chamyo = new Chamyo(moim, darakbangHogee, MoimRole.MOIMER); + chamyoRepository.save(chamyo); + + moimWriter.openChatByMoimer(moim, darakbangHogee); + + assertThat(moim.isChatOpened()).isTrue(); + } + + @DisplayName("모이머가 아니라면 채팅을 열 수 없다.") + @Test + void failToOpenChatRoomWithoutMoimer() { + Moim moim = moimRepository.save(MoimFixture.getCoffeeMoim(darakbang.getId())); + Chamyo chamyo = new Chamyo(moim, darakbangHogee, MoimRole.MOIMEE); + chamyoRepository.save(chamyo); + + assertThatThrownBy(() -> moimWriter.openChatByMoimer(moim, darakbangHogee)) + .isInstanceOf(ChamyoException.class); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/implement/writer/ZzimWriterTest.java b/backend/src/test/java/mouda/backend/moim/implement/writer/ZzimWriterTest.java new file mode 100644 index 000000000..a7ed0ddbf --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/implement/writer/ZzimWriterTest.java @@ -0,0 +1,54 @@ +package mouda.backend.moim.implement.writer; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.Zzim; +import mouda.backend.moim.infrastructure.MoimRepository; +import mouda.backend.moim.infrastructure.ZzimRepository; + +@SpringBootTest +class ZzimWriterTest extends DarakbangSetUp { + + @Autowired + private ZzimWriter zzimWriter; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private ZzimRepository zzimRepository; + + @DisplayName("찜하지 않은 모임의 경우 찜을 생성한다.") + @Test + void createZzimIfAbsent() { + Moim moim = moimRepository.save(MoimFixture.getSoccerMoim(darakbang.getId())); + zzimWriter.updateZzimStatus(moim, darakbangAnna); + + assertThat(zzimRepository.findByMoimIdAndDarakbangMemberId(moim.getId(), darakbangAnna.getId())) + .isPresent(); + } + + @DisplayName("찜한 모임의 경우 찜을 삭제한다.") + @Test + void deleteZzimIfPresent() { + Moim moim = moimRepository.save(MoimFixture.getSoccerMoim(darakbang.getId())); + Zzim zzim = Zzim.builder() + .darakbangMember(darakbangAnna) + .moim(moim) + .build(); + zzimRepository.save(zzim); + + zzimWriter.updateZzimStatus(moim, darakbangAnna); + + assertThat(zzimRepository.findByMoimIdAndDarakbangMemberId(moim.getId(), darakbangAnna.getId())) + .isEmpty(); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/moim/business/CommentServiceTest.java b/backend/src/test/java/mouda/backend/moim/moim/business/CommentServiceTest.java new file mode 100644 index 000000000..667617724 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/moim/business/CommentServiceTest.java @@ -0,0 +1,94 @@ +package mouda.backend.moim.moim.business; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangFixture; +import mouda.backend.common.fixture.DarakbangMemberFixture; +import mouda.backend.common.fixture.MemberFixture; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.member.domain.Member; +import mouda.backend.member.infrastructure.MemberRepository; +import mouda.backend.moim.business.CommentService; +import mouda.backend.moim.domain.Chamyo; +import mouda.backend.moim.domain.Comment; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.domain.MoimRole; +import mouda.backend.moim.exception.CommentException; +import mouda.backend.moim.infrastructure.ChamyoRepository; +import mouda.backend.moim.infrastructure.CommentRepository; +import mouda.backend.moim.infrastructure.MoimRepository; +import mouda.backend.moim.presentation.request.comment.CommentCreateRequest; + +@SpringBootTest +public class CommentServiceTest { + + @Autowired + private DarakbangRepository darakbangRepository; + + @Autowired + private DarakbangMemberRepository darakbangMemberRepository; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private CommentService commentService; + + @Autowired + private CommentRepository commentRepository; + + private Darakbang darakbang; + private DarakbangMember darakbangHogee; + @Autowired + private ChamyoRepository chamyoRepository; + + @BeforeEach + void setUp() { + darakbang = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + Member hogee = memberRepository.save(MemberFixture.getHogee()); + darakbangHogee = darakbangMemberRepository.save( + DarakbangMemberFixture.getDarakbangMemberWithWooteco(darakbang, hogee)); + } + + @DisplayName("댓글을 생성한다.") + @Test + void createComment() { + Moim moim = moimRepository.save(MoimFixture.getBasketballMoim(darakbang.getId())); + chamyoRepository.save(Chamyo.builder() + .moim(moim) + .darakbangMember(darakbangHogee) + .moimRole(MoimRole.MOIMER) + .build()); + CommentCreateRequest commentCreateRequest = new CommentCreateRequest(null, "댓글부대"); + commentService.createComment(darakbang.getId(), moim.getId(), darakbangHogee, commentCreateRequest); + + List comments = commentRepository.findAllByMoimOrderByCreatedAt(moim); + assertThat(comments).hasSize(1); + } + + @DisplayName("부모 댓글이 없이 대댓글을 생성 시 예외가 발생한다.") + @Test + void failToCreateChildCommentWhenParentCommentDoesNotExist() { + Moim moim = moimRepository.save(MoimFixture.getBasketballMoim(darakbang.getId())); + CommentCreateRequest commentCreateRequest = new CommentCreateRequest(1L, "댓글부대"); + + assertThrows(CommentException.class, + () -> commentService.createComment(darakbang.getId(), moim.getId(), darakbangHogee, commentCreateRequest)); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/moim/business/MoimServiceTest.java b/backend/src/test/java/mouda/backend/moim/moim/business/MoimServiceTest.java new file mode 100644 index 000000000..76eb28d3f --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/moim/business/MoimServiceTest.java @@ -0,0 +1,116 @@ +package mouda.backend.moim.moim.business; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDate; +import java.time.LocalTime; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangFixture; +import mouda.backend.common.fixture.DarakbangMemberFixture; +import mouda.backend.common.fixture.MemberFixture; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.darakbang.domain.Darakbang; +import mouda.backend.darakbang.infrastructure.DarakbangRepository; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +import mouda.backend.member.domain.Member; +import mouda.backend.member.infrastructure.MemberRepository; +import mouda.backend.moim.business.MoimService; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.infrastructure.MoimRepository; +import mouda.backend.moim.presentation.request.moim.MoimCreateRequest; +import mouda.backend.moim.presentation.response.moim.MoimDetailsFindResponse; +import mouda.backend.moim.presentation.response.moim.MoimFindAllResponses; + +@SpringBootTest +class MoimServiceTest { + + @Autowired + private MoimService moimService; + + @Autowired + private DarakbangRepository darakbangRepository; + + @Autowired + private DarakbangMemberRepository darakbangMemberRepository; + + @Autowired + private MoimRepository moimRepository; + + @Autowired + private MemberRepository memberRepository; + + private Darakbang darakbang; + private Darakbang mouda; + private DarakbangMember darakbangHogee; + private DarakbangMember moudaHogee; + + @BeforeEach + void setUp() { + darakbang = darakbangRepository.save(DarakbangFixture.getDarakbangWithWooteco()); + mouda = darakbangRepository.save(DarakbangFixture.getDarakbangWithMouda()); + Member hogee = memberRepository.save(MemberFixture.getHogee()); + darakbangHogee = darakbangMemberRepository.save( + DarakbangMemberFixture.getDarakbangMemberWithWooteco(darakbang, hogee)); + moudaHogee = darakbangMemberRepository.save( + DarakbangMemberFixture.getDarakbangMemberWithWooteco(mouda, hogee)); + } + + @DisplayName("모임을 생성한다.") + @Test + void createMoim() { + MoimCreateRequest moimCreateRequest = new MoimCreateRequest( + "title", LocalDate.now().plusDays(1), LocalTime.now(), "place", + 10, "설명" + ); + + Moim moim = moimService.createMoim(darakbang.getId(), darakbangHogee, moimCreateRequest); + + assertThat(moim.getId()).isEqualTo(1L); + } + + @DisplayName("모임을 전체 조회한다.") + @Test + void findAllMoim() { + MoimCreateRequest moimCreateRequest = new MoimCreateRequest( + "title", LocalDate.now().plusDays(1), LocalTime.now(), "place", + 10, "설명" + ); + + moimService.createMoim(darakbang.getId(), darakbangHogee, moimCreateRequest); + moimService.createMoim(darakbang.getId(), darakbangHogee, moimCreateRequest); + + MoimFindAllResponses moimResponses = moimService.findAllMoim(darakbang.getId(), darakbangHogee); + + assertThat(moimResponses.moims()).hasSize(2); + } + + @DisplayName("모임 상세를 조회한다.") + @Test + void findMoimDetails() { + MoimCreateRequest moimCreateRequest = new MoimCreateRequest( + "title", LocalDate.now().plusDays(1), LocalTime.now(), "place", + 10, "설명" + ); + moimService.createMoim(darakbang.getId(), darakbangHogee, moimCreateRequest); + + MoimDetailsFindResponse moimDetails = moimService.findMoimDetails(darakbang.getId(), 1L); + + assertThat(moimDetails.title()).isEqualTo("title"); + } + + @DisplayName("다락방별 모임을 조회한다.") + @Test + void success() { + moimRepository.save(MoimFixture.getBasketballMoim(darakbang.getId())); + + assertThat(moimService.findAllMoim(darakbang.getId(), darakbangHogee).moims()).hasSize(1); + assertThat(moimService.findAllMoim(mouda.getId(), moudaHogee).moims()).hasSize(0); + } +} diff --git a/backend/src/test/java/mouda/backend/moim/moim/domain/MoimTest.java b/backend/src/test/java/mouda/backend/moim/moim/domain/MoimTest.java new file mode 100644 index 000000000..f45076a59 --- /dev/null +++ b/backend/src/test/java/mouda/backend/moim/moim/domain/MoimTest.java @@ -0,0 +1,287 @@ +package mouda.backend.moim.moim.domain; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.MoimFixture; +import mouda.backend.moim.domain.Moim; +import mouda.backend.moim.exception.MoimException; + +class MoimTest extends DarakbangSetUp { + + private static final String TITLE = "이번 주에 축구하실 분 구함"; + private static final LocalDate DATE = LocalDate.now().plusDays(1); + private static final LocalTime TIME = LocalTime.now().plusHours(1); + private static final String PLACE = "서울시 동작구 강원대로 10길 5"; + private static final int MAX_PEOPLE = 11; + private static final String DESCRIPTION = "이번 주 금요일에 퇴근하고 축구하실 분 계신가요? 끝나고 치맥도 할 생각입니다."; + + @DisplayName("모임 객체를 정상적으로 생성한다.") + @Test + void createMoim() { + Assertions.assertDoesNotThrow(() -> MoimFixture.getBasketballMoim(darakbang.getId())); + } + + @DisplayName("제목 길이가 제한을 초과하면 모임 객체 생성에 실패한다.") + @Test + void failToCreateMoimWhenTitleIsTooLong() { + String longTitle = "a".repeat(31); + assertThrows(MoimException.class, () -> Moim.builder() + .title(longTitle) + .date(DATE) + .time(TIME) + .place(PLACE) + .maxPeople(MAX_PEOPLE) + .description(DESCRIPTION) + .build()); + } + + @DisplayName("제목이 빈 문자열이면 모임 객체 생성에 실패한다.") + @Test + void failToCreateMoimWhenTitleDoesNotExists() { + assertThrows(MoimException.class, () -> Moim.builder() + .title("") + .date(DATE) + .time(TIME) + .place(PLACE) + .maxPeople(MAX_PEOPLE) + .description(DESCRIPTION) + .build()); + } + + @DisplayName("모임 날짜가 현재보다 과거이면 모임 객체 생성에 실패한다.") + @Test + void failToCreateMoimWhenDateIsPast() { + assertThrows(MoimException.class, () -> Moim.builder() + .title(TITLE) + .date(LocalDate.now().minusDays(1)) + .time(LocalTime.now().plusHours(1)) + .place(PLACE) + .maxPeople(MAX_PEOPLE) + .description(DESCRIPTION) + .build()); + } + + @DisplayName("날짜는 같고, 시간이 현재보다 과거이면 모임 객체 생성에 실패한다.") + @Test + void failToCreateMoimWhenTimeIsPast() { + LocalDateTime oneHourBefore = LocalDateTime.of(LocalDate.of(2024, 10, 24), LocalTime.of(0, 50)).minusHours(1); + + assertThrows(MoimException.class, () -> Moim.builder() + .title(TITLE) + .date(oneHourBefore.toLocalDate()) + .time(oneHourBefore.toLocalTime()) + .place(PLACE) + .maxPeople(MAX_PEOPLE) + .description(DESCRIPTION) + .build()); + } + + @DisplayName("장소가 빈 문자열이면 모임 객체 생성에 실패한다.") + @Test + void failToCreateMoimWhenPlaceIsBlank() { + assertThrows(MoimException.class, () -> Moim.builder() + .title(TITLE) + .date(LocalDate.now()) + .time(LocalTime.now().minusHours(1)) + .place("") + .maxPeople(MAX_PEOPLE) + .description(DESCRIPTION) + .build()); + } + + @DisplayName("장소 길이가 제한을 초과하면 모임 객체 생성에 실패한다.") + @Test + void failToCreateMoimWhenPlaceIsTooLong() { + String longPlace = "a".repeat(101); + assertThrows(MoimException.class, () -> Moim.builder() + .title(TITLE) + .date(DATE) + .time(TIME) + .place(longPlace) + .maxPeople(MAX_PEOPLE) + .description(DESCRIPTION) + .build()); + } + + @DisplayName("모임 최대 인원이 0보다 작으면 모임 객체 생성에 실패한다.") + @Test + void failToCreateMoimWhenMaxPeopleIsTooSmall() { + assertThrows(MoimException.class, () -> Moim.builder() + .title(TITLE) + .date(DATE) + .time(TIME) + .place(PLACE) + .maxPeople(-1) + .description(DESCRIPTION) + .build()); + } + + @DisplayName("모임 최대 인원이 제한을 초과하면 모임 객체 생성에 실패한다.") + @Test + void failToCreateMoimWhenMaxPeopleIsTooMany() { + assertThrows(MoimException.class, () -> Moim.builder() + .title(TITLE) + .date(DATE) + .time(TIME) + .place(PLACE) + .maxPeople(100) + .description(DESCRIPTION) + .build()); + } + + @DisplayName("설명이 null이면 모임 객체 생성에 성공한다.") + @Test + void createMoimWhenDescriptionIsNull() { + assertDoesNotThrow(() -> Moim.builder() + .title(TITLE) + .date(DATE) + .time(TIME) + .place(PLACE) + .maxPeople(MAX_PEOPLE) + .description(null) + .build()); + } + + @DisplayName("설명의 길이가 길면 모임 객체 생성에 실패한다.") + @Test + void failToCreateMoimWhenDescriptionIsTooLong() { + String longDescription = "a".repeat(1001); + assertThrows(MoimException.class, () -> Moim.builder() + .title(TITLE) + .date(DATE) + .time(TIME) + .place(PLACE) + .maxPeople(MAX_PEOPLE) + .description(longDescription) + .build()); + } + + @Nested + @DisplayName("모임 수정 테스트") + class UpdateMoimTest { + + private Moim moim = Moim.builder() + .title(TITLE) + .date(DATE) + .time(TIME) + .place(PLACE) + .maxPeople(MAX_PEOPLE) + .description(DESCRIPTION) + .build(); + + @DisplayName("날짜, 시간, 장소가 없어도 수정할 수 있다.") + @Test + void success() { + assertDoesNotThrow( + () -> moim.update(TITLE, null, null, null, MAX_PEOPLE, null, MAX_PEOPLE + 1)); + } + + @DisplayName("최대 길이를 초과하는 제목으로는 수정할 수 없다.") + @Test + void fail_whenTitleIsTooLong() { + String longTitle = "a".repeat(31); + assertThrows(MoimException.class, + () -> moim.update(longTitle, DATE, TIME, PLACE, MAX_PEOPLE, DESCRIPTION, MAX_PEOPLE + 1)); + } + + @DisplayName("제목이 빈 문자열이면 수정할 수 없다.") + @Test + void fail_whenTitleDoesNotExists() { + assertThrows(MoimException.class, + () -> moim.update("", DATE, TIME, PLACE, MAX_PEOPLE, DESCRIPTION, MAX_PEOPLE + 1)); + } + + @DisplayName("모임 날짜가 현재보다 과거이면 수정할 수 없다.") + @Test + void fail_whenDateIsPast() { + assertThrows(MoimException.class, + () -> moim.update(TITLE, LocalDate.now().minusDays(1), TIME, PLACE, MAX_PEOPLE, DESCRIPTION, + MAX_PEOPLE + 1)); + } + + @DisplayName("날짜는 같고, 시간이 현재보다 과거이면 수정할 수 없다.") + @Test + void fail_whenTimeIsPast() { + LocalDateTime oneHourBefore = LocalDateTime.of(LocalDate.of(2024, 10, 24), LocalTime.of(0, 50)) + .minusHours(1); + + assertThrows(MoimException.class, + () -> moim.update(TITLE, oneHourBefore.toLocalDate(), oneHourBefore.toLocalTime(), PLACE, MAX_PEOPLE, + DESCRIPTION, MAX_PEOPLE + 1)); + } + + @DisplayName("장소가 빈 문자열이면 수정할 수 없다.") + @Test + void fail_whenPlaceIsBlank() { + assertThrows(MoimException.class, + () -> moim.update(TITLE, DATE, TIME, "", MAX_PEOPLE, DESCRIPTION, MAX_PEOPLE + 1)); + } + + @DisplayName("장소 길이가 제한을 초과하면 수정할 수 없다.") + @Test + void fail_whenPlaceIsTooLong() { + String longPlace = "a".repeat(101); + assertThrows(MoimException.class, + () -> moim.update(TITLE, DATE, TIME, longPlace, MAX_PEOPLE, DESCRIPTION, MAX_PEOPLE + 1)); + } + + @DisplayName("모임 최대 인원이 1보다 작으면 수정할 수 없다.") + @Test + void fail_whenMaxPeopleIsTooSmall() { + assertThrows(MoimException.class, + () -> moim.update(TITLE, DATE, TIME, PLACE, 0, DESCRIPTION, MAX_PEOPLE + 1)); + } + + @DisplayName("모임 최대 인원이 제한을 초과하면 수정할 수 없다.") + @Test + void fail_whenMaxPeopleIsTooMany() { + assertThrows(MoimException.class, + () -> moim.update(TITLE, DATE, TIME, PLACE, 100, DESCRIPTION, MAX_PEOPLE + 1)); + } + + @DisplayName("모임 최대 인원이 현재 참여 인원보다 적으면 수정할 수 없다.") + @Test + void fail_whenMaxPeopleIsLowerThanCurrentPeople() { + assertThrows(MoimException.class, + () -> moim.update(TITLE, DATE, TIME, PLACE, MAX_PEOPLE - 1, DESCRIPTION, MAX_PEOPLE)); + } + + @DisplayName("설명의 길이가 길면 수정할 수 없다.") + @Test + void fail_whenDescriptionIsTooLong() { + String longDescription = "a".repeat(1001); + assertThrows(MoimException.class, + () -> moim.update(TITLE, DATE, TIME, PLACE, MAX_PEOPLE, longDescription, MAX_PEOPLE + 1)); + } + + @DisplayName("모임 객체를 수정한다.") + @Test + void updateMoim() { + String newTitle = "축구 모집합니다."; + LocalDate newDate = LocalDate.now().plusDays(2); + LocalTime newTime = LocalTime.now().plusHours(2); + String newPlace = "서울시 강남구 강남대로 10길 5"; + int newMaxPeople = MAX_PEOPLE + 1; + String newDescription = "축구하실 분 구합니다."; + + moim.update(newTitle, newDate, newTime, newPlace, newMaxPeople, newDescription, MAX_PEOPLE); + + assertEquals(newTitle, moim.getTitle()); + assertEquals(newDate, moim.getDate()); + assertEquals(newTime, moim.getTime()); + assertEquals(newPlace, moim.getPlace()); + assertEquals(newMaxPeople, moim.getMaxPeople()); + assertEquals(newDescription, moim.getDescription()); + } + } +} diff --git a/backend/src/test/java/mouda/backend/notification/business/NotificationServiceTest.java b/backend/src/test/java/mouda/backend/notification/business/NotificationServiceTest.java new file mode 100644 index 000000000..7f777f545 --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/business/NotificationServiceTest.java @@ -0,0 +1,100 @@ +// package mouda.backend.notification.business; +// +// import static org.assertj.core.api.Assertions.*; +// +// import java.util.List; +// +// import org.junit.jupiter.api.DisplayName; +// import org.junit.jupiter.api.Test; +// import org.springframework.beans.factory.annotation.Autowired; +// import org.springframework.boot.test.context.SpringBootTest; +// +// import mouda.backend.common.fixture.DarakbangFixture; +// import mouda.backend.common.fixture.DarakbangMemberFixture; +// import mouda.backend.common.fixture.MemberFixture; +// import mouda.backend.darakbang.domain.Darakbang; +// import mouda.backend.darakbang.infrastructure.DarakbangRepository; +// import mouda.backend.darakbangmember.domain.DarakbangMember; +// import mouda.backend.darakbangmember.infrastructure.DarakbangMemberRepository; +// import mouda.backend.member.domain.Member; +// import mouda.backend.member.infrastructure.MemberRepository; +// import mouda.backend.notification.domain.MemberNotification; +// import mouda.backend.notification.domain.MoudaNotification; +// import mouda.backend.notification.domain.NotificationType; +// import mouda.backend.notification.infrastructure.MemberNotificationRepository; +// import mouda.backend.notification.infrastructure.MoudaNotificationRepository; +// import mouda.backend.notification.presentation.response.NotificationFindAllResponse; +// +// @SpringBootTest +// class NotificationServiceTest { +// +// @Autowired +// private DarakbangRepository darakbangRepository; +// +// @Autowired +// private MemberRepository memberRepository; +// +// @Autowired +// private DarakbangMemberRepository darakbangMemberRepository; +// +// @Autowired +// private MoudaNotificationRepository moudaNotificationRepository; +// +// @Autowired +// private MemberNotificationRepository memberNotificationRepository; +// +// @Autowired +// private NotificationService notificationService; +// +// @DisplayName("회원의 모든 알림을 조회한다.") +// @Test +// void findAllMyNotifications() { +// Darakbang darakbang = DarakbangFixture.getDarakbangWithMouda(); +// darakbangRepository.save(darakbang); +// +// NotificationType type1 = NotificationType.MOIM_CREATED; +// MoudaNotification notification1 = moudaNotificationRepository.save(MoudaNotification.builder() +// .type(type1) +// .body(type1.createMessage("테스트모임")) +// .targetUrl("test") +// .build()); +// +// NotificationType type2 = NotificationType.NEW_CHAT; +// MoudaNotification notification2 = moudaNotificationRepository.save(MoudaNotification.builder() +// .type(type2) +// .body(type2.createMessage("상돌")) +// .targetUrl("test") +// .build()); +// +// Member member = MemberFixture.getAnna(); +// memberRepository.save(member); +// +// DarakbangMember darakbangMember = DarakbangMemberFixture.getDarakbangMemberWithWooteco(darakbang, +// member); +// darakbangMemberRepository.save(darakbangMember); +// +// memberNotificationRepository.save(MemberNotification.builder() +// .memberId(darakbangMember.getMemberId()) +// .moudaNotification(notification1) +// .darakbangId(darakbang.getId()) +// .build()); +// +// memberNotificationRepository.save(MemberNotification.builder() +// .memberId(darakbangMember.getMemberId()) +// .moudaNotification(notification2) +// .darakbangId(darakbang.getId()) +// .build()); +// +// List responses = notificationService.findAllMyNotifications(member, +// darakbang.getId()) +// .notifications(); +// +// assertThat(responses).satisfies(res -> { +// assertThat(res).hasSize(2); +// assertThat(res).extracting(NotificationFindAllResponse::message) +// .containsExactly(type2.createMessage("상돌"), type1.createMessage("테스트모임")); +// assertThat(res).extracting(NotificationFindAllResponse::type) +// .containsExactly(type2.toString(), type1.toString()); +// }); +// } +// } diff --git a/backend/src/test/java/mouda/backend/notification/domain/NotificationSendEventTest.java b/backend/src/test/java/mouda/backend/notification/domain/NotificationSendEventTest.java new file mode 100644 index 000000000..a73c35b1a --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/domain/NotificationSendEventTest.java @@ -0,0 +1,66 @@ +package mouda.backend.notification.domain; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import mouda.backend.notification.exception.NotificationException; + +class NotificationSendEventTest { + + @DisplayName("알림 타입이 채팅이라면 다락방과 채팅방 ID가 null이 아니어야 한다.") + @Test + void fromPayload() { + NotificationPayload payload = NotificationPayload.createChatPayload( + NotificationType.MOIM_PLACE_CONFIRMED, + "모임 제목", + "메시지", + "url", + List.of(), + 1L, + 1L + ); + + assertThatCode(() -> NotificationSendEvent.from(payload)).doesNotThrowAnyException(); + + NotificationSendEvent event = NotificationSendEvent.from(payload); + assertThat(event).isNotNull(); + } + + @DisplayName("알림 타입이 채팅일 때 다락방 ID가 null이면 예외를 던진다.") + @Test + void nullDarakbangId() { + NotificationPayload payload = NotificationPayload.createChatPayload( + NotificationType.MOIM_PLACE_CONFIRMED, + "모임 제목", + "메시지", + "url", + List.of(), + null, + 1L + ); + + assertThatThrownBy(() -> NotificationSendEvent.from(payload)) + .isInstanceOf(NotificationException.class); + } + + @DisplayName("알림 타입이 채팅일 때 채팅방 ID가 null이면 예외를 던진다.") + @Test + void nullChatRoomId() { + NotificationPayload payload = NotificationPayload.createChatPayload( + NotificationType.MOIM_PLACE_CONFIRMED, + "모임 제목", + "메시지", + "url", + List.of(), + 1L, + null + ); + + assertThatThrownBy(() -> NotificationSendEvent.from(payload)) + .isInstanceOf(NotificationException.class); + } +} diff --git a/backend/src/test/java/mouda/backend/notification/implement/NotificationAsyncTest.java b/backend/src/test/java/mouda/backend/notification/implement/NotificationAsyncTest.java new file mode 100644 index 000000000..1f0f83869 --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/implement/NotificationAsyncTest.java @@ -0,0 +1,75 @@ +package mouda.backend.notification.implement; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.domain.NotificationPayload; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.infrastructure.entity.MemberNotificationEntity; +import mouda.backend.notification.infrastructure.repository.MemberNotificationRepository; + +@SpringBootTest +public class NotificationAsyncTest extends DarakbangSetUp { + + @Autowired + private NotificationProcessor notificationProcessor; + + @Autowired + private MemberNotificationRepository notificationRepository; + + @MockBean + private NotificationSender notificationSender; + + @DisplayName("알림 전송 과정에서 예외가 발생해도 회원의 알림은 저장된다.") + @Test + void asyncWhenNotificationSend() { + // given + String title = "비동기 확인 ~"; + String body = "비동기동비"; + NotificationPayload payload = NotificationPayload.createNonChatPayload( + NotificationType.MOIM_CREATED, + title, + body, + "url", + List.of(Recipient.builder() + .memberId(darakbangAnna.getId()) + .darakbangMemberId(darakbangAnna.getId()) + .build()) + ); + + // when + doThrow(new RuntimeException("삐용12")) + .when(notificationSender).sendNotification(any(CommonNotification.class), anyList()); + + notificationProcessor.process(payload); + + // then + Optional notificationOptional = notificationRepository.findById( + getNotificationId(title, body)); + assertThat(notificationOptional).isNotEmpty(); + + MemberNotificationEntity notification = notificationOptional.get(); + assertThat(notification.getTitle()).isEqualTo(title); + assertThat(notification.getBody()).isEqualTo(body); + } + + private long getNotificationId(String title, String body) { + return notificationRepository.findAll().stream() + .filter(notification -> notification.getTitle().equals(title) && notification.getBody().equals(body)) + .findFirst() + .orElseThrow(IllegalArgumentException::new) + .getId(); + } +} diff --git a/backend/src/test/java/mouda/backend/notification/implement/NotificationFinderTest.java b/backend/src/test/java/mouda/backend/notification/implement/NotificationFinderTest.java new file mode 100644 index 000000000..053628f63 --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/implement/NotificationFinderTest.java @@ -0,0 +1,54 @@ +package mouda.backend.notification.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.darakbangmember.domain.DarakbangMember; +import mouda.backend.notification.domain.MemberNotification; +import mouda.backend.notification.infrastructure.entity.MemberNotificationEntity; +import mouda.backend.notification.infrastructure.repository.MemberNotificationRepository; + +@SpringBootTest +class NotificationFinderTest extends DarakbangSetUp { + + @Autowired + private NotificationFinder notificationFinder; + + @Autowired + private MemberNotificationRepository memberNotificationRepository; + + @DisplayName("회원의 모든 알림을 조회한다.") + @Test + void findAllMemberNotification() { + // given + memberNotificationRepository.saveAll(createTestEntity(darakbangHogee, 10)); + + // when + List result = notificationFinder.findAllMemberNotification(darakbangHogee); + + // then + assertThat(result).hasSize(10); + } + + private List createTestEntity(DarakbangMember darakbangMember, int count) { + return IntStream.rangeClosed(1, count) + .mapToObj(i -> MemberNotificationEntity.builder() + .darakbangMemberId(darakbangMember.getId()) + .title("title" + i) + .body("body" + i) + .createdAt(LocalDateTime.now()) + .type("MOIM_CREATED") + .targeturl("targeturl" + i) + .build()) + .toList(); + } +} diff --git a/backend/src/test/java/mouda/backend/notification/implement/fcm/FcmMessageFactoryTest.java b/backend/src/test/java/mouda/backend/notification/implement/fcm/FcmMessageFactoryTest.java new file mode 100644 index 000000000..f2e207a4a --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/implement/fcm/FcmMessageFactoryTest.java @@ -0,0 +1,116 @@ +package mouda.backend.notification.implement.fcm; + +import static org.assertj.core.api.Assertions.*; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import com.google.firebase.messaging.MulticastMessage; +import com.google.firebase.messaging.WebpushConfig; + +import mouda.backend.notification.domain.CommonNotification; +import mouda.backend.notification.domain.NotificationType; + +@SpringBootTest +class FcmMessageFactoryTest { + + @Autowired + private FcmMessageFactory fcmMessageFactory; + + @DisplayName("토큰 500개당 1개의 메시지가 생성된다.") + @Test + void createMessage() { + // given + CommonNotification notification = CommonNotification.builder() + .title("title") + .body("body") + .redirectUrl("redirectUrl") + .type(NotificationType.MOIM_CREATED) + .build(); + int tokenCount = 5196; + List tokens = createTokens(tokenCount); + + // when + List messages = fcmMessageFactory.createMessage(notification, tokens); + int expectedSize = calculateExpectedSize(tokenCount); + + // then + assertThat(messages).hasSize(expectedSize); + } + + @DisplayName("토큰이 500개 미만일 때는 1개의 메시지가 생성된다.") + @Test + void createMessage_WhenTokenCountIsLessThan500() { + // given + CommonNotification notification = CommonNotification.builder() + .title("title") + .body("body") + .redirectUrl("redirectUrl") + .type(NotificationType.MOIM_CREATED) + .build(); + int tokenCount = 499; + List tokens = createTokens(tokenCount); + + // when + List messages = fcmMessageFactory.createMessage(notification, tokens); + + // then + assertThat(messages).hasSize(1); + } + + @DisplayName("각 플랫폼별 설정이 반영된 메시지가 생성된다.") + @Test + void createMessage_WithConfig() { + CommonNotification notification = CommonNotification.builder() + .title("title") + .body("body") + .redirectUrl("redirectUrl") + .type(NotificationType.MOIM_CREATED) + .build(); + List tokens = createTokens(1001); + + List messages = fcmMessageFactory.createMessage(notification, tokens); + List allWebpushConfigs = messages.stream() + .map(message -> getFieldFromMulticastMessage("webpushConfig", WebpushConfig.class, message)) + .toList(); + + assertThat(allWebpushConfigs).doesNotContainNull(); + + // WebpushConfig 는 static 이므로 모든 객체가 동일한 참조를 가져야 한다. + assertThat(allWebpushConfigs).allMatch(webpushConfig -> webpushConfig == allWebpushConfigs.get(0)); + } + + + private List createTokens(int size) { + return IntStream.rangeClosed(1, size) + .mapToObj(i -> "token" + i) + .toList(); + } + + private int calculateExpectedSize(int tokenCount) { + int quotient = (int) tokenCount / 500; + int remainder = tokenCount % 500; + + if (remainder == 0) { + return quotient; + } + return quotient + 1; + } + + private T getFieldFromMulticastMessage(String name, Class type, MulticastMessage message) { + try { + Field field = message.getClass().getDeclaredField(name); + field.setAccessible(true); + + return type.cast(field.get(message)); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/backend/src/test/java/mouda/backend/notification/implement/fcm/token/FcmTokenWriterTest.java b/backend/src/test/java/mouda/backend/notification/implement/fcm/token/FcmTokenWriterTest.java new file mode 100644 index 000000000..eea42aa5d --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/implement/fcm/token/FcmTokenWriterTest.java @@ -0,0 +1,91 @@ +package mouda.backend.notification.implement.fcm.token; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.notification.infrastructure.entity.FcmTokenEntity; +import mouda.backend.notification.infrastructure.repository.FcmTokenRepository; + +@SpringBootTest +class FcmTokenWriterTest extends DarakbangSetUp { + + @Autowired + private FcmTokenRepository fcmTokenRepository; + + @Autowired + private FcmTokenWriter fcmTokenWriter; + + @DisplayName("토큰 등록 / 갱신 테스트") + @Nested + class SaveOrRefreshTest { + + @DisplayName("토큰이 존재하지 않는 경우 새로 등록한다.") + @Test + void saveToken() { + // given + String token = "testToken"; + + // when + fcmTokenWriter.saveOrRefresh(hogee, token); + + // then + List results = fcmTokenRepository.findAllByMemberId(darakbangHogee.getMemberId()); + + assertThat(results).hasSize(1); + assertThat(results).extracting(FcmTokenEntity::getToken).containsExactly(token); + } + + @DisplayName("토큰이 존재하는 경우 갱신한다.") + @Test + void refreshToken() { + // given + FcmTokenEntity existToken = fcmTokenRepository.save(FcmTokenEntity.builder() + .memberId(darakbangHogee.getMemberId()) + .token("testToken") + .build()); + + // when + fcmTokenWriter.saveOrRefresh(hogee, existToken.getToken()); + + // then + List results = fcmTokenRepository.findAllByMemberId(darakbangHogee.getMemberId()); + assertThat(results).hasSize(1); + + FcmTokenEntity result = results.get(0); + assertThat(result.getLastUpdated()).isAfter(existToken.getLastUpdated()); + } + } + + @DisplayName("입력된 모든 토큰을 삭제한다.") + @Test + void deleteAllTest() { + // given + String token1 = "token1"; + String token2 = "token2"; + + fcmTokenRepository.save(FcmTokenEntity.builder() + .memberId(darakbangHogee.getMemberId()) + .token(token1) + .build()); + + fcmTokenRepository.save(FcmTokenEntity.builder() + .memberId(darakbangAnna.getMemberId()) + .token(token2) + .build()); + + // when + List tokens = List.of(token1, token2); + fcmTokenWriter.deleteAll(tokens); + + // then + assertThat(fcmTokenRepository.findAll()).isEmpty(); + } +} diff --git a/backend/src/test/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilterTest.java b/backend/src/test/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilterTest.java new file mode 100644 index 000000000..aafb3d680 --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/implement/filter/ChatRoomSubscriptionFilterTest.java @@ -0,0 +1,107 @@ +package mouda.backend.notification.implement.filter; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.notification.domain.NotificationPayload; +import mouda.backend.notification.domain.NotificationSendEvent; +import mouda.backend.notification.domain.NotificationType; +import mouda.backend.notification.domain.Recipient; +import mouda.backend.notification.implement.subscription.SubscriptionWriter; + +@SpringBootTest +class ChatRoomSubscriptionFilterTest extends DarakbangSetUp { + + @Autowired + private ChatRoomSubscriptionFilter chatRoomSubscriptionFilter; + + @Autowired + private SubscriptionWriter subscriptionWriter; + + @DisplayName("채팅 알림을 허용하지 않아도 확정 채팅인 경우에는 알림을 받는다.") + @Test + void filter_WhenTypeIsConfirmed() { + // given + subscriptionWriter.changeChatRoomSubscription(hogee, darakbang.getId(), 1L); + + // when + NotificationPayload payload = NotificationPayload.createChatPayload( + NotificationType.MOIM_PLACE_CONFIRMED, + "모임 제목", + "메시지", + "url", + List.of(Recipient.builder() + .memberId(hogee.getId()) + .darakbangMemberId(darakbangHogee.getId()) + .build() + ), + darakbang.getId(), + 1L + ); + NotificationSendEvent notificationSendEvent = NotificationSendEvent.from(payload); + + // then + List filteredRecipient = chatRoomSubscriptionFilter.filter(notificationSendEvent); + assertThat(filteredRecipient).hasSize(1); + assertThat(filteredRecipient).extracting(Recipient::getMemberId).containsExactly(darakbangHogee.getMemberId()); + } + + @DisplayName("채팅 알림을 허용하지 않는 경우에는 알림을 받지 않는다.") + @Test + void filter_WhenUnsubscribed() { + // given + subscriptionWriter.changeChatRoomSubscription(hogee, darakbang.getId(), 1L); + + // when + NotificationPayload payload = NotificationPayload.createChatPayload( + NotificationType.NEW_CHAT, + "모임 제목", + "메시지", + "url", + List.of(Recipient.builder() + .memberId(hogee.getId()) + .darakbangMemberId(darakbangHogee.getId()) + .build() + ), + darakbang.getId(), + 1L + ); + NotificationSendEvent notificationSendEvent = NotificationSendEvent.from(payload); + + // then + List filteredRecipient = chatRoomSubscriptionFilter.filter(notificationSendEvent); + assertThat(filteredRecipient).isEmpty(); + } + + @DisplayName("채팅 알림을 허용하는 경우에는 알림을 받는다.") + @Test + void filter_WhenSubscribed() { + // when + NotificationPayload payload = NotificationPayload.createChatPayload( + NotificationType.NEW_CHAT, + "모임 제목", + "메시지", + "url", + List.of(Recipient.builder() + .memberId(hogee.getId()) + .darakbangMemberId(darakbangHogee.getId()) + .build() + ), + darakbang.getId(), + 1L + ); + NotificationSendEvent notificationSendEvent = NotificationSendEvent.from(payload); + + // then + List filteredRecipient = chatRoomSubscriptionFilter.filter(notificationSendEvent); + assertThat(filteredRecipient).hasSize(1); + assertThat(filteredRecipient).extracting(Recipient::getMemberId).containsExactly(darakbangHogee.getMemberId()); + } +} diff --git a/backend/src/test/java/mouda/backend/notification/implement/subscription/SubscriptionFinderTest.java b/backend/src/test/java/mouda/backend/notification/implement/subscription/SubscriptionFinderTest.java new file mode 100644 index 000000000..e7638d8d6 --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/implement/subscription/SubscriptionFinderTest.java @@ -0,0 +1,57 @@ +package mouda.backend.notification.implement.subscription; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.notification.domain.Subscription; +import mouda.backend.notification.infrastructure.entity.SubscriptionEntity; +import mouda.backend.notification.infrastructure.entity.UnsubscribedChatRooms; +import mouda.backend.notification.infrastructure.repository.SubscriptionRepository; + +@SpringBootTest +class SubscriptionFinderTest extends DarakbangSetUp { + + @Autowired + private SubscriptionWriter subscriptionWriter; + + @Autowired + private SubscriptionFinder subscriptionFinder; + + @Autowired + private SubscriptionRepository subscriptionRepository; + + @DisplayName("구독이 존재하지 않으면 새로 생성한 뒤 반환한다.") + @Test + void readSubscription_WhenSubscriptionNotExist() { + Subscription subscription = subscriptionFinder.readSubscription(hogee); + + assertThat(subscription.getUnsubscribedChatRooms()).isEmpty(); + assertThat(subscription.isSubscribedMoimCreate()).isTrue(); + } + + @DisplayName("기존 구독 정보가 조회하면 그대로 반환한다.") + @Test + void readSubscription_WhenSubscriptionExist() { + // given + subscriptionRepository.save(SubscriptionEntity.builder() + .memberId(darakbangHogee.getMemberId()) + .unsubscribedChats(List.of(UnsubscribedChatRooms.create(1L, 10L))) + .build()); + subscriptionWriter.changeMoimSubscription(hogee); + + // when + Subscription result = subscriptionFinder.readSubscription(hogee); + + // then + assertThat(result.isSubscribedMoimCreate()).isFalse(); + assertThat(result.getUnsubscribedChatRooms()).hasSize(1); + assertThat(result.isSubscribedChatRoom(1L, 10L)).isFalse(); + } +} diff --git a/backend/src/test/java/mouda/backend/notification/implement/subscription/SubscriptionWriterTest.java b/backend/src/test/java/mouda/backend/notification/implement/subscription/SubscriptionWriterTest.java new file mode 100644 index 000000000..e6acc9f4e --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/implement/subscription/SubscriptionWriterTest.java @@ -0,0 +1,160 @@ +package mouda.backend.notification.implement.subscription; + +import static org.assertj.core.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.notification.infrastructure.entity.SubscriptionEntity; +import mouda.backend.notification.infrastructure.entity.UnsubscribedChatRooms; +import mouda.backend.notification.infrastructure.repository.SubscriptionRepository; + +@SpringBootTest +class SubscriptionWriterTest extends DarakbangSetUp { + + @Autowired + private SubscriptionWriter subscriptionWriter; + + @Autowired + private SubscriptionRepository subscriptionRepository; + + + @DisplayName("모임 생성에 대한 알림 허용 여부를 변경한다.") + @Nested + class MoimCreateSubscriptionTest { + + @DisplayName("구독 정보가 존재하지 않으면 새로 만든 뒤 비허용 상태로 변경한다.") + @Test + void changeMoimCreateSubscription_WhenNotExist() { + // when + subscriptionWriter.changeMoimSubscription(hogee); + + // then + Optional SubscriptionOptional = subscriptionRepository.findByMemberId( + darakbangHogee.getMemberId()); + assertThat(SubscriptionOptional.isPresent()).isTrue(); + + SubscriptionEntity subscription = SubscriptionOptional.get(); + assertThat(subscription.isSubscribedMoimCreate()).isFalse(); + } + + @DisplayName("알림 허용 상태에서 비허용 상태로 변경한다.") + @Test + void changeMoimCreateSubscription_WhenSubscribedMoimCreate() { + // given + subscriptionRepository.save(SubscriptionEntity.builder() + .memberId(darakbangHogee.getMemberId()) + .unsubscribedChats(new ArrayList<>()) + .build()); + + // when + subscriptionWriter.changeMoimSubscription(hogee); + + // then + Optional SubscriptionOptional = subscriptionRepository.findByMemberId( + darakbangHogee.getMemberId()); + assertThat(SubscriptionOptional.isPresent()).isTrue(); + + SubscriptionEntity subscription = SubscriptionOptional.get(); + assertThat(subscription.isSubscribedMoimCreate()).isFalse(); + } + + @DisplayName("알림 비허용 상태에서 허용 상태로 변경한다.") + @Test + void changeMoimCreateSubscription_WhenUnsubscribedMoimCreate() { + // given + subscriptionRepository.save(SubscriptionEntity.builder() + .memberId(darakbangHogee.getMemberId()) + .unsubscribedChats(new ArrayList<>()) + .build()); + subscriptionWriter.changeMoimSubscription(hogee); + + // when + subscriptionWriter.changeMoimSubscription(hogee); + + // then + Optional SubscriptionOptional = subscriptionRepository.findByMemberId( + darakbangHogee.getMemberId()); + assertThat(SubscriptionOptional.isPresent()).isTrue(); + + SubscriptionEntity subscription = SubscriptionOptional.get(); + assertThat(subscription.isSubscribedMoimCreate()).isTrue(); + } + } + + @DisplayName("특정 채팅방에 대한 알림 허용 여부를 변경한다.") + @Nested + class ChatRoomSubscriptionTest { + + @DisplayName("구독 정보가 존재하지 않으면 새로 만든 뒤 비허용 상태로 변경한다.") + @Test + void changeChatRoomSubscription_WhenNotExist() { + // when + subscriptionWriter.changeChatRoomSubscription(hogee, 1L, 10L); + + // then + Optional SubscriptionOptional = subscriptionRepository.findByMemberId( + darakbangHogee.getMemberId()); + assertThat(SubscriptionOptional.isPresent()).isTrue(); + + SubscriptionEntity subscription = SubscriptionOptional.get(); + UnsubscribedChatRooms unsubscribedChatRooms = subscription.getUnsubscribedChats().get(0); + assertThat(unsubscribedChatRooms.getDarakbangId()).isEqualTo(1L); + assertThat(unsubscribedChatRooms.getChatRoomIds().contains(10L)).isTrue(); + } + + @DisplayName("알림 허용 상태에서 비허용 상태로 변경한다.") + @Test + void changeChatRoomSubscription_WhenSubscribedChatRoom() { + // given + subscriptionRepository.save(SubscriptionEntity.builder() + .memberId(darakbangHogee.getMemberId()) + .unsubscribedChats(new ArrayList<>()) + .build()); + + // when + subscriptionWriter.changeChatRoomSubscription(hogee, 1L, 10L); + + // then + Optional SubscriptionOptional = subscriptionRepository.findByMemberId( + darakbangHogee.getMemberId()); + assertThat(SubscriptionOptional.isPresent()).isTrue(); + + SubscriptionEntity subscription = SubscriptionOptional.get(); + UnsubscribedChatRooms unsubscribedChatRooms = subscription.getUnsubscribedChats().get(0); + assertThat(unsubscribedChatRooms.getDarakbangId()).isEqualTo(1L); + assertThat(unsubscribedChatRooms.getChatRoomIds().contains(10L)).isTrue(); + } + + @DisplayName("알림 비허용 상태에서 허용 상태로 변경한다.") + @Test + void changeChatRoomSubscription_WhenUnSubscribedChatRoom() { + // given + subscriptionRepository.save(SubscriptionEntity.builder() + .memberId(darakbangHogee.getMemberId()) + .unsubscribedChats(new ArrayList<>()) + .build()); + subscriptionWriter.changeChatRoomSubscription(hogee, 1L, 10L); + + // when + subscriptionWriter.changeChatRoomSubscription(hogee, 1L, 10L); + + // then + Optional SubscriptionOptional = subscriptionRepository.findByMemberId( + darakbangHogee.getMemberId()); + assertThat(SubscriptionOptional.isPresent()).isTrue(); + + SubscriptionEntity subscription = SubscriptionOptional.get(); + UnsubscribedChatRooms unsubscribedChatRooms = subscription.getUnsubscribedChats().get(0); + assertThat(unsubscribedChatRooms.getDarakbangId()).isEqualTo(1L); + assertThat(unsubscribedChatRooms.getChatRoomIds().contains(10L)).isFalse(); + } + } +} diff --git a/backend/src/test/java/mouda/backend/notification/infrastructure/SubscriptionEntityTest.java b/backend/src/test/java/mouda/backend/notification/infrastructure/SubscriptionEntityTest.java new file mode 100644 index 000000000..be67122b7 --- /dev/null +++ b/backend/src/test/java/mouda/backend/notification/infrastructure/SubscriptionEntityTest.java @@ -0,0 +1,54 @@ +package mouda.backend.notification.infrastructure; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.notification.infrastructure.entity.SubscriptionEntity; +import mouda.backend.notification.infrastructure.entity.UnsubscribedChatRooms; +import mouda.backend.notification.infrastructure.repository.SubscriptionRepository; + +@SpringBootTest +public class SubscriptionEntityTest { + + @Autowired + private SubscriptionRepository subscriptionRepository; + + @Test + public void testSaveAndRetrieveSubscriptionEntity() { + // given: 구독 정보 JSON 생성 + UnsubscribedChatRooms subscription1 = new UnsubscribedChatRooms(1L, Arrays.asList(1L, 2L, 3L)); + UnsubscribedChatRooms subscription2 = new UnsubscribedChatRooms(2L, Arrays.asList(4L, 5L, 6L)); + List subscriptions = Arrays.asList(subscription1, subscription2); + + SubscriptionEntity subscriptionEntity = SubscriptionEntity.builder() + .memberId(100L) + .unsubscribedChats(subscriptions) + .build(); + + // when: 엔티티 저장 + SubscriptionEntity savedEntity = subscriptionRepository.save(subscriptionEntity); + + // then: 저장된 데이터 다시 조회 + Optional retrievedEntityOpt = subscriptionRepository.findById(savedEntity.getId()); + assertThat(retrievedEntityOpt).isPresent(); + + SubscriptionEntity retrievedEntity = retrievedEntityOpt.get(); + assertThat(retrievedEntity.getMemberId()).isEqualTo(100L); + assertThat(retrievedEntity.isMoimCreate()).isTrue(); + + System.out.println(retrievedEntityOpt); + + // JSON 데이터 비교 + List retrievedSubscriptions = retrievedEntity.getUnsubscribedChats(); + assertThat(retrievedSubscriptions).hasSize(2); + assertThat(retrievedSubscriptions.get(0).getDarakbangId()).isEqualTo(1L); + assertThat(retrievedSubscriptions.get(0).getChatRoomIds()).containsExactly(1L, 2L, 3L); + } +} diff --git a/backend/src/test/java/mouda/backend/please/implement/InterestFinderTest.java b/backend/src/test/java/mouda/backend/please/implement/InterestFinderTest.java new file mode 100644 index 000000000..61f435601 --- /dev/null +++ b/backend/src/test/java/mouda/backend/please/implement/InterestFinderTest.java @@ -0,0 +1,52 @@ +package mouda.backend.please.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.PleaseFixture; +import mouda.backend.please.domain.Interest; +import mouda.backend.please.domain.Please; + +@SpringBootTest +class InterestFinderTest extends DarakbangSetUp { + + @Autowired + private InterestFinder interestFinder; + + @Autowired + private InterestWriter interestWriter; + + @Autowired + private PleaseWriter pleaseWriter; + + @DisplayName("다락방 맴버가 해주세요에 관심을 눌렀는지 확인한다.") + @Test + void findByDarakBangMemberAndPleaseTest() { + Please pleaseChicken = pleaseWriter.savePlease(PleaseFixture.getPleaseChicken()); + interestWriter.changeInterest(pleaseChicken, true, darakbangHogee); + + Optional interest = interestFinder.findInterest( + darakbangHogee.getMemberId(), + pleaseChicken.getId() + ); + + assertThat(interest.isPresent()).isTrue(); + } + + @DisplayName("관심있어요 의 개수를 카운트한다.") + @Test + void countInterestTest() { + Please pleaseChicken = pleaseWriter.savePlease(PleaseFixture.getPleaseChicken()); + interestWriter.changeInterest(pleaseChicken, true, darakbangHogee); + interestWriter.changeInterest(pleaseChicken, true, darakbangAnna); + + assertThat(interestFinder.countInterest(pleaseChicken)).isEqualTo(2); + } +} diff --git a/backend/src/test/java/mouda/backend/please/implement/InterestWriterTest.java b/backend/src/test/java/mouda/backend/please/implement/InterestWriterTest.java new file mode 100644 index 000000000..575a1cf14 --- /dev/null +++ b/backend/src/test/java/mouda/backend/please/implement/InterestWriterTest.java @@ -0,0 +1,36 @@ +package mouda.backend.please.implement; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.PleaseFixture; +import mouda.backend.please.domain.Please; + +@SpringBootTest +class InterestWriterTest extends DarakbangSetUp { + + @Autowired + private InterestWriter interestWriter; + + @Autowired + private InterestFinder interestFinder; + + @Autowired + private PleaseWriter pleaseWriter; + + @DisplayName("관심있어요 상태 변경시 변경한 해주세요만 관심여부가 변경된다.") + @Test + void findAllPlease() { + Please pleaseChicken = pleaseWriter.savePlease(PleaseFixture.getPleaseChicken()); + Please pleasePizza = pleaseWriter.savePlease(PleaseFixture.getPleasePizza()); + + interestWriter.changeInterest(pleaseChicken, true, darakbangHogee); + + assertThat(interestFinder.findInterest(darakbangHogee.getId(), pleaseChicken.getId())).isNotEmpty(); + } +} diff --git a/backend/src/test/java/mouda/backend/please/implement/PleaseFinderTest.java b/backend/src/test/java/mouda/backend/please/implement/PleaseFinderTest.java new file mode 100644 index 000000000..505fd1783 --- /dev/null +++ b/backend/src/test/java/mouda/backend/please/implement/PleaseFinderTest.java @@ -0,0 +1,71 @@ +package mouda.backend.please.implement; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.PleaseFixture; +import mouda.backend.please.domain.Please; +import mouda.backend.please.domain.PleaseWithInterest; +import mouda.backend.please.domain.PleaseWithInterests; + +@SpringBootTest +class PleaseFinderTest extends DarakbangSetUp { + + @Autowired + private PleaseFinder pleaseFinder; + + @Autowired + private PleaseWriter pleaseWriter; + + @Autowired + private InterestWriter interestWriter; + + @Autowired + private InterestFinder interestFinder; + + @DisplayName("해주세요 Id로 해주세요를 조회한다.") + @Test + void findTest() { + Please pleaseChicken = PleaseFixture.getPleaseChicken(); + Please savedPlease = pleaseWriter.savePlease(pleaseChicken); + Please findPlease = pleaseFinder.find(savedPlease.getId(), darakbang.getId()); + + assertThat(findPlease.getId()).isEqualTo(savedPlease.getId()); + } + + @DisplayName("해주세요 전체를 조회한다.") + @Test + void findAllTest() { + Please pleasePizza = pleaseWriter.savePlease(PleaseFixture.getPleasePizza()); + Please pleaseChicken = pleaseWriter.savePlease(PleaseFixture.getPleaseChicken()); + Please pleaseHogee = pleaseWriter.savePlease(PleaseFixture.getPleaseHogee()); + + PleaseWithInterests pleasesDesc = pleaseFinder.findPleasesDesc(darakbang.getId(), darakbangHogee); + assertThat(pleasesDesc.getPleaseWithInterests().size()).isEqualTo(3); + } + + @DisplayName("해주세요 목록 조회시 관심이 많은 순서대로 조회하고, 관심이 같다면 생성된 순서대로 조회한다.") + @Test + void findAllPlease_isSortedByInterestCount() { + Please pleasePizza = pleaseWriter.savePlease(PleaseFixture.getPleasePizza()); + Please pleaseChicken = pleaseWriter.savePlease(PleaseFixture.getPleaseChicken()); + Please pleaseHogee = pleaseWriter.savePlease(PleaseFixture.getPleaseHogee()); + + interestWriter.changeInterest(pleaseHogee, true, darakbangHogee); + interestWriter.changeInterest(pleaseChicken, true, darakbangHogee); + interestWriter.changeInterest(pleaseChicken, true, darakbangAnna); + + PleaseWithInterests pleasesDesc = pleaseFinder.findPleasesDesc(darakbang.getId(), darakbangHogee); + List pleaseWithInterests = pleasesDesc.getPleaseWithInterests(); + assertThat(pleaseWithInterests.get(0).getPlease().getId()).isEqualTo(2L); + assertThat(pleaseWithInterests.get(1).getPlease().getId()).isEqualTo(3L); + assertThat(pleaseWithInterests.get(2).getPlease().getId()).isEqualTo(1L); + } +} diff --git a/backend/src/test/java/mouda/backend/please/implement/PleaseValidatorTest.java b/backend/src/test/java/mouda/backend/please/implement/PleaseValidatorTest.java new file mode 100644 index 000000000..107973e9a --- /dev/null +++ b/backend/src/test/java/mouda/backend/please/implement/PleaseValidatorTest.java @@ -0,0 +1,43 @@ +package mouda.backend.please.implement; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import mouda.backend.common.fixture.DarakbangSetUp; +import mouda.backend.common.fixture.PleaseFixture; +import mouda.backend.please.domain.Please; +import mouda.backend.please.exception.PleaseException; + +@SpringBootTest +class PleaseValidatorTest extends DarakbangSetUp { + + @Autowired + private PleaseValidator pleaseValidator; + + @Autowired + private PleaseWriter pleaseWriter; + + @DisplayName("해주세요는 하나의 다락방에만 종속된다. 아니면 예외가 발생한다.") + @Test + void validateNotInDarakbangTest() { + Please savedPlease = pleaseWriter.savePlease(PleaseFixture.getPleaseChicken()); + + assertThatThrownBy(() -> pleaseValidator.validate(savedPlease, 2L, darakbangHogee)) + .isInstanceOf(PleaseException.class) + .hasMessage("다락방에 존재하는 해주세요가 아닙니다."); + } + + @DisplayName("해주세요 삭제는 작성자만 할 수 있다.") + @Test + void validateAuthorizeTest() { + Please savedPlease = pleaseWriter.savePlease(PleaseFixture.getPleaseChicken()); + + assertThatThrownBy(() -> pleaseValidator.validate(savedPlease, 1L, darakbangAnna)) + .isInstanceOf(PleaseException.class) + .hasMessage("삭제 권한이 없습니다."); + } +} diff --git a/backend/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/backend/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension new file mode 100644 index 000000000..9bbf0ff27 --- /dev/null +++ b/backend/src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension @@ -0,0 +1 @@ +mouda.backend.common.config.NoTransactionExtension diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml new file mode 100644 index 000000000..f05803a47 --- /dev/null +++ b/backend/src/test/resources/application.yml @@ -0,0 +1,48 @@ +spring: + jpa: + show-sql: true + properties: + hibernate: + format_sql: true + hibernate: + ddl-auto: create-drop + defer-datasource-initialization: true +# sql: +# init: +# data-locations: classpath:test-data.sql + +security: + jwt: + token: + secret-key: kksangdolbabokksangdolbabokksangdolbabokksangdolbabokksangdolbabokksangdolbabo + expire-length: 3600000 + +app: + firebase-config-file: serviceAccountKey.json + +url: + base: http://localhost:8080 + moim: /darakbang/%d/moim/%d + chat: /darakbang/%d/chat/%d + chatroom: /darakbang/%d/chatting-room/%d + +oauth: + kakao: + redirect-uri: http://localhost:8081/kakao-o-auth\ + google: + client-secret: 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 + redirect-uri: http://localhost:8081/oauth/google + apple: + redirect-uri: https://dev.mouda.site/oauth/apple + redirection: https://dev.mouda.site/oauth/apple?token=%s&isConverted=%s + +bet: + schedule: "* * * * * *" # 테스트 스케줄러는 매초마다 동작 + +aws: + region: + static: ap-northeast-2 + s3: + bucket: techcourse-project-2024 + key-prefix: mouda/dev/asset/profile/ + prefix: https://dev.mouda.site/profile/ diff --git a/backend/src/test/resources/junit-platform.properties b/backend/src/test/resources/junit-platform.properties new file mode 100644 index 000000000..6efc0d5e8 --- /dev/null +++ b/backend/src/test/resources/junit-platform.properties @@ -0,0 +1 @@ +junit.jupiter.extensions.autodetection.enabled=true diff --git a/frontend/.browserslistrc b/frontend/.browserslistrc new file mode 100644 index 000000000..a565c60d8 --- /dev/null +++ b/frontend/.browserslistrc @@ -0,0 +1 @@ +> 1% in KR and not dead \ No newline at end of file diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs new file mode 100644 index 000000000..8a949f44e --- /dev/null +++ b/frontend/.eslintrc.cjs @@ -0,0 +1,37 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + 'plugin:react/recommended', + 'plugin:storybook/recommended', + 'plugin:cypress/recommended', + 'prettier', + ], + ignorePatterns: [ + 'dist', + '.eslintrc.cjs', + 'node_modules/', + 'webpack.common.js', + 'webpack.dev.js', + 'webpack.prod.js', + ], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh', '@emotion', 'compat'], + settings: { + react: { + version: 'detect', + }, + }, + rules: { + 'compat/compat': 'warn', + 'react/react-in-jsx-scope': 'off', + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + 'react/no-unknown-property': ['error', { ignore: ['css'] }], + }, +}; diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 000000000..f5cf568c1 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,8 @@ +node_modules +dist +*storybook.log +.env +.env* +coverage + +public/firebaseConfig.js \ No newline at end of file diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 000000000..cb31545c5 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,28 @@ +{ + "endOfLine": "auto", + "singleQuote": true, + "semi": true, + "useTabs": false, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 80, + "bracketSpacing": true, + "arrowParens": "always", + + "importOrder": [ + "@/hooks/(.*)$", + "@/@types", + "@/recoil/(atoms|selectors)", + "@/styles/(.*)$", + "@/(components/@common/|components/|pages/)(.*)$", + "@/constants", + "@/utils", + "@/domains", + "@/(api|mocks)/(.*)$", + "react-icons", + "^[./]" + ], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true, + "importOrderCaseInsensitive": true +} diff --git a/frontend/.storybook/main.ts b/frontend/.storybook/main.ts new file mode 100644 index 000000000..ae7dfd428 --- /dev/null +++ b/frontend/.storybook/main.ts @@ -0,0 +1,90 @@ +import { Configuration } from 'webpack'; +import type { StorybookConfig } from '@storybook/react-webpack5'; +import path from 'path'; + +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: [ + '@storybook/addon-webpack5-compiler-swc', + '@storybook/addon-onboarding', + '@storybook/addon-links', + '@storybook/addon-essentials', + '@chromatic-com/storybook', + '@storybook/addon-interactions', + ], + framework: { + name: '@storybook/react-webpack5', + options: { + builder: { + useSWC: true, + }, + }, + }, + swc: () => ({ + jsc: { + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }), + webpackFinal: async (config: Configuration) => { + const { resolve } = config; + + if (resolve) { + resolve.alias = { + ...resolve.alias, + '@_apis': path.resolve(__dirname, '../src/apis'), + '@_constants': path.resolve(__dirname, '../src/constants'), + '@_common': path.resolve(__dirname, '../src/common'), + '@_components': path.resolve(__dirname, '../src/components'), + '@_hooks': path.resolve(__dirname, '../src/hooks'), + '@_layouts': path.resolve(__dirname, '../src/layouts'), + '@_pages': path.resolve(__dirname, '../src/pages'), + '@_types': path.resolve(__dirname, '../src/types'), + '@_utils': path.resolve(__dirname, '../src/utils'), + '@_routes': path.resolve(__dirname, '../src/routes'), + '@_mocks': path.resolve(__dirname, '../src/mocks'), + '@_service': path.resolve(__dirname, '../src/service'), + }; + } + + config?.module?.rules?.push({ + test: /\.(ts|tsx)$/, + loader: require.resolve('babel-loader'), + options: { + presets: [require.resolve('@emotion/babel-preset-css-prop')], + }, + }); + if (config.module?.rules) { + config.module = config.module || {}; + config.module.rules = config.module.rules || []; + + const imageRule = config.module.rules.find((rule) => + rule?.['test']?.test('.svg'), + ); + if (imageRule) { + imageRule['exclude'] = /\.svg$/; + } + + config.module.rules.push({ + test: /\.svg$/i, + oneOf: [ + { + use: ['@svgr/webpack'], + issuer: /\.[jt]sx?$/, + resourceQuery: { not: [/url/] }, + }, + { + type: 'asset/resource', + resourceQuery: /url/, + }, + ], + }); + } + return config; + }, +}; + +export default config; diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx new file mode 100644 index 000000000..cfa17dedf --- /dev/null +++ b/frontend/.storybook/preview.tsx @@ -0,0 +1,39 @@ +import type { Preview } from '@storybook/react'; +import React from 'react'; +import reset from '../src/common/reset.style'; +import { Global, ThemeProvider } from '@emotion/react'; +import { theme } from '../src/common/theme/theme.style'; +import { initialize, mswDecorator } from 'msw-storybook-addon'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { BrowserRouter } from 'react-router-dom'; + +initialize(); + +const queryClient = new QueryClient(); +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, + decorators: [ + mswDecorator, + (Story) => ( + + + + +
+ +
+
+
+
+ ), + ], +}; + +export default preview; diff --git a/frontend/.stylelintrc.json b/frontend/.stylelintrc.json new file mode 100644 index 000000000..aeeb5cb5d --- /dev/null +++ b/frontend/.stylelintrc.json @@ -0,0 +1,36 @@ +{ + "extends": [ + "stylelint-config-standard", + "stylelint-config-clean-order" + ], + "plugins": [ + "stylelint-no-unsupported-browser-features" + ], + "rules": { + "property-no-unknown": [ + true, + { + "ignoreProperties": ["label"] + } + ], + "plugin/no-unsupported-browser-features": [ + true, + { + "severity": "warning" + } + ] + }, + "overrides": [ + { + "files": [ + "**/*.js", + "**/*.cjs", + "**/*.mjs", + "**/*.jsx", + "**/*.ts", + "**/*.tsx" + ], + "customSyntax": "postcss-styled-syntax" + } + ] +} \ No newline at end of file diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json new file mode 100644 index 000000000..5e34c66e5 --- /dev/null +++ b/frontend/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "stylelint.configFile": ".stylelintrc.json", + "editor.codeActionsOnSave": { + "source.fixAll.stylelint": "always" + }, + "stylelint.validate": ["typescript", "typescriptreact"] +} diff --git a/frontend/babel.config.json b/frontend/babel.config.json new file mode 100644 index 000000000..4b08db243 --- /dev/null +++ b/frontend/babel.config.json @@ -0,0 +1,13 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "useBuiltIns": "entry", + "corejs": "3.31.1" + } + ], + ["@babel/preset-react", { "runtime": "automatic" }], + "@babel/preset-typescript" + ] +} diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts new file mode 100644 index 000000000..17161e32e --- /dev/null +++ b/frontend/cypress.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "cypress"; + +export default defineConfig({ + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, + }, +}); diff --git a/frontend/cypress/support/commands.ts b/frontend/cypress/support/commands.ts new file mode 100644 index 000000000..698b01a42 --- /dev/null +++ b/frontend/cypress/support/commands.ts @@ -0,0 +1,37 @@ +/// +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// +// declare global { +// namespace Cypress { +// interface Chainable { +// login(email: string, password: string): Chainable +// drag(subject: string, options?: Partial): Chainable +// dismiss(subject: string, options?: Partial): Chainable +// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable +// } +// } +// } \ No newline at end of file diff --git a/frontend/cypress/support/e2e.ts b/frontend/cypress/support/e2e.ts new file mode 100644 index 000000000..f80f74f8e --- /dev/null +++ b/frontend/cypress/support/e2e.ts @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') \ No newline at end of file diff --git a/frontend/jest.config.json b/frontend/jest.config.json new file mode 100644 index 000000000..b0a3761a6 --- /dev/null +++ b/frontend/jest.config.json @@ -0,0 +1,25 @@ +{ + "preset": "ts-jest", + "testEnvironment": "jest-environment-jsdom", + + + + "moduleNameMapper": { + "^@_apis/(.*)$": "/src/apis/$1", + "^@_constants/(.*)$": "/src/constants/$1", + "^@_common/(.*)$": "/src/common/$1", + "^@_components/(.*)$": "/src/components/$1", + "^@_hooks/(.*)$": "/src/hooks/$1", + "^@_layouts/(.*)$": "/src/layouts/$1", + "^@_pages/(.*)$": "/src/pages/$1", + "^@_types/(.*)$": "/src/types/$1", + "^@_utils/(.*)$": "/src/utils/$1", + "^@_routes/(.*)$": "/src/routes/$1", + "^@_mocks/(.*)$": "/src/mocks/$1" + }, + "testEnvironmentOptions": { + "customExportConditions": [""] + }, + "setupFiles": ["dotenv/config", "./jest.polyfills.js"], + "setupFilesAfterEnv": ["/jest.setup.ts"] +} \ No newline at end of file diff --git a/frontend/jest.polyfills.js b/frontend/jest.polyfills.js new file mode 100644 index 000000000..568333ad4 --- /dev/null +++ b/frontend/jest.polyfills.js @@ -0,0 +1,32 @@ +/** + * @note The block below contains polyfills for Node.js globals + * required for Jest to function when running JSDOM tests. + * These HAVE to be require's and HAVE to be in this exact + * order, since "undici" depends on the "TextEncoder" global API. + * + * Consider migrating to a more modern test runner if + * you don't want to deal with this. + */ + +const { TextDecoder, TextEncoder } = require('node:util'); +const { ReadableStream, TransformStream } = require('node:stream/web'); + +Object.defineProperties(globalThis, { + TextDecoder: { value: TextDecoder }, + TextEncoder: { value: TextEncoder }, + ReadableStream: { value: ReadableStream }, + TransformStream: { value: TransformStream }, +}); + +const { Blob, File } = require('node:buffer'); +const { fetch, Headers, FormData, Request, Response } = require('undici'); + +Object.defineProperties(globalThis, { + fetch: { value: fetch, writable: true }, + Blob: { value: Blob }, + File: { value: File }, + Headers: { value: Headers }, + FormData: { value: FormData }, + Request: { value: Request }, + Response: { value: Response }, +}); \ No newline at end of file diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts new file mode 100644 index 000000000..8f0441d04 --- /dev/null +++ b/frontend/jest.setup.ts @@ -0,0 +1,20 @@ +import '@testing-library/jest-dom'; + +import dotenv from 'dotenv'; +import { server } from './src/mocks/server'; + +dotenv.config({ path: './.env' }); + +beforeAll(() => { + server.listen({ + onUnhandledRequest: 'error', // 이 옵션을 통해 핸들되지 않은 요청을 경고로 표시 + }); +}); + +afterEach(() => { + server.resetHandlers(); +}); +// Clean up after the tests are finished. +afterAll(() => { + server.close(); +}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 000000000..cb3c3d3d0 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,23437 @@ +{ + "name": "mouda", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mouda", + "version": "1.0.0", + "dependencies": { + "@emotion/babel-preset-css-prop": "^11.11.0", + "@emotion/eslint-plugin": "^11.11.0", + "@emotion/react": "^11.11.4", + "@sentry/react": "^8.24.0", + "@sentry/webpack-plugin": "^2.22.0", + "firebase": "^10.12.5", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-ga4": "^2.1.0", + "react-router-dom": "^6.24.1" + }, + "devDependencies": { + "@babel/core": "^7.24.7", + "@babel/preset-env": "^7.24.7", + "@babel/preset-react": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "@chromatic-com/storybook": "^1.6.1", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", + "@storybook/addon-essentials": "^8.1.11", + "@storybook/addon-interactions": "^8.1.11", + "@storybook/addon-links": "^8.1.11", + "@storybook/addon-onboarding": "^8.1.11", + "@storybook/addon-webpack5-compiler-swc": "^1.0.4", + "@storybook/blocks": "^8.1.11", + "@storybook/builder-webpack5": "^8.1.11", + "@storybook/react": "^8.1.11", + "@storybook/react-webpack5": "^8.1.11", + "@storybook/test": "^8.1.11", + "@stylelint/postcss-css-in-js": "^0.38.0", + "@svgr/webpack": "^8.1.0", + "@tanstack/react-query": "^5.51.1", + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^16.0.0", + "@types/jest": "^29.5.12", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.15.0", + "babel-loader": "^9.1.3", + "chromatic": "^11.7.0", + "copy-webpack-plugin": "^12.0.2", + "cypress": "^13.13.3", + "dotenv": "^16.4.5", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-compat": "^5.0.0", + "eslint-plugin-cypress": "^3.5.0", + "eslint-plugin-react": "^7.34.3", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "eslint-plugin-storybook": "^0.8.0", + "html-webpack-plugin": "^5.6.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "msw": "^2.3.5", + "msw-storybook-addon": "^2.0.3", + "postcss": "^8.4.39", + "postcss-styled-syntax": "^0.6.4", + "postcss-syntax": "^0.36.2", + "prettier": "^3.3.2", + "react-refresh": "^0.14.2", + "storybook": "^8.1.11", + "stylelint": "^16.6.1", + "stylelint-config-clean-order": "^6.1.0", + "stylelint-config-standard": "^36.0.1", + "stylelint-no-unsupported-browser-features": "^8.0.1", + "stylelint-order": "^6.0.4", + "ts-jest": "^29.2.3", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "type-fest": "^4.21.0", + "typescript": "^5.5.3", + "undici": "^6.19.5", + "webpack": "^5.92.1", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^5.0.4" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", + "dev": true + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.8.tgz", + "integrity": "sha512-c4IM7OTg6k1Q+AJ153e2mc2QVTezTwnb4VzquwcyiEzGnW0Kedv4do/TrkU98qPeC5LNiMt/QXwIjzYXLBpyZg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.8.tgz", + "integrity": "sha512-6AWcmZC/MZCO0yKys4uhg5NlxL0ESF3K6IAaoQ+xSXvPyPyxNWRafP+GDbI88Oh68O7QkJgmEtedWPM9U0pZNg==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.8", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-module-transforms": "^7.24.8", + "@babel/helpers": "^7.24.8", + "@babel/parser": "^7.24.8", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.8", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.8.tgz", + "integrity": "sha512-47DG+6F5SzOi0uEvK4wMShmn5yY0mVjVJoWTphdY2B4Rx9wHgjK7Yhtr0ru6nE+sn0v38mzrWOlah0p/YlHHOQ==", + "dependencies": { + "@babel/types": "^7.24.8", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.24.7.tgz", + "integrity": "sha512-xZeCVVdwb4MsDBkkyZ64tReWYrLRHlMN72vP7Bdm3OUOuyFZExhsHUUnuWnm2/XOlAJzR0LfPpB56WXZn0X/lA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.8.tgz", + "integrity": "sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==", + "dependencies": { + "@babel/compat-data": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.8.tgz", + "integrity": "sha512-4f6Oqnmyp2PP3olgUMmOwC3akxSm5aBYraQ6YDdKy7NcAMkDECHWG0DEnV6M2UAkERgIBhYt8S27rURPg7SxWA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz", + "integrity": "sha512-03TCmXy2FtXJEZfbXDTSqq1fRJArk7lX9DOFC/47VthYcxyIOx+eXQmdo6DOQvrbpIix+KfXwvuXdFDZHxt+rA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", + "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.8.tgz", + "integrity": "sha512-m4vWKVqvkVAWLXfHCCfff2luJj86U+J0/x+0N3ArG/tP0Fq7zky2dYwMbtPmkc/oulkkbjdL3uWzuoBwQ8R00Q==", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.24.7.tgz", + "integrity": "sha512-9pKLcTlZ92hNZMQfGCHImUpDOlAgkkpqalWEeftW5FBya75k8Li2ilerxkM/uBEj01iBZXcCIB/bwvDYgWyibA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-wrap-function": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz", + "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.7", + "@babel/helper-optimise-call-expression": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.7.tgz", + "integrity": "sha512-N9JIYk3TD+1vq/wn77YnJOqMtfWhNewNE+DJV4puD2X7Ew9J4JvrzrFDfTfyv5EgEXVy9/Wt8QiOErzEmv5Ifw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.8.tgz", + "integrity": "sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.8.tgz", + "integrity": "sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz", + "integrity": "sha512-TiT1ss81W80eQsN+722OaeQMY/G4yTb4G9JrqeiDADs3N8lbPMGldWi9x8tyqCW5NLx1Jh2AvkE6r6QvEltMMQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.7.tgz", + "integrity": "sha512-unaQgZ/iRu/By6tsjMZzpeBZjChYfLYry6HrEXPoz3KmfF0sVBQ1l8zKMQ4xRGLWVsjuvB8nQfjNP/DcfEOCsg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.7.tgz", + "integrity": "sha512-+izXIbke1T33mY4MSNnrqhPXDz01WYhEf3yF5NbnUtkiNnm+XBZJl3kNfoK6NKmYlz/D07+l2GWVK/QfDkNCuQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.7.tgz", + "integrity": "sha512-utA4HuR6F4Vvcr+o4DnjL8fCOlgRFGbeeBEGNg3ZTrLFw6VWG5XmUrvcQ0FjIYMU2ST4XcR2Wsp7t9qOAPnxMg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.24.7.tgz", + "integrity": "sha512-9G8GYT/dxn/D1IIKOUBmGX0mnmj46mGH9NnZyJLwtCpgh5f7D2VbuKodb+2s9m1Yavh1s7ASQN8lf0eqrb1LTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.7.tgz", + "integrity": "sha512-Ec3NRUMoi8gskrkBe3fNmEQfxDvY8bgfQpz6jlk/41kX9eUjvpyqWU7PBP/pLAvMaSQjbMNKJmvX57jP+M6bPg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.7.tgz", + "integrity": "sha512-Dt9LQs6iEY++gXUwY03DNFat5C2NbO48jj+j/bSAz6b3HgPs39qcPiYt77fDObIcFwj3/C2ICX9YMwGflUoSHQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.7.tgz", + "integrity": "sha512-o+iF77e3u7ZS4AoAuJvapz9Fm001PuD2V3Lp6OSE4FYQke+cSewYtnek+THqGRWyQloRCyvWL1OkyfNEl9vr/g==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.7.tgz", + "integrity": "sha512-SQY01PcJfmQ+4Ash7NE+rpbLFbmqA2GPIgqzxfFTL4t1FKRq4zTms/7htKpoCUI9OcFYgzqfmCdH53s6/jn5fA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-remap-async-to-generator": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.7.tgz", + "integrity": "sha512-yO7RAz6EsVQDaBH18IDJcMB1HnrUn2FJ/Jslc/WtPPWcjhpUJXU/rjbwmluzp7v/ZzWcEhTMXELnnsz8djWDwQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.7.tgz", + "integrity": "sha512-Nd5CvgMbWc+oWzBsuaMcbwjJWAcp5qzrbg69SZdHSP7AMY0AbWFqFO0WTFCA1jxhMCwodRwvRec8k0QUbZk7RQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.7.tgz", + "integrity": "sha512-vKbfawVYayKcSeSR5YYzzyXvsDFWU2mD8U5TFeXtbCPLFUqe7GyCgvO6XDHzje862ODrOwy6WCPmKeWHbCFJ4w==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.7.tgz", + "integrity": "sha512-HMXK3WbBPpZQufbMG4B46A90PkuuhN9vBCb5T8+VAHqvAqvcLi+2cKoukcpmUYkszLhScU3l1iudhrks3DggRQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.8.tgz", + "integrity": "sha512-VXy91c47uujj758ud9wx+OMgheXm4qJfyhj1P18YvlrQkNOSrwsteHk+EFS3OMGfhMhpZa0A+81eE7G4QC+3CA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-replace-supers": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.7.tgz", + "integrity": "sha512-25cS7v+707Gu6Ds2oY6tCkUwsJ9YIDbggd9+cu9jzzDgiNq7hR/8dkzxWfKWnTic26vsI3EsCXNd4iEB6e8esQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/template": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.8.tgz", + "integrity": "sha512-36e87mfY8TnRxc7yc6M9g9gOB7rKgSahqkIKwLpz4Ppk2+zC2Cy1is0uwtuSG6AE4zlTOUa+7JGz9jCJGLqQFQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.7.tgz", + "integrity": "sha512-ZOA3W+1RRTSWvyqcMJDLqbchh7U4NRGqwRfFSVbOLS/ePIP4vHB5e8T8eXcuqyN1QkgKyj5wuW0lcS85v4CrSw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.7.tgz", + "integrity": "sha512-JdYfXyCRihAe46jUIliuL2/s0x0wObgwwiGxw/UbgJBr20gQBThrokO4nYKgWkD7uBaqM7+9x5TU7NkExZJyzw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.7.tgz", + "integrity": "sha512-sc3X26PhZQDb3JhORmakcbvkeInvxz+A8oda99lj7J60QRuPZvNAk9wQlTBS1ZynelDrDmTU4pw1tyc5d5ZMUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.7.tgz", + "integrity": "sha512-Rqe/vSc9OYgDajNIK35u7ot+KeCoetqQYFXM4Epf7M7ez3lWlOjrDjrwMei6caCVhfdw+mIKD4cgdGNy5JQotQ==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.7.tgz", + "integrity": "sha512-v0K9uNYsPL3oXZ/7F9NNIbAj2jv1whUEtyA6aujhekLs56R++JDQuzRcP2/z4WX5Vg/c5lE9uWZA0/iUoFhLTA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.24.7.tgz", + "integrity": "sha512-cjRKJ7FobOH2eakx7Ja+KpJRj8+y+/SiB3ooYm/n2UJfxu0oEaOoxOinitkJcPqv9KxS0kxTGPUaR7L2XcXDXA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-flow": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.7.tgz", + "integrity": "sha512-wo9ogrDG1ITTTBsy46oGiN1dS9A7MROBTcYsfS8DtsImMkHk9JXJ3EWQM6X2SUw4x80uGPlwj0o00Uoc6nEE3g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.7.tgz", + "integrity": "sha512-U9FcnA821YoILngSmYkW6FjyQe2TyZD5pHt4EVIhmcTkrJw/3KqcrRSxuOo5tFZJi7TE19iDyI1u+weTI7bn2w==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.7.tgz", + "integrity": "sha512-2yFnBGDvRuxAaE/f0vfBKvtnvvqU8tGpMHqMNpTN2oWMKIR3NqFkjaAgGwawhqK/pIN2T3XdjGPdaG0vDhOBGw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.7.tgz", + "integrity": "sha512-vcwCbb4HDH+hWi8Pqenwnjy+UiklO4Kt1vfspcQYFhJdpthSnW8XvWGyDZWKNVrVbVViI/S7K9PDJZiUmP2fYQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.7.tgz", + "integrity": "sha512-4D2tpwlQ1odXmTEIFWy9ELJcZHqrStlzK/dAOWYyxX3zT0iXQB6banjgeOJQXzEc4S0E0a5A+hahxPaEFYftsw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.7.tgz", + "integrity": "sha512-T/hRC1uqrzXMKLQ6UCwMT85S3EvqaBXDGf0FaMf4446Qx9vKwlghvee0+uuZcDUCZU5RuNi4781UQ7R308zzBw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.7.tgz", + "integrity": "sha512-9+pB1qxV3vs/8Hdmz/CulFB8w2tuu6EB94JZFsjdqxQokwGa9Unap7Bo2gGBGIvPmDIVvQrom7r5m/TCDMURhg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz", + "integrity": "sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-simple-access": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.7.tgz", + "integrity": "sha512-GYQE0tW7YoaN13qFh3O1NCY4MPkUiAH3fiF7UcV/I3ajmDKEdG3l+UOcbAm4zUE3gnvUU+Eni7XrVKo9eO9auw==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.7.tgz", + "integrity": "sha512-3aytQvqJ/h9z4g8AsKPLvD4Zqi2qT+L3j7XoFFu1XBlZWEl2/1kWnhmAbxpLgPrHSY0M6UA02jyTiwUVtiKR6A==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.24.7.tgz", + "integrity": "sha512-/jr7h/EWeJtk1U/uz2jlsCioHkZk1JJZVcc8oQsJ1dUlaJD83f4/6Zeh2aHt9BIFokHIsSeDfhUmju0+1GPd6g==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.7.tgz", + "integrity": "sha512-RNKwfRIXg4Ls/8mMTza5oPF5RkOW8Wy/WgMAp1/F1yZ8mMbtwXW+HDoJiOsagWrAhI5f57Vncrmr9XeT4CVapA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.7.tgz", + "integrity": "sha512-Ts7xQVk1OEocqzm8rHMXHlxvsfZ0cEF2yomUqpKENHWMF4zKk175Y4q8H5knJes6PgYad50uuRmt3UJuhBw8pQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.7.tgz", + "integrity": "sha512-e6q1TiVUzvH9KRvicuxdBTUj4AdKSRwzIyFFnfnezpCfP2/7Qmbb8qbU2j7GODbl4JMkblitCQjKYUaX/qkkwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.7.tgz", + "integrity": "sha512-4QrHAr0aXQCEFni2q4DqKLD31n2DL+RxcwnNjDFkSG0eNQ/xCavnRkfCUjsyqGC2OviNJvZOF/mQqZBw7i2C5Q==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.7.tgz", + "integrity": "sha512-A/vVLwN6lBrMFmMDmPPz0jnE6ZGx7Jq7d6sT/Ev4H65RER6pZ+kczlf1DthF5N0qaPHBsI7UXiE8Zy66nmAovg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-replace-supers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.7.tgz", + "integrity": "sha512-uLEndKqP5BfBbC/5jTwPxLh9kqPWWgzN/f8w6UwAIirAEqiIVJWWY312X72Eub09g5KF9+Zn7+hT7sDxmhRuKA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.8.tgz", + "integrity": "sha512-5cTOLSMs9eypEy8JUVvIKOu6NgvbJMnpG62VpIHrTmROdQ+L5mDAaI40g25k5vXti55JWNX5jCkq3HZxXBQANw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.7.tgz", + "integrity": "sha512-yGWW5Rr+sQOhK0Ot8hjDJuxU3XLRQGflvT4lhlSY0DFvdb3TwKaY26CJzHtYllU0vT9j58hc37ndFPsqT1SrzA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.7.tgz", + "integrity": "sha512-COTCOkG2hn4JKGEKBADkA8WNb35TGkkRbI5iT845dB+NyqgO8Hn+ajPbSnIQznneJTa3d30scb6iz/DhH8GsJQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.7.tgz", + "integrity": "sha512-9z76mxwnwFxMyxZWEgdgECQglF2Q7cFLm0kMf8pGwt+GSJsY0cONKj/UuO4bOH0w/uAel3ekS4ra5CEAyJRmDA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.7.tgz", + "integrity": "sha512-EMi4MLQSHfd2nrCqQEWxFdha2gBCqU4ZcCng4WBGZ5CJL4bBRW0ptdqqDdeirGZcpALazVVNJqRmsO8/+oNCBA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.24.7.tgz", + "integrity": "sha512-7LidzZfUXyfZ8/buRW6qIIHBY8wAZ1OrY9c/wTr8YhZ6vMPo+Uc/CVFLYY1spZrEQlD4w5u8wjqk5NQ3OVqQKA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.7.tgz", + "integrity": "sha512-H/Snz9PFxKsS1JLI4dJLtnJgCJRoo0AUm3chP6NYr+9En1JMKloheEiLIhlp5MDVznWo+H3AAC1Mc8lmUEpsgg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz", + "integrity": "sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz", + "integrity": "sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.7.tgz", + "integrity": "sha512-PLgBVk3fzbmEjBJ/u8kFzOqS9tUeDjiaWud/rRym/yjCo/M9cASPlnrd2ZmmZpQT40fOOrvR8jh+n8jikrOhNA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.7.tgz", + "integrity": "sha512-lq3fvXPdimDrlg6LWBoqj+r/DEWgONuwjuOuQCSYgRroXDH/IdM1C0IZf59fL5cHLpjEH/O6opIRBbqv7ELnuA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.7.tgz", + "integrity": "sha512-0DUq0pHcPKbjFZCfTss/pGkYMfy3vFWydkUBd9r0GHpIyfs2eCDENvqadMycRS9wZCXR41wucAfJHJmwA0UmoQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.7.tgz", + "integrity": "sha512-KsDsevZMDsigzbA09+vacnLpmPH4aWjcZjXdyFKGzpplxhbeB4wYtury3vglQkg6KM/xEPKt73eCjPPf1PgXBA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.7.tgz", + "integrity": "sha512-x96oO0I09dgMDxJaANcRyD4ellXFLLiWhuwDxKZX5g2rWP1bTPkBSwCYv96VDXVT1bD9aPj8tppr5ITIh8hBng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.7.tgz", + "integrity": "sha512-kHPSIJc9v24zEml5geKg9Mjx5ULpfncj0wRpYtxbvKyTtHCYDkVE3aHQ03FrpEo4gEe2vrJJS1Y9CJTaThA52g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.7.tgz", + "integrity": "sha512-AfDTQmClklHCOLxtGoP7HkeMw56k1/bTQjwsfhL6pppo/M4TOBSq+jjBUBLmV/4oeFg4GWMavIl44ZeCtmmZTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.8.tgz", + "integrity": "sha512-adNTUpDCVnmAE58VEqKlAA6ZBlNkMnWD0ZcW76lyNFN3MJniyGFZfNwERVk8Ap56MCnXztmDr19T4mPTztcuaw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.8.tgz", + "integrity": "sha512-CgFgtN61BbdOGCP4fLaAMOPkzWUh6yQZNMr5YSt8uz2cZSSiQONCQFWqsE4NeVfOIhqDOlS9CR3WD91FzMeB2Q==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/plugin-syntax-typescript": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.7.tgz", + "integrity": "sha512-U3ap1gm5+4edc2Q/P+9VrBNhGkfnf+8ZqppY71Bo/pzZmXhhLdqgaUl6cuB07O1+AQJtCLfaOmswiNbSQ9ivhw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.7.tgz", + "integrity": "sha512-uH2O4OV5M9FZYQrwc7NdVmMxQJOCCzFeYudlZSzUAHRFeOujQefa92E74TQDVskNHCzOXoigEuoyzHDhaEaK5w==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.7.tgz", + "integrity": "sha512-hlQ96MBZSAXUq7ltkjtu3FJCCSMx/j629ns3hA3pXnBXjanNP0LHi+JpPeA81zaWgVK1VGH95Xuy7u0RyQ8kMg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.7.tgz", + "integrity": "sha512-2G8aAvF4wy1w/AGZkemprdGMRg5o6zPNhbHVImRz3lss55TYCBd6xStN19rt8XJHq20sqV0JbyWjOWwQRwV/wg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.8.tgz", + "integrity": "sha512-vObvMZB6hNWuDxhSaEPTKCwcqkAIuDtE+bQGn4XMXne1DSLzFVY8Vmj1bm+mUQXYNN8NmaQEO+r8MMbzPr1jBQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.8", + "@babel/helper-compilation-targets": "^7.24.8", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-validator-option": "^7.24.8", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.24.7", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.24.7", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoped-functions": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.24.7", + "@babel/plugin-transform-classes": "^7.24.8", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-dotall-regex": "^7.24.7", + "@babel/plugin-transform-duplicate-keys": "^7.24.7", + "@babel/plugin-transform-dynamic-import": "^7.24.7", + "@babel/plugin-transform-exponentiation-operator": "^7.24.7", + "@babel/plugin-transform-export-namespace-from": "^7.24.7", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.24.7", + "@babel/plugin-transform-json-strings": "^7.24.7", + "@babel/plugin-transform-literals": "^7.24.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-member-expression-literals": "^7.24.7", + "@babel/plugin-transform-modules-amd": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-modules-systemjs": "^7.24.7", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-new-target": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-object-super": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-property-literals": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-reserved-words": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-template-literals": "^7.24.7", + "@babel/plugin-transform-typeof-symbol": "^7.24.8", + "@babel/plugin-transform-unicode-escapes": "^7.24.7", + "@babel/plugin-transform-unicode-property-regex": "^7.24.7", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.37.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-flow": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.24.7.tgz", + "integrity": "sha512-NL3Lo0NorCU607zU3NwRyJbpaB6E3t0xtd3LfAQKDfkeX4/ggcDXvkmkW42QWT5owUeW/jAe4hn+2qvkV1IbfQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-transform-flow-strip-types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.7.tgz", + "integrity": "sha512-AAH4lEkpmzFWrGVlHaxJB7RLH21uPQ9+He+eFLWHmF9IuFQVugz8eAsamaW0DXRrTfco5zj1wWtpdcXJUOfsag==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.24.7", + "@babel/plugin-transform-react-jsx-development": "^7.24.7", + "@babel/plugin-transform-react-pure-annotations": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz", + "integrity": "sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.24.6.tgz", + "integrity": "sha512-WSuFCc2wCqMeXkz/i3yfAAsxwWflEgbVkZzivgAmXl/MxrXeoYFZOOPllbC8R8WTF7u61wSRQtDVZ1879cdu6w==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register/node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/register/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/register/node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/register/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz", + "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.8.tgz", + "integrity": "sha512-t0P1xxAPzEDcEPmjprAQq19NWum4K0EQPjMwZQZbHt+GiZqvjCHjj755Weq1YRPVzBI+3zSfvScfpnuIecVFJQ==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.8", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.8", + "@babel/types": "^7.24.8", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.8.tgz", + "integrity": "sha512-SkSBEHwwJRU52QEVZBmMBnE5Ux2/6WU1grdYyOhpbCNxbmJrDuDCphBzKZSO3taf0zztp+qkWlymE5tVL5l0TA==", + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@base2/pretty-print-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@base2/pretty-print-object/-/pretty-print-object-1.0.1.tgz", + "integrity": "sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==", + "dev": true + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz", + "integrity": "sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==", + "dev": true, + "dependencies": { + "cookie": "^0.5.0" + } + }, + "node_modules/@bundled-es-modules/cookie/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dev": true, + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "dev": true, + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, + "node_modules/@chromatic-com/storybook": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@chromatic-com/storybook/-/storybook-1.6.1.tgz", + "integrity": "sha512-x1x1NB3j4xpfeSWKr96emc+7ZvfsvH+/WVb3XCjkB24PPbT8VZXb3mJSAQMrSzuQ8+eQE9kDogYHH9Fj3tb/Cw==", + "dev": true, + "dependencies": { + "chromatic": "^11.4.0", + "filesize": "^10.0.12", + "jsonfile": "^6.1.0", + "react-confetti": "^6.1.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=16.0.0", + "yarn": ">=1.22.18" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", + "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^2.4.1" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", + "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz", + "integrity": "sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.1.1.tgz", + "integrity": "sha512-a7cxGcJ2wIlMFLlh8z2ONm+715QkPHiyJcxwQlKOz/03GPw1COpfhcmC9wm4xlZfp//jWHNNMwzjtqHXVWU9KA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.13" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "http-signature": "~1.3.6", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.10.4", + "safe-buffer": "^5.1.2", + "tough-cookie": "^4.1.3", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/@cypress/request/node_modules/qs": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", + "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@cypress/request/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@dual-bundle/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin-jsx-pragmatic": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin-jsx-pragmatic/-/babel-plugin-jsx-pragmatic-0.2.1.tgz", + "integrity": "sha512-xy1SlgEJygAAIvIuC2idkGKJYa6v5iwoyILkvNKgk347bV+IImXrUat5Z86EmLGyWhEoTplVT9EHqTnHZG4HFw==", + "dependencies": { + "@babel/plugin-syntax-jsx": "^7.17.12" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/@emotion/babel-preset-css-prop": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-preset-css-prop/-/babel-preset-css-prop-11.11.0.tgz", + "integrity": "sha512-+1Cba68IyBeltWzvbBSXcBWqP2eKQuQcSUpIu3ma4pOUeRol4EvwWrYS2Rv51aIVqg066fLB+Z9O/8NKR7uUlQ==", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.17.12", + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/babel-plugin-jsx-pragmatic": "^0.2.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "dependencies": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/eslint-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/eslint-plugin/-/eslint-plugin-11.11.0.tgz", + "integrity": "sha512-jCOYqU/0Sqm+g+6D7QuIlG99q8YAF0T7BP98zQF/MPZKfbcm46z5mizXn0YlhZ9AYZfNtZ1DeODXdncYxZzR4Q==", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "eslint": "6 || 7 || 8" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/react": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz", + "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz", + "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==", + "dependencies": { + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "node_modules/@emotion/unitless": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.7.tgz", + "integrity": "sha512-GE29uTT6y/Jv2EP0OjpTezeTQZ5FTCTaZXKrrdVGjb/t35AU4u/jiU+hUwUPpuK8fqhhiHkS/AawE3a3ZK/a9Q==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/installations": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.13.tgz", + "integrity": "sha512-aZ4wGfNDMsCxhKzDbK2g1aV0JKsdQ9FbeIsjpNJPzhahV0XYj+z36Y4RNLPpG/6hHU4gxnezxs+yn3HhHkNL8w==", + "dependencies": { + "@firebase/analytics": "0.10.7", + "@firebase/analytics-types": "0.8.2", + "@firebase/component": "0.6.8", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.2.tgz", + "integrity": "sha512-EnzNNLh+9/sJsimsA/FGqzakmrAUKLeJvjRHlg8df1f97NLUlFidk9600y0ZgWOp3CAxn6Hjtk+08tixlUOWyw==" + }, + "node_modules/@firebase/app": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.10.8.tgz", + "integrity": "sha512-xSLmW0/RShcnUEXH7l+wC0AFWaUtty4tUFF2loIgbtXTRmra0UH/SqYDf/IcfreUninRrCsusNmvoTidGkXJPw==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "idb": "7.1.1", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.7.tgz", + "integrity": "sha512-EkOeJcMKVR0zZ6z/jqcFTqHb/xq+TVIRIuBNGHdpcIuFU1czhSlegvqv2+nC+nFrkD8M6Xvd3tAlUOkdbMeS6A==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.14", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.14.tgz", + "integrity": "sha512-kK3bPfojAfXE53W+20rxMqIxrloFswXG9vh4kEdYL6Wa2IB3sD5++2dPiK3yGxl8oQiqS8qL2wcKB5/xLpEVEg==", + "dependencies": { + "@firebase/app-check": "0.8.7", + "@firebase/app-check-types": "0.5.2", + "@firebase/component": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz", + "integrity": "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.2.tgz", + "integrity": "sha512-FSOEzTzL5bLUbD2co3Zut46iyPWML6xc4x+78TeaXMSuJap5QObfb+rVvZJtla3asN4RwU7elaQaduP+HFizDA==" + }, + "node_modules/@firebase/app-compat": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.38.tgz", + "integrity": "sha512-36ZrSvkYLW7QR01Sii2X+IY18ErMpRg6e2B2f/DVTtJBolthwXOnNBps+wvaVBvegdvdVPspgDXZUV0ppqh45w==", + "dependencies": { + "@firebase/app": "0.10.8", + "@firebase/component": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz", + "integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==" + }, + "node_modules/@firebase/auth-compat": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.11.tgz", + "integrity": "sha512-7rE3MkQDoWwI2qd8qsra4/QZCO2GzQSbCL6AVQpult9+Nbimg+5A+YeHxpLTcYAxUV6HDg2CqTDQreFLhcm1CQ==", + "dependencies": { + "@firebase/auth": "1.7.6", + "@firebase/auth-types": "0.12.2", + "@firebase/component": "0.6.8", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0", + "undici": "5.28.4" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-compat/node_modules/@firebase/auth": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.7.6.tgz", + "integrity": "sha512-T+lA5xoug9CByGYkD5WkfTh2ujEYq/frGZPbk0H+fNU6fNl7nqg88KcsmzsC6Fsqbjm3LLEb/i6wJvF6NSNEig==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0", + "undici": "5.28.4" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat/node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz", + "integrity": "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==" + }, + "node_modules/@firebase/auth-types": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.12.2.tgz", + "integrity": "sha512-qsEBaRMoGvHO10unlDJhaKSuPn4pyoTtlQuP1ghZfzB6rNQPuhp/N/DcFZxm9i4v0SogjCbf9reWupwIvfmH6w==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.8.tgz", + "integrity": "sha512-LcNvxGLLGjBwB0dJUsBGCej2fqAepWyBubs4jt1Tiuns7QLbXHuyObZ4aMeBjZjWx4m8g1LoVI9QFpSaq/k4/g==", + "dependencies": { + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.7.tgz", + "integrity": "sha512-wjXr5AO8RPxVVg7rRCYffT7FMtBjHRfJ9KMwi19MbOf0vBf0H9YqW3WCgcnLpXI6ehiUcU3z3qgPnnU0nK6SnA==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.2", + "@firebase/auth-interop-types": "0.2.3", + "@firebase/component": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.7.tgz", + "integrity": "sha512-R/3B+VVzEFN5YcHmfWns3eitA8fHLTL03io+FIoMcTYkajFnrBdS3A+g/KceN9omP7FYYYGTQWF9lvbEx6eMEg==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/database": "1.0.7", + "@firebase/database-types": "1.0.4", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.4.tgz", + "integrity": "sha512-mz9ZzbH6euFXbcBo+enuJ36I5dR5w+enJHHjy9Y5ThCdKUseqfDjW3vCp1YxE9zygFCSjJJ/z1cQ+zodvUcwPQ==", + "dependencies": { + "@firebase/app-types": "0.9.2", + "@firebase/util": "1.9.7" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.6.5.tgz", + "integrity": "sha512-0+Ascaht4qUzj4pCopMPWmoAujk8HKjwCpaNYOOjbYMZ65RVfZPsfZwwbWi/zWMXj6xvPsai5oBiErUUkrLwNw==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "@firebase/webchannel-wrapper": "1.0.1", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0", + "undici": "5.28.4" + }, + "engines": { + "node": ">=10.10.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.34", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.34.tgz", + "integrity": "sha512-OBP2F/Ccydl2U2j8XIfpKBxf0EnQHEhbZ4LTwbSS2QlG9+8TwhvKFkKk/ZljWYqaype+qFKPuXZ5flCqYEETeA==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/firestore": "4.6.5", + "@firebase/firestore-types": "3.0.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.2.tgz", + "integrity": "sha512-wp1A+t5rI2Qc/2q7r2ZpjUXkRVPtGMd6zCLsiWurjsQpqPgFin3AhNibKcIzoF2rnToNa/XYtyWXuifjOOwDgg==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/firestore/node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/@firebase/functions": { + "version": "0.11.6", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.11.6.tgz", + "integrity": "sha512-GPfIBPtpwQvsC7SQbgaUjLTdja0CsNwMoKSgrzA1FGGRk4NX6qO7VQU6XCwBiAFWbpbQex6QWkSMsCzLx1uibQ==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.2", + "@firebase/auth-interop-types": "0.2.3", + "@firebase/component": "0.6.8", + "@firebase/messaging-interop-types": "0.2.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0", + "undici": "5.28.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.12.tgz", + "integrity": "sha512-r3XUb5VlITWpML46JymfJPkK6I9j4SNlO7qWIXUc0TUmkv0oAfVoiIt1F83/NuMZXaGr4YWA/794nVSy4GV8tw==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/functions": "0.11.6", + "@firebase/functions-types": "0.6.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.2.tgz", + "integrity": "sha512-0KiJ9lZ28nS2iJJvimpY4nNccV21rkQyor5Iheu/nq8aKXJqtJdeSlZDspjPSBBiHRzo7/GMUttegnsEITqR+w==" + }, + "node_modules/@firebase/functions/node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/@firebase/installations": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.8.tgz", + "integrity": "sha512-57V374qdb2+wT5v7+ntpLXBjZkO6WRgmAUbVkRfFTM/4t980p0FesbqTAcOIiM8U866UeuuuF8lYH70D3jM/jQ==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/util": "1.9.7", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.8.tgz", + "integrity": "sha512-pI2q8JFHB7yIq/szmhzGSWXtOvtzl6tCUmyykv5C8vvfOVJUH6mP4M4iwjbK8S1JotKd/K70+JWyYlxgQ0Kpyw==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/installations": "0.6.8", + "@firebase/installations-types": "0.5.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.2.tgz", + "integrity": "sha512-que84TqGRZJpJKHBlF2pkvc1YcXrtEDOVGiDjovP/a3s6W4nlbohGXEsBJo0JCeeg/UG9A+DEZVDUV9GpklUzA==", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz", + "integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.10", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.10.tgz", + "integrity": "sha512-fGbxJPKpl2DIKNJGhbk4mYPcM+qE2gl91r6xPoiol/mN88F5Ym6UeRdMVZah+pijh9WxM55alTYwXuW40r1Y2Q==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/installations": "0.6.8", + "@firebase/messaging-interop-types": "0.2.2", + "@firebase/util": "1.9.7", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.10.tgz", + "integrity": "sha512-FXQm7rcowkDm8kFLduHV35IRYCRo+Ng0PIp/t1+EBuEbyplaKkGjZ932pE+owf/XR+G/60ku2QRBptRGLXZydg==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/messaging": "0.12.10", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.2.tgz", + "integrity": "sha512-l68HXbuD2PPzDUOFb3aG+nZj5KA3INcPwlocwLZOzPp9rFM9yeuI9YLl6DQfguTX5eAGxO0doTR+rDLDvQb5tA==" + }, + "node_modules/@firebase/performance": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.8.tgz", + "integrity": "sha512-F+alziiIZ6Yn8FG47mxwljq+4XkgkT2uJIFRlkyViUQRLzrogaUJW6u/+6ZrePXnouKlKIwzqos3PVJraPEcCA==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/installations": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.8.tgz", + "integrity": "sha512-o7TFClRVJd3VIBoY7KZQqtCeW0PC6v9uBzM6Lfw3Nc9D7hM6OonqecYvh7NwJ6R14k+xM27frLS4BcCvFHKw2A==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/performance": "0.6.8", + "@firebase/performance-types": "0.2.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.2.tgz", + "integrity": "sha512-gVq0/lAClVH5STrIdKnHnCo2UcPLjJlDUoEB/tB4KM+hAeHUxWKnpT0nemUPvxZ5nbdY/pybeyMe8Cs29gEcHA==" + }, + "node_modules/@firebase/remote-config": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.4.8.tgz", + "integrity": "sha512-AMLqe6wfIRnjc6FkCWOSUjhc1fSTEf8o+cv1NolFvbiJ/tU+TqN4pI7pT+MIKQzNiq5fxLehkOx+xtAQBxPJKQ==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/installations": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.8.tgz", + "integrity": "sha512-UxSFOp6dzFj2AHB8Bq/BYtbq5iFyizKx4Rd6WxAdaKYM8cnPMeK+l2v+Oogtjae+AeyHRI+MfL2acsfVe5cd2A==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/remote-config": "0.4.8", + "@firebase/remote-config-types": "0.3.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.3.2.tgz", + "integrity": "sha512-0BC4+Ud7y2aPTyhXJTMTFfrGGLqdYXrUB9sJVAB8NiqJswDTc4/2qrE/yfUbnQJhbSi6ZaTTBKyG3n1nplssaA==" + }, + "node_modules/@firebase/storage": { + "version": "0.12.6", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.12.6.tgz", + "integrity": "sha512-Zgb9WuehJxzhj7pGXUvkAEaH+3HvLjD9xSZ9nepuXf5f8378xME7oGJtREr/RnepdDA5YW0XIxe0QQBNHpe1nw==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0", + "undici": "5.28.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.9.tgz", + "integrity": "sha512-WWgAp5bTW961oIsCc9+98m4MIVKpEqztAlIngfHfwO/x3DYoBPRl/awMRG3CAXyVxG+7B7oHC5IsnqM+vTwx2A==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/storage": "0.12.6", + "@firebase/storage-types": "0.8.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.2.tgz", + "integrity": "sha512-0vWu99rdey0g53lA7IShoA2Lol1jfnPovzLDUBuon65K7uKG9G+L5uO05brD9pMw+l4HRFw23ah3GwTGpEav6g==", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/storage/node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.7.tgz", + "integrity": "sha512-fBVNH/8bRbYjqlbIhZ+lBtdAAS4WqZumx03K06/u7fJSpz1TGjEMm1ImvKD47w+xaFKIP2ori6z8BrbakRfjJA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/vertexai-preview": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@firebase/vertexai-preview/-/vertexai-preview-0.0.3.tgz", + "integrity": "sha512-KVtUWLp+ScgiwkDKAvNkVucAyhLVQp6C6lhnVEuIg4mWhWcS3oerjAeVhZT4uNofKwWxRsOaB2Yec7DMTXlQPQ==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.2", + "@firebase/component": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.1.tgz", + "integrity": "sha512-jmEnr/pk0yVkA7mIlHNnxCi+wWzOFUg0WyIotgkKAb2u1J7fAeDBcVNSTjTihbAYNusCLQdW5s9IJ5qwnEufcQ==" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead" + }, + "node_modules/@inquirer/confirm": { + "version": "3.1.16", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.16.tgz", + "integrity": "sha512-DXgLZim+YVTk05zRywvFRfJt2Jje7sZ4DO6Ss9RpGtgXEd/T0IiTqubHWst0IazCwdPI9g/06Rtm/nm4IBFJBA==", + "dev": true, + "dependencies": { + "@inquirer/core": "^9.0.4", + "@inquirer/type": "^1.5.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.0.4.tgz", + "integrity": "sha512-46LaWACIctSfVKTu71ziFlqO8SVLhWGSxvaHpf0frfDTphSSpIfeNo5ZH/kJPHYJw4VgPGf/9c3zJN/FnCdaIQ==", + "dev": true, + "dependencies": { + "@inquirer/figures": "^1.0.4", + "@inquirer/type": "^1.5.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^20.14.11", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-spinners": "^2.9.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/@types/node": { + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@inquirer/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.4.tgz", + "integrity": "sha512-R7Gsg6elpuqdn55fBH2y9oYzrU/yKrSmIsDX4ROT51vohrECFzTf2zw9BfUbOW8xjfmM2QbVoVYdTwhrtEKWSQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.0.tgz", + "integrity": "sha512-L/UdayX9Z1lLN+itoTKqJ/X4DX5DaWu2Sruwt4XgZzMNv32x4qllbzMX4MbJlz0yxAQtU19UvABGOjmdq1u3qA==", + "dev": true, + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "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 + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/@jest/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "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 + } + } + }, + "node_modules/@jest/reporters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.0.4.tgz", + "integrity": "sha512-aOcSN4MeAtFROysrbqG137b7gaDDSmVrl5mpo6sT/w+kcXpWnzhMjmY/Fh/sDx26NBxyIE7MB1seqLeCAzy9Sg==", + "dev": true, + "dependencies": { + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", + "hyperdyperid": "^1.2.0", + "thingies": "^1.20.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.2.0.tgz", + "integrity": "sha512-4B8B+3vFsY4eo33DMKyJPlQ3sBMpPFUZK2dr3O3rXrOGKKbYG44J0XSFkDo1VOQiri5HFEhIeVvItjR2xcazmg==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true + }, + "node_modules/@mdn/browser-compat-data": { + "version": "5.5.39", + "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-5.5.39.tgz", + "integrity": "sha512-22awGsC5t7sGOT2u5EU1RA64L+F87GWYXHZkh0ofjJsLGObqNDDVSTlumd/+6YK3QwlOIEVWAsqmJymrrSqBlA==", + "dev": true + }, + "node_modules/@mdx-js/react": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.0.1.tgz", + "integrity": "sha512-9ZrPIU4MGf6et1m1ov3zKf+q9+deetI51zprKB1D/z3NOb+rUxxtEl3mCjW5wTGh6VhRdwPueh1oRzi6ezkA8A==", + "dev": true, + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.29.1.tgz", + "integrity": "sha512-3rDakgJZ77+RiQUuSK69t1F0m8BQKA8Vh5DCS5V0DWvNY67zob2JhhQrhCO0AKLGINTRSFd1tBaHcJTkhefoSw==", + "dev": true, + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.2.1", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.15.tgz", + "integrity": "sha512-LFWllMA55pzB9D34w/wXUCf8+c+IYKuJDgxiZ3qMhl64KRMBHYM1I3VdGaD2BV5FNPV2/S2596bppxHbv2ZydQ==", + "dev": true, + "dependencies": { + "ansi-html": "^0.0.9", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "html-entities": "^2.1.0", + "loader-utils": "^2.0.4", + "schema-utils": "^4.2.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "@types/webpack": "4.x || 5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <5.0.0", + "webpack": ">=4.43.0 <6.0.0", + "webpack-dev-server": "3.x || 4.x || 5.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "0.x || 1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, + "type-fest": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-hot-middleware": { + "optional": true + }, + "webpack-plugin-serve": { + "optional": true + } + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@remix-run/router": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.17.1.tgz", + "integrity": "sha512-mCOMec4BKd6BRGBZeSnGiIgwsbLGp3yhVqAD8H+PxiRNEHgDpZb8J1TnrSDlg97t0ySKMQJTHCWBCmBpSmkF6Q==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@sentry-internal/browser-utils": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.24.0.tgz", + "integrity": "sha512-U5dVZ4JM+UeN3YWBUHZcNLF038C3ccTTsTICIw+zfCQbpPhPms8DOEDVpd0So18XoNDzYmLo07hC1BwByRAfGw==", + "dependencies": { + "@sentry/core": "8.24.0", + "@sentry/types": "8.24.0", + "@sentry/utils": "8.24.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.24.0.tgz", + "integrity": "sha512-0tWRp8SOSTSPTViRJnB6+HHixFgkEWjKPciuLsAZkobRhi+VVedPj3zVztORy5AvARGr6AgyVSdnviilcrKl6g==", + "dependencies": { + "@sentry/core": "8.24.0", + "@sentry/types": "8.24.0", + "@sentry/utils": "8.24.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.24.0.tgz", + "integrity": "sha512-+3d+3Ln7iDOZo2wOBv7EWojVHigEskjKsz8vR3WFdxYyue8e3zPQ/xg/t9A6BtEVRPQsEyhM3oN6LyjqFv2nfg==", + "dependencies": { + "@sentry-internal/browser-utils": "8.24.0", + "@sentry/core": "8.24.0", + "@sentry/types": "8.24.0", + "@sentry/utils": "8.24.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.24.0.tgz", + "integrity": "sha512-MI+j9tUab1d5oer2xKQ2lxdXSzBeZ1DF2dwlVxQDOfSAQqRfZJpmLcmSPb6M+GJsf2xHg6n4dAQvWQuM0qGQPQ==", + "dependencies": { + "@sentry-internal/replay": "8.24.0", + "@sentry/core": "8.24.0", + "@sentry/types": "8.24.0", + "@sentry/utils": "8.24.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/babel-plugin-component-annotate": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.0.tgz", + "integrity": "sha512-UzH+NNhgnOo6UFku3C4TEz+pO/yDcIA5FKTJvLbJ7lQwAjsqLs3DZWm4cCA08skICb8mULArF6S/dn5/butVCA==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@sentry/browser": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.24.0.tgz", + "integrity": "sha512-WdCLUoMAE0ZWsZDb3G/FQI5YgkH59VVEpnPqrWI08m2KuqLz8eU724JZvNzaDv/L2yzksgS4HDDUXkNRzDeCrQ==", + "dependencies": { + "@sentry-internal/browser-utils": "8.24.0", + "@sentry-internal/feedback": "8.24.0", + "@sentry-internal/replay": "8.24.0", + "@sentry-internal/replay-canvas": "8.24.0", + "@sentry/core": "8.24.0", + "@sentry/types": "8.24.0", + "@sentry/utils": "8.24.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/bundler-plugin-core": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.0.tgz", + "integrity": "sha512-/xXN8o7565WMsewBnQFfjm0E5wqhYsegg++HJ5RjrY/cTM4qcd/ven44GEMxqGFJitZizvkk3NHszaHylzcRUw==", + "dependencies": { + "@babel/core": "^7.18.5", + "@sentry/babel-plugin-component-annotate": "2.22.0", + "@sentry/cli": "^2.33.1", + "dotenv": "^16.3.1", + "find-up": "^5.0.0", + "glob": "^9.3.2", + "magic-string": "0.30.8", + "unplugin": "1.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/minimatch": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", + "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/unplugin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz", + "integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==", + "dependencies": { + "acorn": "^8.8.1", + "chokidar": "^3.5.3", + "webpack-sources": "^3.2.3", + "webpack-virtual-modules": "^0.5.0" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/webpack-virtual-modules": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz", + "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==" + }, + "node_modules/@sentry/cli": { + "version": "2.33.1", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.33.1.tgz", + "integrity": "sha512-dUlZ4EFh98VFRPJ+f6OW3JEYQ7VvqGNMa0AMcmvk07ePNeK/GicAWmSQE4ZfJTTl80ul6HZw1kY01fGQOQlVRA==", + "hasInstallScript": true, + "dependencies": { + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.7", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "which": "^2.0.2" + }, + "bin": { + "sentry-cli": "bin/sentry-cli" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@sentry/cli-darwin": "2.33.1", + "@sentry/cli-linux-arm": "2.33.1", + "@sentry/cli-linux-arm64": "2.33.1", + "@sentry/cli-linux-i686": "2.33.1", + "@sentry/cli-linux-x64": "2.33.1", + "@sentry/cli-win32-i686": "2.33.1", + "@sentry/cli-win32-x64": "2.33.1" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.33.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.33.1.tgz", + "integrity": "sha512-+4/VIx/E1L2hChj5nGf5MHyEPHUNHJ/HoG5RY+B+vyEutGily1c1+DM2bum7RbD0xs6wKLIyup5F02guzSzG8A==", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm": { + "version": "2.33.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.33.1.tgz", + "integrity": "sha512-zbxEvQju+tgNvzTOt635le4kS/Fbm2XC2RtYbCTs034Vb8xjrAxLnK0z1bQnStUV8BkeBHtsNVrG+NSQDym2wg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.33.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.33.1.tgz", + "integrity": "sha512-DbGV56PRKOLsAZJX27Jt2uZ11QfQEMmWB4cIvxkKcFVE+LJP4MVA+MGGRUL6p+Bs1R9ZUuGbpKGtj0JiG6CoXw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-i686": { + "version": "2.33.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.33.1.tgz", + "integrity": "sha512-g2LS4oPXkPWOfKWukKzYp4FnXVRRSwBxhuQ9eSw2peeb58ZIObr4YKGOA/8HJRGkooBJIKGaAR2mH2Pk1TKaiA==", + "cpu": [ + "x86", + "ia32" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-x64": { + "version": "2.33.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.33.1.tgz", + "integrity": "sha512-IV3dcYV/ZcvO+VGu9U6kuxSdbsV2kzxaBwWUQxtzxJ+cOa7J8Hn1t0koKGtU53JVZNBa06qJWIcqgl4/pCuKIg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-i686": { + "version": "2.33.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.33.1.tgz", + "integrity": "sha512-F7cJySvkpzIu7fnLKNHYwBzZYYwlhoDbAUnaFX0UZCN+5DNp/5LwTp37a5TWOsmCaHMZT4i9IO4SIsnNw16/zQ==", + "cpu": [ + "x86", + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-x64": { + "version": "2.33.1", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.33.1.tgz", + "integrity": "sha512-8VyRoJqtb2uQ8/bFRKNuACYZt7r+Xx0k2wXRGTyH05lCjAiVIXn7DiS2BxHFty7M1QEWUCMNsb/UC/x/Cu2wuA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/core": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.24.0.tgz", + "integrity": "sha512-nyy7po78Ef5KNzehHJCCyLGGR/FceHyw2IRzDQUVD6M4tos8G1OML1gcnALChWhyeq1SIoDsC1ofxFlbkIWuog==", + "dependencies": { + "@sentry/types": "8.24.0", + "@sentry/utils": "8.24.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/react": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-8.24.0.tgz", + "integrity": "sha512-UaNmGEtYUFMoE1lKlsedOYGvQX72/A+/CiDg5umQwwS33XfGY4geh3zMo3jjEKTjhR1T5gofBz74sXnTCyrW4A==", + "dependencies": { + "@sentry/browser": "8.24.0", + "@sentry/core": "8.24.0", + "@sentry/types": "8.24.0", + "@sentry/utils": "8.24.0", + "hoist-non-react-statics": "^3.3.2" + }, + "engines": { + "node": ">=14.18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, + "node_modules/@sentry/types": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.24.0.tgz", + "integrity": "sha512-5QWXARoFrvTvnS19ip+ha0x4nWIv/RvoCTnqCsgrNTjypbk1+KMSMQQhGMo8OuEBFhdGyTs1BqfxVV82URHh3w==", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/utils": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.24.0.tgz", + "integrity": "sha512-AGo5PldxCJYn3g0IYXeBkeALNa+NieJaaCDpYyzrKAFdxoA6Qp+Z/wmN9m5BYZ9eHx9N+xMOoz2aIh4hG48VbQ==", + "dependencies": { + "@sentry/types": "8.24.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/@sentry/webpack-plugin": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-2.22.0.tgz", + "integrity": "sha512-u2brctki0AMCoZksdAConQSYE6PokRVeZ4YYsbnJYkAi0KuaQnczsRwS9e2L0bK2CmZ7QdyYcrjaXHNlXaFDbQ==", + "dependencies": { + "@sentry/bundler-plugin-core": "2.22.0", + "unplugin": "1.0.1", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "webpack": ">=4.40.0" + } + }, + "node_modules/@sentry/webpack-plugin/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/@sentry/webpack-plugin/node_modules/unplugin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz", + "integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==", + "dependencies": { + "acorn": "^8.8.1", + "chokidar": "^3.5.3", + "webpack-sources": "^3.2.3", + "webpack-virtual-modules": "^0.5.0" + } + }, + "node_modules/@sentry/webpack-plugin/node_modules/webpack-virtual-modules": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz", + "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@storybook/addon-actions": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.2.2.tgz", + "integrity": "sha512-SN4cSRt3f0qXi5te+yhMseSdQuZntA8lGlASbRmN77YQTpIaGsNiH88xFoky0s9qz531hiRfU1R0ZSMylBwSKw==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0", + "@types/uuid": "^9.0.1", + "dequal": "^2.0.2", + "polished": "^4.2.2", + "uuid": "^9.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + } + }, + "node_modules/@storybook/addon-backgrounds": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.2.2.tgz", + "integrity": "sha512-m/xJe7uKL+kfJx7pQcHwAeIvJ3tdLIpDGrMAVDNDJHcAxfe44cFjIInaV/1HKf3y5Awap+DZFW66ekkxuI9zzA==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0", + "memoizerific": "^1.11.3", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + } + }, + "node_modules/@storybook/addon-controls": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.2.2.tgz", + "integrity": "sha512-y241aOANGzT5XBADUIvALwG/xF5eC6UItzmWJaFvOzSBCq74GIA0+Hu9atyFdvFQbXOrdvPWC4jR+9iuBFRxAA==", + "dev": true, + "dependencies": { + "dequal": "^2.0.2", + "lodash": "^4.17.21", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + } + }, + "node_modules/@storybook/addon-docs": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.2.2.tgz", + "integrity": "sha512-qk/yjAR9RpsSrKLLbeCgb6u58c8TmYqyJSnXgbAozZZNKHBWlIpvZ/hTNYud8qo0coPlxnLdjnZf32TykWGlAg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.4", + "@mdx-js/react": "^3.0.0", + "@storybook/blocks": "8.2.2", + "@storybook/csf-plugin": "8.2.2", + "@storybook/global": "^5.0.0", + "@storybook/react-dom-shim": "8.2.2", + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "fs-extra": "^11.1.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "rehype-external-links": "^3.0.0", + "rehype-slug": "^6.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + } + }, + "node_modules/@storybook/addon-essentials": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-8.2.2.tgz", + "integrity": "sha512-yN//BFMbSvNV0+Sll2hcKmgJX06TUKQDm6pZimUjkXczFtOmK7K/UdDmKjWS+qjhfJdWpxdRoEpxoHvvRmNfsA==", + "dev": true, + "dependencies": { + "@storybook/addon-actions": "8.2.2", + "@storybook/addon-backgrounds": "8.2.2", + "@storybook/addon-controls": "8.2.2", + "@storybook/addon-docs": "8.2.2", + "@storybook/addon-highlight": "8.2.2", + "@storybook/addon-measure": "8.2.2", + "@storybook/addon-outline": "8.2.2", + "@storybook/addon-toolbars": "8.2.2", + "@storybook/addon-viewport": "8.2.2", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + } + }, + "node_modules/@storybook/addon-highlight": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-8.2.2.tgz", + "integrity": "sha512-yDTRzzL+IJAymgY32xoZl09BGBVmPOUV2wVNGYcZkkBLvz2GSQMTfUe1/7F4jAx//+rFBu48/MQzsTC7Bk8kPw==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + } + }, + "node_modules/@storybook/addon-interactions": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-8.2.2.tgz", + "integrity": "sha512-zRRuUwm/l41JtTUgjIoQTUgLT99Hsdz9cqKca/8NYo1MGBdEcKE41DH4aBIzKaOKFu7p9q00/o/X1EqYX4LMUA==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/instrumenter": "8.2.2", + "@storybook/test": "8.2.2", + "polished": "^4.2.2", + "ts-dedent": "^2.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + } + }, + "node_modules/@storybook/addon-links": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-8.2.2.tgz", + "integrity": "sha512-eGh7O7SgTJMtnuXC0HlRPOegu1njcJS2cnVqjbzjvjxsPSBhbHpdYMi9Q9E7al/FKuqMUOjIR9YLIlmK1AJaqA==", + "dev": true, + "dependencies": { + "@storybook/csf": "0.1.11", + "@storybook/global": "^5.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^8.2.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, + "node_modules/@storybook/addon-measure": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.2.2.tgz", + "integrity": "sha512-3rCo/aMltt5FrBVdr2dYlD8HlE2q9TLKGJZnwh9on4QyL6ArHbdYw0LmyHe/LrFahJ49w1XQZBMSJcAdRkkS7w==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0", + "tiny-invariant": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + } + }, + "node_modules/@storybook/addon-onboarding": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-onboarding/-/addon-onboarding-8.2.2.tgz", + "integrity": "sha512-dCdE8Mt/JW6cq6dY7co35Sul/bAkUT3ixaxBrUagFUYUQ/PTYM6p4/B+45RURD5S9z8LVHH1rVgmEeScm3U78w==", + "dev": true, + "dependencies": { + "react-confetti": "^6.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + } + }, + "node_modules/@storybook/addon-outline": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.2.2.tgz", + "integrity": "sha512-Y+PQtfTNO8GLX5nz+3x5AMfHNvdGvBXazJ29+Rl1ygYN1+Q9ZhRJDE1kAK0wLxb7CG14peAgdYEaQb3Rduv7HQ==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + } + }, + "node_modules/@storybook/addon-toolbars": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.2.2.tgz", + "integrity": "sha512-JGOueOc3EPljlCl9dVSQee0aMYoqGNvN0UH+R6wYJ3bDZ+tUG/iYpsZVPUOvS8vzp3Imk5Is1kzQbQYJtzdGLg==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + } + }, + "node_modules/@storybook/addon-viewport": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.2.2.tgz", + "integrity": "sha512-gkZ8bsjGGP0NuevkT2iKC+szezSy+w4BrBDknf490mRU2K/B2e7TGojf/j/AtxzILMzD4IKzKUXbE/zwcqjZvA==", + "dev": true, + "dependencies": { + "memoizerific": "^1.11.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + } + }, + "node_modules/@storybook/addon-webpack5-compiler-swc": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@storybook/addon-webpack5-compiler-swc/-/addon-webpack5-compiler-swc-1.0.4.tgz", + "integrity": "sha512-S/ypdAK9oqwUAt3ZOn44qi3RWdH5uBLbBgtfHSXckqTpQRu7F7A9bRzjK+H5ti4xVADRhxu/xzIBwxWgcCeIXA==", + "dev": true, + "dependencies": { + "@swc/core": "1.5.7", + "swc-loader": "^0.2.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@storybook/blocks": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.2.2.tgz", + "integrity": "sha512-av0Tryg4toDl2L/d1ABErtsAk9wvM1su6+M4wq5/Go50sk5IjGTldhbZFa9zNOohxLkZwaj0Q5xAgJ1Y+m5KrQ==", + "dev": true, + "dependencies": { + "@storybook/csf": "0.1.11", + "@storybook/global": "^5.0.0", + "@storybook/icons": "^1.2.5", + "@types/lodash": "^4.14.167", + "color-convert": "^2.0.1", + "dequal": "^2.0.2", + "lodash": "^4.17.21", + "markdown-to-jsx": "^7.4.5", + "memoizerific": "^1.11.3", + "polished": "^4.2.2", + "react-colorful": "^5.1.2", + "telejson": "^7.2.0", + "ts-dedent": "^2.0.0", + "util-deprecate": "^1.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "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.2.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/@storybook/builder-webpack5": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-8.2.2.tgz", + "integrity": "sha512-ud6a3pRusbC/TvT1ed15INxSivyL2y2zI61O/MWQZmM8sZOIC6ObdHLtzU4+535IIqiXhPoQ/QiOBbejqjgZvw==", + "dev": true, + "dependencies": { + "@storybook/core-webpack": "8.2.2", + "@types/node": "^18.0.0", + "@types/semver": "^7.3.4", + "browser-assert": "^1.2.1", + "case-sensitive-paths-webpack-plugin": "^2.4.0", + "cjs-module-lexer": "^1.2.3", + "constants-browserify": "^1.0.0", + "css-loader": "^6.7.1", + "es-module-lexer": "^1.5.0", + "express": "^4.19.2", + "fork-ts-checker-webpack-plugin": "^8.0.0", + "fs-extra": "^11.1.0", + "html-webpack-plugin": "^5.5.0", + "magic-string": "^0.30.5", + "path-browserify": "^1.0.1", + "process": "^0.11.10", + "semver": "^7.3.7", + "style-loader": "^3.3.1", + "terser-webpack-plugin": "^5.3.1", + "ts-dedent": "^2.0.0", + "url": "^0.11.0", + "util": "^0.12.4", + "util-deprecate": "^1.0.2", + "webpack": "5", + "webpack-dev-middleware": "^6.1.2", + "webpack-hot-middleware": "^2.25.1", + "webpack-virtual-modules": "^0.6.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/builder-webpack5/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@storybook/codemod": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@storybook/codemod/-/codemod-8.2.4.tgz", + "integrity": "sha512-QcZdqjX4NvkVcWR3yI9it3PfqmBOCR+3iY6j4PmG7p5IE0j9kXMKBbeFrBRprSijHKlwcjbc3bRx2SnKF6AFEg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/preset-env": "^7.24.4", + "@babel/types": "^7.24.0", + "@storybook/core": "8.2.4", + "@storybook/csf": "0.1.11", + "@types/cross-spawn": "^6.0.2", + "cross-spawn": "^7.0.3", + "globby": "^14.0.1", + "jscodeshift": "^0.15.1", + "lodash": "^4.17.21", + "prettier": "^3.1.1", + "recast": "^0.23.5", + "tiny-invariant": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/codemod/node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/codemod/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/codemod/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/core": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.2.4.tgz", + "integrity": "sha512-jePmsGZT2hhUNQs8ED6+hFVt2m4hrMseO8kkN7Mcsve1MIujzHUS7Gjo4uguBwHJJOtiXB2fw4OSiQCmsXscZA==", + "dev": true, + "dependencies": { + "@storybook/csf": "0.1.11", + "@types/express": "^4.17.21", + "@types/node": "^18.0.0", + "browser-assert": "^1.2.1", + "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0", + "esbuild-register": "^3.5.0", + "express": "^4.19.2", + "process": "^0.11.10", + "recast": "^0.23.5", + "util": "^0.12.4", + "ws": "^8.2.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/@storybook/core-webpack": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-8.2.2.tgz", + "integrity": "sha512-M5wzgNbotVXcfo7WkXIuDxcBl7tTjnQ27lmlSBk+cu63pDvNn4UMDan621FcvxWq2DbjgIj+PASZ4DzM5O+ovA==", + "dev": true, + "dependencies": { + "@types/node": "^18.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + } + }, + "node_modules/@storybook/csf": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.1.11.tgz", + "integrity": "sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg==", + "dev": true, + "dependencies": { + "type-fest": "^2.19.0" + } + }, + "node_modules/@storybook/csf-plugin": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-8.2.2.tgz", + "integrity": "sha512-3K2RUpDDvq3DT46qAIj2VBC+fzTTebRUcZUsRfS6G1AzaX9p25iClEHiwcJacFkgQKhkci8A/Ly3Z4JJ3b4Pgw==", + "dev": true, + "dependencies": { + "unplugin": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + } + }, + "node_modules/@storybook/csf/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/global": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", + "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", + "dev": true + }, + "node_modules/@storybook/icons": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.2.9.tgz", + "integrity": "sha512-cOmylsz25SYXaJL/gvTk/dl3pyk7yBFRfeXTsHvTA3dfhoU/LWSq0NKL9nM7WBasJyn6XPSGnLS4RtKXLw5EUg==", + "dev": true, + "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" + } + }, + "node_modules/@storybook/instrumenter": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-8.2.2.tgz", + "integrity": "sha512-refwnHqKHhya45MgqakhMG0jKhTiEIAl0aOwAaQy9+zf9ncMIYQAXRQsSZ2Z188lFWE24wbeHKteb62a5ZfWwQ==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0", + "@vitest/utils": "^1.3.1", + "util": "^0.12.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + } + }, + "node_modules/@storybook/preset-react-webpack": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/preset-react-webpack/-/preset-react-webpack-8.2.2.tgz", + "integrity": "sha512-GJkDtw4Ac8icD66fotGXYE3rmZkIwASpNLOeGzyP4eMMNaf5vlvTDxwkY551cGbnA5P7r4UkGjDiWinB9XE4VQ==", + "dev": true, + "dependencies": { + "@storybook/core-webpack": "8.2.2", + "@storybook/react": "8.2.2", + "@storybook/react-docgen-typescript-plugin": "1.0.6--canary.9.0c3f3b7.0", + "@types/node": "^18.0.0", + "@types/semver": "^7.3.4", + "find-up": "^5.0.0", + "fs-extra": "^11.1.0", + "magic-string": "^0.30.5", + "react-docgen": "^7.0.0", + "resolve": "^1.22.8", + "semver": "^7.3.7", + "tsconfig-paths": "^4.2.0", + "webpack": "5" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "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.2.2" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/preset-react-webpack/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@storybook/react": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-8.2.2.tgz", + "integrity": "sha512-U4p/RV78yhjEwEzem8U7wE5/3sSpnqreGsPdAHMCIHd69e9tVeF0rwrTJGp917RClPjBKgEcfelCuvOlby4MrA==", + "dev": true, + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/react-dom-shim": "8.2.2", + "@types/escodegen": "^0.0.6", + "@types/estree": "^0.0.51", + "@types/node": "^18.0.0", + "acorn": "^7.4.1", + "acorn-jsx": "^5.3.1", + "acorn-walk": "^7.2.0", + "escodegen": "^2.1.0", + "html-tags": "^3.1.0", + "lodash": "^4.17.21", + "prop-types": "^15.7.2", + "react-element-to-jsx-string": "^15.0.0", + "semver": "^7.3.7", + "ts-dedent": "^2.0.0", + "type-fest": "~2.19", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "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.2.2", + "typescript": ">= 4.2.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/react-docgen-typescript-plugin": { + "version": "1.0.6--canary.9.0c3f3b7.0", + "resolved": "https://registry.npmjs.org/@storybook/react-docgen-typescript-plugin/-/react-docgen-typescript-plugin-1.0.6--canary.9.0c3f3b7.0.tgz", + "integrity": "sha512-KUqXC3oa9JuQ0kZJLBhVdS4lOneKTOopnNBK4tUAgoxWQ3u/IjzdueZjFr7gyBrXMoU6duutk3RQR9u8ZpYJ4Q==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "endent": "^2.0.1", + "find-cache-dir": "^3.3.1", + "flat-cache": "^3.0.4", + "micromatch": "^4.0.2", + "react-docgen-typescript": "^2.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "typescript": ">= 4.x", + "webpack": ">= 4" + } + }, + "node_modules/@storybook/react-dom-shim": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.2.2.tgz", + "integrity": "sha512-4fb1/yT9WXHzHjs0In6orIEZxga5eXd9UaXEFGudBgowCjDUVP9LabDdKTbGusz20lfaAkATsRG/W+EcSLoh8w==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "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.2.2" + } + }, + "node_modules/@storybook/react-webpack5": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/react-webpack5/-/react-webpack5-8.2.2.tgz", + "integrity": "sha512-JPR2Lp88KbfRWgnAd4lKFRKuc9Up6YeqbaDb6sptOXXzDM4nOhlRXKqp2tIqyhfiKp3wmu3PksixqD8f8VS9CA==", + "dev": true, + "dependencies": { + "@storybook/builder-webpack5": "8.2.2", + "@storybook/preset-react-webpack": "8.2.2", + "@storybook/react": "8.2.2", + "@types/node": "^18.0.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "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.2.2", + "typescript": ">= 4.2.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/react/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@storybook/react/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/test": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@storybook/test/-/test-8.2.2.tgz", + "integrity": "sha512-X2qAKErjTh1X7XLAZqCMtU0ZK8JuwdKmgiqU0oXWxIDmCX6/Dm9ZIcdMZHs/S+K/UnIByjNlQpTShLVfRUeN1w==", + "dev": true, + "dependencies": { + "@storybook/csf": "0.1.11", + "@storybook/instrumenter": "8.2.2", + "@testing-library/dom": "10.1.0", + "@testing-library/jest-dom": "6.4.5", + "@testing-library/user-event": "14.5.2", + "@vitest/expect": "1.6.0", + "@vitest/spy": "1.6.0", + "util": "^0.12.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.2.2" + } + }, + "node_modules/@storybook/test/node_modules/@testing-library/dom": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.1.0.tgz", + "integrity": "sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@storybook/test/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@storybook/test/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@storybook/test/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@storybook/test/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@stylelint/postcss-css-in-js": { + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.38.0.tgz", + "integrity": "sha512-XOz5CAe49kS95p5yRd+DAIWDojTjfmyAQ4bbDlXMdbZTQ5t0ThjSLvWI6JI2uiS7MFurVBkZ6zUqcimzcLTBoQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "dependencies": { + "@babel/core": "^7.17.9" + }, + "peerDependencies": { + "postcss": ">=7.0.0", + "postcss-syntax": ">=0.36.2" + } + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/core/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", + "dev": true, + "dependencies": { + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } + }, + "node_modules/@svgr/plugin-svgo/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@svgr/webpack": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", + "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@babel/plugin-transform-react-constant-elements": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "@svgr/plugin-svgo": "8.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@swc/core": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.5.7.tgz", + "integrity": "sha512-U4qJRBefIJNJDRCCiVtkfa/hpiZ7w0R6kASea+/KLp+vkus3zcLSB8Ub8SvKgTIxjWpwsKcZlPf5nrv4ls46SQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.2", + "@swc/types": "0.1.7" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.5.7", + "@swc/core-darwin-x64": "1.5.7", + "@swc/core-linux-arm-gnueabihf": "1.5.7", + "@swc/core-linux-arm64-gnu": "1.5.7", + "@swc/core-linux-arm64-musl": "1.5.7", + "@swc/core-linux-x64-gnu": "1.5.7", + "@swc/core-linux-x64-musl": "1.5.7", + "@swc/core-win32-arm64-msvc": "1.5.7", + "@swc/core-win32-ia32-msvc": "1.5.7", + "@swc/core-win32-x64-msvc": "1.5.7" + }, + "peerDependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.7.tgz", + "integrity": "sha512-bZLVHPTpH3h6yhwVl395k0Mtx8v6CGhq5r4KQdAoPbADU974Mauz1b6ViHAJ74O0IVE5vyy7tD3OpkQxL/vMDQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.5.7.tgz", + "integrity": "sha512-RpUyu2GsviwTc2qVajPL0l8nf2vKj5wzO3WkLSHAHEJbiUZk83NJrZd1RVbEknIMO7+Uyjh54hEh8R26jSByaw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.5.7.tgz", + "integrity": "sha512-cTZWTnCXLABOuvWiv6nQQM0hP6ZWEkzdgDvztgHI/+u/MvtzJBN5lBQ2lue/9sSFYLMqzqff5EHKlFtrJCA9dQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.5.7.tgz", + "integrity": "sha512-hoeTJFBiE/IJP30Be7djWF8Q5KVgkbDtjySmvYLg9P94bHg9TJPSQoC72tXx/oXOgXvElDe/GMybru0UxhKx4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.5.7.tgz", + "integrity": "sha512-+NDhK+IFTiVK1/o7EXdCeF2hEzCiaRSrb9zD7X2Z7inwWlxAntcSuzZW7Y6BRqGQH89KA91qYgwbnjgTQ22PiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.5.7.tgz", + "integrity": "sha512-25GXpJmeFxKB+7pbY7YQLhWWjkYlR+kHz5I3j9WRl3Lp4v4UD67OGXwPe+DIcHqcouA1fhLhsgHJWtsaNOMBNg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.5.7.tgz", + "integrity": "sha512-0VN9Y5EAPBESmSPPsCJzplZHV26akC0sIgd3Hc/7S/1GkSMoeuVL+V9vt+F/cCuzr4VidzSkqftdP3qEIsXSpg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.5.7.tgz", + "integrity": "sha512-RtoNnstBwy5VloNCvmvYNApkTmuCe4sNcoYWpmY7C1+bPR+6SOo8im1G6/FpNem8AR5fcZCmXHWQ+EUmRWJyuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.5.7.tgz", + "integrity": "sha512-Xm0TfvcmmspvQg1s4+USL3x8D+YPAfX2JHygvxAnCJ0EHun8cm2zvfNBcsTlnwYb0ybFWXXY129aq1wgFC9TpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.5.7.tgz", + "integrity": "sha512-tp43WfJLCsKLQKBmjmY/0vv1slVywR5Q4qKjF5OIY8QijaEW7/8VwPyUyVoJZEnDgv9jKtUTG5PzqtIYPZGnyg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true + }, + "node_modules/@swc/types": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.7.tgz", + "integrity": "sha512-scHWahbHF0eyj3JsxG9CFJgFdFNaVQCNAimBlT6PzS3n/HptxqREjsm4OH6AN3lYcffZYSPxXW8ua2BEHp0lJQ==", + "dev": true, + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.51.1", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.1.tgz", + "integrity": "sha512-fJBMQMpo8/KSsWW5ratJR5+IFr7YNJ3K2kfP9l5XObYHsgfVy1w3FJUWU4FT2fj7+JMaEg33zOcNDBo0LMwHnw==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.51.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.1.tgz", + "integrity": "sha512-s47HKFnQ4HOJAHoIiXcpna/roMMPZJPy6fJ6p4ZNVn8+/onlLBEDd1+xc8OnDuwgvecqkZD7Z2mnSRbcWefrKw==", + "dev": true, + "dependencies": { + "@tanstack/query-core": "5.51.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.5.tgz", + "integrity": "sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "^4.3.2", + "@babel/runtime": "^7.9.2", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@jest/globals": ">= 28", + "@types/bun": "latest", + "@types/jest": ">= 28", + "jest": ">= 28", + "vitest": ">= 0.32" + }, + "peerDependenciesMeta": { + "@jest/globals": { + "optional": true + }, + "@types/bun": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "jest": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true + }, + "node_modules/@testing-library/jest-dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/react": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.0.tgz", + "integrity": "sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "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 + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true + }, + "node_modules/@types/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/doctrine": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", + "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", + "dev": true + }, + "node_modules/@types/emscripten": { + "version": "1.39.13", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.13.tgz", + "integrity": "sha512-cFq+fO/isvhvmuP/+Sl4K4jtU6E23DoivtbO4r50e3odaxAiVdbfSYRDdJ4gCdxx+3aRjhphS5ZMwIH4hFy/Cw==", + "dev": true + }, + "node_modules/@types/escodegen": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/escodegen/-/escodegen-0.0.6.tgz", + "integrity": "sha512-AjwI4MvWx3HAOaZqYsjKWyEObT9lcVV0Y0V8nXo6cXzN8ZiMxVhf6F3d/UNvXVGKrEzL/Dluc5p+y9GkzlTWig==", + "dev": true + }, + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==" + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/lodash": { + "version": "4.17.6", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz", + "integrity": "sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==", + "dev": true + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "18.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.39.tgz", + "integrity": "sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", + "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", + "dev": true + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true + }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "dev": true + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true + }, + "node_modules/@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", + "dev": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true + }, + "node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==", + "dev": true + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "8.5.11", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.11.tgz", + "integrity": "sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", + "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/type-utils": "7.16.0", + "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", + "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", + "dev": true, + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/typescript-estree": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", + "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", + "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.16.0", + "@typescript-eslint/utils": "7.16.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", + "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", + "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/visitor-keys": "7.16.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", + "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.16.0", + "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/typescript-estree": "7.16.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", + "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.16.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "node_modules/@vitest/expect": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", + "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "dev": true, + "dependencies": { + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", + "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "dev": true, + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", + "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "dev": true, + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@vitest/utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@vitest/utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + }, + "node_modules/@yarnpkg/fslib": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@yarnpkg/fslib/-/fslib-2.10.3.tgz", + "integrity": "sha512-41H+Ga78xT9sHvWLlFOZLIhtU6mTGZ20pZ29EiZa97vnxdohJD2AF42rCoAoWfqUz486xY6fhjMH+DYEM9r14A==", + "dev": true, + "dependencies": { + "@yarnpkg/libzip": "^2.3.0", + "tslib": "^1.13.0" + }, + "engines": { + "node": ">=12 <14 || 14.2 - 14.9 || >14.10.0" + } + }, + "node_modules/@yarnpkg/fslib/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@yarnpkg/libzip": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/libzip/-/libzip-2.3.0.tgz", + "integrity": "sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==", + "dev": true, + "dependencies": { + "@types/emscripten": "^1.39.6", + "tslib": "^1.13.0" + }, + "engines": { + "node": ">=12 <14 || 14.2 - 14.9 || >14.10.0" + } + }, + "node_modules/@yarnpkg/libzip/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-globals/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals/node_modules/acorn-walk": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz", + "integrity": "sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-styles/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/ansi-styles/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-differ": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz", + "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.toreversed": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", + "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/ast-metadata-inferer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/ast-metadata-inferer/-/ast-metadata-inferer-0.8.0.tgz", + "integrity": "sha512-jOMKcHht9LxYIEQu+RVd22vtgrPaVCtDRQ/16IGmurdzxvYbDd5ynxjnyrzLnieG96eTcAyaoj/wN/4/1FyyeA==", + "dev": true, + "dependencies": { + "@mdn/browser-compat-data": "^5.2.34" + } + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.1.tgz", + "integrity": "sha512-u5w79Rd7SU4JaIlA/zFqG+gOiuq25q5VLyZ8E+ijJeILuTxVzZgp2CaGw/UTw6pXYN9XMO9yiqj/nEHmhTG5CA==", + "dev": true + }, + "node_modules/babel-core": { + "version": "7.0.0-bridge.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", + "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", + "dev": true, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-loader/node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/babel-loader/node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-loader/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/bonjour-service": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-assert": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/browser-assert/-/browser-assert-1.2.1.tgz", + "integrity": "sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==", + "dev": true + }, + "node_modules/browserslist": { + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001640", + "electron-to-chromium": "^1.4.820", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001642", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz", + "integrity": "sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/case-sensitive-paths-webpack-plugin": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz", + "integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "node_modules/chai": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chromatic": { + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.7.0.tgz", + "integrity": "sha512-Afblm4MWK6GXutxHPJVWKoY1PxCD98Uw0S3/f1a2wu4VTQy97g4+G8vPVqutSMpZFGzG5NjH9QdzKPFMmZczpw==", + "dev": true, + "bin": { + "chroma": "dist/bin.js", + "chromatic": "dist/bin.js", + "chromatic-cli": "dist/bin.js" + }, + "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 + } + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/confbox": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", + "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", + "dev": true + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-js-compat": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-pure": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.37.1.tgz", + "integrity": "sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/create-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "dev": true, + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/css-functions-list": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.2.tgz", + "integrity": "sha512-c+N0v6wbKVxTu5gOBBFkr9BEdBWaqqjQeiJ8QvSRIJOf+UxlJh930m8e6/WNeODIK0mYLFkoONrnj16i2EcvfQ==", + "dev": true, + "engines": { + "node": ">=12 || >=16" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tokenize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/css-tokenize/-/css-tokenize-1.0.1.tgz", + "integrity": "sha512-gLmmbJdwH9HLY4bcA17lnZ8GgPwEXRbvxBJGHnkiB6gLhRpTzjkjtMIvz7YORGW/Ptv2oMk8b5g+u7mRD6Dd7A==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^1.0.33" + } + }, + "node_modules/css-tokenize/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/css-tokenize/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/css-tokenize/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/cypress": { + "version": "13.13.3", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.3.tgz", + "integrity": "sha512-hUxPrdbJXhUOTzuML+y9Av7CKoYznbD83pt8g3klgpioEha0emfx4WNIuVRx0C76r0xV2MIwAW9WYiXfVJYFQw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@cypress/request": "^3.0.1", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-ci": "^3.0.1", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", + "tmp": "~0.2.3", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + } + }, + "node_modules/cypress/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cypress/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cypress/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cypress/node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/cypress/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cypress/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cypress/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/cypress/node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true + }, + "node_modules/cypress/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cypress/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dayjs": { + "version": "1.11.12", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.12.tgz", + "integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/doiuse": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/doiuse/-/doiuse-6.0.2.tgz", + "integrity": "sha512-eBTs23NOX+EAYPr4RbCR6J4DRW/TML3uMo37y0X1whlkersDYFCk9HmCl09KX98cis22VKsV1QaxfVNauJ3NBw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.5", + "caniuse-lite": "^1.0.30001487", + "css-tokenize": "^1.0.1", + "duplexify": "^4.1.2", + "ldjson-stream": "^1.2.1", + "multimatch": "^5.0.0", + "postcss": "^8.4.21", + "source-map": "^0.7.4", + "yargs": "^17.7.1" + }, + "bin": { + "doiuse": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/doiuse/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dev": true, + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.827", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.827.tgz", + "integrity": "sha512-VY+J0e4SFcNfQy19MEoMdaIcZLmDCprqvBtkii1WTCTQHpRvf5N8+3kTYCgL/PcntvwQvmMJWTuDPsq+IlhWKQ==" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/endent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/endent/-/endent-2.1.0.tgz", + "integrity": "sha512-r8VyPX7XL8U01Xgnb1CjZ3XV+z90cXIJ9JPE/R9SEC9vpw2P6CfsRPJmp20DppC5N7ZAMCmjYkJIa744Iyg96w==", + "dev": true, + "dependencies": { + "dedent": "^0.7.0", + "fast-json-parse": "^1.0.3", + "objectorarray": "^1.0.5" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", + "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/enquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", + "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dev": true, + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz", + "integrity": "sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==" + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "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" + } + }, + "node_modules/esbuild-register": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.5.0.tgz", + "integrity": "sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-compat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-compat/-/eslint-plugin-compat-5.0.0.tgz", + "integrity": "sha512-29KNWyFkUbNVf6TIKVe9SVCGCtHjML3HnUg9C8LG2GsXf7miAeBOgdMc1n2B5n0sHUzg1/A4IFly7Jyf1gSbgQ==", + "dev": true, + "dependencies": { + "@mdn/browser-compat-data": "^5.5.19", + "ast-metadata-inferer": "^0.8.0", + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001605", + "find-up": "^5.0.0", + "globals": "^13.24.0", + "lodash.memoize": "^4.1.2", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=14.x" + }, + "peerDependencies": { + "eslint": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-compat/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-compat/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-compat/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-cypress": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-3.5.0.tgz", + "integrity": "sha512-JZQ6XnBTNI8h1B9M7wJSFzc48SYbh7VMMKaNTQOFa3BQlnmXPrVc4PKen8R+fpv6VleiPeej6VxloGb42zdRvw==", + "dev": true, + "dependencies": { + "globals": "^13.20.0" + }, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-plugin-cypress/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-cypress/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.34.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.4.tgz", + "integrity": "sha512-Np+jo9bUwJNxCsT12pXtrGhJgT3T44T1sHhn1Ssr42XFn8TES0267wPGo5nNrMHi8qkyimDAX2BUmkf9pSaVzA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.toreversed": "^1.1.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.8.tgz", + "integrity": "sha512-MIKAclwaDFIiYtVBLzDdm16E+Ty4GwhB6wZlCAG1R3Ur+F9Qbo6PRxpA5DK7XtDgm+WlCoAY2WxAwqhmIDHg6Q==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-storybook": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.8.0.tgz", + "integrity": "sha512-CZeVO5EzmPY7qghO2t64oaFM+8FTaD4uzOEjHKp516exyTKo+skKAL9GI3QALS2BXhyALJjNtwbmr1XinGE8bA==", + "dev": true, + "dependencies": { + "@storybook/csf": "^0.0.1", + "@typescript-eslint/utils": "^5.62.0", + "requireindex": "^1.2.0", + "ts-dedent": "^2.2.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "eslint": ">=6" + } + }, + "node_modules/eslint-plugin-storybook/node_modules/@storybook/csf": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.0.1.tgz", + "integrity": "sha512-USTLkZze5gkel8MYCujSRBVIrUQ3YPBrLOx7GNk/0wttvVtlzWXAq9eLbQ4p/NicGxP+3T7KPEMVV//g+yubpw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-storybook/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-storybook/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-plugin-storybook/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-plugin-storybook/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/estree-walker/node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/executable/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "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" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-parse": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-json-parse/-/fast-json-parse-1.0.3.tgz", + "integrity": "sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "node_modules/fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "dev": true + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fd-package-json": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fd-package-json/-/fd-package-json-1.2.0.tgz", + "integrity": "sha512-45LSPmWf+gC5tdCQMNH4s9Sr00bIkiD9aN7dc5hqkrEw1geRYyDQS1v1oMHAW3ysfxfndqGsrDREHHjNNbKUfA==", + "dev": true, + "dependencies": { + "walk-up-path": "^3.0.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filesize": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.4.tgz", + "integrity": "sha512-ryBwPIIeErmxgPnm6cbESAzXjuEFubs+yKYLBZvg3CaiNcmkJChoOGcBSrZ6IwkMwPABwPpVXE6IlNdGJJrvEg==", + "dev": true, + "engines": { + "node": ">= 10.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/firebase": { + "version": "10.12.5", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-10.12.5.tgz", + "integrity": "sha512-J0yL3yh12CfFprTkSOQ9HqBugERyqvWwOuOoo1j1QHmYe9cYLKnBmtNCvGIYInDcsVUnJoRXCM+hxbGf48oVhg==", + "dependencies": { + "@firebase/analytics": "0.10.7", + "@firebase/analytics-compat": "0.2.13", + "@firebase/app": "0.10.8", + "@firebase/app-check": "0.8.7", + "@firebase/app-check-compat": "0.3.14", + "@firebase/app-compat": "0.2.38", + "@firebase/app-types": "0.9.2", + "@firebase/auth": "1.7.6", + "@firebase/auth-compat": "0.5.11", + "@firebase/database": "1.0.7", + "@firebase/database-compat": "1.0.7", + "@firebase/firestore": "4.6.5", + "@firebase/firestore-compat": "0.3.34", + "@firebase/functions": "0.11.6", + "@firebase/functions-compat": "0.3.12", + "@firebase/installations": "0.6.8", + "@firebase/installations-compat": "0.2.8", + "@firebase/messaging": "0.12.10", + "@firebase/messaging-compat": "0.2.10", + "@firebase/performance": "0.6.8", + "@firebase/performance-compat": "0.2.8", + "@firebase/remote-config": "0.4.8", + "@firebase/remote-config-compat": "0.2.8", + "@firebase/storage": "0.12.6", + "@firebase/storage-compat": "0.3.9", + "@firebase/util": "1.9.7", + "@firebase/vertexai-preview": "0.0.3" + } + }, + "node_modules/firebase/node_modules/@firebase/auth": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.7.6.tgz", + "integrity": "sha512-T+lA5xoug9CByGYkD5WkfTh2ujEYq/frGZPbk0H+fNU6fNl7nqg88KcsmzsC6Fsqbjm3LLEb/i6wJvF6NSNEig==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0", + "undici": "5.28.4" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/firebase/node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + }, + "node_modules/flow-parser": { + "version": "0.241.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.241.0.tgz", + "integrity": "sha512-82yKXpz7iWknWFsognZUf5a6mBQLnVrYoYSU9Nbu7FTOpKlu3v9ehpiI9mYXuaIO3J0ojX1b83M/InXvld9HUw==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/fork-ts-checker-webpack-plugin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", + "integrity": "sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cosmiconfig": "^7.0.1", + "deepmerge": "^4.2.2", + "fs-extra": "^10.0.0", + "memfs": "^3.4.1", + "minimatch": "^3.0.4", + "node-abort-controller": "^3.0.1", + "schema-utils": "^3.1.1", + "semver": "^7.3.5", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">=12.13.0", + "yarn": ">=1.0.0" + }, + "peerDependencies": { + "typescript": ">3.6.0", + "webpack": "^5.11.0" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/giget": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz", + "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", + "dev": true, + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.3", + "nypm": "^0.3.8", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "tar": "^6.2.0" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-dirs/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "dev": true + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + }, + "node_modules/graphql": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", + "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.0.tgz", + "integrity": "sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "dev": true, + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/html-webpack-plugin": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz", + "integrity": "sha512-iwaY4wzbe48AfKLZ/Cc8k0L+FKG6oSNRaZ8x5A/T/IVDGyXcbHncM9TdDa93wn0FsSm82FhTKW7f3vS61thXAw==", + "dev": true, + "dependencies": { + "@types/html-minifier-terser": "^6.0.0", + "html-minifier-terser": "^6.0.2", + "lodash": "^4.17.21", + "pretty-error": "^4.0.0", + "tapable": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/html-webpack-plugin" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.20.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-signature": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "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.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-absolute-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", + "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", + "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz", + "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", + "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", + "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz", + "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "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 + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/jest-circus/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-circus/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "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 + } + } + }, + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "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 + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-config/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-each/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-haste-map/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/jest-validate/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "node_modules/jscodeshift": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.15.2.tgz", + "integrity": "sha512-FquR7Okgmc4Sd0aEDwqho3rEiKR3BdvuG9jfdHjLJ6JQoWSMpavug3AoIfnfWhxFlf+5pzQh8qjqz0DWFrNQzA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/parser": "^7.23.0", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.23.0", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.11", + "@babel/plugin-transform-optional-chaining": "^7.23.0", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/preset-flow": "^7.22.15", + "@babel/preset-typescript": "^7.23.0", + "@babel/register": "^7.22.15", + "babel-core": "^7.0.0-bridge.0", + "chalk": "^4.1.2", + "flow-parser": "0.*", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.4", + "neo-async": "^2.5.0", + "node-dir": "^0.1.17", + "recast": "^0.23.3", + "temp": "^0.8.4", + "write-file-atomic": "^2.3.0" + }, + "bin": { + "jscodeshift": "bin/jscodeshift.js" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + }, + "peerDependenciesMeta": { + "@babel/preset-env": { + "optional": true + } + } + }, + "node_modules/jscodeshift/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jscodeshift/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jscodeshift/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jscodeshift/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.34.0.tgz", + "integrity": "sha512-tBECoUqNFbyAY4RrbqsBQqDFpGXAEbdD5QKr8kACx3+rnArmuuR22nKQWKazvp07N9yjTyDZaw/20UIH8tL9DQ==", + "dev": true + }, + "node_modules/launch-editor": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.8.0.tgz", + "integrity": "sha512-vJranOAJrI/llyWGRQqiDM+adrw+k83fvmmx3+nV47g3+36xM15jE+zyZ6Ffel02+xSvuM0b2GDRosXZkbb6wA==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "engines": { + "node": "> 0.8" + } + }, + "node_modules/ldjson-stream": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ldjson-stream/-/ldjson-stream-1.2.1.tgz", + "integrity": "sha512-xw/nNEXafuPSLu8NjjG3+atVVw+8U1APZAQylmwQn19Hgw6rC7QjHvP6MupnHWCrzSm9m0xs5QWkCLuRvBPjgQ==", + "dev": true, + "dependencies": { + "split2": "^0.2.1", + "through2": "^0.6.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/map-or-similar": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz", + "integrity": "sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==", + "dev": true + }, + "node_modules/markdown-to-jsx": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.4.7.tgz", + "integrity": "sha512-0+ls1IQZdU6cwM1yu0ZjjiVWYtkbExSyUIFU2ZeDIFuZM1W42Mh4OlJ4nb4apX4H8smxDHRdFaoIVJGwfv5hkg==", + "dev": true, + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, + "node_modules/mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memoizerific": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz", + "integrity": "sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==", + "dev": true, + "dependencies": { + "map-or-similar": "^1.5.0" + } + }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mlly": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", + "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.1.1", + "ufo": "^1.5.3" + } + }, + "node_modules/mlly/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/msw": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.3.5.tgz", + "integrity": "sha512-+GUI4gX5YC5Bv33epBrD+BGdmDvBg2XGruiWnI3GbIbRmMMBeZ5gs3mJ51OWSGHgJKztZ8AtZeYMMNMVrje2/Q==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.0", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^3.0.0", + "@mswjs/interceptors": "^0.29.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "chalk": "^4.1.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.2", + "path-to-regexp": "^6.2.0", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.9.0", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.7.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw-storybook-addon": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/msw-storybook-addon/-/msw-storybook-addon-2.0.3.tgz", + "integrity": "sha512-CzHmGO32JeOPnyUnRWnB0PFTXCY1HKfHiEB/6fYoUYiFm2NYosLjzs9aBd3XJUryYEN0avJqMNh7nCRDxE5JjQ==", + "dev": true, + "dependencies": { + "is-node-process": "^1.0.1" + }, + "peerDependencies": { + "msw": "^2.0.0" + } + }, + "node_modules/msw/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/msw/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/msw/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/msw/node_modules/path-to-regexp": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", + "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "dev": true + }, + "node_modules/msw/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/multimatch": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz", + "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "array-differ": "^3.0.0", + "array-union": "^2.1.0", + "arrify": "^2.0.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/multimatch/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/multimatch/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "dev": true + }, + "node_modules/node-dir": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", + "integrity": "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==", + "dev": true, + "dependencies": { + "minimatch": "^3.0.2" + }, + "engines": { + "node": ">= 0.10.5" + } + }, + "node_modules/node-dir/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-dir/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", + "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", + "dev": true + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", + "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", + "dev": true + }, + "node_modules/nypm": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.9.tgz", + "integrity": "sha512-BI2SdqqTHg2d4wJh8P9A1W+bslg33vOE9IZDY6eR2QC+Pu1iNBVZUqczrd43rJb+fMzHU7ltAYKsEFY/kHMFcw==", + "dev": true, + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "execa": "^8.0.1", + "pathe": "^1.1.2", + "pkg-types": "^1.1.1", + "ufo": "^1.5.3" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/nypm/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/nypm/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/nypm/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nypm/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/objectorarray": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/objectorarray/-/objectorarray-1.0.5.tgz", + "integrity": "sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg==", + "dev": true + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/ohash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz", + "integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.0.tgz", + "integrity": "sha512-JA6nkq6hKyWLLasXQXUrO4z8BUZGUt/LjlJxx8Gb2+2ntodU/SS63YZ8b0LUTbQ8ZB9iwOfhEPhg4ykKnn2KsA==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-types": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.3.tgz", + "integrity": "sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==", + "dev": true, + "dependencies": { + "confbox": "^0.1.7", + "mlly": "^1.7.1", + "pathe": "^1.1.2" + } + }, + "node_modules/polished": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", + "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.17.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-resolve-nested-selector": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", + "integrity": "sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==", + "dev": true + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.0.tgz", + "integrity": "sha512-ovehqRNVCpuFzbXoTb4qLtyzK3xn3t/CUBxOs8LsnQjQrShaB4lKiHoVqY8ANaC0hBMHq5QVWk77rwGklFUDrg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", + "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-sorting": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-8.0.2.tgz", + "integrity": "sha512-M9dkSrmU00t/jK7rF6BZSZauA5MAaBW4i5EnJXspMwt4iqTh/L9j6fgMnbElEOfyRyfLfVbIHj/R52zHzAPe1Q==", + "dev": true, + "peerDependencies": { + "postcss": "^8.4.20" + } + }, + "node_modules/postcss-styled-syntax": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/postcss-styled-syntax/-/postcss-styled-syntax-0.6.4.tgz", + "integrity": "sha512-uWiLn+9rKgIghUYmTHvXMR6MnyPULMe9Gv3bV537Fg4FH6CA6cn21WMjKss2Qb98LUhT847tKfnRGG3FhSOgUQ==", + "dev": true, + "dependencies": { + "typescript": "^5.3.3" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-syntax": { + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz", + "integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==", + "dev": true, + "peerDependencies": { + "postcss": ">=5.0.0" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/protobufjs": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz", + "integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-colorful": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz", + "integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==", + "dev": true, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/react-confetti": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.1.0.tgz", + "integrity": "sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==", + "dev": true, + "dependencies": { + "tween-functions": "^1.2.0" + }, + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "react": "^16.3.0 || ^17.0.1 || ^18.0.0" + } + }, + "node_modules/react-docgen": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-7.0.3.tgz", + "integrity": "sha512-i8aF1nyKInZnANZ4uZrH49qn1paRgBZ7wZiCNBMnenlPzEv0mRl+ShpTVEI6wZNl8sSc79xZkivtgLKQArcanQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.18.9", + "@babel/traverse": "^7.18.9", + "@babel/types": "^7.18.9", + "@types/babel__core": "^7.18.0", + "@types/babel__traverse": "^7.18.0", + "@types/doctrine": "^0.0.9", + "@types/resolve": "^1.20.2", + "doctrine": "^3.0.0", + "resolve": "^1.22.1", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": ">=16.14.0" + } + }, + "node_modules/react-docgen-typescript": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz", + "integrity": "sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==", + "dev": true, + "peerDependencies": { + "typescript": ">= 4.3.x" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-element-to-jsx-string": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/react-element-to-jsx-string/-/react-element-to-jsx-string-15.0.0.tgz", + "integrity": "sha512-UDg4lXB6BzlobN60P8fHWVPX3Kyw8ORrTeBtClmIlGdkOOE+GYQSFvmEU5iLLpwp/6v42DINwNcwOhOLfQ//FQ==", + "dev": true, + "dependencies": { + "@base2/pretty-print-object": "1.0.1", + "is-plain-object": "5.0.0", + "react-is": "18.1.0" + }, + "peerDependencies": { + "react": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0", + "react-dom": "^0.14.8 || ^15.0.1 || ^16.0.0 || ^17.0.1 || ^18.0.0" + } + }, + "node_modules/react-element-to-jsx-string/node_modules/react-is": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.1.0.tgz", + "integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==", + "dev": true + }, + "node_modules/react-ga4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-ga4/-/react-ga4-2.1.0.tgz", + "integrity": "sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ==" + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.24.1.tgz", + "integrity": "sha512-PTXFXGK2pyXpHzVo3rR9H7ip4lSPZZc0bHG5CARmj65fTT6qG7sTngmb6lcYu1gf3y/8KxORoy9yn59pGpCnpg==", + "dependencies": { + "@remix-run/router": "1.17.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.24.1.tgz", + "integrity": "sha512-U19KtXqooqw967Vw0Qcn5cOvrX5Ejo9ORmOtJMzYWtCT4/WOfFLIZGGsVLxcd9UkBO0mSTZtXqhZBsWlHr7+Sg==", + "dependencies": { + "@remix-run/router": "1.17.1", + "react-router": "6.24.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recast": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.9.tgz", + "integrity": "sha512-Hx/BGIbwj+Des3+xy5uAtAbdCyqK9y9wbBcDFDYanLS9JnMqf7OeF87HQwUimE87OEc72mr6tkKUKMBBL+hF9Q==", + "dev": true, + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redent/node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", + "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/rehype-external-links": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", + "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-is-element": "^3.0.0", + "is-absolute-url": "^4.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dev": true, + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/renderkid/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "dev": true, + "engines": { + "node": ">=0.10.5" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/split2": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/split2/-/split2-0.2.1.tgz", + "integrity": "sha512-D/oTExYAkC9nWleOCTOyNmAuzfAT/6rHGBA9LIK7FVnGo13CSvrKCUzKenwH6U1s2znY9MqH6v0UQTEDa3vJmg==", + "dev": true, + "dependencies": { + "through2": "~0.6.1" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "dev": true + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/storybook": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.2.4.tgz", + "integrity": "sha512-ASavW8vIHiWpFY+4M6ngeqK5oL4OkxqdpmQYxvRqH0gA1G1hfq/vmDw4YC4GnqKwyWPQh2kaV5JFurKZVaeaDQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/types": "^7.24.0", + "@storybook/codemod": "8.2.4", + "@storybook/core": "8.2.4", + "@types/semver": "^7.3.4", + "@yarnpkg/fslib": "2.10.3", + "@yarnpkg/libzip": "2.3.0", + "chalk": "^4.1.0", + "commander": "^6.2.1", + "cross-spawn": "^7.0.3", + "detect-indent": "^6.1.0", + "envinfo": "^7.7.3", + "execa": "^5.0.0", + "fd-package-json": "^1.2.0", + "find-up": "^5.0.0", + "fs-extra": "^11.1.0", + "giget": "^1.0.0", + "globby": "^14.0.1", + "jscodeshift": "^0.15.1", + "leven": "^3.1.0", + "ora": "^5.4.1", + "prettier": "^3.1.1", + "prompts": "^2.4.0", + "semver": "^7.3.7", + "strip-json-comments": "^3.0.1", + "tempy": "^3.1.0", + "tiny-invariant": "^1.3.1", + "ts-dedent": "^2.0.0" + }, + "bin": { + "getstorybook": "bin/index.cjs", + "sb": "bin/index.cjs", + "storybook": "bin/index.cjs" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + } + }, + "node_modules/storybook/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/storybook/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/storybook/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/storybook/node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/storybook/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/storybook/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/storybook/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/storybook/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/storybook/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "dev": true + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz", + "integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", + "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/stylelint": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.7.0.tgz", + "integrity": "sha512-Q1ATiXlz+wYr37a7TGsfvqYn2nSR3T/isw3IWlZQzFzCNoACHuGBb6xBplZXz56/uDRJHIygxjh7jbV/8isewA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "dependencies": { + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/media-query-list-parser": "^2.1.13", + "@csstools/selector-specificity": "^3.1.1", + "@dual-bundle/import-meta-resolve": "^4.1.0", + "balanced-match": "^2.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^9.0.0", + "css-functions-list": "^3.2.2", + "css-tree": "^2.3.1", + "debug": "^4.3.5", + "fast-glob": "^3.3.2", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^9.0.0", + "global-modules": "^2.0.0", + "globby": "^11.1.0", + "globjoin": "^0.1.4", + "html-tags": "^3.3.1", + "ignore": "^5.3.1", + "imurmurhash": "^0.1.4", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.34.0", + "mathml-tag-names": "^2.1.3", + "meow": "^13.2.0", + "micromatch": "^4.0.7", + "normalize-path": "^3.0.0", + "picocolors": "^1.0.1", + "postcss": "^8.4.39", + "postcss-resolve-nested-selector": "^0.1.1", + "postcss-safe-parser": "^7.0.0", + "postcss-selector-parser": "^6.1.0", + "postcss-value-parser": "^4.2.0", + "resolve-from": "^5.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^7.1.0", + "supports-hyperlinks": "^3.0.0", + "svg-tags": "^1.0.0", + "table": "^6.8.2", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/stylelint-config-clean-order": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/stylelint-config-clean-order/-/stylelint-config-clean-order-6.1.0.tgz", + "integrity": "sha512-Xe1U0stw57Evdcx+7q7XYAniyE7XAKv/bwfH9LcsFCcKTPZflzTiJLXGkQUsPMlA4cfMyxEebqm5bRN2doTD3w==", + "dev": true, + "dependencies": { + "stylelint-order": "^6.0.4" + }, + "peerDependencies": { + "stylelint": ">=14" + } + }, + "node_modules/stylelint-config-recommended": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-14.0.1.tgz", + "integrity": "sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.1.0" + } + }, + "node_modules/stylelint-config-standard": { + "version": "36.0.1", + "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-36.0.1.tgz", + "integrity": "sha512-8aX8mTzJ6cuO8mmD5yon61CWuIM4UD8Q5aBcWKGSf6kg+EC3uhB+iOywpTK4ca6ZL7B49en8yanOFtUW0qNzyw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "dependencies": { + "stylelint-config-recommended": "^14.0.1" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.1.0" + } + }, + "node_modules/stylelint-no-unsupported-browser-features": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/stylelint-no-unsupported-browser-features/-/stylelint-no-unsupported-browser-features-8.0.1.tgz", + "integrity": "sha512-tc8Xn5DaqJhxTmbA4H8gZbYdAz027NfuSZv5+cVieQb7BtBrF/1/iKYdpcGwXPl3GtqkQrisiXuGqKkKnzWcLw==", + "dev": true, + "dependencies": { + "doiuse": "^6.0.2", + "postcss": "^8.4.32" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.0.2" + } + }, + "node_modules/stylelint-order": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/stylelint-order/-/stylelint-order-6.0.4.tgz", + "integrity": "sha512-0UuKo4+s1hgQ/uAxlYU4h0o0HS4NiQDud0NAUNI0aa8FJdmYHA5ZZTFHiV5FpmE3071e9pZx5j0QpVJW5zOCUA==", + "dev": true, + "dependencies": { + "postcss": "^8.4.32", + "postcss-sorting": "^8.0.2" + }, + "peerDependencies": { + "stylelint": "^14.0.0 || ^15.0.0 || ^16.0.1" + } + }, + "node_modules/stylelint/node_modules/balanced-match": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", + "dev": true + }, + "node_modules/stylelint/node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/stylelint/node_modules/file-entry-cache": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.0.0.tgz", + "integrity": "sha512-6MgEugi8p2tiUhqO7GnPsmbCCzj0YRCwwaTbpGRyKZesjRSzkqkAE9fPp7V2yMs5hwfgbQLgdvSSkGNg1s5Uvw==", + "dev": true, + "dependencies": { + "flat-cache": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/stylelint/node_modules/flat-cache": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-5.0.0.tgz", + "integrity": "sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ==", + "dev": true, + "dependencies": { + "flatted": "^3.3.1", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/stylelint/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylelint/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stylelint/node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz", + "integrity": "sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + } + }, + "node_modules/supports-hyperlinks/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "dev": true, + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/svgo/node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/svgo/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/svgo/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/svgo/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/svgo/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/swc-loader": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/swc-loader/-/swc-loader-0.2.6.tgz", + "integrity": "sha512-9Zi9UP2YmDpgmQVbyOPJClY0dwf58JDyDMQ7uRc4krmc72twNI2fvlBWHLqVekBpPc7h5NJkGVT1zNDxFrqhvg==", + "dev": true, + "dependencies": { + "@swc/counter": "^0.1.3" + }, + "peerDependencies": { + "@swc/core": "^1.2.147", + "webpack": ">=2" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "node_modules/table": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz", + "integrity": "sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/telejson": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/telejson/-/telejson-7.2.0.tgz", + "integrity": "sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==", + "dev": true, + "dependencies": { + "memoizerific": "^1.11.3" + } + }, + "node_modules/temp": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz", + "integrity": "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==", + "dev": true, + "dependencies": { + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", + "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", + "dev": true, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/tempy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz", + "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==", + "dev": true, + "dependencies": { + "is-stream": "^3.0.0", + "temp-dir": "^3.0.0", + "type-fest": "^2.12.2", + "unique-string": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.31.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.2.tgz", + "integrity": "sha512-LGyRZVFm/QElZHy/CPr/O4eNZOZIzsrQ92y4v9UJe/pFJjypje2yI3C2FmPtvUEnhadlSbmG2nXtdcjHOjCfxw==", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, + "node_modules/thingies": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "dev": true, + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==", + "dev": true, + "dependencies": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "node_modules/through2/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/tree-dump": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", + "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "dev": true, + "engines": { + "node": ">=6.10" + } + }, + "node_modules/ts-jest": { + "version": "29.2.3", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.3.tgz", + "integrity": "sha512-yCcfVdiBFngVz9/keHin9EnsrQtQtEu3nRykNy9RVp+FiPFFbPJ3Sg6Qg4+TkmH0vMP5qsTKgXSsk80HRwvdgQ==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "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 + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", + "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ts-loader/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-loader/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ts-loader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "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 + } + } + }, + "node_modules/ts-node/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ts-node/node_modules/acorn-walk": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tween-functions": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz", + "integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==", + "dev": true + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.21.0.tgz", + "integrity": "sha512-ADn2w7hVPcK6w1I0uWnM//y1rLXZhzB9mr0a3OirzclKF1Wp6VzevUmzz/NRAWunOT6E8HrnpGY7xOfc6K57fA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", + "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", + "dev": true + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici": { + "version": "6.19.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.5.tgz", + "integrity": "sha512-LryC15SWzqQsREHIOUybavaIHF5IoL0dJ9aWWxL/PgT1KfqAW5225FZpDUFlt9xiDMS2/S7DOKhFWA7RLksWdg==", + "dev": true, + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "dev": true, + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unplugin": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.11.0.tgz", + "integrity": "sha512-3r7VWZ/webh0SGgJScpWl2/MRCZK5d3ZYFcNaeci/GQ7Teop7zf0Nl2pUuz7G21BwPd9pcUPOC5KmJ2L3WgC5g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "chokidar": "^3.6.0", + "webpack-sources": "^3.2.3", + "webpack-virtual-modules": "^0.6.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url": { + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", + "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", + "dev": true, + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.11.2" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + }, + "node_modules/url/node_modules/qs": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.3.tgz", + "integrity": "sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/walk-up-path": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-3.0.1.tgz", + "integrity": "sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==", + "dev": true + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/watchpack": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.3.tgz", + "integrity": "sha512-A4ChP0Qj8oGociTs6UdlRUGANIGrCDL3y+pmQMc+dSsraXHCatFpmMey4mYELA+juqwUqwQsUgJJISXl1KWmiw==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.0.4.tgz", + "integrity": "sha512-dljXhUgx3HqKP2d8J/fUMvhxGhzjeNVarDLcbO/EWMSgRizDkxHQDZQaLFL5VJY9tRBj2Gz+rvCEYYvhbqPHNA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.4.0", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "rimraf": "^5.0.5", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.1.0", + "ws": "^8.16.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webpack-dev-server/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-server/node_modules/memfs": { + "version": "4.9.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.9.3.tgz", + "integrity": "sha512-bsYSSnirtYTWi1+OPMFb0M048evMKyUYe0EbtuGQgq6BVQM1g1W8/KIUJCCvjgI/El0j6Q4WsmMiBwLUBSw8LA==", + "dev": true, + "dependencies": { + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.1.2", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/webpack-dev-server/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/webpack-dev-server/node_modules/rimraf": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.9.tgz", + "integrity": "sha512-3i7b8OcswU6CpU8Ej89quJD4O98id7TtVM5U4Mybh84zQXdrFmDLouWBEEaD/QfO3gDDfH+AGFCGsR7kngzQnA==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "14 >=14.20 || 16 >=16.20 || >=18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.2.1.tgz", + "integrity": "sha512-hRLz+jPQXo999Nx9fXVdKlg/aehsw1ajA9skAneGmT03xwmyuhvF93p6HUKKbWhXdcERtGTzUCtIQr+2IQegrA==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.6.0", + "mime-types": "^2.1.31", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-hot-middleware": { + "version": "2.26.1", + "resolved": "https://registry.npmjs.org/webpack-hot-middleware/-/webpack-hot-middleware-2.26.1.tgz", + "integrity": "sha512-khZGfAeJx6I8K9zKohEWWYN6KDlVw2DHownoe+6Vtwj1LP9WFgegXnVMSkZ/dBEBtXFwrkkydsaPFlB7f8wU2A==", + "dev": true, + "dependencies": { + "ansi-html-community": "0.0.8", + "html-entities": "^2.1.0", + "strip-ansi": "^6.0.0" + } + }, + "node_modules/webpack-hot-middleware/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true + }, + "node_modules/webpack/node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + }, + "node_modules/webpack/node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack/node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz", + "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==", + "dev": true, + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "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 + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 000000000..74568bd5d --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,106 @@ +{ + "name": "mouda", + "version": "1.0.0", + "description": "모우다", + "main": "index.js", + "scripts": { + "test": "jest", + "start": "webpack serve --config webpack.dev.js", + "start:mock": "MSW=true webpack serve --config webpack.dev.js", + "start:prod": "webpack serve --config webpack.prod.js", + "build": "webpack --config webpack.prod.js", + "sb": "storybook dev -p 6006", + "build-sb": "storybook build", + "build-storybook": "storybook build", + "lint:eslint": "eslint 'src/**/*.{ts,tsx}'", + "lint:style": "stylelint **/*.style.ts --fix", + "lint": "npm run lint:eslint && npm run lint:style", + "cy": "cypress open" + }, + "repository": { + "type": "git", + "url": "https://github.com/woowacourse-teams/2024-mouda.git" + }, + "devDependencies": { + "@babel/core": "^7.24.7", + "@babel/preset-env": "^7.24.7", + "@babel/preset-react": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "@chromatic-com/storybook": "^1.6.1", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", + "@storybook/addon-essentials": "^8.1.11", + "@storybook/addon-interactions": "^8.1.11", + "@storybook/addon-links": "^8.1.11", + "@storybook/addon-onboarding": "^8.1.11", + "@storybook/addon-webpack5-compiler-swc": "^1.0.4", + "@storybook/blocks": "^8.1.11", + "@storybook/builder-webpack5": "^8.1.11", + "@storybook/react": "^8.1.11", + "@storybook/react-webpack5": "^8.1.11", + "@storybook/test": "^8.1.11", + "@stylelint/postcss-css-in-js": "^0.38.0", + "@svgr/webpack": "^8.1.0", + "@tanstack/react-query": "^5.51.1", + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^16.0.0", + "@types/jest": "^29.5.12", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.15.0", + "babel-loader": "^9.1.3", + "chromatic": "^11.7.0", + "copy-webpack-plugin": "^12.0.2", + "cypress": "^13.13.3", + "dotenv": "^16.4.5", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-compat": "^5.0.0", + "eslint-plugin-cypress": "^3.5.0", + "eslint-plugin-react": "^7.34.3", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "eslint-plugin-storybook": "^0.8.0", + "html-webpack-plugin": "^5.6.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "msw": "^2.3.5", + "msw-storybook-addon": "^2.0.3", + "postcss": "^8.4.39", + "postcss-styled-syntax": "^0.6.4", + "postcss-syntax": "^0.36.2", + "prettier": "^3.3.2", + "react-refresh": "^0.14.2", + "storybook": "^8.1.11", + "stylelint": "^16.6.1", + "stylelint-config-clean-order": "^6.1.0", + "stylelint-config-standard": "^36.0.1", + "stylelint-no-unsupported-browser-features": "^8.0.1", + "stylelint-order": "^6.0.4", + "ts-jest": "^29.2.3", + "ts-loader": "^9.5.1", + "ts-node": "^10.9.2", + "type-fest": "^4.21.0", + "typescript": "^5.5.3", + "undici": "^6.19.5", + "webpack": "^5.92.1", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^5.0.4" + }, + "dependencies": { + "@emotion/babel-preset-css-prop": "^11.11.0", + "@emotion/eslint-plugin": "^11.11.0", + "@emotion/react": "^11.11.4", + "@sentry/react": "^8.24.0", + "@sentry/webpack-plugin": "^2.22.0", + "firebase": "^10.12.5", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-ga4": "^2.1.0", + "react-router-dom": "^6.24.1" + }, + "msw": { + "workerDirectory": [ + "public" + ] + } +} diff --git a/frontend/public/android/android-launchericon-144-144.png b/frontend/public/android/android-launchericon-144-144.png new file mode 100644 index 0000000000000000000000000000000000000000..4184c6062aae0752921824aeed9fe77dd588ce72 GIT binary patch literal 9440 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 V=SfJWFNf6uD9ETtS4oQkRWgJ{(4`% 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 literal 0 HcmV?d00001 diff --git a/frontend/public/android/android-launchericon-48-48.png b/frontend/public/android/android-launchericon-48-48.png new file mode 100644 index 0000000000000000000000000000000000000000..181113815939d883eb0da3a0984f4b18725c09ed GIT binary patch literal 3431 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 XKQx+Z_p*?+bPZr`Vq;uy=n?-fchTv7 literal 0 HcmV?d00001 diff --git a/frontend/public/android/android-launchericon-512-512.png b/frontend/public/android/android-launchericon-512-512.png new file mode 100644 index 0000000000000000000000000000000000000000..49fd631dd2dd4bd074054a02bd0644e5b4c90064 GIT binary patch literal 35721 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

4fTU3T^b+mN@M`>gVJ2pPGlWo3? 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 neE { + // 이미 열린 창이 있는지 확인 + for (let i = 0; i < windowClients.length; i++) { + const client = windowClients[i]; + if (client.url === link && 'focus' in client) { + return client.focus(); + } + } + // 새 창을 열거나 이미 있는 창으로 이동 + if (clients.openWindow) { + return clients.openWindow(link); + } + }) + ); + } +}); + +firebase.initializeApp(firebaseConfig); + +const messaging = firebase.messaging(); + +messaging.onBackgroundMessage((payload) => { + console.log('[firebase-messaging-sw.js] 백그라운드 메시지 수신:', payload); +}); diff --git a/frontend/public/main-logo.png b/frontend/public/main-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..25c886cc4d133024261441c481ec4b1cbf2c5139 GIT binary patch literal 16188 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 aLr733bEAjMw#~nj>naKw@^!LSVgC=2EhXOo literal 0 HcmV?d00001 diff --git a/frontend/public/main-logo2.png b/frontend/public/main-logo2.png new file mode 100644 index 0000000000000000000000000000000000000000..c4fc2480e4a0933078af376c2e94c68db1185887 GIT binary patch literal 9745 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 E0MldHNB{r; literal 0 HcmV?d00001 diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json new file mode 100644 index 000000000..9f25cf702 --- /dev/null +++ b/frontend/public/manifest.json @@ -0,0 +1,51 @@ +{ + "description": "", + "background_color": "#ffffff", + "dir": "ltr", + "display": "standalone", + "name": "모우다", + "orientation": "portrait", + "scope": "/", + "short_name": "모우다", + "start_url": "/", + "theme_color": "#ffffff", + "icons": [ + { + "src": "./android/android-launchericon-512-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "./android/android-launchericon-192-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "./android/android-launchericon-144-144.png", + "sizes": "144x144", + "type": "image/png" + }, + { + "src": "./android/android-launchericon-96-96.png", + "sizes": "96x96", + "type": "image/png" + }, + { + "src": "./android-launchericon-72-72.png", + "sizes": "72x72", + "type": "image/png" + }, + { + "src": "./android-launchericon-48-48.png", + "sizes": "48x48", + "type": "image/png" + } + ], + "lang": "ko", + "display_override": [ + "window-controls-overlay" + ], + "categories": [ + "social" + ] +} diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js new file mode 100644 index 000000000..15751fa19 --- /dev/null +++ b/frontend/public/mockServiceWorker.js @@ -0,0 +1,284 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = '2.3.5' +const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: true, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId)) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone() + + function passthrough() { + const headers = Object.fromEntries(requestClone.headers.entries()) + + // Remove internal MSW request header so the passthrough request + // complies with any potential CORS preflight checks on the server. + // Some servers forbid unknown request headers. + delete headers['x-msw-intention'] + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer() + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ) + }) +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} diff --git a/frontend/public/preview-image.png b/frontend/public/preview-image.png new file mode 100644 index 0000000000000000000000000000000000000000..b6b51005300b31289169f8d0e2896e340d5c10ba GIT binary patch literal 24037 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 { + window.addEventListener('beforeunload', removeInviteCode); + + return window.removeEventListener('beforeunload', removeInviteCode); + }); + + return ( + + + + + + + ); +} diff --git a/frontend/src/RouteChageTracker.ts b/frontend/src/RouteChageTracker.ts new file mode 100644 index 000000000..4b41d5748 --- /dev/null +++ b/frontend/src/RouteChageTracker.ts @@ -0,0 +1,34 @@ +// Temp: 확인 후 삭제 + +// import { useEffect, useState } from 'react'; +// import { useLocation } from 'react-router-dom'; +// import ReactGA from 'react-ga4'; + +// /** +// * uri 변경 추적 컴포넌트 +// * uri가 변경될 때마다 pageview 이벤트 전송 +// */ +// const RouteChangeTracker = () => { +// const location = useLocation(); +// const [initialized, setInitialized] = useState(false); + +// // 구글 애널리틱스 운영서버만 적용 +// useEffect(() => { +// if (process.env.REACT_APP_GOOGLE_ANALYTICS) { +// ReactGA.initialize(process.env.REACT_APP_GOOGLE_ANALYTICS); +// setInitialized(true); +// } +// }, []); + +// // location 변경 감지시 pageview 이벤트 전송 +// useEffect(() => { +// if (initialized) { +// ReactGA.set({ page: location.pathname }); +// ReactGA.send('pageview'); +// } +// }, [initialized, location]); + +// return null; // JSX 요소로 사용하기 위해 null을 반환합니다. +// }; + +// export default RouteChangeTracker; diff --git a/frontend/src/apis/apiClient.ts b/frontend/src/apis/apiClient.ts new file mode 100644 index 000000000..6022682f5 --- /dev/null +++ b/frontend/src/apis/apiClient.ts @@ -0,0 +1,245 @@ +import { ApiError } from '@_utils/customError/ApiError'; +import { getLastDarakbangId } from '@_common/lastDarakbangManager'; +import { getToken } from '@_utils/tokenManager'; + +type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; + +const DEFAULT_HEADERS = { + 'Content-Type': 'application/json', +}; + +const BASE_URL = `${process.env.BASE_URL}/v1`; + +function addBaseUrl(endpoint: string, isNeedLastDarakbang: boolean = false) { + if (isNeedLastDarakbang) + endpoint = '/darakbang/' + (getLastDarakbangId() || 0) + endpoint; + return BASE_URL + endpoint; +} + +function getHeaders(isRequiredAuth: boolean) { + const headers = new Headers(DEFAULT_HEADERS); + if (isRequiredAuth) { + const token = getToken(); + headers.append('Authorization', `Bearer ${token}`); + } + return headers; +} + +async function request( + method: Method, + endpoint: string, + data: object = {}, + config: RequestInit = {}, + isRequiredAuth: boolean = false, + isRequiredLastDarakbang: boolean = false, +) { + const url = addBaseUrl(endpoint, isRequiredLastDarakbang); + + const options: RequestInit = { + method, + headers: getHeaders(isRequiredAuth), + ...config, + }; + + if (method !== 'GET') { + options.body = JSON.stringify(data); + } + + const response = await fetch(url, options); + + if (!response.ok) { + const json = await response.json(); + throw new ApiError(response.status, json.message); + } + + return response; +} + +async function get( + endpoint: string, + config: RequestInit = {}, + isRequiredAuth: boolean = false, + isRequiredLastDarakbang: boolean = false, +) { + return request( + 'GET', + endpoint, + {}, + config, + isRequiredAuth, + isRequiredLastDarakbang, + ); +} + +async function post( + endpoint: string, + data: object = {}, + config: RequestInit = {}, + isRequiredAuth: boolean = false, + isRequiredLastDarakbang: boolean = false, +) { + return request( + 'POST', + endpoint, + data, + config, + isRequiredAuth, + isRequiredLastDarakbang, + ); +} + +async function put( + endpoint: string, + data: object = {}, + config: RequestInit = {}, + isRequiredAuth: boolean = false, + isRequiredLastDarakbang: boolean = false, +) { + return request( + 'PUT', + endpoint, + data, + config, + isRequiredAuth, + isRequiredLastDarakbang, + ); +} + +async function patch( + endpoint: string, + data: object = {}, + config: RequestInit = {}, + isRequiredAuth: boolean = false, + isRequiredLastDarakbang: boolean = false, +) { + return request( + 'PATCH', + endpoint, + data, + config, + isRequiredAuth, + isRequiredLastDarakbang, + ); +} + +/** + * delete는 예약어로 사용할 수 없는 함수 이름이라 부득이 `deleteMethod`로 이름을 지었습니다. + */ +async function deleteMethod( + endpoint: string, + data: object = {}, + config: RequestInit = {}, + isRequiredAuth: boolean = false, + isRequiredLastDarakbang: boolean = false, +) { + return request( + 'DELETE', + endpoint, + data, + config, + isRequiredAuth, + isRequiredLastDarakbang, + ); +} + +const ApiClient = { + async getWithoutAuth(endpoint: string, config: RequestInit = {}) { + return get(endpoint, config, false); + }, + async getWithAuth(endpoint: string, config: RequestInit = {}) { + return get(endpoint, config, true); + }, + async getWithLastDarakbangId(endpoint: string, config: RequestInit = {}) { + return get(endpoint, config, true, true); + }, + + async postWithoutAuth( + endpoint: string, + data: object = {}, + config: RequestInit = {}, + ) { + return post(endpoint, data, config, false); + }, + async postWithAuth( + endpoint: string, + data: object = {}, + config: RequestInit = {}, + ) { + return post(endpoint, data, config, true); + }, + async postWithLastDarakbangId( + endpoint: string, + data: object = {}, + config: RequestInit = {}, + ) { + return post(endpoint, data, config, true, true); + }, + + async putWithoutAuth( + endpoint: string, + data: object = {}, + config: RequestInit = {}, + ) { + return put(endpoint, data, config, false); + }, + async putWithAuth( + endpoint: string, + data: object = {}, + config: RequestInit = {}, + ) { + return put(endpoint, data, config, true); + }, + async putWithLastDarakbangId( + endpoint: string, + data: object = {}, + config: RequestInit = {}, + ) { + return put(endpoint, data, config, true, true); + }, + + async patchWithoutAuth( + endpoint: string, + data: object = {}, + config: RequestInit = {}, + ) { + return patch(endpoint, data, config, false); + }, + async patchWithAuth( + endpoint: string, + data: object = {}, + config: RequestInit = {}, + ) { + return patch(endpoint, data, config, true); + }, + async patchWithLastDarakbangId( + endpoint: string, + data: object = {}, + config: RequestInit = {}, + ) { + return patch(endpoint, data, config, true, true); + }, + + async deleteWithoutAuth( + endpoint: string, + data: object = {}, + config: RequestInit = {}, + ) { + return deleteMethod(endpoint, data, config, false); + }, + async deleteWithAuth( + endpoint: string, + data: object = {}, + config: RequestInit = {}, + ) { + return deleteMethod(endpoint, data, config, true); + }, + async deleteWithLastDarakbangId( + endpoint: string, + data: object = {}, + config: RequestInit = {}, + ) { + return deleteMethod(endpoint, data, config, true, true); + }, +}; + +export default ApiClient; diff --git a/frontend/src/apis/auth.ts b/frontend/src/apis/auth.ts new file mode 100644 index 000000000..5b77c6e66 --- /dev/null +++ b/frontend/src/apis/auth.ts @@ -0,0 +1,17 @@ +import ApiClient from './apiClient'; + +export const login = async (loginInputInfo: { nickname: string }) => { + const response = await ApiClient.postWithoutAuth( + '/auth/login', + loginInputInfo, + ); + return response.json(); +}; + +export const kakaoOAuth = async (code: string) => { + const response = await ApiClient.postWithoutAuth('/auth/kakao/oauth', { + code, + }); + console.log(response); + return response.json(); +}; diff --git a/frontend/src/apis/deletes.ts b/frontend/src/apis/deletes.ts new file mode 100644 index 000000000..9126fbd0e --- /dev/null +++ b/frontend/src/apis/deletes.ts @@ -0,0 +1,7 @@ +import ApiClient from './apiClient'; + +export const deleteCancelChamyo = async (moimId: number) => { + await ApiClient.deleteWithLastDarakbangId(`/chamyo`, { + moimId, + }); +}; diff --git a/frontend/src/apis/endPoints.ts b/frontend/src/apis/endPoints.ts new file mode 100644 index 000000000..dde077503 --- /dev/null +++ b/frontend/src/apis/endPoints.ts @@ -0,0 +1,16 @@ +const getEndpoint = (string: string) => { + return `${process.env.BASE_URL}/${string}`; +}; + +const ENDPOINTS = { + moim: getEndpoint('v1/moim'), + moims: getEndpoint('v1/moim'), + auth: getEndpoint('v1/auth'), + chamyo: getEndpoint('v1/chamyo'), + chat: getEndpoint('v1/chat'), + zzim: getEndpoint('v1/zzim'), + interest: getEndpoint('v1/interest'), + please: getEndpoint('v1/please'), + notification: getEndpoint('v1/notification'), +}; +export default ENDPOINTS; diff --git a/frontend/src/apis/gets.ts b/frontend/src/apis/gets.ts new file mode 100644 index 000000000..89d161672 --- /dev/null +++ b/frontend/src/apis/gets.ts @@ -0,0 +1,189 @@ +import { + Chat, + ChattingPreview, + MoimInfo, + Participation, + Role, +} from '@_types/index'; +import { + GetChamyoAll, + GetChamyoMine, + GetChat, + GetChattingPreview, + GetDarakbangInviteCode, + GetDarakbangMembers, + GetDarakbangMine, + GetDarakbangNameByCode, + GetMoim, + GetMoims, + GetMyInfo, + GetMyRoleInDarakbang, + GetNotifications, + GetPleases, + GetZzimMine, +} from './responseTypes'; + +import ApiClient from './apiClient'; +import { Filter } from '@_components/MyMoimListFilters/MyMoimListFilters'; +import { ApiError } from '@_utils/customError/ApiError'; + +export const getMoims = async (): Promise => { + const response = await ApiClient.getWithLastDarakbangId('/moim'); + + const json: GetMoims = await response.json(); + return json.data.moims; +}; + +export const getMyFilteredMoims = async ( + filter: Filter['api'], +): Promise => { + const response = await ApiClient.getWithLastDarakbangId( + `/moim/mine?filter=${filter}`, + ); + + const json: GetMoims = await response.json(); + return json.data.moims; +}; + +export const getMyZzimMoims = async (): Promise => { + const response = await ApiClient.getWithLastDarakbangId('/moim/zzim'); + + const json: GetMoims = await response.json(); + return json.data.moims; +}; + +export const getMoim = async (moimId: number): Promise => { + const response = await ApiClient.getWithLastDarakbangId(`/moim/${moimId}`); + + const json: GetMoim = await response.json(); + return json.data; +}; + +export const getChatPreview = async (): Promise => { + const response = await ApiClient.getWithLastDarakbangId(`/chat/preview`); + + const json: GetChattingPreview = await response.json(); + return json.data.chatPreviewResponses; +}; + +export const getChat = async ( + moimId: number, + recentChatId?: number, +): Promise => { + const response = await ApiClient.getWithLastDarakbangId( + `/chat?moimId=${moimId}&recentChatId=${recentChatId || 0}`, + ); + + const json: GetChat = await response.json(); + return json.data.chats; +}; + +export const getMyMoims = async (): Promise => { + const response = await ApiClient.getWithLastDarakbangId(`/moim/mine`); + + const json: GetMoims = await response.json(); + return json.data.moims; +}; + +export const getChamyoMine = async (moimId: number): Promise => { + const response = await ApiClient.getWithLastDarakbangId( + `/chamyo/mine?moimId=${moimId}`, + ); + + const json: GetChamyoMine = await response.json(); + return json.data.role; +}; + +export const getZzimMine = async (moimId: number): Promise => { + const response = await ApiClient.getWithLastDarakbangId( + `/zzim/mine?moimId=${moimId}`, + ); + + const json: GetZzimMine = await response.json(); + return json.data.isZzimed; +}; + +export const getChamyoAll = async ( + moimId: number, +): Promise => { + const response = await ApiClient.getWithLastDarakbangId( + `/chamyo/all?moimId=${moimId}`, + ); + + const json: GetChamyoAll = await response.json(); + return json.data.chamyos; +}; + +export const getPleases = async () => { + const response = await ApiClient.getWithLastDarakbangId('/please'); + + const json: GetPleases = await response.json(); + return json.data.pleases; +}; + +export const getMyInfo = async () => { + const response = await ApiClient.getWithLastDarakbangId('/member/mine'); + + const json: GetMyInfo = await response.json(); + return json.data; +}; + +export const getNotifications = async () => { + const response = await ApiClient.getWithLastDarakbangId('/notification/mine'); + + const json: GetNotifications = await response.json(); + return json.data.notifications; +}; + +export const getMyDarakbangs = async () => { + const response = await ApiClient.getWithAuth('/darakbang/mine'); + + const json: GetDarakbangMine = await response.json(); + return json.data.darakbangResponses; +}; + +export const getMyRoleInDarakbang = async () => { + const response = await ApiClient.getWithLastDarakbangId('/role'); + + const json: GetMyRoleInDarakbang = await response.json(); + return json.data.role; +}; + +export const getDarakbangMembers = async () => { + const response = await ApiClient.getWithLastDarakbangId('/members'); + + const json: GetDarakbangMembers = await response.json(); + return json.data.darakbangMemberResponses; +}; + +export const getDarakbangInviteCode = async () => { + const response = await ApiClient.getWithLastDarakbangId('/code'); + + const json: GetDarakbangInviteCode = await response.json(); + return json.data.code; +}; + +export const getDarakbangNameByCode = async (code: string) => { + const response = await ApiClient.getWithAuth( + '/darakbang/validation?code=' + code, + ).catch((e) => { + if (e instanceof ApiError && e.status === 401) { + throw e; + } + return { + json: () => { + return { data: { name: '' } }; + }, + }; + }); + + const json: GetDarakbangNameByCode = await response.json(); + return json.data.name; +}; + +export const getDarakbangNameById = async () => { + const response = await ApiClient.getWithLastDarakbangId(''); + + const json: GetDarakbangNameByCode = await response.json(); + return json.data.name; +}; diff --git a/frontend/src/apis/patches.ts b/frontend/src/apis/patches.ts new file mode 100644 index 000000000..f7930b6c8 --- /dev/null +++ b/frontend/src/apis/patches.ts @@ -0,0 +1,39 @@ +import ApiClient from './apiClient'; +import { MoimInputInfo } from '@_types/index'; +import { PostMoimBody } from './responseTypes'; + +export const patchCompleteMoim = async (moimId: number) => { + await ApiClient.patchWithLastDarakbangId(`/moim/${moimId}/complete`, { + moimId, + }); +}; + +export const patchCancelMoim = async (moimId: number) => { + await ApiClient.patchWithLastDarakbangId(`/moim/${moimId}/cancel`, { + moimId, + }); +}; + +export const patchModifyMoim = async (moimId: number, moim: MoimInputInfo) => { + const parsedMoim: PostMoimBody = { + ...moim, + date: moim.date || undefined, + time: moim.time || undefined, + place: moim.place || undefined, + }; + + await ApiClient.patchWithLastDarakbangId(`/moim`, { + moimId, + ...parsedMoim, + }); +}; + +export const patchReopenMoim = async (moimId: number) => { + await ApiClient.patchWithLastDarakbangId(`/moim/${moimId}/reopen`, { + moimId, + }); +}; + +export const patchOpenChat = async (moimId: number) => { + await ApiClient.patchWithLastDarakbangId(`/chat/open?moimId=${moimId}`); +}; diff --git a/frontend/src/apis/posts.ts b/frontend/src/apis/posts.ts new file mode 100644 index 000000000..67f8d5cf2 --- /dev/null +++ b/frontend/src/apis/posts.ts @@ -0,0 +1,134 @@ +import { MoimInputInfo, PleaseInfoInput } from '@_types/index'; +import { PostMoim, PostMoimBody } from './responseTypes'; + +import ApiClient from './apiClient'; + +export const postMoim = async (moim: MoimInputInfo): Promise => { + const parsedMoim: PostMoimBody = { + ...moim, + date: moim.date || undefined, + time: moim.time || undefined, + place: moim.place || undefined, + }; + + const response = await ApiClient.postWithLastDarakbangId('/moim', parsedMoim); + + const json: PostMoim = await response.json(); + return json.data; +}; + +export const postJoinMoim = async (moimId: number) => { + await ApiClient.postWithLastDarakbangId('/chamyo', { + moimId, + }); +}; + +export const postChangeZzim = async (moimId: number) => { + await ApiClient.postWithLastDarakbangId('/zzim', { + moimId, + }); +}; +export const postWriteComment = async ( + moimId: number, + selectedComment: number, + message: string, +) => { + if (selectedComment === 0) { + await ApiClient.postWithLastDarakbangId(`/moim/${moimId}`, { + content: message, + }); + } else { + await ApiClient.postWithLastDarakbangId(`/moim/${moimId}`, { + parentId: selectedComment, + content: message, + }); + } +}; + +export const postChat = async (moimId: number, content: string) => { + await ApiClient.postWithLastDarakbangId('/chat', { + moimId, + content, + }); +}; + +export const postInterest = async (pleaseId: number, isInterested: boolean) => { + await ApiClient.postWithLastDarakbangId('/interest', { + pleaseId, + isInterested, + }); +}; +export const postLastReadChatId = async ( + moimId: number, + lastReadChatId: number, +) => { + await ApiClient.postWithLastDarakbangId('/chat/last', { + moimId, + lastReadChatId, + }); +}; + +export const postConfirmDatetime = async ( + moimId: number, + date: string, + time: string, +) => { + await ApiClient.postWithLastDarakbangId('/chat/datetime', { + moimId, + date, + time, + }); +}; + +export const postConfirmPlace = async (moimId: number, place: string) => { + await ApiClient.postWithLastDarakbangId('/chat/place', { + moimId, + place, + }); +}; + +export const postPlease = async (please: PleaseInfoInput) => { + await ApiClient.postWithLastDarakbangId('/please', please); +}; + +export const postNotificationToken = async (currentToken: string) => { + await ApiClient.postWithAuth('/notification/register', { + token: currentToken, + }); +}; + +export const postDarakbang = async ({ + name, + nickname, +}: { + name: string; + nickname: string; +}) => { + const response = await ApiClient.postWithAuth('/darakbang', { + name, + nickname, + }); + + const json = await response.json(); + + return json?.data; +}; + +export const postDarakbangEntrance = async ({ + code, + nickname, +}: { + code: string; + nickname: string; +}) => { + const data = await ApiClient.postWithAuth( + '/darakbang/entrance?code=' + code, + { + nickname, + }, + ); + + const json = await data.json(); + + return json.data as number; +}; diff --git a/frontend/src/apis/responseTypes.ts b/frontend/src/apis/responseTypes.ts new file mode 100644 index 000000000..6a9e6acdd --- /dev/null +++ b/frontend/src/apis/responseTypes.ts @@ -0,0 +1,109 @@ +import { + Chat, + ChattingPreview, + Darakbang, + DarakbangRole, + MoimInfo, + Notification, + Participation, + Please, + Role, +} from '../types'; + +export interface GetMoims { + data: { moims: MoimInfo[] }; +} + +export interface GetMoim { + data: MoimInfo; +} + +export interface PostMoimBody { + place?: string; + date?: string; + time?: string; + description?: string; + title: string; + maxPeople: number; +} +export interface PostMoim { + data: number; +} + +export interface GetChattingPreview { + data: { chatPreviewResponses: ChattingPreview[] }; +} +export interface GetChat { + data: { chats: Chat[] }; +} + +export interface GetChamyoMine { + data: { + role: Role; + }; +} + +export interface GetChamyoAll { + data: { + chamyos: Participation[]; + }; +} + +export interface GetZzimMine { + data: { + isZzimed: boolean; + }; +} + +export interface GetPleases { + data: { + pleases: Please[]; + }; +} + +export interface GetMyInfo { + data: { + nickname: string; + profile: string; + }; +} + +export interface GetNotifications { + data: { + notifications: Notification[]; + }; +} + +export interface GetDarakbangMine { + data: { + darakbangResponses: Darakbang[]; + }; +} + +export interface GetMyRoleInDarakbang { + data: { + role: DarakbangRole; + }; +} + +export interface GetDarakbangMembers { + data: { + darakbangMemberResponses: { + memberId: number; + nickname: string; + profile: string; + }[]; + }; +} + +export interface GetDarakbangInviteCode { + data: { + code: string; + }; +} + +export interface GetDarakbangNameByCode { + data: { + name: string; + }; +} diff --git a/frontend/src/common/assets/back.svg b/frontend/src/common/assets/back.svg new file mode 100644 index 000000000..fd518f694 --- /dev/null +++ b/frontend/src/common/assets/back.svg @@ -0,0 +1,15 @@ + + + \ No newline at end of file diff --git a/frontend/src/common/assets/crown.svg b/frontend/src/common/assets/crown.svg new file mode 100644 index 000000000..3cda192a2 --- /dev/null +++ b/frontend/src/common/assets/crown.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/common/assets/default_profile.png b/frontend/src/common/assets/default_profile.png new file mode 100644 index 0000000000000000000000000000000000000000..3143fc76109163b22cf45da22d74966982de5bcb GIT binary patch literal 2349 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 TgKGiO00000NkvXXu0mjf2Qyej literal 0 HcmV?d00001 diff --git a/frontend/src/common/assets/empty_profile.svg b/frontend/src/common/assets/empty_profile.svg new file mode 100644 index 000000000..ecb9cc7fa --- /dev/null +++ b/frontend/src/common/assets/empty_profile.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/src/common/assets/fonts/woff2-subset/Pretendard-Black.subset.woff2 b/frontend/src/common/assets/fonts/woff2-subset/Pretendard-Black.subset.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..5f983530d53767c2ee18a48fbe0a67dfa3169d21 GIT binary patch literal 273460 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

    78ummF*1rtt)>e}U8m)|je2P}%iWmb9NVb~L#* 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%0DGOVLs8eC9ZD>L?l50`%Ar~ENIHi)JN-xO0-Gd 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