Commit 17a83ce
develop -> main (모니터링 관련) (#186)
* chore: 로컬 환경에서의 모니터링 서비스 설정 추가 (#184)
* 모니터링 ec2 구축 및 프로메테우스 그라파나 구동 (#185)
* develop -> main ( 강의, 수강신청 ) (#180)
* [fix] 대기열 인원 수 확인 로직 변경 테스트 반영 (#179)
* feat: Redis 기반 동시성 제어 및 큐 기반 수강신청·취소 비동기 처리 구현 (#176)
* test: 현재 동시성 문제가 발생하는지 확인하기 위한 테스트 추가
- 동시에 100명이 신청했을 경우 최대 수강 인원이 30명인 강좌에 몇명이 등록되는지 확인
- 수강신청 내역이 30개가 아닌 100개가 생성되고있음
- 강의 정보에 현재 수강인원이 제대로 갱신되고 있지 않음
- 30명이 채워져야하지만 11, 12명 등 이상한 값으로 채워지고 있음
# Conflicts:
# backend/src/main/java/com/WEB4_5_GPT_BE/unihub/domain/enrollment/repository/EnrollmentRepository.java
* feat: 수강신청 로그를 추가하여 수강인원이 제대로 증가하지 않는 문제점 파악
- 동시에 수강신청을 보내고 인원을 변경하기 때문에 해당 강좌의 현재수강신청 인원 수가 제대로 증가하지 않고 오히려 적게 나타나는 문제였음
```
수강 신청이 완료되었습니다. 학생: 25020004, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020010, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020002, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020005, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020006, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020003, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020001, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020009, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020008, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020007, 강좌: 0
현재 수강인원 변경이 완료되었습니다. 학생: 25020001, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020006, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020004, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020005, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020002, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020007, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020010, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020003, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020008, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020009, 강좌: 1
```
* refactor : 변경된 회원 도메인에 맞게 테스트 코드 수정
* feat: Redisson을 통한 수강신청 동시성 제어 구현
- 코드별 주석으로 상세 설명
* feat: 동시성 테스트는 프로젝트 테스트에서 진행하지 않도록 @Profile("!test") 수정
- 현재 동시성 로직은 별도의 트랜잭션을 생성하기 때문에 test 코드 상에 transational 을 붙여줄 수 없음
- test 코드에서 @transational 을 붙이지 않으면 테스트 과정에서 수정된 데이터가 롤백되지 못해 다른 테스트에 영향을 줄 수 있음
- 또한 동시성 관련 테스트는 생성해야 하는 회원 데이터 등이 많아 테스트에 부하가 많이 걸림
- 프로젝트 테스트에서 동시성을 검증하지 않고 별도의 툴 (jmeter) 을 통해 외부에서 테스트 할 예정
* feat: 동시성 테스트용 데이터 (4학년 학생 100명 및 신청용 강좌) 생성
# Conflicts:
# backend/src/main/java/com/WEB4_5_GPT_BE/unihub/global/init/InitDevData.java
* feat: 동시성 문제 해결을 위해 redis의 원자성이 보장되는 AtomicLong 적용 및 Queue를 통한 수강신청 비동기 처리 구현
- Redis RAtomicLong 을 이용해 capacity 및 enrolled 카운터를 원자적으로 관리하고, 수강신청 시 increment하여 동시성 보장
- 수강신청 요청 시 DB 트랜잭션 대신에 Redis 카운터만 업데이트하여 빠른 응답을 제공
- EnrollmentCommand DTO 를 통해 수강신청 정보를 큐(RBlockingQueue<EnrollmentCommand>)에 저장
- EnrollmentCommandConsumer 가 큐에서 명령을 순차적으로 꺼내 EnrollmentCommandHandler 에게 위임
* feat: Redisson을 활용하여 강의 생성 시 강의 수강 인원 및 수용 인원 정보를 Redis에 저장하는 기능 추가
* docs: k6를 활용한 수강신청 동시성 테스트 (100명 동시 신청 시 30명 정상 등록 완료) 파일 추가
- local 테스트
* test: 변경된 로직에 맞게 테스트 코드 수정
* refactor: 잘못 적용되어있는 ConcurrencyGuard 제거
* feat: EnrollmentDuplicateChecker를 구현하여 중복 queue 삽입 (동일 학생이 연속으로 신청하는 경우 (따닥)) 방지
- k6 테스트 스크립트 추가 (한 학생이 동일강좌에 5번 동시에 신청)
* docs : 주석 추가
* refactor: EnrollmentAlreadyQueuedException 예외처리를 공용으로 사용할 수 있도록 수정
* feat: 강의 취소의 경우에도 순차적으로 queue를 통해 처리될 수 있도록 수강 신청 로직과 동일하게 구현
* docs: 30명 동시 수강신청 및 동시 수강 취소 k6 테스트 스크립트 추가
* refactor : 수강신청 api 주석 수정 및 수강신청 queue 처리 후 flag를 제거하도록 추가
* refactor : 테스트용 대기열 설정 제거
* test : 테스트 코드 수정
* refactor: 패키지 이동
* feat: 강의 등록, 수정 시 redis에 저장된 Enrolled와 Capacity를 업데이트하도록 추가
* feat: 강의 삭제 시 redis에 저장된 Enrolled와 Capacity를 삭제하도록 구현
* feat: redis counter 관련 로직을 별도의 service로 분리
* feat: 프로젝트 재부팅 시 redis에 저장된 counter 값을 현재 강의 정보와 동기화 하도록 구현
* test: 변경된 로직에 맞게 테스트 코드 수정
* feat: initData 과정에서 강의 생성 시 counter를 redis에 저장하도록 추가
* refactor: 필요없는 주석 제거
* merge: [BUGFIX] 강의 생성시 대학 정보 획득 방식 변경 (#178)
* fix: 강의 생성 및 수정 시 인증된 유저 정보도 가져오도록 변경
* fix: 강의 생성 및 수정 시 요청 본문 대신 인증된 유저 정보로부터 대학 정보를 가져오도록 변경
* test: 변경 사항을 테스트에 반영
* fix: 강의 요청 DTO에서 대학 이름을 필수에서 선택으로 변경
* [fix] 대기열 인원 수 확인 로직 변경 테스트 반영 (#179)
* feat: Redis 기반 동시성 제어 및 큐 기반 수강신청·취소 비동기 처리 구현 (#176)
* test: 현재 동시성 문제가 발생하는지 확인하기 위한 테스트 추가
- 동시에 100명이 신청했을 경우 최대 수강 인원이 30명인 강좌에 몇명이 등록되는지 확인
- 수강신청 내역이 30개가 아닌 100개가 생성되고있음
- 강의 정보에 현재 수강인원이 제대로 갱신되고 있지 않음
- 30명이 채워져야하지만 11, 12명 등 이상한 값으로 채워지고 있음
# Conflicts:
# backend/src/main/java/com/WEB4_5_GPT_BE/unihub/domain/enrollment/repository/EnrollmentRepository.java
* feat: 수강신청 로그를 추가하여 수강인원이 제대로 증가하지 않는 문제점 파악
- 동시에 수강신청을 보내고 인원을 변경하기 때문에 해당 강좌의 현재수강신청 인원 수가 제대로 증가하지 않고 오히려 적게 나타나는 문제였음
```
수강 신청이 완료되었습니다. 학생: 25020004, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020010, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020002, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020005, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020006, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020003, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020001, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020009, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020008, 강좌: 0
수강 신청이 완료되었습니다. 학생: 25020007, 강좌: 0
현재 수강인원 변경이 완료되었습니다. 학생: 25020001, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020006, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020004, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020005, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020002, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020007, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020010, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020003, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020008, 강좌: 1
현재 수강인원 변경이 완료되었습니다. 학생: 25020009, 강좌: 1
```
* refactor : 변경된 회원 도메인에 맞게 테스트 코드 수정
* feat: Redisson을 통한 수강신청 동시성 제어 구현
- 코드별 주석으로 상세 설명
* feat: 동시성 테스트는 프로젝트 테스트에서 진행하지 않도록 @Profile("!test") 수정
- 현재 동시성 로직은 별도의 트랜잭션을 생성하기 때문에 test 코드 상에 transational 을 붙여줄 수 없음
- test 코드에서 @transational 을 붙이지 않으면 테스트 과정에서 수정된 데이터가 롤백되지 못해 다른 테스트에 영향을 줄 수 있음
- 또한 동시성 관련 테스트는 생성해야 하는 회원 데이터 등이 많아 테스트에 부하가 많이 걸림
- 프로젝트 테스트에서 동시성을 검증하지 않고 별도의 툴 (jmeter) 을 통해 외부에서 테스트 할 예정
* feat: 동시성 테스트용 데이터 (4학년 학생 100명 및 신청용 강좌) 생성
# Conflicts:
# backend/src/main/java/com/WEB4_5_GPT_BE/unihub/global/init/InitDevData.java
* feat: 동시성 문제 해결을 위해 redis의 원자성이 보장되는 AtomicLong 적용 및 Queue를 통한 수강신청 비동기 처리 구현
- Redis RAtomicLong 을 이용해 capacity 및 enrolled 카운터를 원자적으로 관리하고, 수강신청 시 increment하여 동시성 보장
- 수강신청 요청 시 DB 트랜잭션 대신에 Redis 카운터만 업데이트하여 빠른 응답을 제공
- EnrollmentCommand DTO 를 통해 수강신청 정보를 큐(RBlockingQueue<EnrollmentCommand>)에 저장
- EnrollmentCommandConsumer 가 큐에서 명령을 순차적으로 꺼내 EnrollmentCommandHandler 에게 위임
* feat: Redisson을 활용하여 강의 생성 시 강의 수강 인원 및 수용 인원 정보를 Redis에 저장하는 기능 추가
* docs: k6를 활용한 수강신청 동시성 테스트 (100명 동시 신청 시 30명 정상 등록 완료) 파일 추가
- local 테스트
* test: 변경된 로직에 맞게 테스트 코드 수정
* refactor: 잘못 적용되어있는 ConcurrencyGuard 제거
* feat: EnrollmentDuplicateChecker를 구현하여 중복 queue 삽입 (동일 학생이 연속으로 신청하는 경우 (따닥)) 방지
- k6 테스트 스크립트 추가 (한 학생이 동일강좌에 5번 동시에 신청)
* docs : 주석 추가
* refactor: EnrollmentAlreadyQueuedException 예외처리를 공용으로 사용할 수 있도록 수정
* feat: 강의 취소의 경우에도 순차적으로 queue를 통해 처리될 수 있도록 수강 신청 로직과 동일하게 구현
* docs: 30명 동시 수강신청 및 동시 수강 취소 k6 테스트 스크립트 추가
* refactor : 수강신청 api 주석 수정 및 수강신청 queue 처리 후 flag를 제거하도록 추가
* refactor : 테스트용 대기열 설정 제거
* test : 테스트 코드 수정
* refactor: 패키지 이동
* feat: 강의 등록, 수정 시 redis에 저장된 Enrolled와 Capacity를 업데이트하도록 추가
* feat: 강의 삭제 시 redis에 저장된 Enrolled와 Capacity를 삭제하도록 구현
* feat: redis counter 관련 로직을 별도의 service로 분리
* feat: 프로젝트 재부팅 시 redis에 저장된 counter 값을 현재 강의 정보와 동기화 하도록 구현
* test: 변경된 로직에 맞게 테스트 코드 수정
* feat: initData 과정에서 강의 생성 시 counter를 redis에 저장하도록 추가
* refactor: 필요없는 주석 제거
* fix: 강의 생성 및 수정 시 인증된 유저 정보도 가져오도록 변경
* fix: 강의 생성 및 수정 시 요청 본문 대신 인증된 유저 정보로부터 대학 정보를 가져오도록 변경
* merge: Rebase 박주원/fix-177 onto develop
* fix: 강의 요청 DTO에서 대학 이름을 필수에서 선택으로 변경
* test: 변경 사항을 테스트에 반영
---------
Co-authored-by: TheGreatKang <77500386+TheGreatKang@users.noreply.github.com>
Co-authored-by: OJH <ok6737@naver.com>
---------
Co-authored-by: TheGreatKang <77500386+TheGreatKang@users.noreply.github.com>
Co-authored-by: OJH <ok6737@naver.com>
Co-authored-by: SalinatedCoffee <74612242+SalinatedCoffee@users.noreply.github.com>
* docs : 기존 ec2 t3.micro 에서 t3.small로 업그레이드
* docs : 모니터링용 ec2 생성
* feat : prod에 테스트용 100명 데이터 생성
* refactor : 동시성 테스트를 위해 세션 검증로직 임시 주석처리
* docs : 로컬 및 prod 환경 테스트 코드 추가
* refactor: DuplicateChecker를 제거하고 학생 id, 강의 id로 된 분산락을 획득하여 연속 접근을 방지하도록 로직을 수정
* test: DuplicateChecker를 제거
* feat : 스프링 종료 시 redisson도 정상종료 되도록 추가
* docs : 수강신청 날짜 변경
* refactor: test에서도 동작하도록 redisson 설정 수정
* feat : 수강신청 검증 로직이 별도의 트랜잭션을 탈수있도록 EnrollmentValidator 클래스로 분리
* teat: 변경된 로직에 맞게 테스트 수정
* refactor: 대기열 최대 인원 100명으로 증가
- 수강신청 시 세션검증 로직 주석 해제
* docs: prod 테스트 코드 추가
- 100명 동시 신청
* docs: 프로메테우스 설정 추가
* docs: 프로메테우스 설정 추가 및 k6 코드 추가
* refactor : 발표자료에 시각자료로 사용하기 위해 코드 수정 및 리팩토링
* docs : prometheus main ec2 private ip를 참조하도록 수정
* docs : 테스트용 브랜치 제거
---------
Co-authored-by: sojunsik <113578269+sojunsik@users.noreply.github.com>
Co-authored-by: TheGreatKang <77500386+TheGreatKang@users.noreply.github.com>
Co-authored-by: SalinatedCoffee <74612242+SalinatedCoffee@users.noreply.github.com>
---------
Co-authored-by: SalinatedCoffee <74612242+SalinatedCoffee@users.noreply.github.com>
Co-authored-by: sojunsik <113578269+sojunsik@users.noreply.github.com>
Co-authored-by: TheGreatKang <77500386+TheGreatKang@users.noreply.github.com>1 parent fa11139 commit 17a83ce
38 files changed
Lines changed: 4884 additions & 680 deletions
File tree
- backend
- k6
- prod
- src
- main
- java/com/WEB4_5_GPT_BE/unihub
- domain/enrollment/service
- async
- cancel
- enroll
- global
- concurrent
- init
- resources
- test/java/com/WEB4_5_GPT_BE/unihub/domain/enrollment
- controller
- service
- infra/prod
- monitoring/test
- grafana/provisioning
- dashboards
- general
- datasources
- prometheus
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
93 | 97 | | |
94 | 98 | | |
95 | 99 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
0 commit comments