From 4d213a1fe64ac2b2aa62ccce4d4f779fc2e0e280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=BB=A4=EC=B0=AC?= <44027393+leegwichan@users.noreply.github.com> Date: Fri, 27 Sep 2024 12:11:13 +0900 Subject: [PATCH] [RELEASE] v1.1.1 (#310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 알림이 필요없는 에러 핸들링 실시 #265 * refactor: jest maxWorker 제거 #262 * fix: swc 설정 충돌 해결 #262 * refactor: front-CI self hosted 취소 #262 * refactor: 선택된 카테고리를 filter 대신 find 로 대체 #230 * style: 시작 버튼 텍스트 수정 #230 * feat: skeleton 스토리북 구현 #230 * refactor: 스토리북 폴더로 분리 #230 * refactor: 불필요해진 폴백 텍스트 제거 #230 * chore: queryClient 를 새로 생성해 테스트가 독립적으로 동작하도록 수정 #230 * fix: GameSkeleton aria-label 추가 #230 * test: 로딩 UI 테스트 분리 #230 * fix: self-hosted 대신 github actions 서버 사용 #230 * refactor: Timer hooks 디렉토리로 분리 #261 * refactor: 쿼리 발생 개수 저하되도록 만료된 방 마이그레이션 로직 수정 #268 * refactor: deleteAllInBatch -> deleteAll 사용 #268 * refactor: resetRoom 로직에서 roomBalanceBote migrate와 delete 로직 분리 #268 * refactor: 타임아웃되도 선택 API를 호출하도록 구조 리팩토링 #261 * refactor: 미사용 메서드 제거 #268 * test: 게임 화면 UI 테스트를 위해 storybook 작성 #261 * design: timer 구조 변경으로 인한 스타일 수정 #261 * design: 대기 화면 공통 레이아웃 적용 #261 * refactor: 시간 재는 타이머 관련 로직을 API 로직과 분리 #261 * refactor: 투표 시간 측정 타이머에 맞게 voteTimer로 이름 수정 #261 * refactor: 선택 완료 버튼 클릭 이벤트리스너명 vote로 수정 #261 * test: 타이머가 종료되었을 때 선택된 옵션이 있으면 투표 테스트 코드 작성 #261 * refactor: 투표 종료 여부를 Timer 컴포넌트가 가지면서 리렌더링 최적화 #261 * refactor: roundVoteIsFinished 네이밍을 voteIsFinished로 수정 #261 * refactor: Timer를 SelectContainer 하위 디렉토리로 위치 수정 #261 * feat: 게임 시작 전 카운트다운 구현 #270 * test: Countdown 스토리북 구현 #270 * refactor: 벌크 삭제 시 deleteAllInBatch() 사용하도록 변경 #268 * refactor: Countdown dimmed 영역 추가 #270 * design: Countdown 텍스트가 잘 안보인다는 피드백 반영 #270 * design: Countdown 땅콩이 카운트에 따라 점점 커지도록 구현 #270 * test: 게임 대기 화면 스토리북 구현 #270 * feat: 카운트 다운 끝난 후 게임 화면 라우팅 #270 * refactor: 게임 대기 화면 스토리북 폴더 수정 #270 * refactor: 타이머 관련 방어코드 작성 #270 * style: 버튼과 모달 가까이 위치 #270 * refactor: 게임 시작과 관련된 로컬 상태를 방정보 커스텀 훅과 분리 #270 * refactor: 카운트다운에 aria-label 추가 #270 * refactor: 시작 버튼을 isMaster로 관리 #270 * test: 게임 시작 버튼을 클릭하면 카운트 다운을 시작하는 테스트 코드 작성 #270 * test: 카운트 다운이 시작되고 3초 후 게임 화면으로 넘어가는 테스트 코드 작성 #270 * refactor: 카운트다운 관련 로직 커스텀 훅 분리 #270 * refactor: Countdown 폴더 위치 수정 #270 * refactor: useTimer 의존성 배열 수정 #261 * refactor: timer 유틸함수 분리 #261 * refactor: 불필요한 패키지 제거 #262 * refactor: 불필요한 옵션 제거 #262 * refactor: 모호한 함수명 수정 #261 * refactor: sudo로 변경하여 logback 쓰기권한 흭득 #81 * fix: HttpMediaTypeNotSupportedException를 415 Unsupported Media Type으로 처리하도록 수정 * fix: ClientErrorCode를 이전 버전으로 수정 및 새로운 코드 추가 * chore: build는 ubuntu 환경에서 처리하도록 변경 #81 * refactor: 불필요한 리스트 생성 제거 #268 * refactor: 미사용 메서드 제거 #268 * fix: 테스트용 메서드 제거 #268 * fix: 임시 사용 로직 제거 #268 * fix: EntityManger 로직 제거 #268 * fix: 테스트에서 트랜잭션 제거 #268 * refactor: refetchInterval 및 refetchIntervalInBackground 를 통해 게임 화면으로 안넘어간 오류 개선 #261 * refactor: 초대하기 버튼 위치 변경 #274 * fix: 투표를 한 상태여도 타이머가 끝난 후 투표 API 요청을 하는 오류 해결 #261 * test: 타이머가 종료되었을 때 이미 투표를 했다면 또 투표를 하지 않는 테스트 코드 구현 #261 * feat: category mouse cursor pointer 추가 #274 * feat: 매칭 결과로 제목 수정 및 설명 추가 #273 * refactor: Modal을 context로 관리 #272 * test: Modal 리팩토링을 위한 게임 시작 테스트 코드 작성 #272 * refactor: 방장 여부 recoil 값을 넣어 렌더링하는 테스트 유틸 함수 공용화 #272 * refactor: customRenderWithIsMaster 테스트코드 적용 #272 * refactor: 시작 버튼을 isMaster로 관리 #272 * refactor: Modal Context 게임 시작 부분 적용 #272 * refactor: Modal Context 투표 부분 적용 #272 * refactor: Modal UI 역할이 사라져 StartButtonContainer 제거 #272 * refactor: 게임 결과에도 Modal Context 적용 #272 * feat: 카테고리 클릭 시 방 설정 모달 #274 * refactor: RoomSettingHeader에 Modal Context 적용 #272 * refactor: 다른 모달도 적용할 수 있도록 Modal props 설정 #272 * fix: modal에서 toast를 사용하므로 toast를 modal 부모 요소로 수정 #272 * refactor: 다음 라운드 안내 모달 Modal Context 적용 #272 * refactor: 초대하기 모달 Modal Context 적용 #272 * refactor: 방 생성 및 참가 에러 모달 Modal Context 적용 #272 * fix: onConfirm 함수가 동작하지 않는 문제 해결 #272 * refactor: 중복된 모달 하나로 합치기 #272 * fix: Modal storybook 에 Provider 추가 #272 * refactor: webpack 설정 추가 #274 * style: 변수명 수정 #270 * refactor: 이미지 형식 webp로 변환 및 이미지 크기 조절 #278 * refactor: 폰트 preload 적용 #278 * refactor: meta tag 추가 #278 * refactor: favicon 설정 #278 * refactor: font subset 적용하여 리소스 용량 줄이기 #278 * feat: categoryContainer에 방 정보 추가 #274 * refactor: modal close 후 스크롤이 다시 생기는 버그 해결 #274 * feat: resize 시 버튼 위치 변경 #274 * chore: webpack-bundle-analyzer 설정 #278 * chore: js output contenthash 적용 #278 * refactor: 코드 스플리팅 적용 #278 * refactor: RoomBalanceVote 삭제 로직 ExpiredRoomMigrator에서 RoomBalanceVoteService로 이동 #268 * refactor: migrate 로직 메서드 분리 #268 * refactor: 종료된 방 마이그레이션 로직도 RoomMigrator에서 관리 #268 * style: 만료된 방뿐만 아니라 종료된 방도 마이그레이션 하므로 ExpiredRoomMigrator에서 RoomMigrator로 네이밍 변경 #268 * test: 종료된 방 마이그레이션 로직 관련 테스트 코드 수정 #268 * test: 종료된 방 투표 마이그레이션 테스트 작성 #268 * style: 방 투표로 전체 투표 생성하여 저장하는 메서드 네이밍 구체적으로 변경 #268 * refactor: 멤버 투표 마이그레이션하는 로직 네이밍 구체적으로 변경 #268 * test: 만료된 방 정보 마이그레이션 테스트 작성 #268 * refactor: 방 종료 검증 로직 위치 Migrator에서 RoomFacade.resetRoom() 으로 수정 #268 * style: migrator에서 룸의 상태에 대한 정보 제거 #268 * refactor: 서브셋 폰트 preload 적용 #278 * feat: nickname input focus에 따라 button 위치 변경 #274 * feat: SpringActuator 의존성 추가 #279 * chore: prod 환경은 health check만 가능하도록 설정 #279 * feat: random nickname 컴포넌트 외부에서 생성 #274 * chore: CI/CD 스크립트에 분산 prod 환경 (prod-a, prod-b) 설정 적용 #281 * chore: 운영환경 AZ에 따른 스크립트 네이밍 수정 #281 * feat: categoryContainer 테스트 코드 when given 추가 #274 * refactor: isFinalPage 이름을 isMatchingPage로 수정 #273 * refactor: 최대 인원, 최대 닉네임 글자를 테스트 하기 위해 mock data 수정 #273 * refactor: 헤더 컴포넌트에 매칭 결과 화면 헤더 추가 #273 * fix: Modal에서 navigate 사용하지 못하는 오류 해결 #272 * fix: 브라우저 환경과 Provider 구조가 다른 문제 해결 #272 * fix: 매칭 인원이 8명 이상인 경우 화면에 다 보이지 않는 문제 해결 #273 * refactor: 카운트다운을 스크린 리더가 읽도록 aria-live 추가 #270 * design: 카운트다운이 시작버튼 위로 오도록 z-index 설정 #270 * design: 매칭도 바의 길이보다 닉네임이 긴 경우 바 영역 밖으로 나오도록 수정 #273 * refactor: 배열 생성 자체를 막도록 조건문 추가 #270 * design: 매칭 순위가 두 자리인 경우 정렬 어긋남 개선 #273 * feat: resize 이벤트로 모바일 키보드 여부 판단 기능 추가 #274 * test: CategoryContainer 컴포넌트 테스트 추가 #274 * chore: prod 환경 application-prod.yml DB Replication 로직 설정 #287 * feat: Replication DB DataSource Routing 로직 작성 #287 * chore: 변경된 secret property key name 반영 #287 * feat: 매칭 결과가 정해진 크기 이상인 경우 스크롤, 플로팅 버튼으로 위 아래 이동 할 수 있는 기능 #273 * fix: Source DB 라우팅 네이밍 오류 수정 #287 * design: 위/아래 플로팅 버튼 스타일 추가 #273 * fix: url properties 이름 jdbc-url로 변경 #287 * refactor: 스크롤 관련 비즈니스 코드 별도의 커스텀 훅으로 분리 #273 * refactor: 스크롤 상태와 스크롤 제어 로직을 분리 #273 * refactor: 매칭 결과가 있는 경우에만 플로팅 버튼이 뜨도록 수정 #273 * refactor: 라운드 결과의 투표 현황 탭에서 투표 현황을 확인할 수 있도록 수정 #290 * refactor: 라운드 결과 페이지에서 빈 헤더 영역 차지하지 않도록 수정 #290 * refactor: 라운드 결과 레이아웃 수정 및 불필요한 컴포넌트 삭제 #290 * style: master, slave 네이밍 source, replica로 변경 #287 * style: RoutingReplicas <> 추가 #287 * feat: 투표 결과에 대한 동률 여부 및 우세한 선택지를 반환하는 유틸 함수 구현 #290 * feat: 해당 문항의 전체 응답 데이터를 요약해서 보여주는 기능 #290 * fix: 방장이 아닌 사용자가 카운트다운 후 게임 시작 안되는 오류 해결 #293 * feat: 투표 현황에서 나의 닉네임을 강조하여 표시하는 기능 #290 * refactor: 라운드 결과 탭 이름을 투표 결과, 투표 현황으로 수정 #290 * refactor: 탭이 방의 퍼센트와 전체 퍼센트에서 투표 결과 퍼센트와 투표 현황으로 수정됨에 따라 불 필요한 코드 삭제 #290 * fix: 카운트다운 테스트 코드 오류 해결 #293 * feat: 라운드 결과 페이지에 라운드 헤더 추가 #290 * refactor: 탭 안에 토픽이 위치하도록 로직 수정 #290 * design: 라운드 페이지 스타일 수정 #290 * refactor: 라운드 결과 탭 이름을 투표 결과에서 투표 통계로 수정 #290 * refactor: 투표 현황 페이지 삭제 #290 * refactor: 사용하지 않는 스타일 삭제 #290 * refactor: 라운드 결과 탭에서 그룹 관련된 코드 투표 통계로 수정 #290 * refactor: TabContentContainer 스토리북 수정 #290 * refactor: RoundVoteContainer 테스트 코드 수정 #290 * refactor: TabContentContainer 프로퍼티 이름 수정 #290 * refactor: 불 필요한 코드 삭제 #290 * refactor: CategoryContainer test 코드 명시적으로 변경 #274 * refactor: 이미지 포맷 및 크기 최적화 #292 * refactor: 불필요한 코드 제거 #274 * refactor: useKeyboard hook export -> default export로 변경 #274 * refactor: 불필요한 useState 제거 #274 * refactor: CategoryContainer 테스트 코드 오류 수정 #274 * merge: conflict 해결 #272 * refactor: 매칭 결과 높이 주석 추가 #273 * refactor: 초대 버튼 글씨 굵기 변경 #274 * refactor: 불필요한 코드 제거 #274 * refactor: 타입 단언을 통해 스크롤 로직 개선 #273 * design: 매칭 결과 설명 텍스트 진하기 수정 #273 * refactor: 내 닉네임인지 여부 변수명 수정 #290 * refactor: 선택지 두 개가 수치가 동등한지 여부를 나타내는 변수명 수정 #290 * refactor: 100 퍼센트 기준 대신 1 을 비율의 최댓값으로 수정 #292 * refactor: width와 right 대신 transform을 활용하여 reflow 발생 최적화 #292 * refactor: nickname을 표시하는 컴포넌트의 prop에 알맞게 수정 #290 * refactor: 불 필요한 타임 아웃 코드 삭제 #290 * refactor: 선택지의 퍼센트보다 멤버수로 투표 여부를 판단하도록 로직 수정 #290 * refactor: TabContentContainer에서 사용되는 util 파일 이름 수정 #290 * refactor: 투표 현황 페이지 삭제됨에 따라 불필요한 코드 삭제 #290 * refactor: rate와 scale 네이밍 수정 #292 * fix: 화면을 벗어나는 문제로 인해 100이 아닌 98로 계산 #292 * style: DB Routing log 설정 #287 * style: TODO 제거 #287 * style: 다중 개행 제거 #287 * style: RoutingDataSource에서 Slf4j 설정 제거 #287 * chore: prod 환경 자원을 아끼기 위해 be-ci-prod 스크립트 Git Actions 서버 사용하도록 변경 #287 * refactor: DataSourceType Enum으로 관리 #301 * refactor: 패키지 구조 변경 #301 * merge: develop 충돌 해결 # * refactor: settingIcon webp로 변경 #291 * chore: github actions 스크립트 수정 #278 * fix: image 확장자 에러 해결 #291 * feat: 첫 라운드에 게임 준비 시간 추가 #302 * refactor: png 확장자 이미지를 webp 확장자로 수정 후 적용 #300 * fix: BundleAnalyzerPlugin를 dev 환경에 설정 #305 * feat: 요청 성공 시, Response URI, Body 로깅 기능 구현 #306 * fix: analyzer 플러그인 제거 #305 * feat: 응답정보에 요청에 대한 HttpMethod도 로깅 #306 * refactor: 정해진 영역보다 컨텐츠 길이가 긴 경우에만 스크롤 생기도록 수정 #300 * refactor: 스피너 로딩시 레이아웃 시프트 개선 #300 * refactor: 매칭 결과에서 매칭된 사람이 아무도 없는 경우 레이아웃 시프트 개선 #300 * refactor: 매칭 결과에서 퍼센트 숫자가 오르면서 발생하는 레이아웃 시프트 개선 #300 * fix: Replica1 -> Replica로 설정값 변경 #301 * chore: 변수명 의미 더 잘 전달되게 변경 #287 * style: 개행 추가 #306 * merge: 충돌 삭제되지 않은 파일 삭제 --------- Co-authored-by: novice0840 Co-authored-by: rbgksqkr Co-authored-by: PgmJun <84304802+PgmJun@users.noreply.github.com> Co-authored-by: useon Co-authored-by: novice0840 <111696934+novice0840@users.noreply.github.com> Co-authored-by: jhon3242 Co-authored-by: Yuseon Kim(썬데이) <74897720+useon@users.noreply.github.com> Co-authored-by: Wonjun Choi(타칸) <78288539+jhon3242@users.noreply.github.com> --- .github/workflows/fe-cd-storybook.yml | 2 +- .github/workflows/fe-ci-dev.yml | 2 +- ...pect.java => ControllerLoggingAspect.java} | 16 +- .../logging/DevControllerLoggingAspect.java | 25 +++ .../aop/logging/DevRequestLoggingAspect.java | 18 --- ....java => ProdControllerLoggingAspect.java} | 7 +- .../config/sql/DataSourceConfig.java | 54 +++++++ .../config/sql/RoutingDataSource.java | 18 +++ .../config/sql/type/DataSourceType.java | 7 + .../room/balance/roomcontent/RoomContent.java | 9 +- .../src/main/resources/application-prod.yml | 8 +- .../balance/roomcontent/RoomContentTest.java | 32 +++- frontend/index.html | 23 --- frontend/package-lock.json | 143 ++++++++++++++++++ frontend/package.json | 12 +- frontend/public/favicon.ico | Bin 0 -> 124795 bytes frontend/public/index.html | 56 +++++++ .../src/assets/images/angryDdangkong.webp | Bin 0 -> 31116 bytes frontend/src/assets/images/crownIcon.png | Bin 420 -> 0 bytes frontend/src/assets/images/crownIcon.webp | Bin 0 -> 204 bytes frontend/src/assets/images/ddangkong.webp | Bin 0 -> 16714 bytes .../src/assets/images/ddangkongTimer.webp | Bin 0 -> 2084 bytes .../src/assets/images/errorDdangkong.webp | Bin 0 -> 24756 bytes frontend/src/assets/images/exitIcon.png | Bin 247 -> 0 bytes frontend/src/assets/images/exitIcon.webp | Bin 0 -> 120 bytes frontend/src/assets/images/logoIcon.png | Bin 27841 -> 0 bytes frontend/src/assets/images/plusIcon.png | Bin 319 -> 0 bytes frontend/src/assets/images/plusIcon.webp | Bin 0 -> 158 bytes frontend/src/assets/images/sadDdangkong.webp | Bin 0 -> 22958 bytes frontend/src/assets/images/settingsIcon.svg | 1 - frontend/src/assets/images/settingsIcon.webp | Bin 0 -> 1356 bytes frontend/src/assets/images/sillyDdangkong.png | Bin 67412 -> 0 bytes .../src/assets/images/sillyDdangkong.webp | Bin 0 -> 23420 bytes frontend/src/assets/images/spinDdangkong.webp | Bin 0 -> 32002 bytes .../GameResult/GameResult.styled.ts | 1 + .../src/components/GameResult/GameResult.tsx | 2 +- .../GameResultItem/GameResultItem.styled.ts | 3 +- .../GameResultItem/GameResultItem.tsx | 2 +- .../components/NicknameItem/NicknameItem.tsx | 3 +- .../ReadyMembersContainer.tsx | 4 +- .../SelectContainer/Timer/Timer.styled.ts | 24 ++- .../SelectContainer/Timer/Timer.test.tsx | 2 - .../SelectContainer/Timer/Timer.tsx | 8 +- .../SelectContainer/Timer/Timer.util.ts | 4 +- .../SelectContainer/Timer/hooks/useTimer.ts | 12 +- .../Timer/hooks/useVoteTimer.ts | 4 +- .../Countdown/Countdown.tsx | 2 +- .../TabContentContainer.styled.ts | 2 +- .../TabContentContainer.tsx | 2 +- .../ErrorBoundary/AsyncErrorBoundary.tsx | 5 +- .../AsyncErrorFallback/AsyncErrorFallback.tsx | 2 +- .../RootErrorFallback/RootErrorFallback.tsx | 2 +- .../RouterErrorFallback.tsx | 2 +- .../common/Spinner/Spinner.styled.ts | 2 +- .../src/components/common/Spinner/Spinner.tsx | 2 +- .../src/components/layout/Header/Header.tsx | 4 +- frontend/src/custom.d.ts | 1 + frontend/src/pages/GamePage/GamePage.tsx | 6 +- frontend/src/pages/MainPage/MainPage.tsx | 4 +- .../src/pages/NicknamePage/NicknamePage.tsx | 5 +- frontend/src/pages/ReadyPage/ReadyPage.tsx | 14 +- frontend/src/router/index.tsx | 49 ++++-- frontend/src/router/lazyPages.ts | 8 + frontend/webpack.config.common.js | 9 +- 64 files changed, 483 insertions(+), 140 deletions(-) rename backend/src/main/java/ddangkong/aop/logging/{RequestLoggingAspect.java => ControllerLoggingAspect.java} (84%) create mode 100644 backend/src/main/java/ddangkong/aop/logging/DevControllerLoggingAspect.java delete mode 100644 backend/src/main/java/ddangkong/aop/logging/DevRequestLoggingAspect.java rename backend/src/main/java/ddangkong/aop/logging/{ProdRequestLoggingAspect.java => ProdControllerLoggingAspect.java} (65%) create mode 100644 backend/src/main/java/ddangkong/config/sql/DataSourceConfig.java create mode 100644 backend/src/main/java/ddangkong/config/sql/RoutingDataSource.java create mode 100644 backend/src/main/java/ddangkong/config/sql/type/DataSourceType.java delete mode 100644 frontend/index.html create mode 100644 frontend/public/favicon.ico create mode 100644 frontend/public/index.html create mode 100644 frontend/src/assets/images/angryDdangkong.webp delete mode 100644 frontend/src/assets/images/crownIcon.png create mode 100644 frontend/src/assets/images/crownIcon.webp create mode 100644 frontend/src/assets/images/ddangkong.webp create mode 100644 frontend/src/assets/images/ddangkongTimer.webp create mode 100644 frontend/src/assets/images/errorDdangkong.webp delete mode 100644 frontend/src/assets/images/exitIcon.png create mode 100644 frontend/src/assets/images/exitIcon.webp delete mode 100644 frontend/src/assets/images/logoIcon.png delete mode 100644 frontend/src/assets/images/plusIcon.png create mode 100644 frontend/src/assets/images/plusIcon.webp create mode 100644 frontend/src/assets/images/sadDdangkong.webp delete mode 100644 frontend/src/assets/images/settingsIcon.svg create mode 100644 frontend/src/assets/images/settingsIcon.webp delete mode 100644 frontend/src/assets/images/sillyDdangkong.png create mode 100644 frontend/src/assets/images/sillyDdangkong.webp create mode 100644 frontend/src/assets/images/spinDdangkong.webp create mode 100644 frontend/src/router/lazyPages.ts diff --git a/.github/workflows/fe-cd-storybook.yml b/.github/workflows/fe-cd-storybook.yml index 6f3b4d846..654c65c66 100644 --- a/.github/workflows/fe-cd-storybook.yml +++ b/.github/workflows/fe-cd-storybook.yml @@ -46,7 +46,7 @@ jobs: API_BASE_URL: ${{ secrets.API_BASE_URL }} - name: build storybook - run: npm run build-storybook + run: npm run build:storybook - name: upload storybook uses: peaceiris/actions-gh-pages@v3 diff --git a/.github/workflows/fe-ci-dev.yml b/.github/workflows/fe-ci-dev.yml index bd4944970..9dc2c5f90 100644 --- a/.github/workflows/fe-ci-dev.yml +++ b/.github/workflows/fe-ci-dev.yml @@ -39,7 +39,7 @@ jobs: ${{ runner.os }}-node- - name: Build - run: npm run build-dev + run: npm run build:dev working-directory: ./frontend - name: Test diff --git a/backend/src/main/java/ddangkong/aop/logging/RequestLoggingAspect.java b/backend/src/main/java/ddangkong/aop/logging/ControllerLoggingAspect.java similarity index 84% rename from backend/src/main/java/ddangkong/aop/logging/RequestLoggingAspect.java rename to backend/src/main/java/ddangkong/aop/logging/ControllerLoggingAspect.java index 27622ae63..893e5e51b 100644 --- a/backend/src/main/java/ddangkong/aop/logging/RequestLoggingAspect.java +++ b/backend/src/main/java/ddangkong/aop/logging/ControllerLoggingAspect.java @@ -13,7 +13,7 @@ import org.springframework.web.context.request.ServletRequestAttributes; @Slf4j -abstract class RequestLoggingAspect { +abstract class ControllerLoggingAspect { @Pointcut("execution(* ddangkong.controller..*Controller.*(..))") public void allController() { @@ -27,11 +27,7 @@ public void polling() { public void allControllerWithoutPolling() { } - protected void logController(JoinPoint joinPoint) { - logRequest(joinPoint); - } - - private void logRequest(JoinPoint joinPoint) { + protected void logControllerRequest(JoinPoint joinPoint) { HttpServletRequest request = getHttpServletRequest(); String uri = request.getRequestURI(); String httpMethod = request.getMethod(); @@ -41,6 +37,14 @@ private void logRequest(JoinPoint joinPoint) { log.info("Request Logging: {} {} body - {} parameters - {}", httpMethod, uri, body, queryParameters); } + protected void logControllerResponse(JoinPoint joinPoint, Object responseBody) { + HttpServletRequest request = getHttpServletRequest(); + String uri = request.getRequestURI(); + String httpMethod = request.getMethod(); + + log.info("Response Logging: SUCCESS {} {} Body: {}", httpMethod, uri, responseBody); + } + private HttpServletRequest getHttpServletRequest() { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); return requestAttributes.getRequest(); diff --git a/backend/src/main/java/ddangkong/aop/logging/DevControllerLoggingAspect.java b/backend/src/main/java/ddangkong/aop/logging/DevControllerLoggingAspect.java new file mode 100644 index 000000000..40d68829a --- /dev/null +++ b/backend/src/main/java/ddangkong/aop/logging/DevControllerLoggingAspect.java @@ -0,0 +1,25 @@ +package ddangkong.aop.logging; + + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Aspect +@Component +@Profile({"dev", "local"}) +public class DevControllerLoggingAspect extends ControllerLoggingAspect { + + @Before("allController()") + public void logControllerRequest(JoinPoint joinPoint) { + super.logControllerRequest(joinPoint); + } + + @AfterReturning(pointcut = "allController()", returning = "responseBody") + protected void logControllerResponse(JoinPoint joinPoint, Object responseBody) { + super.logControllerResponse(joinPoint, responseBody); + } +} diff --git a/backend/src/main/java/ddangkong/aop/logging/DevRequestLoggingAspect.java b/backend/src/main/java/ddangkong/aop/logging/DevRequestLoggingAspect.java deleted file mode 100644 index 0572cde47..000000000 --- a/backend/src/main/java/ddangkong/aop/logging/DevRequestLoggingAspect.java +++ /dev/null @@ -1,18 +0,0 @@ -package ddangkong.aop.logging; - - -import org.aspectj.lang.JoinPoint; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Before; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Aspect -@Component -@Profile({"dev", "local"}) -public class DevRequestLoggingAspect extends RequestLoggingAspect { - @Before("allController()") - public void logController(JoinPoint joinPoint) { - super.logController(joinPoint); - } -} diff --git a/backend/src/main/java/ddangkong/aop/logging/ProdRequestLoggingAspect.java b/backend/src/main/java/ddangkong/aop/logging/ProdControllerLoggingAspect.java similarity index 65% rename from backend/src/main/java/ddangkong/aop/logging/ProdRequestLoggingAspect.java rename to backend/src/main/java/ddangkong/aop/logging/ProdControllerLoggingAspect.java index ab8f0ddb8..51429c6e2 100644 --- a/backend/src/main/java/ddangkong/aop/logging/ProdRequestLoggingAspect.java +++ b/backend/src/main/java/ddangkong/aop/logging/ProdControllerLoggingAspect.java @@ -10,9 +10,10 @@ @Aspect @Component @Profile("prod") -public class ProdRequestLoggingAspect extends RequestLoggingAspect { +public class ProdControllerLoggingAspect extends ControllerLoggingAspect { + @Before("allControllerWithoutPolling()") - public void logController(JoinPoint joinPoint) { - super.logController(joinPoint); + public void logControllerRequest(JoinPoint joinPoint) { + super.logControllerRequest(joinPoint); } } diff --git a/backend/src/main/java/ddangkong/config/sql/DataSourceConfig.java b/backend/src/main/java/ddangkong/config/sql/DataSourceConfig.java new file mode 100644 index 000000000..9c4a29d78 --- /dev/null +++ b/backend/src/main/java/ddangkong/config/sql/DataSourceConfig.java @@ -0,0 +1,54 @@ +package ddangkong.config.sql; + +import ddangkong.config.sql.type.DataSourceType; +import java.util.HashMap; +import java.util.Map; +import javax.sql.DataSource; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; + +@Profile("prod") +@Configuration +public class DataSourceConfig { + + @Bean + @ConfigurationProperties(prefix = "spring.datasource.source") + public DataSource sourceDataSource() { + return DataSourceBuilder.create() + .build(); + } + + @Bean + @ConfigurationProperties(prefix = "spring.datasource.replica") + public DataSource replicaDataSource() { + return DataSourceBuilder.create() + .build(); + } + + @Bean + public DataSource routingDataSource( + DataSource sourceDataSource, + DataSource replicaDataSource + ) { + Map dataSources = new HashMap<>(); + dataSources.put(DataSourceType.SOURCE, sourceDataSource); + dataSources.put(DataSourceType.REPLICA, replicaDataSource); + + RoutingDataSource routingDataSource = new RoutingDataSource(); + routingDataSource.setDefaultTargetDataSource(dataSources.get(DataSourceType.SOURCE)); + routingDataSource.setTargetDataSources(dataSources); + + return routingDataSource; + } + + @Primary + @Bean + public DataSource dataSource() { + return new LazyConnectionDataSourceProxy(routingDataSource(sourceDataSource(), replicaDataSource())); + } +} diff --git a/backend/src/main/java/ddangkong/config/sql/RoutingDataSource.java b/backend/src/main/java/ddangkong/config/sql/RoutingDataSource.java new file mode 100644 index 000000000..8a73602c0 --- /dev/null +++ b/backend/src/main/java/ddangkong/config/sql/RoutingDataSource.java @@ -0,0 +1,18 @@ +package ddangkong.config.sql; + +import ddangkong.config.sql.type.DataSourceType; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +public class RoutingDataSource extends AbstractRoutingDataSource { + + @Override + protected Object determineCurrentLookupKey() { + boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); + if (isReadOnly) { + return DataSourceType.REPLICA; + } else { + return DataSourceType.SOURCE; + } + } +} diff --git a/backend/src/main/java/ddangkong/config/sql/type/DataSourceType.java b/backend/src/main/java/ddangkong/config/sql/type/DataSourceType.java new file mode 100644 index 000000000..c46a4a559 --- /dev/null +++ b/backend/src/main/java/ddangkong/config/sql/type/DataSourceType.java @@ -0,0 +1,7 @@ +package ddangkong.config.sql.type; + +public enum DataSourceType { + SOURCE, + REPLICA, + ; +} diff --git a/backend/src/main/java/ddangkong/domain/room/balance/roomcontent/RoomContent.java b/backend/src/main/java/ddangkong/domain/room/balance/roomcontent/RoomContent.java index 9b7dacff6..b10287f90 100644 --- a/backend/src/main/java/ddangkong/domain/room/balance/roomcontent/RoomContent.java +++ b/backend/src/main/java/ddangkong/domain/room/balance/roomcontent/RoomContent.java @@ -24,6 +24,9 @@ public class RoomContent { private static final int DELAY_MSEC = 2_000; // TODO SEC로 변경 + private static final int GAME_START_WAITING_MSEC = 3_000; // TODO SEC로 변경 + private static final int FIRST_ROUND = 1; + private static final int MSEC = 1_000; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -58,7 +61,11 @@ public void updateVoteDeadline(LocalDateTime now, int timeLimit) { throw new VoteDeadlineConfiguredException(); } - int afterSec = (timeLimit + DELAY_MSEC) / 1_000; + int afterSec = (timeLimit + DELAY_MSEC) / MSEC; + if (round == FIRST_ROUND) { + afterSec += GAME_START_WAITING_MSEC / MSEC; + } + voteDeadline = now.plusSeconds(afterSec); } diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index 61b2885b7..05efe6406 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -7,11 +7,11 @@ spring: username: ${secret.datasource.source.username} password: ${secret.datasource.source.password} jdbc-url: jdbc:mysql://${secret.datasource.source.host}:${secret.datasource.source.port}/${secret.datasource.database}?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&useSSL=false - replica1: + replica: driver-class-name: com.mysql.cj.jdbc.Driver - username: ${secret.datasource.replica1.username} - password: ${secret.datasource.replica1.password} - jdbc-url: jdbc:mysql://${secret.datasource.replica1.host}:${secret.datasource.replica1.port}/${secret.datasource.database}?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&useSSL=false + username: ${secret.datasource.replica.username} + password: ${secret.datasource.replica.password} + jdbc-url: jdbc:mysql://${secret.datasource.replica.host}:${secret.datasource.replica.port}/${secret.datasource.database}?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&useSSL=false sql: init: diff --git a/backend/src/test/java/ddangkong/domain/room/balance/roomcontent/RoomContentTest.java b/backend/src/test/java/ddangkong/domain/room/balance/roomcontent/RoomContentTest.java index 20c8b4787..125c0c5a1 100644 --- a/backend/src/test/java/ddangkong/domain/room/balance/roomcontent/RoomContentTest.java +++ b/backend/src/test/java/ddangkong/domain/room/balance/roomcontent/RoomContentTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; import ddangkong.domain.balance.content.BalanceContent; import ddangkong.domain.balance.content.Category; @@ -25,7 +26,7 @@ class 투표_마감_시간_설정 { @Test void 투표_마감_시간을_설정한다() { // given - int currentRound = 1; + int currentRound = 2; int timeLimit = 10_000; RoomSetting roomSetting = new RoomSetting(5, timeLimit, Category.IF); @@ -62,7 +63,7 @@ class 투표_마감_시간_지남_여부 { private static final Room ROOM = Room.createNewRoom(); private static final BalanceContent BALANCE_CONTENT = new BalanceContent(Category.IF, "치킨 vs 피자"); - private static final int ROUND = 1; + private static final int ROUND = 2; @Test void 투표_마감_시간보다_이전_시간이면_투표가_마감되지_않은_것이다() { @@ -100,4 +101,31 @@ class 투표_마감_시간_지남_여부 { .isExactlyInstanceOf(MismatchRoundException.class); } } + + @Nested + class 게임_대기_시간_포함_투표_마감_시간_지남_여부 { + + private static final Room ROOM = Room.createNewRoom(); + private static final BalanceContent BALANCE_CONTENT = new BalanceContent(Category.IF, "치킨 vs 피자"); + private static final int START_ROUND = 1; + + @Test + void 첫_라운드는_투표_마감_시간에는_게임_대기_시간을_추가된다() { + // given + LocalDateTime voteDeadline = LocalDateTime.parse("2024-08-03T20:00:03"); + RoomContent roomContent = RoomContent.newRoomContent(ROOM, BALANCE_CONTENT, START_ROUND); + int timeLimit = 3; + LocalDateTime inTime = LocalDateTime.parse("2024-08-03T20:00:08"); + LocalDateTime overTime = LocalDateTime.parse("2024-08-03T20:00:09"); + + // when + roomContent.updateVoteDeadline(voteDeadline, timeLimit); + + // then + assertAll( + () -> assertThat(roomContent.isOverVoteDeadline(inTime, START_ROUND)).isFalse(), + () -> assertThat(roomContent.isOverVoteDeadline(overTime, START_ROUND)).isTrue() + ); + } + } } diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index bf2da022d..000000000 --- a/frontend/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - ddangkong - - - - - - -
- - - diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 064f918fb..4f6ba7ed8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -81,6 +81,7 @@ "typescript": "^5.5.3", "undici": "^6.19.2", "webpack": "^5.92.1", + "webpack-bundle-analyzer": "^4.10.2", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4", "webpack-merge": "^6.0.1" @@ -4066,6 +4067,12 @@ "node": ">=14" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true + }, "node_modules/@remix-run/router": { "version": "1.17.1", "license": "MIT", @@ -10227,6 +10234,12 @@ "integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==", "dev": true }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true + }, "node_modules/debug": { "version": "4.3.5", "license": "MIT", @@ -10614,6 +10627,12 @@ "url": "https://dotenvx.com" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, "node_modules/eastasianwidth": { "version": "0.2.0", "dev": true, @@ -13037,6 +13056,21 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/hamt_plus": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", @@ -17173,6 +17207,15 @@ "ufo": "^1.5.3" } }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "license": "MIT" @@ -17856,6 +17899,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.9.4", "dev": true, @@ -19616,6 +19668,20 @@ "dev": true, "license": "ISC" }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "dev": true, @@ -21076,6 +21142,15 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "dev": true, @@ -21860,6 +21935,74 @@ } } }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/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/webpack-bundle-analyzer/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/webpack-cli": { "version": "5.1.4", "dev": true, diff --git a/frontend/package.json b/frontend/package.json index a0473c76e..ab62dc39a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,17 +4,18 @@ "description": "ddangkong-frontend repository", "main": "index.js", "scripts": { - "start": "webpack serve --config webpack.config.dev.js", - "start:open": "webpack serve --config webpack.config.dev.js --host 0.0.0.0", - "build-dev": "webpack --config webpack.config.dev.js", - "build-prod": "webpack --config webpack.config.prod.js", + "dev": "webpack serve --config webpack.config.dev.js", + "dev:open": "webpack serve --config webpack.config.dev.js --host 0.0.0.0", + "prod": "webpack serve --config webpack.config.prod.js", + "build:dev": "webpack --config webpack.config.dev.js", + "build:prod": "webpack --config webpack.config.prod.js", "test": "jest", "test:watch": "jest --watch", "lint": "npx eslint --ext .ts,.tsx .", "lint:styled": "stylelint ./src/**/*.styled.ts --fix", "prepare": "cd .. && husky frontend/.husky", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "build:storybook": "storybook build" }, "keywords": [], "author": "", @@ -92,6 +93,7 @@ "typescript": "^5.5.3", "undici": "^6.19.2", "webpack": "^5.92.1", + "webpack-bundle-analyzer": "^4.10.2", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4", "webpack-merge": "^6.0.1" diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1c647d597c10781b63c1393701428e8d5b0e05f7 GIT binary patch literal 124795 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 z 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%0DG{7Da21iDIoo}*}Vb$yo%_^B&Z>^rtB7bi zr2a{C>WNP^MugBbi+6`iG%TI(OJCfdnjCWiEI|Vb)z!J?j=SNMb}av`X;l55*qkk= zM~|9$`pNY=RuSbXIIjLtAEeCMXBET5be$%VtYKN594Embq%3d3m0ED(5c5vngA<`&4C+H^D&$!ZxH?#rxJPN(ljTJMkkZi3rWi&jYV8{NeHG6 zb``6O_uHW(VgXsWUv9S8_i?%QbidbP3e^YJNHp>{?9{g0Qn(M-X!IOnCT&zwQrdlU z$Ghn__tuNcDj9LzS{5OyondZemJl>mct_p1nIZ3H&#=dj8zd4KBzFH^5QN_vJSMrR z@?qirfqaU%IaZezuKFCXUw-b_nU&;~UNNt{x5uAe|2^!w#PK`ZGcM^NQ1HB&ur+6N z_<+q*{3hs9t+g^ER*eqWqcW^gt>&Q^|AwV2a??~?9_%PDKF6`yn|SAX@-?BclK9Wj z%kM-Bt3KqsU{j$vDZn#E=cN~=KzCB(2%#2vPIU1}-PN7u$=!fTN zYU6)Mq!0{ox1!c_ewjILkh%7bBgbpBrKad=Y21D&Y5w6mX}*Hb+(V)3^JYjmFV!`a z=RbZ_hNzyxuunLyGK&irK;eJnvD@&&GFzmM@mA&olhtKyFGPI4B*P=7=dKfPV`Bz< z%(ahV6ttA4baZ0kUNAjR!X4FpB&PeEPP*gBWp(v<(oxg-Wd{yL66#*KR>*;0_6q@_ zJtuwd)S0H^h)dSzE!J0ebXmL7!sxa2Lb3AP+IM@al7g2?23cL&FpW0g#r3S0MdH+w zQ7I30F4>YEOm(E!`BV$0(CV~!7)NzY-XA+d{`xU${CCopB_HpMjahzsw(j<0^52Bi zCPbYsk22l2RLWfE=~_|Z_VoJ8h1ZQpmU(H%&6{41*|h&%QT5a>uh&l*;xj+840rT| z22pVP6#VU=b#xw&lS9;{@Kd&YAHs3c!{-sBT#QhT9OaE$n%D%o|oXMBlo=A3*|IAVs>%KAMa$E_lv|8I|ce>C{n zIs1^?Y2~9BdAroNSvXaBdKOo#%6!k^ZlkY1#%S}!`|dB_96Ic?Z0q6b%y`u}U7