├── .editorconfig ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── ci-cd-production.yml │ ├── ci-cd-sandbox.yml │ └── ci.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── adapter-in └── web │ ├── build.gradle.kts │ └── src │ ├── main │ ├── kotlin │ │ └── com │ │ │ └── yapp │ │ │ └── bol │ │ │ ├── EmptyResponse.kt │ │ │ ├── ExceptionHandler.kt │ │ │ ├── TestController.kt │ │ │ ├── WebApplication.kt │ │ │ ├── auth │ │ │ ├── AuthController.kt │ │ │ ├── SecurityConfiguration.kt │ │ │ ├── dto │ │ │ │ ├── LoginRequest.kt │ │ │ │ └── LoginResponse.kt │ │ │ └── security │ │ │ │ ├── SecurityExceptionHandler.kt │ │ │ │ ├── TokenAuthentication.kt │ │ │ │ └── TokenAuthenticationFilter.kt │ │ │ ├── config │ │ │ └── BolProperties.kt │ │ │ ├── file │ │ │ ├── FileController.kt │ │ │ └── dto │ │ │ │ └── FileResponse.kt │ │ │ ├── game │ │ │ ├── GameController.kt │ │ │ └── dto │ │ │ │ ├── GameListResponse.kt │ │ │ │ └── GameResponse.kt │ │ │ ├── group │ │ │ ├── GroupController.kt │ │ │ ├── dto │ │ │ │ ├── CheckAccessCodeRequest.kt │ │ │ │ ├── CheckAccessCodeResponse.kt │ │ │ │ ├── CreateGroupRequest.kt │ │ │ │ ├── CreateGroupResponse.kt │ │ │ │ ├── GroupDetailResponse.kt │ │ │ │ ├── GroupListResponse.kt │ │ │ │ ├── GroupReponse.kt │ │ │ │ └── LeaderBoardResponse.kt │ │ │ └── member │ │ │ │ ├── MemberController.kt │ │ │ │ └── dto │ │ │ │ ├── AddGuestRequest.kt │ │ │ │ ├── JoinGroupRequest.kt │ │ │ │ ├── MemberResponse.kt │ │ │ │ └── ValidateMemberNameResponse.kt │ │ │ ├── match │ │ │ ├── MatchController.kt │ │ │ └── dto │ │ │ │ └── CreateMatchRequest.kt │ │ │ ├── setting │ │ │ └── SettingController.kt │ │ │ ├── terms │ │ │ ├── TermsController.kt │ │ │ └── dto │ │ │ │ ├── AgreeTermsRequest.kt │ │ │ │ └── TermsResponse.kt │ │ │ ├── user │ │ │ ├── UserController.kt │ │ │ └── dto │ │ │ │ ├── CheckOnboardResponse.kt │ │ │ │ ├── JoinedGroupResponse.kt │ │ │ │ ├── MyInfoResponse.kt │ │ │ │ └── PutUserInfoRequest.kt │ │ │ └── utils │ │ │ ├── ApiMinVersion.kt │ │ │ ├── ForceUpdateAspect.kt │ │ │ └── Logger.kt │ └── resources │ │ ├── application.yml │ │ └── static │ │ ├── privacy.html │ │ └── terms.html │ └── test │ └── kotlin │ └── com │ └── yapp │ └── bol │ ├── TestControllerTest.kt │ ├── auth │ └── AuthControllerTest.kt │ ├── base │ ├── ControllerTest.kt │ ├── DocumentField.kt │ ├── DocumentFieldType.kt │ └── OpenApiTag.kt │ ├── file │ └── FileControllerTest.kt │ ├── game │ └── GameControllerTest.kt │ ├── group │ ├── GroupControllerTest.kt │ └── member │ │ └── MemberControllerTest.kt │ ├── match │ └── MatchControllerTest.kt │ ├── setting │ └── SettingControllerTest.kt │ ├── terms │ └── TermsControllerTest.kt │ └── user │ └── UserControllerTest.kt ├── adapter-out ├── rdb │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── yapp │ │ │ │ └── bol │ │ │ │ ├── AuditingEntity.kt │ │ │ │ ├── auth │ │ │ │ ├── AuthCommandRepositoryImpl.kt │ │ │ │ ├── AuthQueryRepositoryImpl.kt │ │ │ │ ├── AuthSocialEntity.kt │ │ │ │ ├── AuthSocialRepository.kt │ │ │ │ ├── SocialInfo.kt │ │ │ │ ├── SocialType.kt │ │ │ │ └── token │ │ │ │ │ ├── AccessTokenEntity.kt │ │ │ │ │ ├── AccessTokenRepository.kt │ │ │ │ │ ├── RefreshTokenEntity.kt │ │ │ │ │ ├── RefreshTokenRepository.kt │ │ │ │ │ ├── TokenCommandRepositoryImpl.kt │ │ │ │ │ └── TokenQueryRepositoryImpl.kt │ │ │ │ ├── aws │ │ │ │ ├── AwsConfiguration.kt │ │ │ │ └── AwsProperties.kt │ │ │ │ ├── config │ │ │ │ └── QueryDslConfiguration.kt │ │ │ │ ├── file │ │ │ │ ├── FileClient.kt │ │ │ │ ├── FileEntity.kt │ │ │ │ ├── FileNameConverter.kt │ │ │ │ └── FileRepository.kt │ │ │ │ ├── game │ │ │ │ ├── GameClient.kt │ │ │ │ ├── GameEntity.kt │ │ │ │ ├── GameRepository.kt │ │ │ │ └── member │ │ │ │ │ ├── GameMemberCommandRepositoryImpl.kt │ │ │ │ │ ├── GameMemberEntity.kt │ │ │ │ │ ├── GameMemberQueryRepositoryImpl.kt │ │ │ │ │ └── GameMemberRepository.kt │ │ │ │ ├── group │ │ │ │ ├── GroupCommandRepositoryImpl.kt │ │ │ │ ├── GroupEntity.kt │ │ │ │ ├── GroupQueryRepositoryImpl.kt │ │ │ │ ├── GroupRepository.kt │ │ │ │ └── member │ │ │ │ │ ├── CustomMemberRepository.kt │ │ │ │ │ ├── CustomMemberRepositoryImpl.kt │ │ │ │ │ ├── MemberCommandRepositoryImpl.kt │ │ │ │ │ ├── MemberEntity.kt │ │ │ │ │ ├── MemberQueryRepositoryImpl.kt │ │ │ │ │ └── MemberRepository.kt │ │ │ │ ├── match │ │ │ │ ├── MatchCommandRepositoryImpl.kt │ │ │ │ ├── MatchEntity.kt │ │ │ │ ├── MatchRepository.kt │ │ │ │ └── member │ │ │ │ │ └── MatchMemberEntity.kt │ │ │ │ ├── season │ │ │ │ ├── SeasonCommandRepositoryImpl.kt │ │ │ │ ├── SeasonEntity.kt │ │ │ │ ├── SeasonQueryRepositoryImpl.kt │ │ │ │ └── SeasonRepository.kt │ │ │ │ ├── terms │ │ │ │ ├── TermsAgreeEntity.kt │ │ │ │ ├── TermsAgreeRepository.kt │ │ │ │ ├── TermsCommandRepositoryImpl.kt │ │ │ │ └── TermsQueryRepositoryImpl.kt │ │ │ │ └── user │ │ │ │ ├── UserClient.kt │ │ │ │ ├── UserEntity.kt │ │ │ │ └── UserRepository.kt │ │ └── resources │ │ │ ├── application-rdb.yml │ │ │ └── sql │ │ │ ├── ddl │ │ │ ├── 2023_05_14_CREATE_AUTH.sql │ │ │ ├── 2023_05_14_CREATE_USER.sql │ │ │ ├── 2023_06_03_CREATE_FILE.sql │ │ │ ├── 2023_06_06_CREATE_GROUP.sql │ │ │ ├── 2023_06_06_CREATE_MEMBER.sql │ │ │ ├── 2023_06_13_CREATE_GAME.sql │ │ │ ├── 2023_07_01_CREATE_GAME_MEMBER.sql │ │ │ ├── 2023_07_01_CREATE_MATCH.sql │ │ │ ├── 2023_07_01_CREATE_MATCH_MEMBER.sql │ │ │ ├── 2023_07_01_CREATE_SEASON.sql │ │ │ └── 2023_07_10_CREATE_AGREED_TERMS.sql │ │ │ └── dml │ │ │ └── 2023_06_13_INSERT_GAMES.sql │ │ └── test │ │ └── kotlin │ │ └── com │ │ └── yapp │ │ └── bol │ │ ├── auth │ │ ├── AuthCommandRepositoryImplTest.kt │ │ ├── AuthQueryRepositoryImplTest.kt │ │ └── token │ │ │ └── TokenCommandRepositoryImplTest.kt │ │ └── group │ │ └── member │ │ └── MemberQueryRepositoryImplTest.kt └── social │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ └── main │ ├── kotlin │ └── com │ │ └── yapp │ │ └── bol │ │ └── social │ │ ├── SocialLoginClientFacade.kt │ │ ├── google │ │ ├── GoogleApiConfiguration.kt │ │ ├── GoogleApiProperties.kt │ │ ├── GoogleIdTokenLoginClient.kt │ │ ├── GoogleIdTokenService.kt │ │ └── GoogleSocialUser.kt │ │ ├── kakao │ │ ├── KakaoAccessTokenLoginClient.kt │ │ ├── KakaoConfiguration.kt │ │ ├── KakaoOpenApiClient.kt │ │ └── dto │ │ │ └── KakaoUserResponse.kt │ │ └── naver │ │ ├── NaverAccessTokenLoginClient.kt │ │ ├── NaverConfiguration.kt │ │ ├── NaverOpenApiClient.kt │ │ └── dto │ │ └── NaverAuthResponse.kt │ └── resources │ └── application-social.yml ├── build.gradle.kts ├── core ├── build.gradle.kts └── src │ ├── main │ ├── kotlin │ │ └── com │ │ │ └── yapp │ │ │ └── bol │ │ │ ├── Extension.kt │ │ │ ├── auth │ │ │ ├── AuthConfiguration.kt │ │ │ ├── AuthServiceImpl.kt │ │ │ ├── TokenService.kt │ │ │ └── token │ │ │ │ ├── JwtUtils.kt │ │ │ │ ├── OpaqueTokenUtils.kt │ │ │ │ ├── TokenPolicy.kt │ │ │ │ └── TokenUtils.kt │ │ │ ├── file │ │ │ └── FileServiceImpl.kt │ │ │ ├── game │ │ │ ├── GameServiceImpl.kt │ │ │ └── member │ │ │ │ └── GameMemberServiceImpl.kt │ │ │ ├── group │ │ │ ├── GroupServiceImpl.kt │ │ │ └── member │ │ │ │ └── MemberServiceImpl.kt │ │ │ ├── match │ │ │ └── MatchServiceImpl.kt │ │ │ ├── onboarding │ │ │ └── OnboardingServiceImpl.kt │ │ │ ├── season │ │ │ └── SeasonServiceImpl.kt │ │ │ ├── terms │ │ │ └── TermsServiceImpl.kt │ │ │ └── user │ │ │ └── UserServiceImpl.kt │ └── resources │ │ └── application-core.yml │ └── test │ └── kotlin │ └── com │ └── yapp │ └── bol │ ├── ExtensionTest.kt │ ├── auth │ ├── AuthServiceImplTest.kt │ ├── TokenServiceTest.kt │ └── token │ │ ├── JwtUtilsTest.kt │ │ ├── OpaqueTokenUtilsTest.kt │ │ └── TokenPolicyTest.kt │ ├── group │ ├── GroupServiceImplTest.kt │ └── member │ │ └── MemberServiceImplTest.kt │ └── validate │ └── NicknameValidatorTest.kt ├── docs └── image │ └── hexagonal-architecture.png ├── domain ├── build.gradle.kts └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── yapp │ │ └── bol │ │ ├── Exception.kt │ │ ├── auth │ │ ├── AuthToken.kt │ │ ├── AuthUser.kt │ │ ├── LoginType.kt │ │ ├── SocialUser.kt │ │ └── Token.kt │ │ ├── date │ │ └── DateTimeUtils.kt │ │ ├── file │ │ ├── FileAccessLevel.kt │ │ ├── FileInfo.kt │ │ ├── FilePurpose.kt │ │ └── dto │ │ │ └── RawFileData.kt │ │ ├── game │ │ ├── Game.kt │ │ ├── GameRankType.kt │ │ └── member │ │ │ └── GameMember.kt │ │ ├── group │ │ ├── Group.kt │ │ ├── LeaderBoardMember.kt │ │ ├── dto │ │ │ ├── GroupMemberList.kt │ │ │ └── GroupWithMemberCount.kt │ │ └── member │ │ │ ├── GuestMember.kt │ │ │ ├── HostMember.kt │ │ │ ├── Member.kt │ │ │ ├── MemberList.kt │ │ │ ├── MemberRole.kt │ │ │ ├── OwnerMember.kt │ │ │ ├── ParticipantMember.kt │ │ │ └── dto │ │ │ └── PaginationCursorMemberRequest.kt │ │ ├── match │ │ ├── Match.kt │ │ └── member │ │ │ └── MatchMember.kt │ │ ├── onboarding │ │ └── OnboardingType.kt │ │ ├── pagination │ │ ├── cursor │ │ │ ├── PaginationCursorRequest.kt │ │ │ ├── PaginationCursorResponse.kt │ │ │ ├── SimplePaginationCursorRequest.kt │ │ │ └── SimplePaginationCursorResponse.kt │ │ └── offset │ │ │ └── PaginationOffsetResponse.kt │ │ ├── season │ │ └── Season.kt │ │ ├── terms │ │ ├── TermsAgreeInfo.kt │ │ └── TermsCode.kt │ │ ├── user │ │ └── User.kt │ │ └── validate │ │ └── NicknameValidator.kt │ └── test │ └── kotlin │ └── com │ └── yapp │ └── bol │ ├── date │ └── DateTimeUtilsTest.kt │ ├── game │ └── member │ │ └── GameMemberTest.kt │ ├── group │ └── GroupTest.kt │ └── member │ ├── MemberListTest.kt │ └── MemberTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── port-in ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── com │ └── yapp │ └── bol │ ├── auth │ └── AuthService.kt │ ├── file │ └── FileService.kt │ ├── game │ ├── GameService.kt │ └── member │ │ └── GameMemberService.kt │ ├── group │ ├── GroupService.kt │ ├── dto │ │ ├── AddGuestDto.kt │ │ ├── CreateGroupDto.kt │ │ └── JoinGroupDto.kt │ └── member │ │ ├── MemberService.kt │ │ └── dto │ │ └── CreateMemberDto.kt │ ├── match │ ├── MatchService.kt │ └── dto │ │ └── CreateMatchDto.kt │ ├── onboarding │ └── OnboardingService.kt │ ├── season │ └── SeasonService.kt │ ├── terms │ └── TermsService.kt │ └── user │ └── UserService.kt ├── port-out ├── build.gradle.kts └── src │ └── main │ └── kotlin │ └── com │ └── yapp │ └── bol │ ├── auth │ ├── AuthCommandRepository.kt │ ├── AuthQueryRepository.kt │ ├── TokenQueryRepository.kt │ ├── social │ │ └── SocialLoginClient.kt │ └── token │ │ └── TokenCommandRepository.kt │ ├── file │ ├── FileCommandRepository.kt │ └── FileQueryRepository.kt │ ├── game │ ├── GameQueryRepository.kt │ └── member │ │ ├── GameMemberCommandRepository.kt │ │ └── GameMemberQueryRepository.kt │ ├── group │ ├── GroupCommandRepository.kt │ ├── GroupQueryRepository.kt │ └── member │ │ ├── MemberCommandRepository.kt │ │ └── MemberQueryRepository.kt │ ├── match │ └── MatchCommandRepository.kt │ ├── season │ ├── SeasonCommandRepository.kt │ └── SeasonQueryRepository.kt │ ├── terms │ ├── TermsCommandRepository.kt │ └── TermsQueryRepository.kt │ └── user │ ├── UserCommandRepository.kt │ └── UserQueryRepository.kt ├── settings.gradle.kts └── support ├── logging ├── build.gradle.kts └── src │ └── main │ └── resources │ ├── application-logging.yml │ ├── logback-appender.xml │ ├── logback-local.xml │ ├── logback-production.xml │ └── logback-sandbox.xml └── yaml ├── build.gradle.kts └── src └── main ├── kotlin └── com │ └── yapp │ └── bol │ └── support │ └── yaml │ └── YamlEnvironmentPostProcessor.kt └── resources └── META-INF └── spring.factories /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @comforest @seung-00 @YAPP-Github/22nd-android-team-2 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Reference 2 | - [Jira Ticket](https://yapp22-aos2.atlassian.net/browse/BOL-) 3 | 4 | ### 참고 사항 5 | 6 | P1: 꼭 반영해주세요 (Request changes) 7 | P2: 적극적으로 고려해주세요 (Request changes) 8 | P3: 웬만하면 반영해 주세요 (Comment) 9 | P4: 반영해도 좋고 넘어가도 좋습니다 (Approve) 10 | P5: 그냥 사소한 의견입니다 (Approve) 11 | -------------------------------------------------------------------------------- /.github/workflows/ci-cd-production.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD-Production 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | environment: Production # TODO: DELETE - 해당 줄은 GITHUB 버그 대처방안으로 임시로 추가된 줄 https://github.com/actions/runner/issues/2414 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Docker build 17 | run: | 18 | docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} 19 | docker build \ 20 | --build-arg PHASE=${{ vars.PHASE }} \ 21 | --build-arg AWS_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }} \ 22 | --build-arg SERVER_HOST=${{ secrets.BOL_API_FULL_HOST }} \ 23 | -t app . 24 | docker tag app ${{ secrets.DOCKER_USERNAME }}/board-prod:${{ github.run_number }} 25 | docker push ${{ secrets.DOCKER_USERNAME }}/board-prod:${{ github.run_number }} 26 | 27 | deploy: 28 | needs: build 29 | runs-on: ubuntu-latest 30 | environment: Production # TODO: DELETE - 해당 줄은 GITHUB 버그 대처방안으로 임시로 추가된 줄 https://github.com/actions/runner/issues/2414 31 | steps: 32 | - name: Deploy 33 | uses: appleboy/ssh-action@master 34 | with: 35 | host: ${{ secrets.BOL_API_SERVER_HOST }} 36 | username: ${{ secrets.BOL_API_SERVER_USERNAME }} 37 | password: ${{ secrets.BOL_API_SERVER_PASSWORD }} 38 | # PRODUCTION 이면 host port 8081 사용 39 | script: | 40 | docker pull ${{ secrets.DOCKER_USERNAME }}/board-prod:${{ github.run_number }} 41 | docker stop $(docker ps -a -q --filter ancestor=${{ secrets.DOCKER_USERNAME }}/board-prod) 42 | docker run -d -p 80:8080 \ 43 | --log-driver=awslogs \ 44 | --log-opt awslogs-region=ap-northeast-2 \ 45 | --log-opt awslogs-group=prod-api-server \ 46 | --log-opt awslogs-stream=prod-api-console \ 47 | ${{ secrets.DOCKER_USERNAME }}/board-prod:${{ github.run_number }} 48 | docker rm $(docker ps -a -q --filter ancestor=${{ secrets.DOCKER_USERNAME }}/board-prod --filter 'status=exited') 49 | docker image prune -a -f 50 | -------------------------------------------------------------------------------- /.github/workflows/ci-cd-sandbox.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD-Sandbox 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'sandbox' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | environment: Sandbox # TODO: DELETE - 해당 줄은 GITHUB 버그 대처방안으로 임시로 추가된 줄 https://github.com/actions/runner/issues/2414 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Docker build 17 | run: | 18 | docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} 19 | docker build \ 20 | --build-arg PHASE=${{ vars.PHASE }} \ 21 | --build-arg AWS_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }} \ 22 | --build-arg SERVER_HOST=${{ secrets.BOL_API_FULL_HOST }} \ 23 | -t app . 24 | docker tag app ${{ secrets.DOCKER_USERNAME }}/board:${{ github.run_number }} 25 | docker push ${{ secrets.DOCKER_USERNAME }}/board:${{ github.run_number }} 26 | 27 | deploy: 28 | needs: build 29 | runs-on: ubuntu-latest 30 | environment: Sandbox # TODO: DELETE - 해당 줄은 GITHUB 버그 대처방안으로 임시로 추가된 줄 https://github.com/actions/runner/issues/2414 31 | steps: 32 | - name: Deploy 33 | uses: appleboy/ssh-action@master 34 | with: 35 | host: ${{ secrets.BOL_API_SERVER_HOST }} 36 | username: ${{ secrets.BOL_API_SERVER_USERNAME }} 37 | password: ${{ secrets.BOL_API_SERVER_PASSWORD }} 38 | script: | 39 | docker pull ${{ secrets.DOCKER_USERNAME }}/board:${{ github.run_number }} 40 | docker stop $(docker ps -a -q) 41 | docker stop $(docker ps -a -q --filter ancestor=${{ secrets.DOCKER_USERNAME }}/board) 42 | docker run -d -p 80:8080 ${{ secrets.DOCKER_USERNAME }}/board:${{ github.run_number }} 43 | docker rm $(docker ps --filter 'status=exited' -a -q) 44 | docker image prune -a -f 45 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'main' 7 | - 'dev' 8 | - 'feat/*' 9 | - 'fix/*' 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Set up JDK 17 19 | uses: actions/setup-java@v3 20 | with: 21 | java-version: '17' 22 | distribution: 'adopt' 23 | 24 | - name: Grant execute permission for gradlew 25 | run: chmod +x gradlew 26 | 27 | - name: Gradle Clean & Build 28 | run: ./gradlew clean build 29 | 30 | - name: Check ktlint format 31 | run: ./gradlew ktlintCheck 32 | 33 | - name: Test with Gradle 34 | run: ./gradlew test 35 | 36 | - name: Upload CI results 37 | uses: actions/upload-artifact@v2 38 | with: 39 | name: ci-results 40 | path: build/ci-results 41 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:17-jdk-alpine AS BUILDER 2 | 3 | RUN mkdir /app_source 4 | COPY . /app_source 5 | 6 | WORKDIR /app_source 7 | 8 | RUN chmod +x ./gradlew 9 | RUN ./gradlew :adapter-in:web:copySwaggerUI 10 | RUN ./gradlew :adapter-in:web:bootJar 11 | 12 | FROM eclipse-temurin:17-jdk-alpine AS RUNNER 13 | 14 | RUN mkdir /app 15 | 16 | COPY --from=BUILDER /app_source/adapter-in/web/build/libs /app 17 | 18 | WORKDIR /app 19 | 20 | ENV TZ=Asia/Seoul 21 | 22 | EXPOSE 8080 23 | USER nobody 24 | 25 | ARG PHASE 26 | ARG AWS_SECRET_KEY 27 | ARG SERVER_HOST 28 | 29 | ENV ENV_PHASE=${PHASE} 30 | ENV ENV_AWS_SECRET_KEY=${AWS_SECRET_KEY} 31 | ENV ENV_SERVER_HOST=${SERVER_HOST} 32 | 33 | ENTRYPOINT java -jar \ 34 | -Dspring.profiles.active=${ENV_PHASE:-sandbox} \ 35 | -Dcloud.aws.credentials.secret-key=${ENV_AWS_SECRET_KEY} \ 36 | -Dbol.server.host=${ENV_SERVER_HOST} \ 37 | /app/*.jar 38 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/EmptyResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol 2 | 3 | object EmptyResponse 4 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/ExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol 2 | 3 | import com.yapp.bol.utils.logger 4 | import org.springframework.core.Ordered 5 | import org.springframework.core.annotation.Order 6 | import org.springframework.http.HttpStatus 7 | import org.springframework.http.ResponseEntity 8 | import org.springframework.security.core.AuthenticationException 9 | import org.springframework.web.bind.annotation.ExceptionHandler 10 | import org.springframework.web.bind.annotation.ResponseStatus 11 | import org.springframework.web.bind.annotation.RestControllerAdvice 12 | 13 | @RestControllerAdvice 14 | class ExceptionHandler { 15 | private val logger = logger() 16 | 17 | @ExceptionHandler(BolRatingException::class) 18 | fun handleException(e: BolRatingException): ResponseEntity = e.toResponse().apply { 19 | if (e.status >= 500) { 20 | logger.error(e.message, e) 21 | } else { 22 | logger.info(e.message, e) 23 | } 24 | } 25 | 26 | @ExceptionHandler(AuthenticationException::class) 27 | fun handleException(e: AuthenticationException): ResponseEntity = 28 | handleException(UnAuthenticationException(e)).apply { 29 | logger.info(e.message, e) 30 | } 31 | 32 | @ExceptionHandler(AccessDeniedException::class) 33 | fun handleException(e: AccessDeniedException): ResponseEntity = 34 | handleException(UnAuthorizationException(e)).apply { 35 | logger.info(e.message, e) 36 | } 37 | 38 | @Order(value = Ordered.LOWEST_PRECEDENCE) 39 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 40 | @ExceptionHandler(Exception::class) 41 | fun handleException(e: Exception): ErrorResponse = ErrorResponse( 42 | "UNKNOWN", 43 | e.message ?: DEFAULT_MESSAGE, 44 | ).apply { 45 | logger.error(e.message, e) 46 | } 47 | 48 | private fun BolRatingException.toResponse(): ResponseEntity = 49 | ResponseEntity.status(this.status).body(ErrorResponse(code, message)) 50 | 51 | data class ErrorResponse( 52 | val code: String, 53 | val message: String, 54 | ) 55 | 56 | companion object { 57 | const val DEFAULT_MESSAGE = "에러가 발생했습니다. 다시 시도해주세요." 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/TestController.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol 2 | 3 | import com.yapp.bol.utils.ApiMinVersion 4 | import org.springframework.core.env.Environment 5 | import org.springframework.web.bind.annotation.GetMapping 6 | import org.springframework.web.bind.annotation.RequestMapping 7 | import org.springframework.web.bind.annotation.RestController 8 | 9 | // TODO: Delete 테스트 코드 전용 클래스 10 | @RestController 11 | @RequestMapping("/v1/test") 12 | class TestController(private val environment: Environment) { 13 | @GetMapping 14 | fun testGet(): TestResponse { 15 | val activeProfiles = environment.activeProfiles.joinToString(", ") 16 | 17 | return TestResponse("Good! You're running in the $activeProfiles phase.") 18 | } 19 | 20 | @ApiMinVersion("2.1.0") 21 | @GetMapping("/force-update") 22 | fun testForceUpdate(): EmptyResponse { 23 | return EmptyResponse 24 | } 25 | 26 | data class TestResponse( 27 | val value: String 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/WebApplication.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol 2 | 3 | import com.yapp.bol.config.BolProperties 4 | import org.springframework.boot.autoconfigure.SpringBootApplication 5 | import org.springframework.boot.context.properties.EnableConfigurationProperties 6 | import org.springframework.boot.runApplication 7 | 8 | @EnableConfigurationProperties(BolProperties::class) 9 | @SpringBootApplication 10 | class WebApplication 11 | 12 | fun main(args: Array) { 13 | runApplication(*args) 14 | } 15 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/auth/AuthController.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | import com.yapp.bol.auth.dto.LoginRequest 4 | import com.yapp.bol.auth.dto.LoginResponse 5 | import org.springframework.web.bind.annotation.PostMapping 6 | import org.springframework.web.bind.annotation.RequestBody 7 | import org.springframework.web.bind.annotation.RequestMapping 8 | import org.springframework.web.bind.annotation.RestController 9 | 10 | @RestController 11 | @RequestMapping("/v1/auth") 12 | class AuthController( 13 | private val authService: AuthService, 14 | ) { 15 | @PostMapping("/login") 16 | fun login(@RequestBody request: LoginRequest): LoginResponse { 17 | val authToken = authService.login(request.type, request.token) 18 | 19 | return LoginResponse( 20 | accessToken = authToken.accessToken.value, 21 | refreshToken = authToken.refreshToken?.value, 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/auth/dto/LoginRequest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.dto 2 | 3 | import com.yapp.bol.auth.LoginType 4 | 5 | data class LoginRequest( 6 | val type: LoginType, 7 | val token: String, 8 | ) 9 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/auth/dto/LoginResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.dto 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude 4 | 5 | @JsonInclude(JsonInclude.Include.NON_NULL) 6 | data class LoginResponse( 7 | val accessToken: String, 8 | val refreshToken: String?, 9 | ) 10 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/auth/security/SecurityExceptionHandler.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.security 2 | 3 | import jakarta.servlet.http.HttpServletRequest 4 | import jakarta.servlet.http.HttpServletResponse 5 | import org.springframework.beans.factory.annotation.Qualifier 6 | import org.springframework.security.access.AccessDeniedException 7 | import org.springframework.security.core.AuthenticationException 8 | import org.springframework.security.web.AuthenticationEntryPoint 9 | import org.springframework.security.web.access.AccessDeniedHandler 10 | import org.springframework.stereotype.Component 11 | import org.springframework.web.servlet.HandlerExceptionResolver 12 | 13 | @Component 14 | class SecurityExceptionHandler( 15 | @Qualifier("handlerExceptionResolver") private val handler: HandlerExceptionResolver, 16 | ) : AuthenticationEntryPoint, AccessDeniedHandler { 17 | override fun commence( 18 | request: HttpServletRequest, 19 | response: HttpServletResponse, 20 | authException: AuthenticationException 21 | ) { 22 | handler.resolveException(request, response, null, authException) 23 | } 24 | 25 | override fun handle( 26 | request: HttpServletRequest, 27 | response: HttpServletResponse, 28 | accessDeniedException: AccessDeniedException 29 | ) { 30 | handler.resolveException(request, response, null, accessDeniedException) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/auth/security/TokenAuthentication.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.security 2 | 3 | import com.yapp.bol.auth.UserId 4 | import org.springframework.security.core.Authentication 5 | import org.springframework.security.core.GrantedAuthority 6 | 7 | class TokenAuthentication( 8 | private val token: String, 9 | private val userId: UserId, 10 | ) : Authentication { 11 | private var isAuthenticated = true 12 | 13 | override fun getName(): String = userId.toString() 14 | 15 | override fun getAuthorities(): Collection = emptyList() 16 | 17 | override fun getCredentials(): String = token 18 | 19 | override fun getDetails(): Any? = null 20 | 21 | override fun getPrincipal(): UserId = userId 22 | 23 | override fun isAuthenticated(): Boolean = isAuthenticated 24 | 25 | override fun setAuthenticated(isAuthenticated: Boolean) { 26 | this.isAuthenticated = isAuthenticated 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/auth/security/TokenAuthenticationFilter.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.security 2 | 3 | import com.yapp.bol.auth.AuthService 4 | import jakarta.servlet.FilterChain 5 | import jakarta.servlet.http.HttpServletRequest 6 | import jakarta.servlet.http.HttpServletResponse 7 | import org.springframework.security.core.context.SecurityContextHolder 8 | import org.springframework.stereotype.Component 9 | import org.springframework.web.filter.OncePerRequestFilter 10 | 11 | @Component 12 | class TokenAuthenticationFilter( 13 | private val authService: AuthService, 14 | ) : OncePerRequestFilter() { 15 | override fun doFilterInternal( 16 | request: HttpServletRequest, 17 | response: HttpServletResponse, 18 | filterChain: FilterChain 19 | ) { 20 | try { 21 | val authHeader = request.getHeader(HEADER_AUTHORIZATION) ?: return 22 | val headerData = authHeader.split(' ') 23 | 24 | if (headerData.size != 2) return 25 | if (headerData[0].lowercase() != AUTHORIZATION_METHOD) return 26 | if (headerData[1].isBlank()) return 27 | 28 | val accessToken = headerData[1] 29 | 30 | val authUser = authService.getAuthUserByAccessToken(accessToken) ?: return 31 | SecurityContextHolder.getContext().authentication = TokenAuthentication(accessToken, authUser.id) 32 | } finally { 33 | filterChain.doFilter(request, response) 34 | } 35 | } 36 | 37 | companion object { 38 | private const val HEADER_AUTHORIZATION = "Authorization" 39 | private const val AUTHORIZATION_METHOD = "bearer" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/config/BolProperties.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.config 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties 4 | 5 | @ConfigurationProperties(prefix = "bol.server") 6 | data class BolProperties( 7 | val host: String 8 | ) 9 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/file/FileController.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.file 2 | 3 | import com.yapp.bol.InvalidRequestException 4 | import com.yapp.bol.auth.getSecurityUserId 5 | import com.yapp.bol.auth.getSecurityUserIdOrThrow 6 | import com.yapp.bol.file.dto.FileResponse 7 | import com.yapp.bol.file.dto.RawFileData 8 | import org.springframework.core.io.InputStreamResource 9 | import org.springframework.core.io.Resource 10 | import org.springframework.http.MediaType 11 | import org.springframework.http.ResponseEntity 12 | import org.springframework.security.access.prepost.PreAuthorize 13 | import org.springframework.web.bind.annotation.GetMapping 14 | import org.springframework.web.bind.annotation.PathVariable 15 | import org.springframework.web.bind.annotation.PostMapping 16 | import org.springframework.web.bind.annotation.RequestMapping 17 | import org.springframework.web.bind.annotation.RequestParam 18 | import org.springframework.web.bind.annotation.RequestPart 19 | import org.springframework.web.bind.annotation.RestController 20 | import org.springframework.web.multipart.MultipartFile 21 | 22 | @RequestMapping("/v1/file") 23 | @RestController 24 | class FileController( 25 | private val fileService: FileService, 26 | ) { 27 | 28 | @PostMapping 29 | @PreAuthorize("isAuthenticated()") 30 | fun uploadFile( 31 | @RequestPart file: MultipartFile, 32 | @RequestParam purpose: FilePurpose, 33 | ): FileResponse { 34 | val request = RawFileData( 35 | userId = getSecurityUserIdOrThrow(), 36 | contentType = file.contentType ?: throw InvalidRequestException(), 37 | content = file.inputStream, 38 | purpose = purpose, 39 | ) 40 | val result = fileService.uploadFile(request) 41 | 42 | return FileResponse(result.name) 43 | } 44 | 45 | @GetMapping("/{name}") 46 | fun downloadFile(@PathVariable("name") fileName: String): ResponseEntity { 47 | val file = fileService.downloadFile(getSecurityUserId(), fileName) 48 | val resource = InputStreamResource(file.content) 49 | 50 | return ResponseEntity.ok() 51 | .contentType(MediaType.valueOf(file.contentType)) 52 | .body(resource) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/file/dto/FileResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.file.dto 2 | 3 | data class FileResponse( 4 | val url: String, 5 | ) 6 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/game/GameController.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game 2 | 3 | import com.yapp.bol.game.dto.GameListResponse 4 | import com.yapp.bol.game.dto.toResponse 5 | import com.yapp.bol.group.GroupId 6 | import org.springframework.web.bind.annotation.GetMapping 7 | import org.springframework.web.bind.annotation.PathVariable 8 | import org.springframework.web.bind.annotation.RequestMapping 9 | import org.springframework.web.bind.annotation.RestController 10 | 11 | @RestController 12 | @RequestMapping("/v1/group") 13 | class GameController( 14 | private val gameService: GameService, 15 | ) { 16 | 17 | @GetMapping("/{groupId}/game") 18 | fun groupId( 19 | @PathVariable("groupId") groupId: GroupId, 20 | ): GameListResponse { 21 | val games = gameService.getGameList(groupId) 22 | 23 | return GameListResponse( 24 | games.map { it.toResponse() } 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/game/dto/GameListResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game.dto 2 | 3 | data class GameListResponse( 4 | val list: List 5 | ) 6 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/game/dto/GameResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game.dto 2 | 3 | import com.yapp.bol.game.GameId 4 | import com.yapp.bol.game.GameWithMatchCount 5 | 6 | data class GameResponse( 7 | val id: GameId, 8 | val name: String, 9 | val minMember: Int, 10 | val maxMember: Int, 11 | val img: String, 12 | val matchCount: Int, 13 | ) 14 | 15 | fun GameWithMatchCount.toResponse(): GameResponse = GameResponse( 16 | id = this.id, 17 | name = this.name, 18 | minMember = this.minMember, 19 | maxMember = this.maxMember, 20 | img = this.img, 21 | matchCount = this.matchCount, 22 | ) 23 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/group/dto/CheckAccessCodeRequest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.dto 2 | 3 | data class CheckAccessCodeRequest( 4 | val accessCode: String, 5 | ) 6 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/group/dto/CheckAccessCodeResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.dto 2 | 3 | data class CheckAccessCodeResponse( 4 | val result: Boolean, 5 | ) 6 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/group/dto/CreateGroupRequest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.dto 2 | 3 | import com.yapp.bol.auth.UserId 4 | 5 | data class CreateGroupRequest( 6 | val name: String, 7 | val description: String, 8 | val organization: String?, 9 | val profileImageUrl: String?, 10 | val nickname: String?, 11 | ) 12 | 13 | fun CreateGroupRequest.toDto(ownerId: UserId) = CreateGroupDto( 14 | name = name, 15 | description = description, 16 | organization = organization, 17 | profileImageUrl = profileImageUrl, 18 | ownerId = ownerId, 19 | nickname = nickname 20 | ) 21 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/group/dto/CreateGroupResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.dto 2 | 3 | import com.yapp.bol.group.GroupId 4 | 5 | data class CreateGroupResponse( 6 | val id: GroupId, 7 | val name: String, 8 | val description: String, 9 | val owner: String, 10 | val organization: String?, 11 | val profileImageUrl: String, 12 | val accessCode: String, 13 | ) 14 | 15 | fun GroupMemberList.toCreateGroupResponse() = CreateGroupResponse( 16 | id = group.id, 17 | name = group.name, 18 | description = group.description, 19 | owner = members.owner.nickname, 20 | organization = group.organization, 21 | profileImageUrl = group.profileImageUrl, 22 | accessCode = group.accessCode, 23 | ) 24 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/group/dto/GroupDetailResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.dto 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude 4 | import com.yapp.bol.group.GroupId 5 | import com.yapp.bol.group.member.OwnerMember 6 | import com.yapp.bol.group.member.dto.MemberResponse 7 | import com.yapp.bol.group.member.dto.toResponse 8 | 9 | @JsonInclude(JsonInclude.Include.NON_NULL) 10 | data class GroupDetailResponse( 11 | val id: GroupId, 12 | val name: String, 13 | val description: String, 14 | val organization: String?, 15 | val profileImageUrl: String, 16 | val accessCode: String, 17 | val memberCount: Int, 18 | val owner: MemberResponse, 19 | val isRegister: Boolean?, 20 | ) { 21 | companion object { 22 | fun of(group: GroupWithMemberCount, owner: OwnerMember, isRegister: Boolean?): GroupDetailResponse = 23 | GroupDetailResponse( 24 | id = group.id, 25 | name = group.name, 26 | description = group.description, 27 | organization = group.organization, 28 | profileImageUrl = group.profileImageUrl, 29 | accessCode = group.accessCode, 30 | memberCount = group.memberCount, 31 | owner = owner.toResponse(), 32 | isRegister = isRegister, 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/group/dto/GroupListResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.dto 2 | 3 | import com.yapp.bol.group.GroupId 4 | 5 | data class GroupListResponse( 6 | val id: GroupId, 7 | val name: String, 8 | val description: String, 9 | val organization: String?, 10 | val profileImageUrl: String, 11 | val memberCount: Int, 12 | ) 13 | 14 | fun GroupWithMemberCount.toListResponse(): GroupListResponse = 15 | GroupListResponse( 16 | id = this.id, 17 | name = this.name, 18 | description = this.description, 19 | organization = this.organization, 20 | profileImageUrl = this.profileImageUrl, 21 | memberCount = this.memberCount, 22 | ) 23 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/group/dto/GroupReponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.dto 2 | 3 | import com.yapp.bol.group.GroupBasicInfo 4 | import com.yapp.bol.group.GroupId 5 | 6 | data class GroupResponse( 7 | val id: GroupId, 8 | val name: String, 9 | val description: String, 10 | val organization: String?, 11 | val profileImageUrl: String, 12 | ) 13 | 14 | fun GroupBasicInfo.toResponse(): GroupResponse = 15 | GroupResponse( 16 | id = this.id, 17 | name = this.name, 18 | description = this.description, 19 | organization = this.organization, 20 | profileImageUrl = this.profileImageUrl, 21 | ) 22 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/group/dto/LeaderBoardResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.dto 2 | 3 | import com.yapp.bol.auth.UserId 4 | import com.yapp.bol.group.LeaderBoardMember 5 | import com.yapp.bol.group.member.MemberId 6 | import com.yapp.bol.group.member.MemberRole 7 | 8 | data class LeaderBoardResponse( 9 | val contents: List 10 | ) 11 | 12 | data class RankMemberResponse( 13 | @Deprecated("memberId로 대체 사용") val id: MemberId, 14 | val memberId: MemberId, 15 | val userId: UserId?, 16 | val nickname: String, 17 | val role: MemberRole, 18 | val rank: Int?, 19 | val score: Int?, 20 | val matchCount: Int?, 21 | val isChangeRecent: Boolean, 22 | ) 23 | 24 | fun LeaderBoardMember.toResponse(rank: Int): RankMemberResponse = RankMemberResponse( 25 | id = this.member.id, 26 | memberId = this.member.id, 27 | userId = this.member.userId, 28 | nickname = this.member.nickname, 29 | role = this.member.role, 30 | rank = if (this.score == null) null else rank, 31 | score = this.score, 32 | matchCount = this.matchCount, 33 | isChangeRecent = this.isChangeRecent, 34 | ) 35 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/group/member/dto/AddGuestRequest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member.dto 2 | 3 | data class AddGuestRequest( 4 | val nickname: String, 5 | ) 6 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/group/member/dto/JoinGroupRequest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member.dto 2 | 3 | data class JoinGroupRequest( 4 | val nickname: String?, 5 | val accessCode: String, 6 | val guestId: Long?, 7 | ) 8 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/group/member/dto/MemberResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member.dto 2 | 3 | import com.yapp.bol.group.member.Member 4 | import com.yapp.bol.group.member.MemberId 5 | import com.yapp.bol.group.member.MemberRole 6 | 7 | data class MemberResponse( 8 | val id: MemberId?, 9 | val role: MemberRole, 10 | val nickname: String, 11 | val level: Int, 12 | ) 13 | 14 | fun Member.toResponse(): MemberResponse = MemberResponse( 15 | id = this.id, 16 | nickname = this.nickname, 17 | role = this.role, 18 | level = this.level, 19 | ) 20 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/group/member/dto/ValidateMemberNameResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member.dto 2 | 3 | data class ValidateMemberNameResponse(val isAvailable: Boolean) 4 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/match/MatchController.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.match 2 | 3 | import com.yapp.bol.EmptyResponse 4 | import com.yapp.bol.match.dto.CreateMatchRequest 5 | import com.yapp.bol.match.dto.toDto 6 | import org.springframework.security.access.prepost.PreAuthorize 7 | import org.springframework.web.bind.annotation.PostMapping 8 | import org.springframework.web.bind.annotation.RequestBody 9 | import org.springframework.web.bind.annotation.RequestMapping 10 | import org.springframework.web.bind.annotation.RestController 11 | 12 | @RestController 13 | @RequestMapping("/v1/match") 14 | class MatchController( 15 | private val matchService: MatchService, 16 | ) { 17 | @PreAuthorize("isAuthenticated()") 18 | @PostMapping 19 | fun createMatch(@RequestBody request: CreateMatchRequest): EmptyResponse { 20 | matchService.createMatch(request.toDto()) 21 | 22 | return EmptyResponse 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/match/dto/CreateMatchRequest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.match.dto 2 | 3 | import com.yapp.bol.date.DateTimeUtils 4 | import com.yapp.bol.game.GameId 5 | import com.yapp.bol.group.GroupId 6 | import com.yapp.bol.group.member.MemberId 7 | 8 | data class MatchMemberRequest( 9 | val memberId: Long, 10 | val score: Int, 11 | val ranking: Int, 12 | ) 13 | 14 | data class CreateMatchRequest( 15 | val gameId: Long, 16 | val groupId: Long, 17 | val matchedDate: String, 18 | val matchMembers: List 19 | ) 20 | 21 | internal fun MatchMemberRequest.toDto() = CreateMatchMemberDto( 22 | memberId = MemberId(this.memberId), 23 | score = this.score, 24 | ranking = this.ranking, 25 | ) 26 | 27 | internal fun CreateMatchRequest.toDto() = CreateMatchDto( 28 | gameId = GameId(this.gameId), 29 | groupId = GroupId(this.groupId), 30 | matchedDate = DateTimeUtils.parseString(this.matchedDate), 31 | createMatchMemberDtos = this.matchMembers.map { it.toDto() } 32 | ) 33 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/setting/SettingController.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.setting 2 | 3 | import com.yapp.bol.terms.TermsService 4 | import com.yapp.bol.terms.dto.TermsResponse 5 | import com.yapp.bol.terms.dto.toResponse 6 | import org.springframework.beans.factory.annotation.Value 7 | import org.springframework.web.bind.annotation.GetMapping 8 | import org.springframework.web.bind.annotation.RequestMapping 9 | import org.springframework.web.bind.annotation.RestController 10 | 11 | @RestController 12 | @RequestMapping("/v1/setting") 13 | class SettingController( 14 | @Value("\${bol.server.host}") private val host: String, 15 | private val termsService: TermsService, 16 | ) { 17 | @GetMapping("/terms") 18 | fun getSetting(): TermsResponse { 19 | return TermsResponse(termsService.getWholeTerms().map { it.toResponse(host) }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/terms/TermsController.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.terms 2 | 3 | import com.yapp.bol.EmptyResponse 4 | import com.yapp.bol.auth.getSecurityUserIdOrThrow 5 | import com.yapp.bol.terms.dto.AgreeTermsRequest 6 | import com.yapp.bol.terms.dto.TermsResponse 7 | import com.yapp.bol.terms.dto.toResponse 8 | import org.springframework.beans.factory.annotation.Value 9 | import org.springframework.security.access.prepost.PreAuthorize 10 | import org.springframework.web.bind.annotation.GetMapping 11 | import org.springframework.web.bind.annotation.PostMapping 12 | import org.springframework.web.bind.annotation.RequestBody 13 | import org.springframework.web.bind.annotation.RequestMapping 14 | import org.springframework.web.bind.annotation.RestController 15 | 16 | @RestController 17 | @RequestMapping("/v1/terms") 18 | class TermsController( 19 | @Value("\${bol.server.host}") private val host: String, 20 | private val termsService: TermsService, 21 | ) { 22 | 23 | @GetMapping 24 | @PreAuthorize("isAuthenticated()") 25 | fun getTerms(): TermsResponse { 26 | val userId = getSecurityUserIdOrThrow() 27 | val list = termsService.getNeedTermsAgreeList(userId) 28 | return TermsResponse( 29 | list.map { it.toResponse(host) } 30 | ) 31 | } 32 | 33 | @PostMapping 34 | @PreAuthorize("isAuthenticated()") 35 | fun agreeTerms( 36 | @RequestBody request: AgreeTermsRequest, 37 | ): EmptyResponse { 38 | val userId = getSecurityUserIdOrThrow() 39 | 40 | val termsInfo = mutableListOf() 41 | 42 | request.agree?.forEach { 43 | termsInfo.add(TermsAgreeInfo(it, true)) 44 | } 45 | 46 | request.disagree?.forEach { 47 | termsInfo.add(TermsAgreeInfo(it, false)) 48 | } 49 | 50 | termsService.agreeTerms(userId, termsInfo) 51 | 52 | return EmptyResponse 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/terms/dto/AgreeTermsRequest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.terms.dto 2 | 3 | import com.yapp.bol.terms.TermsCode 4 | 5 | data class AgreeTermsRequest( 6 | val agree: List?, 7 | val disagree: List?, 8 | ) 9 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/terms/dto/TermsResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.terms.dto 2 | 3 | import com.yapp.bol.terms.TermsCode 4 | 5 | data class TermsResponse( 6 | val contents: List 7 | ) 8 | 9 | data class TermsItemResponse( 10 | val code: TermsCode, 11 | val title: String, 12 | val url: String, 13 | val isRequired: Boolean, 14 | ) 15 | 16 | fun TermsCode.toResponse(host: String): TermsItemResponse = TermsItemResponse( 17 | code = this, 18 | title = this.title, 19 | url = "$host/${this.path}", 20 | isRequired = this.isRequired, 21 | ) 22 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/user/dto/CheckOnboardResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.user.dto 2 | 3 | import com.yapp.bol.onboarding.OnboardingType 4 | 5 | data class CheckOnboardResponse( 6 | val onboarding: List 7 | ) 8 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/user/dto/JoinedGroupResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.user.dto 2 | 3 | import com.yapp.bol.group.dto.GroupResponse 4 | 5 | data class JoinedGroupResponse( 6 | val contents: List 7 | ) 8 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/user/dto/MyInfoResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.user.dto 2 | 3 | import com.yapp.bol.auth.UserId 4 | import com.yapp.bol.user.User 5 | 6 | data class MyInfoResponse( 7 | val id: UserId, 8 | val nickname: String?, 9 | ) 10 | 11 | fun User.toResponse(): MyInfoResponse = MyInfoResponse( 12 | id = this.id, 13 | nickname = this.nickname 14 | ) 15 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/user/dto/PutUserInfoRequest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.user.dto 2 | 3 | data class PutUserInfoRequest( 4 | val nickname: String, 5 | ) 6 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/utils/ApiMinVersion.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.utils 2 | 3 | @Target(allowedTargets = [AnnotationTarget.FUNCTION]) 4 | annotation class ApiMinVersion( 5 | val androidVersion: String, 6 | ) 7 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/utils/ForceUpdateAspect.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.utils 2 | 3 | import com.yapp.bol.NeedForceUpdateException 4 | import org.aspectj.lang.ProceedingJoinPoint 5 | import org.aspectj.lang.annotation.Around 6 | import org.aspectj.lang.annotation.Aspect 7 | import org.aspectj.lang.reflect.MethodSignature 8 | import org.springframework.stereotype.Component 9 | import org.springframework.web.context.request.RequestContextHolder 10 | import org.springframework.web.context.request.ServletRequestAttributes 11 | 12 | @Aspect 13 | @Component 14 | class ForceUpdateAspect { 15 | 16 | @Around("@annotation(com.yapp.bol.utils.ApiMinVersion)") 17 | fun checkForceUpdate(joinPoint: ProceedingJoinPoint): Any { 18 | try { 19 | val attributes = RequestContextHolder.currentRequestAttributes() as ServletRequestAttributes 20 | val userAgent = attributes.request.getHeader("User-Agent") 21 | val appVersion = userAgent.split(" ")[0].split("/") 22 | val appType = appVersion[0] 23 | val version = appVersion[1] 24 | 25 | val signature = joinPoint.signature as MethodSignature 26 | val method = signature.method 27 | val annotation = method.getAnnotation(ApiMinVersion::class.java) 28 | 29 | if (appType.lowercase() == "android") { 30 | if (Version.of(annotation.androidVersion) > Version.of(version)) 31 | throw NeedForceUpdateException 32 | } 33 | } catch (e: Exception) { 34 | throw NeedForceUpdateException 35 | } 36 | 37 | return joinPoint.proceed() 38 | } 39 | 40 | class Version( 41 | val major: Int, 42 | val minor: Int, 43 | val patch: Int, 44 | ) : Comparable { 45 | override fun compareTo(other: Version): Int { 46 | if (major > other.major) return 1 47 | if (major < other.major) return -1 48 | 49 | if (minor > other.minor) return 1 50 | if (minor < other.minor) return -1 51 | 52 | return patch.compareTo(patch) 53 | } 54 | 55 | companion object { 56 | fun of(version: String): Version { 57 | val split = version.split(".") 58 | return Version(split[0].toInt(), split[1].toInt(), split[2].toInt()) 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/kotlin/com/yapp/bol/utils/Logger.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.utils 2 | 3 | import org.slf4j.LoggerFactory 4 | 5 | inline fun T.logger() = LoggerFactory.getLogger(T::class.java) 6 | -------------------------------------------------------------------------------- /adapter-in/web/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | servlet: 3 | multipart: 4 | max-file-size: 20MB 5 | max-request-size: 20MB 6 | -------------------------------------------------------------------------------- /adapter-in/web/src/test/kotlin/com/yapp/bol/TestControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol 2 | 3 | import com.yapp.bol.base.ControllerTest 4 | import com.yapp.bol.base.NUMBER 5 | import com.yapp.bol.base.OpenApiTag 6 | import com.yapp.bol.base.STRING 7 | import io.mockk.every 8 | import io.mockk.mockk 9 | import org.springframework.core.env.Environment 10 | 11 | class TestControllerTest : ControllerTest() { 12 | private val environment: Environment = mockk() 13 | 14 | override val controller = TestController(environment) 15 | 16 | init { 17 | test("Get Test") { 18 | every { environment.activeProfiles } returns arrayOf("phase") 19 | 20 | get("/v1/test") {} 21 | .isStatus(200) 22 | .makeDocument( 23 | DocumentInfo(identifier = "test", tag = OpenApiTag.TEST), 24 | responseFields( 25 | "value" type STRING means "English??", 26 | "test" type NUMBER means "이건 몬지몰라" isOptional true 27 | ) 28 | ) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /adapter-in/web/src/test/kotlin/com/yapp/bol/auth/AuthControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | import com.yapp.bol.auth.dto.LoginRequest 4 | import com.yapp.bol.base.ControllerTest 5 | import com.yapp.bol.base.ENUM 6 | import com.yapp.bol.base.OpenApiTag 7 | import com.yapp.bol.base.STRING 8 | import io.mockk.every 9 | import io.mockk.mockk 10 | import java.time.LocalDateTime 11 | 12 | class AuthControllerTest : ControllerTest() { 13 | private val authService: AuthService = mockk() 14 | override val controller = AuthController(authService) 15 | 16 | init { 17 | test("POST /v1/auth/login") { 18 | val request = LoginRequest(LoginType.KAKAO_ACCESS_TOKEN, "Token") 19 | val userId = UserId(123L) 20 | val authToken = AuthToken( 21 | Token("ACCESS_TOKEN", userId, LocalDateTime.now()), 22 | Token("REFRESH_TOKEN", userId, LocalDateTime.now()) 23 | ) 24 | every { authService.login(any(), any()) } returns authToken 25 | 26 | post("/v1/auth/login", request) {} 27 | .isStatus(200) 28 | .makeDocument( 29 | DocumentInfo(identifier = "test", tag = OpenApiTag.AUTH), 30 | requestFields( 31 | "type" type ENUM(LoginType::class) means "로그인 방법", 32 | "token" type STRING means "로그인에 사용되는 토큰", 33 | ), 34 | responseFields( 35 | "accessToken" type STRING means "Access 토큰", 36 | "refreshToken" type STRING means "Refresh 토큰" isOptional true, 37 | ) 38 | ) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /adapter-in/web/src/test/kotlin/com/yapp/bol/base/DocumentFieldType.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.base 2 | 3 | import org.springframework.restdocs.payload.JsonFieldType 4 | import kotlin.reflect.KClass 5 | 6 | sealed class DocumentFieldType( 7 | val type: JsonFieldType 8 | ) 9 | 10 | object ARRAY : DocumentFieldType(JsonFieldType.ARRAY) 11 | object BOOLEAN : DocumentFieldType(JsonFieldType.BOOLEAN) 12 | object OBJECT : DocumentFieldType(JsonFieldType.OBJECT) 13 | object NUMBER : DocumentFieldType(JsonFieldType.NUMBER) 14 | object NULL : DocumentFieldType(JsonFieldType.NULL) 15 | object STRING : DocumentFieldType(JsonFieldType.STRING) 16 | object ANY : DocumentFieldType(JsonFieldType.VARIES) 17 | 18 | data class ENUM>(val enums: List) : DocumentFieldType(JsonFieldType.STRING) { 19 | constructor(clazz: KClass) : this(clazz.java.enumConstants.asList()) // (1) 20 | } 21 | -------------------------------------------------------------------------------- /adapter-in/web/src/test/kotlin/com/yapp/bol/base/OpenApiTag.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.base 2 | 3 | enum class OpenApiTag( 4 | val value: String 5 | ) { 6 | TEST("test"), 7 | AUTH("Auth"), 8 | TERMS("Terms"), 9 | SETTING("Setting"), 10 | FILE("File"), 11 | GAME("Game"), 12 | GROUP("Group"), 13 | MEMBER("Member"), 14 | USER("User"), 15 | MATCH("Match"), 16 | } 17 | -------------------------------------------------------------------------------- /adapter-in/web/src/test/kotlin/com/yapp/bol/game/GameControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game 2 | 3 | import com.yapp.bol.base.ARRAY 4 | import com.yapp.bol.base.ControllerTest 5 | import com.yapp.bol.base.NUMBER 6 | import com.yapp.bol.base.OpenApiTag 7 | import com.yapp.bol.base.STRING 8 | import com.yapp.bol.group.GroupId 9 | import io.mockk.every 10 | import io.mockk.mockk 11 | 12 | class GameControllerTest : ControllerTest() { 13 | private val gameService: GameService = mockk() 14 | override val controller = GameController(gameService) 15 | 16 | init { 17 | test("게임 목록 가져오기") { 18 | val groupId = GroupId(1) 19 | val games = listOf( 20 | GameWithMatchCount(Game(GameId(0), "게임 1", 2, 4, GameRankType.SCORE_HIGH, "ImgUrl"), 3), 21 | GameWithMatchCount(Game(GameId(1), "게임 2", 2, 5, GameRankType.SCORE_HIGH, "ImgUrl"), 2), 22 | GameWithMatchCount(Game(GameId(2), "게임 3", 1, 4, GameRankType.SCORE_HIGH, "ImgUrl"), 0), 23 | ) 24 | every { gameService.getGameList(groupId) } returns games 25 | 26 | get("/v1/group/{groupId}/game", arrayOf(groupId.value)) {} 27 | .isStatus(200) 28 | .makeDocument( 29 | DocumentInfo(identifier = "game/{method-name}", tag = OpenApiTag.GAME), 30 | pathParameters( 31 | "groupId" type NUMBER means "게임 목록을 조회하고자 하는 groupId, 현재는 아무값이나 넣어도 검증 없이 동일하게 내려감" 32 | ), 33 | responseFields( 34 | "list" type ARRAY means "게임 목록", 35 | "list[].id" type NUMBER means "게임 ID", 36 | "list[].name" type STRING means "게임 이름", 37 | "list[].minMember" type NUMBER means "게임 최소 인원수", 38 | "list[].maxMember" type NUMBER means "게임 최대 인원수", 39 | "list[].img" type STRING means "게임 Img Url", 40 | "list[].matchCount" type NUMBER means "해당 그룹의 Match Count", 41 | ) 42 | ) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /adapter-in/web/src/test/kotlin/com/yapp/bol/setting/SettingControllerTest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.setting 2 | 3 | import com.yapp.bol.base.ARRAY 4 | import com.yapp.bol.base.BOOLEAN 5 | import com.yapp.bol.base.ControllerTest 6 | import com.yapp.bol.base.OpenApiTag 7 | import com.yapp.bol.base.STRING 8 | import com.yapp.bol.terms.TermsCode 9 | import com.yapp.bol.terms.TermsService 10 | import io.mockk.every 11 | import io.mockk.mockk 12 | 13 | class SettingControllerTest : ControllerTest() { 14 | private val termsService: TermsService = mockk() 15 | override val controller = SettingController("http://localhost:8080/", termsService) 16 | 17 | init { 18 | test("설정에서 이용 약관 가져오기") { 19 | every { termsService.getWholeTerms() } returns listOf(TermsCode.SERVICE_V1, TermsCode.PRIVACY_V1) 20 | 21 | get("/v1/setting/terms") {} 22 | .isStatus(200) 23 | .makeDocument( 24 | DocumentInfo( 25 | identifier = "terms/{method-name}", 26 | description = "설정 화면에서 보여줄 이용약관 가져오기", 27 | tag = OpenApiTag.SETTING, 28 | ), 29 | responseFields( 30 | "contents" type ARRAY means "약관 목록", 31 | "contents[].code" type STRING means "약관 Id", 32 | "contents[].title" type STRING means "약관 제목", 33 | "contents[].isRequired" type BOOLEAN means "약관 필수 여부", 34 | "contents[].url" type STRING means "약관 내용 url", 35 | ) 36 | ) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /adapter-out/rdb/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | plugins { 4 | id("org.springframework.boot") 5 | id("io.spring.dependency-management") 6 | kotlin("plugin.spring") 7 | kotlin("plugin.jpa") 8 | kotlin("kapt") 9 | } 10 | 11 | allOpen { 12 | annotation("javax.persistence.Entity") 13 | annotation("javax.persistence.MappedSuperclass") 14 | annotation("javax.persistence.Embeddable") 15 | } 16 | 17 | noArg { 18 | annotation("javax.persistence.Entity") 19 | annotation("javax.persistence.MappedSuperclass") 20 | annotation("javax.persistence.Embeddable") 21 | } 22 | 23 | dependencies { 24 | val springVersion by properties 25 | 26 | api(project(":domain")) 27 | api(project(":port-out")) 28 | 29 | implementation("org.springframework.boot:spring-boot-starter-data-jpa:$springVersion") 30 | implementation("org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE") 31 | implementation("org.jetbrains.kotlin:kotlin-reflect") 32 | runtimeOnly("mysql:mysql-connector-java:8.0.33") 33 | 34 | val kapt by configurations 35 | implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta") 36 | kapt("com.querydsl:querydsl-apt:5.0.0:jakarta") 37 | 38 | testImplementation("org.springframework.boot:spring-boot-starter-test:$springVersion") 39 | testRuntimeOnly("com.h2database:h2") 40 | } 41 | 42 | tasks { 43 | withType { enabled = true } 44 | withType { enabled = false } 45 | } 46 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/AuditingEntity.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol 2 | 3 | import jakarta.persistence.Column 4 | import jakarta.persistence.EntityListeners 5 | import jakarta.persistence.MappedSuperclass 6 | import java.time.LocalDateTime 7 | import org.springframework.data.annotation.CreatedDate 8 | import org.springframework.data.annotation.LastModifiedDate 9 | import org.springframework.data.jpa.domain.support.AuditingEntityListener 10 | 11 | @MappedSuperclass 12 | @EntityListeners(AuditingEntityListener::class) 13 | abstract class AuditingEntity { 14 | @CreatedDate 15 | @Column(name = "created_at", updatable = false, nullable = false) 16 | var createdDate: LocalDateTime = LocalDateTime.now() 17 | 18 | @LastModifiedDate 19 | @Column(name = "updated_at", nullable = false) 20 | var updatedDate: LocalDateTime = LocalDateTime.now() 21 | } 22 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/auth/AuthCommandRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | import com.yapp.bol.user.UserEntity 4 | import com.yapp.bol.user.UserRepository 5 | import org.springframework.stereotype.Repository 6 | 7 | @Repository 8 | internal class AuthCommandRepositoryImpl( 9 | private val authSocialRepository: AuthSocialRepository, 10 | private val userRepository: UserRepository, 11 | ) : AuthCommandRepository { 12 | override fun registerUser(loginType: LoginType, socialId: String): AuthUser { 13 | val user = userRepository.save(UserEntity()) 14 | authSocialRepository.save(AuthSocialEntity(loginType.toSocialType(), socialId, user.id)) 15 | 16 | return AuthUser(UserId(user.id)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/auth/AuthQueryRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | import org.springframework.data.repository.findByIdOrNull 4 | import org.springframework.stereotype.Repository 5 | 6 | @Repository 7 | internal class AuthQueryRepositoryImpl( 8 | private val authSocialRepository: AuthSocialRepository, 9 | ) : AuthQueryRepository { 10 | override fun findAuthUser(id: UserId): AuthUser? { 11 | val authSocial = authSocialRepository.findByUserId(id.value) ?: return null 12 | 13 | return AuthUser(UserId(authSocial.userId)) 14 | } 15 | 16 | override fun findAuthUser(socialType: LoginType, socialId: String): AuthUser? { 17 | val authSocial = 18 | authSocialRepository.findByIdOrNull(SocialInfo(socialType.toSocialType(), socialId)) ?: return null 19 | 20 | return AuthUser(UserId(authSocial.userId)) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/auth/AuthSocialEntity.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | import com.yapp.bol.AuditingEntity 4 | import jakarta.persistence.Column 5 | import jakarta.persistence.Entity 6 | import jakarta.persistence.EnumType 7 | import jakarta.persistence.Enumerated 8 | import jakarta.persistence.Id 9 | import jakarta.persistence.IdClass 10 | import jakarta.persistence.Table 11 | 12 | @Table(name = "auth_social") 13 | @Entity 14 | @IdClass(SocialInfo::class) 15 | internal class AuthSocialEntity( 16 | socialType: SocialType, 17 | socialId: String, 18 | userId: Long, 19 | ) : AuditingEntity() { 20 | @Id 21 | @Enumerated(EnumType.STRING) 22 | @Column(name = "social_type") 23 | var socialType: SocialType = socialType 24 | protected set 25 | 26 | @Id 27 | @Column(name = "social_id") 28 | var socialId: String = socialId 29 | protected set 30 | 31 | @Column(name = "users_id") 32 | var userId: Long = userId 33 | protected set 34 | } 35 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/auth/AuthSocialRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | 5 | internal interface AuthSocialRepository : JpaRepository { 6 | fun findByUserId(userId: Long): AuthSocialEntity? 7 | } 8 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/auth/SocialInfo.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | import java.io.Serializable 4 | 5 | data class SocialInfo( 6 | val socialType: SocialType, 7 | val socialId: String, 8 | ) : Serializable { 9 | constructor() : this(SocialType.KAKAO, "") 10 | } 11 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/auth/SocialType.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | import java.lang.UnsupportedOperationException 4 | 5 | enum class SocialType { 6 | KAKAO, 7 | NAVER, 8 | GOOGLE, 9 | } 10 | 11 | fun LoginType.toSocialType(): SocialType = 12 | when (this) { 13 | LoginType.KAKAO_ACCESS_TOKEN -> SocialType.KAKAO 14 | LoginType.NAVER_ACCESS_TOKEN -> SocialType.NAVER 15 | LoginType.GOOGLE_ID_TOKEN -> SocialType.GOOGLE 16 | LoginType.REFRESH -> throw UnsupportedOperationException() 17 | } 18 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/auth/token/AccessTokenEntity.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.token 2 | 3 | import com.yapp.bol.AuditingEntity 4 | import jakarta.persistence.Column 5 | import jakarta.persistence.Entity 6 | import jakarta.persistence.Id 7 | import jakarta.persistence.Table 8 | import java.time.LocalDateTime 9 | 10 | @Entity 11 | @Table(name = "auth_access_token") 12 | internal class AccessTokenEntity( 13 | userId: Long, 14 | accessToken: ByteArray, 15 | expiredAt: LocalDateTime, 16 | ) : AuditingEntity() { 17 | @Id 18 | @Column(name = "access_token_id") 19 | var id: Long = 0 20 | protected set 21 | 22 | @Column(name = "users_id") 23 | var userId: Long = userId 24 | protected set 25 | 26 | @Column(name = "access_token", columnDefinition = "BINARY(30)") 27 | var accessToken: ByteArray = accessToken 28 | protected set 29 | 30 | @Column(name = "expired_at") 31 | var expiredAt: LocalDateTime = expiredAt 32 | protected set 33 | } 34 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/auth/token/AccessTokenRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.token 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | 5 | internal interface AccessTokenRepository : JpaRepository { 6 | fun findByAccessToken(accessToken: ByteArray): AccessTokenEntity? 7 | } 8 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/auth/token/RefreshTokenEntity.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.token 2 | 3 | import com.yapp.bol.AuditingEntity 4 | import jakarta.persistence.Column 5 | import jakarta.persistence.Entity 6 | import jakarta.persistence.Id 7 | import jakarta.persistence.Table 8 | import java.time.LocalDateTime 9 | 10 | @Entity 11 | @Table(name = "auth_refresh_token") 12 | internal class RefreshTokenEntity( 13 | userId: Long, 14 | refreshToken: ByteArray, 15 | expiredAt: LocalDateTime, 16 | ) : AuditingEntity() { 17 | @Id 18 | @Column(name = "refresh_token_id") 19 | var id: Long = 0 20 | protected set 21 | 22 | @Column(name = "users_id") 23 | var userId: Long = userId 24 | protected set 25 | 26 | @Column(name = "refresh_token", columnDefinition = "BINARY(45)") 27 | var refreshToken: ByteArray = refreshToken 28 | protected set 29 | 30 | @Column(name = "expired_at") 31 | var expiredAt: LocalDateTime = expiredAt 32 | protected set 33 | } 34 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/auth/token/RefreshTokenRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.token 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | 5 | internal interface RefreshTokenRepository : JpaRepository { 6 | fun findByRefreshToken(refreshToken: ByteArray): RefreshTokenEntity? 7 | fun deleteByRefreshToken(refreshToken: ByteArray) 8 | } 9 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/auth/token/TokenCommandRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.token 2 | 3 | import com.yapp.bol.auth.Token 4 | import org.springframework.stereotype.Repository 5 | 6 | @Repository 7 | internal class TokenCommandRepositoryImpl( 8 | private val accessTokenRepository: AccessTokenRepository, 9 | private val refreshTokenRepository: RefreshTokenRepository, 10 | ) : TokenCommandRepository { 11 | override fun saveAccessToken(token: Token) { 12 | accessTokenRepository.save(token.toAccessToken()) 13 | } 14 | 15 | override fun saveRefreshToken(token: Token) { 16 | refreshTokenRepository.save(token.toRefreshToken()) 17 | } 18 | 19 | override fun removeRefreshToken(token: Token) { 20 | refreshTokenRepository.deleteByRefreshToken(token.toBinary()) 21 | } 22 | 23 | private fun Token.toAccessToken(): AccessTokenEntity = 24 | AccessTokenEntity( 25 | userId = this.userId.value, 26 | accessToken = this.toBinary(), 27 | expiredAt = this.expiredAt, 28 | ) 29 | 30 | private fun Token.toRefreshToken(): RefreshTokenEntity = 31 | RefreshTokenEntity( 32 | userId = this.userId.value, 33 | refreshToken = this.toBinary(), 34 | expiredAt = this.expiredAt, 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/auth/token/TokenQueryRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.token 2 | 3 | import com.yapp.bol.auth.Token 4 | import com.yapp.bol.auth.TokenQueryRepository 5 | import com.yapp.bol.auth.UserId 6 | import com.yapp.bol.auth.toBinary 7 | import org.springframework.stereotype.Repository 8 | 9 | @Repository 10 | internal class TokenQueryRepositoryImpl( 11 | private val accessTokenRepository: AccessTokenRepository, 12 | private val refreshTokenRepository: RefreshTokenRepository, 13 | ) : TokenQueryRepository { 14 | override fun findAccessToken(value: String): Token? { 15 | val entity = accessTokenRepository.findByAccessToken(value.toBinary()) ?: return null 16 | return entity.toToken() 17 | } 18 | 19 | override fun findRefreshToken(token: String): Token? { 20 | val entity = refreshTokenRepository.findByRefreshToken(token.toBinary()) ?: return null 21 | return entity.toToken() 22 | } 23 | 24 | private fun AccessTokenEntity.toToken(): Token { 25 | return Token( 26 | value = this.accessToken, 27 | userId = UserId(this.userId), 28 | expiredAt = this.expiredAt, 29 | ) 30 | } 31 | 32 | private fun RefreshTokenEntity.toToken(): Token { 33 | return Token( 34 | value = this.refreshToken, 35 | userId = UserId(this.userId), 36 | expiredAt = this.expiredAt, 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/aws/AwsConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.aws 2 | 3 | import com.amazonaws.auth.AWSStaticCredentialsProvider 4 | import com.amazonaws.auth.BasicAWSCredentials 5 | import com.amazonaws.services.s3.AmazonS3 6 | import com.amazonaws.services.s3.AmazonS3ClientBuilder 7 | import org.springframework.boot.context.properties.EnableConfigurationProperties 8 | import org.springframework.context.annotation.Bean 9 | import org.springframework.context.annotation.Configuration 10 | 11 | @Configuration 12 | @EnableConfigurationProperties(AwsProperties::class) 13 | class AwsConfiguration { 14 | 15 | @Bean 16 | fun s3Client( 17 | properties: AwsProperties, 18 | ): AmazonS3 { 19 | val credential = BasicAWSCredentials(properties.credentials.accessKey, properties.credentials.secretKey) 20 | 21 | return AmazonS3ClientBuilder.standard() 22 | .withRegion(properties.region) 23 | .withCredentials(AWSStaticCredentialsProvider(credential)) 24 | .build() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/aws/AwsProperties.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.aws 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties 4 | 5 | @ConfigurationProperties(prefix = "cloud.aws") 6 | data class AwsProperties( 7 | val region: String, 8 | val credentials: AwsCredentialsProperties, 9 | val s3: S3Properties, 10 | ) 11 | 12 | data class AwsCredentialsProperties( 13 | val accessKey: String, 14 | val secretKey: String, 15 | ) 16 | 17 | data class S3Properties( 18 | val bucket: String, 19 | ) 20 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/config/QueryDslConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.config 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory 4 | import jakarta.persistence.EntityManager 5 | import jakarta.persistence.PersistenceContext 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | 9 | @Configuration 10 | class QueryDslConfiguration { 11 | @PersistenceContext 12 | private lateinit var entityManager: EntityManager 13 | 14 | @Bean 15 | fun jPAQueryFactory(): JPAQueryFactory = JPAQueryFactory(entityManager) 16 | } 17 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/file/FileEntity.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.file 2 | 3 | import com.yapp.bol.AuditingEntity 4 | import jakarta.persistence.Column 5 | import jakarta.persistence.Entity 6 | import jakarta.persistence.EnumType 7 | import jakarta.persistence.Enumerated 8 | import jakarta.persistence.GeneratedValue 9 | import jakarta.persistence.GenerationType 10 | import jakarta.persistence.Id 11 | import jakarta.persistence.Table 12 | 13 | @Table(name = "file") 14 | @Entity 15 | class FileEntity( 16 | name: String, 17 | userId: Long, 18 | purpose: FilePurpose, 19 | ) : AuditingEntity() { 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | @Column(name = "file_id", nullable = false) 23 | var id: Long = 0 24 | protected set 25 | 26 | @Column(name = "name") 27 | var name: String = name 28 | protected set 29 | 30 | @Column(name = "users_id") 31 | var userId: Long = userId 32 | protected set 33 | 34 | @Column(name = "purpose") 35 | @Enumerated(value = EnumType.STRING) 36 | var purpose: FilePurpose = purpose 37 | protected set 38 | } 39 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/file/FileNameConverter.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.file 2 | 3 | import org.springframework.beans.factory.annotation.Value 4 | import org.springframework.stereotype.Component 5 | 6 | @Component 7 | class FileNameConverter { 8 | @Value("\${bol.server.host}") 9 | fun setHost(host: String) { 10 | Companion.host = host 11 | } 12 | 13 | companion object { 14 | private lateinit var host: String 15 | 16 | fun convertFileUrl(name: String): String = "$host/v1/file/$name" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/file/FileRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.file 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | 5 | interface FileRepository : JpaRepository { 6 | fun findAllByPurpose(purpose: FilePurpose): List 7 | } 8 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/game/GameClient.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game 2 | 3 | import com.yapp.bol.file.FileNameConverter 4 | import com.yapp.bol.group.GroupId 5 | import org.springframework.data.repository.findByIdOrNull 6 | import org.springframework.stereotype.Component 7 | 8 | @Component 9 | class GameClient( 10 | private val gameRepository: GameRepository, 11 | ) : GameQueryRepository { 12 | override fun getGameListByGroupId(groupId: GroupId): List { 13 | val games = gameRepository.getAll() 14 | 15 | val matchCounts = gameRepository.getMatchCount(groupId.value).associate { 16 | val gameId = it[0] as Long 17 | val count = (it[1] as Long).toInt() 18 | 19 | gameId to count 20 | } 21 | 22 | return games.map { 23 | GameWithMatchCount(it.toDomain(), matchCounts[it.id] ?: 0) 24 | }.sortedByDescending { it.matchCount } 25 | } 26 | 27 | override fun findById(id: GameId): Game? { 28 | return gameRepository.findByIdOrNull(id.value)?.toDomain() 29 | } 30 | 31 | private fun GameEntity.toDomain(): Game = Game( 32 | id = GameId(this.id), 33 | name = this.name, 34 | minMember = this.minMember, 35 | maxMember = this.maxMember, 36 | rankType = this.rankType, 37 | img = FileNameConverter.convertFileUrl(img.name), 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/game/GameEntity.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game 2 | 3 | import com.yapp.bol.AuditingEntity 4 | import com.yapp.bol.file.FileEntity 5 | import jakarta.persistence.Column 6 | import jakarta.persistence.Entity 7 | import jakarta.persistence.EnumType 8 | import jakarta.persistence.Enumerated 9 | import jakarta.persistence.FetchType 10 | import jakarta.persistence.GeneratedValue 11 | import jakarta.persistence.GenerationType 12 | import jakarta.persistence.Id 13 | import jakarta.persistence.JoinColumn 14 | import jakarta.persistence.OneToOne 15 | import jakarta.persistence.Table 16 | 17 | @Entity 18 | @Table(name = "game") 19 | class GameEntity : AuditingEntity() { 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | @Column(name = "game_id") 23 | var id: Long = 0 24 | protected set 25 | 26 | @Column(name = "name") 27 | lateinit var name: String 28 | protected set 29 | 30 | @Column(name = "min_member") 31 | var minMember: Int = 2 32 | protected set 33 | 34 | @Column(name = "max_member") 35 | var maxMember: Int = 4 36 | protected set 37 | 38 | @Enumerated(EnumType.STRING) 39 | @Column(name = "rank_type") 40 | lateinit var rankType: GameRankType 41 | protected set 42 | 43 | @OneToOne(fetch = FetchType.LAZY) 44 | @JoinColumn(name = "img_id") 45 | lateinit var img: FileEntity 46 | protected set 47 | 48 | companion object { 49 | fun of(id: Long) { 50 | val game = GameEntity() 51 | game.id = id 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/game/GameRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | import org.springframework.data.jpa.repository.Query 5 | 6 | interface GameRepository : JpaRepository { 7 | @Query("FROM GameEntity e JOIN FETCH e.img") 8 | fun getAll(): List 9 | 10 | @Query( 11 | "SELECT g.id, COUNT(m) AS cnt FROM GameEntity g " + 12 | "LEFT JOIN MatchEntity m ON m.gameId = g.id " + 13 | "WHERE m.groupId = :groupId " + 14 | "GROUP BY g.id " + 15 | "ORDER BY cnt DESC" 16 | ) 17 | fun getMatchCount(groupId: Long): List> 18 | } 19 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/game/member/GameMemberCommandRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game.member 2 | 3 | import com.yapp.bol.group.GroupId 4 | import org.springframework.stereotype.Repository 5 | import org.springframework.transaction.annotation.Transactional 6 | 7 | @Repository 8 | class GameMemberCommandRepositoryImpl( 9 | private val gameMemberRepository: GameMemberRepository 10 | ) : GameMemberCommandRepository { 11 | @Transactional 12 | override fun createGameMember(gameMember: GameMember, groupId: GroupId): GameMember { 13 | return gameMemberRepository.save(gameMember.toEntity()) 14 | .toDomain() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/game/member/GameMemberQueryRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game.member 2 | 3 | import com.yapp.bol.game.GameId 4 | import com.yapp.bol.group.GroupId 5 | import com.yapp.bol.group.member.MemberId 6 | import org.springframework.stereotype.Repository 7 | 8 | @Repository 9 | class GameMemberQueryRepositoryImpl( 10 | private val gameMemberRepository: GameMemberRepository 11 | ) : GameMemberQueryRepository { 12 | override fun findGameMember(memberId: MemberId, gameId: GameId, groupId: GroupId): GameMember? { 13 | return gameMemberRepository.findByMemberIdAndGameId(memberId = memberId.value, gameId = gameId.value)?.toDomain() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/game/member/GameMemberRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game.member 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | 5 | interface GameMemberRepository : JpaRepository { 6 | fun findByMemberIdAndGameId(memberId: Long, gameId: Long): GameMemberEntity? 7 | } 8 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/group/GroupCommandRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group 2 | 3 | import com.yapp.bol.group.dto.GroupMemberList 4 | import com.yapp.bol.group.member.MemberList 5 | import com.yapp.bol.group.member.MemberRepository 6 | import com.yapp.bol.group.member.OwnerMember 7 | import com.yapp.bol.group.member.toDomain 8 | import com.yapp.bol.group.member.toEntity 9 | import org.springframework.stereotype.Repository 10 | import org.springframework.transaction.annotation.Transactional 11 | 12 | @Repository 13 | internal class GroupCommandRepositoryImpl( 14 | private val groupRepository: GroupRepository, 15 | private val memberRepository: MemberRepository, 16 | ) : GroupCommandRepository { 17 | @Transactional 18 | override fun createGroup(group: Group, owner: OwnerMember): GroupMemberList { 19 | val groupEntity = groupRepository.save(group.toEntity()) 20 | 21 | val ownerEntity = memberRepository.save(owner.toEntity(groupEntity.id)) 22 | val ownerMember = ownerEntity.toDomain() as OwnerMember 23 | 24 | val memberList = MemberList(ownerMember) 25 | 26 | return GroupMemberList(groupEntity.toDomain(), memberList) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/group/GroupEntity.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group 2 | 3 | import com.yapp.bol.AuditingEntity 4 | import jakarta.persistence.Column 5 | import jakarta.persistence.Entity 6 | import jakarta.persistence.GeneratedValue 7 | import jakarta.persistence.GenerationType 8 | import jakarta.persistence.Id 9 | import jakarta.persistence.Table 10 | 11 | @Entity 12 | @Table(name = "group_table") 13 | internal class GroupEntity( 14 | id: GroupId = GroupId(0), 15 | name: String, 16 | description: String, 17 | organization: String?, 18 | profileImageUrl: String, 19 | accessCode: String, 20 | ) : AuditingEntity() { 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | @Column(name = "group_id", nullable = false) 24 | val id: Long = id.value 25 | 26 | @Column(name = "name") 27 | val name: String = name 28 | 29 | @Column(name = "description") 30 | val description: String = description 31 | 32 | @Column(name = "organization") 33 | val organization: String? = organization 34 | 35 | @Column(name = "profileImageUrl") 36 | val profileImageUrl: String = profileImageUrl 37 | 38 | @Column(name = "access_code") 39 | val accessCode: String = accessCode 40 | 41 | @Column(name = "deleted") 42 | val deleted: Boolean = false 43 | } 44 | 45 | internal fun Group.toEntity(): GroupEntity = GroupEntity( 46 | id = id, 47 | name = name, 48 | description = description, 49 | organization = organization, 50 | profileImageUrl = profileImageUrl, 51 | accessCode = accessCode, 52 | ) 53 | 54 | internal fun GroupEntity.toDomain(): Group = Group( 55 | id = GroupId(id), 56 | name = name, 57 | description = description, 58 | organization = organization, 59 | profileImageUrl = profileImageUrl, 60 | accessCode = accessCode, 61 | ) 62 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/group/GroupRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group 2 | 3 | import org.springframework.data.domain.Pageable 4 | import org.springframework.data.domain.Slice 5 | import org.springframework.data.jpa.repository.JpaRepository 6 | import org.springframework.data.jpa.repository.Query 7 | 8 | internal interface GroupRepository : JpaRepository { 9 | 10 | fun findByNameLikeOrOrganizationLike(name: String, organization: String, pageable: Pageable): Slice 11 | 12 | @Query("SELECT g FROM MemberEntity m JOIN FETCH GroupEntity g ON m.groupId = g.id WHERE m.userId=:userId") 13 | fun findByUserId(userId: Long): List 14 | } 15 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/group/member/CustomMemberRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member 2 | 3 | import com.yapp.bol.group.member.dto.PaginationCursorMemberRequest 4 | 5 | interface CustomMemberRepository { 6 | 7 | fun getByGroupIdWithCursor(request: PaginationCursorMemberRequest): List 8 | } 9 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/group/member/CustomMemberRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory 4 | import com.yapp.bol.group.member.dto.PaginationCursorMemberRequest 5 | 6 | class CustomMemberRepositoryImpl( 7 | val queryFactory: JPAQueryFactory 8 | ) : CustomMemberRepository { 9 | override fun getByGroupIdWithCursor(request: PaginationCursorMemberRequest): List { 10 | val groupId = request.groupId.value 11 | val nickname = request.nickname 12 | val role = request.role 13 | val size = request.size 14 | val cursor = request.cursor 15 | 16 | val member = QMemberEntity.memberEntity 17 | 18 | var express = member.groupId.eq(groupId).and(member.deleted.isFalse) 19 | 20 | if (nickname != null) { 21 | express = express.and(member.nickname.like("%$nickname%")) 22 | } 23 | 24 | if (role != null) { 25 | express = express.and(member.role.eq(role)) 26 | } 27 | 28 | if (cursor != null) { 29 | express = express.and(member.nickname.gt(cursor)) 30 | } 31 | 32 | return queryFactory.select(member) 33 | .from(member) 34 | .where(express) 35 | .orderBy(member.nickname.asc()) 36 | .limit(size.toLong()) 37 | .fetch() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/group/member/MemberCommandRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member 2 | 3 | import com.yapp.bol.NotFoundMemberException 4 | import com.yapp.bol.auth.UserId 5 | import com.yapp.bol.group.GroupId 6 | import org.springframework.data.repository.findByIdOrNull 7 | import org.springframework.stereotype.Repository 8 | 9 | @Repository 10 | internal class MemberCommandRepositoryImpl( 11 | private val memberRepository: MemberRepository, 12 | ) : MemberCommandRepository { 13 | override fun createMember(groupId: GroupId, member: Member): Member { 14 | return memberRepository.save(member.toEntity(groupId.value)).toDomain() 15 | } 16 | 17 | override fun updateGuestToHost(groupId: GroupId, memberId: MemberId, userId: UserId) { 18 | val member = memberRepository.findByIdOrNull(memberId.value) ?: throw NotFoundMemberException 19 | if (member.userId != null || member.groupId != groupId.value) throw NotFoundMemberException 20 | 21 | member.toHost(userId.value) 22 | memberRepository.save(member) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/group/member/MemberRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | import org.springframework.data.jpa.repository.Query 5 | 6 | internal interface MemberRepository : JpaRepository, CustomMemberRepository { 7 | fun findByNickname(nickname: String): MemberEntity? 8 | 9 | fun findByGroupId(groupId: Long): List 10 | 11 | fun findByNicknameAndGroupId(nickname: String, groupId: Long): MemberEntity? 12 | 13 | fun findByGroupIdAndUserId(groupId: Long, userId: Long): MemberEntity? 14 | 15 | @Query( 16 | "FROM MemberEntity m " + 17 | "LEFT JOIN FETCH m.gameMembers gm " + 18 | "WHERE m.groupId = :groupId " 19 | ) 20 | fun findWithGameMember(groupId: Long): List 21 | 22 | fun findByGroupIdAndRole(groupId: Long, role: MemberRole): List 23 | 24 | fun countByGroupId(groupId: Long): Long 25 | } 26 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/match/MatchCommandRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.match 2 | 3 | import com.yapp.bol.game.member.GameMember 4 | import com.yapp.bol.game.member.GameMemberRepository 5 | import com.yapp.bol.game.member.toEntity 6 | import org.springframework.stereotype.Repository 7 | import org.springframework.transaction.annotation.Transactional 8 | 9 | @Repository 10 | internal class MatchCommandRepositoryImpl( 11 | private val matchRepository: MatchRepository, 12 | private val gameMemberRepository: GameMemberRepository 13 | ) : MatchCommandRepository { 14 | @Transactional 15 | override fun createMatch(match: Match, gameMembers: List): Match { 16 | val match = matchRepository.save(match.toEntity()).toDomain() 17 | 18 | gameMemberRepository.saveAll( 19 | gameMembers.map { 20 | it.toEntity() 21 | } 22 | ) 23 | 24 | return match 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/match/MatchRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.match 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | 5 | internal interface MatchRepository : JpaRepository 6 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/match/member/MatchMemberEntity.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.match.member 2 | 3 | import com.yapp.bol.AuditingEntity 4 | import com.yapp.bol.group.member.MemberId 5 | import com.yapp.bol.match.MatchEntity 6 | import jakarta.persistence.Column 7 | import jakarta.persistence.Entity 8 | import jakarta.persistence.GeneratedValue 9 | import jakarta.persistence.GenerationType 10 | import jakarta.persistence.Id 11 | import jakarta.persistence.JoinColumn 12 | import jakarta.persistence.ManyToOne 13 | import jakarta.persistence.Table 14 | 15 | @Entity 16 | @Table(name = "match_member") 17 | class MatchMemberEntity : AuditingEntity() { 18 | @Id 19 | @Column(name = "match_member_id") 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | var id: Long = 0 22 | protected set 23 | 24 | @Column(name = "member_id") 25 | var memberId: Long = 0 26 | protected set 27 | 28 | @Column(name = "score") 29 | var score: Int = 0 30 | protected set 31 | 32 | @Column(name = "ranking") 33 | var ranking: Int = 0 34 | protected set 35 | 36 | @Column(name = "previous_score") 37 | var previousScore: Int = 0 38 | protected set 39 | 40 | @ManyToOne 41 | @JoinColumn(name = "match_id") 42 | lateinit var match: MatchEntity 43 | protected set 44 | 45 | companion object { 46 | fun of( 47 | id: Long, 48 | memberId: Long, 49 | score: Int, 50 | ranking: Int, 51 | match: MatchEntity 52 | ) = MatchMemberEntity().apply { 53 | this.id = id 54 | this.memberId = memberId 55 | this.score = score 56 | this.ranking = ranking 57 | this.match = match 58 | } 59 | } 60 | } 61 | 62 | internal fun MatchMember.toEntity(match: MatchEntity): MatchMemberEntity = MatchMemberEntity.of( 63 | id = this.id.value, 64 | memberId = this.memberId.value, 65 | score = this.score, 66 | ranking = this.ranking, 67 | match = match, 68 | ) 69 | 70 | internal fun MatchMemberEntity.toDomain(): MatchMember = MatchMember( 71 | id = MatchMemberId(id), 72 | memberId = MemberId(memberId), 73 | score = score, 74 | ranking = ranking, 75 | ) 76 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/season/SeasonCommandRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.season 2 | 3 | import org.springframework.stereotype.Repository 4 | 5 | @Repository 6 | internal class SeasonCommandRepositoryImpl( 7 | private val seasonRepository: SeasonRepository 8 | ) : SeasonCommandRepository { 9 | override fun createSeason(season: Season): Season { 10 | return seasonRepository.save(season.toEntity()).toDomain() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/season/SeasonEntity.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.season 2 | 3 | import com.yapp.bol.AuditingEntity 4 | import com.yapp.bol.group.GroupId 5 | import jakarta.persistence.Column 6 | import jakarta.persistence.Entity 7 | import jakarta.persistence.GeneratedValue 8 | import jakarta.persistence.GenerationType 9 | import jakarta.persistence.Id 10 | import jakarta.persistence.Table 11 | 12 | @Entity 13 | @Table(name = "season") 14 | class SeasonEntity : AuditingEntity() { 15 | @Id 16 | @Column(name = "season_id") 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | var id: Long = 0 19 | protected set 20 | 21 | @Column(name = "group_id") 22 | var groupId: Long = 0 23 | protected set 24 | 25 | companion object { 26 | fun of(id: Long, groupId: Long): SeasonEntity { 27 | return SeasonEntity().apply { 28 | this.id = id 29 | this.groupId = groupId 30 | } 31 | } 32 | } 33 | } 34 | 35 | internal fun Season.toEntity(): SeasonEntity = SeasonEntity.of( 36 | id = this.id.value, 37 | groupId = this.groupId.value, 38 | ) 39 | 40 | internal fun SeasonEntity.toDomain(): Season = Season( 41 | id = SeasonId(id), 42 | groupId = GroupId(groupId), 43 | ) 44 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/season/SeasonQueryRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.season 2 | 3 | import com.yapp.bol.group.GroupId 4 | import org.springframework.stereotype.Repository 5 | 6 | @Repository 7 | internal class SeasonQueryRepositoryImpl( 8 | private val seasonRepository: SeasonRepository 9 | ) : SeasonQueryRepository { 10 | override fun getSeason(groupId: GroupId): Season? { 11 | return seasonRepository.findByGroupId(groupId.value)?.toDomain() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/season/SeasonRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.season 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | 5 | internal interface SeasonRepository : JpaRepository { 6 | fun findByGroupId(groupId: Long): SeasonEntity? 7 | } 8 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/terms/TermsAgreeEntity.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.terms 2 | 3 | import com.yapp.bol.AuditingEntity 4 | import jakarta.persistence.Column 5 | import jakarta.persistence.Entity 6 | import jakarta.persistence.EnumType 7 | import jakarta.persistence.Enumerated 8 | import jakarta.persistence.GeneratedValue 9 | import jakarta.persistence.GenerationType 10 | import jakarta.persistence.Id 11 | import jakarta.persistence.Table 12 | 13 | @Entity 14 | @Table(name = "agreed_terms") 15 | internal class TermsAgreeEntity : AuditingEntity() { 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | @Column(name = "agreed_terms_id") 19 | var id: Long = 0 20 | protected set 21 | 22 | @Column(name = "user_id") 23 | var userId: Long = 0 24 | protected set 25 | 26 | @Column(name = "code") 27 | @Enumerated(EnumType.STRING) 28 | lateinit var code: TermsCode 29 | protected set 30 | 31 | @Column(name = "is_agree") 32 | var isAgree: Boolean = true 33 | protected set 34 | 35 | companion object { 36 | fun of( 37 | userId: Long, 38 | code: TermsCode, 39 | isAgree: Boolean 40 | ): TermsAgreeEntity = TermsAgreeEntity().apply { 41 | this.userId = userId 42 | this.code = code 43 | this.isAgree = isAgree 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/terms/TermsAgreeRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.terms 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | 5 | internal interface TermsAgreeRepository : JpaRepository { 6 | 7 | fun findByUserId(userId: Long): List 8 | fun findByUserIdAndCode(userId: Long, code: TermsCode): TermsAgreeEntity? 9 | } 10 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/terms/TermsCommandRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.terms 2 | 3 | import com.yapp.bol.auth.UserId 4 | import jakarta.transaction.Transactional 5 | import org.springframework.stereotype.Component 6 | 7 | @Component 8 | internal class TermsCommandRepositoryImpl( 9 | private val termsAgreeRepository: TermsAgreeRepository, 10 | ) : TermsCommandRepository { 11 | 12 | @Transactional 13 | override fun saveTermsAgreeInfo(userId: UserId, termsCode: List) { 14 | val list = termsAgreeRepository.findByUserId(userId.value) 15 | 16 | val entities = termsCode 17 | .filter { info -> list.any { entity -> info.termsCode == entity.code }.not() } 18 | .map { TermsAgreeEntity.of(userId.value, it.termsCode, it.isAgree) } 19 | 20 | termsAgreeRepository.saveAll(entities) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/terms/TermsQueryRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.terms 2 | 3 | import com.yapp.bol.auth.UserId 4 | import org.springframework.stereotype.Component 5 | 6 | @Component 7 | internal class TermsQueryRepositoryImpl( 8 | private val termsAgreeRepository: TermsAgreeRepository, 9 | ) : TermsQueryRepository { 10 | 11 | override fun getSavedTermsByUserId(userId: UserId): List { 12 | return termsAgreeRepository.findByUserId(userId.value) 13 | .map { TermsAgreeInfo(it.code, it.isAgree) } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/user/UserClient.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.user 2 | 3 | import com.yapp.bol.NotFoundUserException 4 | import com.yapp.bol.auth.UserId 5 | import jakarta.transaction.Transactional 6 | import org.springframework.data.repository.findByIdOrNull 7 | import org.springframework.stereotype.Component 8 | 9 | @Component 10 | internal class UserClient( 11 | private val userRepository: UserRepository, 12 | ) : UserQueryRepository, UserCommandRepository { 13 | override fun getUser(userId: UserId): User? { 14 | val userEntity = userRepository.findByIdOrNull(userId.value) ?: throw NotFoundUserException 15 | 16 | return userEntity.toDomain() 17 | } 18 | 19 | @Transactional 20 | override fun updateUser(user: User) { 21 | val entity = userRepository.findByIdOrNull(user.id.value) ?: throw NotFoundUserException 22 | 23 | entity.name = user.nickname 24 | 25 | userRepository.save(entity) 26 | } 27 | } 28 | 29 | private fun UserEntity.toDomain(): User = User( 30 | id = UserId(this.id), 31 | nickname = this.name 32 | ) 33 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/user/UserEntity.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.user 2 | 3 | import com.yapp.bol.AuditingEntity 4 | import jakarta.persistence.Column 5 | import jakarta.persistence.Entity 6 | import jakarta.persistence.GeneratedValue 7 | import jakarta.persistence.GenerationType 8 | import jakarta.persistence.Id 9 | import jakarta.persistence.Table 10 | 11 | @Entity 12 | @Table(name = "users") 13 | internal class UserEntity : AuditingEntity() { 14 | @Id 15 | @GeneratedValue(strategy = GenerationType.IDENTITY) 16 | @Column(name = "users_id", nullable = false) 17 | var id: Long = 0 18 | protected set 19 | 20 | @Column(name = "name") 21 | var name: String? = null 22 | 23 | @Column(name = "deleted") 24 | var deleted: Boolean = false 25 | protected set 26 | 27 | companion object { 28 | fun of(id: Long): UserEntity = 29 | UserEntity().apply { 30 | this.id = id 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/kotlin/com/yapp/bol/user/UserRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.user 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository 4 | 5 | internal interface UserRepository : JpaRepository 6 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/resources/application-rdb.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | type: com.zaxxer.hikari.HikariDataSource 4 | driver-class-name: com.mysql.cj.jdbc.Driver 5 | hikari: 6 | poolName: Hikari 7 | auto-commit: true 8 | minimum-idle: 1 9 | maximum-pool-size: 5 10 | jpa: 11 | database-platform: org.hibernate.dialect.MySQL8Dialect 12 | database: MYSQL 13 | generate-ddl: false 14 | show-sql: false 15 | open-in-view: false 16 | hibernate: 17 | ddl-auto: validate 18 | cloud: 19 | aws: 20 | region: ap-northeast-2 21 | credentials: 22 | access-key: AKIATGEE7OOGP65GA6OG 23 | 24 | --- 25 | spring.config.activate.on-profile: local, sandbox 26 | spring: 27 | datasource: 28 | url: jdbc:mysql://board-sandbox.c5oh2un2pvf0.ap-northeast-2.rds.amazonaws.com/board_sandbox?useUnicode=true&character_set_server=utf8mb4 29 | username: yapp 30 | password: yappyapp2 31 | jpa: 32 | show-sql: true 33 | properties: 34 | hibernate: 35 | format_sql: true 36 | 37 | cloud: 38 | aws: 39 | s3: 40 | bucket: bol-sandbox 41 | 42 | --- 43 | spring.config.activate.on-profile: production 44 | spring: 45 | datasource: 46 | url: jdbc:mysql://board-sandbox.c5oh2un2pvf0.ap-northeast-2.rds.amazonaws.com/board_prod?useUnicode=true&character_set_server=utf8mb4 47 | username: yapp 48 | password: yappyapp2 49 | 50 | cloud: 51 | aws: 52 | s3: 53 | bucket: bol-prod 54 | --- 55 | spring.config.activate.on-profile: local 56 | 57 | bol: 58 | server: 59 | host: http://localhost:8080 60 | --- 61 | spring.config.activate.on-profile: test 62 | spring: 63 | datasource: 64 | type: com.zaxxer.hikari.HikariDataSource 65 | url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE; 66 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/resources/sql/ddl/2023_05_14_CREATE_AUTH.sql: -------------------------------------------------------------------------------- 1 | create table auth_social 2 | ( 3 | social_type varchar(10) NOT NULL, 4 | social_id varchar(50) NOT NULL, 5 | users_id bigint NOT NULL, 6 | created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 8 | PRIMARY KEY auth_social (social_id, social_type) 9 | ); 10 | 11 | CREATE TABLE auth_access_token 12 | ( 13 | access_token_id bigint NOT NULL AUTO_INCREMENT, 14 | access_token binary(30) NOT NULL, 15 | users_id bigint NOT NULL, 16 | expired_at DATETIME NOT NULL, 17 | created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 18 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 19 | PRIMARY KEY PK_authAccessToken (access_token_id), 20 | UNIQUE KEY idx_accesstoken (access_token) 21 | ); 22 | 23 | CREATE TABLE auth_refresh_token 24 | ( 25 | refresh_token_id bigint NOT NULL AUTO_INCREMENT, 26 | refresh_token binary(45) NOT NULL, 27 | users_id bigint NOT NULL, 28 | expired_at DATETIME NOT NULL, 29 | created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 30 | updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 31 | PRIMARY KEY PK_authRefreshToken (refresh_token_id), 32 | UNIQUE KEY idx_refreshtoken (refresh_token) 33 | ); 34 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/resources/sql/ddl/2023_05_14_CREATE_USER.sql: -------------------------------------------------------------------------------- 1 | create table users 2 | ( 3 | users_id bigint NOT NULL AUTO_INCREMENT, 4 | name varchar(20), 5 | created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 6 | updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 7 | deleted bit NOT NULL, 8 | PRIMARY KEY PK_users (users_id) 9 | ); 10 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/resources/sql/ddl/2023_06_03_CREATE_FILE.sql: -------------------------------------------------------------------------------- 1 | create table file 2 | ( 3 | file_id bigint NOT NULL AUTO_INCREMENT, 4 | name varchar(50) NOT NULL, 5 | users_id bigint NOT NULL, 6 | purpose varchar(20) NOT NULL, 7 | created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 9 | PRIMARY KEY PK_file (file_id) 10 | ); 11 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/resources/sql/ddl/2023_06_06_CREATE_GROUP.sql: -------------------------------------------------------------------------------- 1 | create table group_table 2 | ( 3 | group_id bigint not null auto_increment, 4 | name varchar(14) not null, 5 | description varchar(72) not null, 6 | organization varchar(15), 7 | profile_image_url varchar(255) null, 8 | access_code varchar(8) not null, 9 | deleted tinyint(1) default 0 not null, 10 | created_at datetime not null default current_timestamp, 11 | updated_at datetime not null default current_timestamp on update current_timestamp, 12 | primary key pk_groups (group_id) 13 | ); 14 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/resources/sql/ddl/2023_06_06_CREATE_MEMBER.sql: -------------------------------------------------------------------------------- 1 | create table member 2 | ( 3 | member_id bigint not null auto_increment, 4 | users_id bigint null, 5 | role varchar(255) null, 6 | nickname varchar(255) null, 7 | level int not null default 0, 8 | deleted tinyint(1) default 0 not null, 9 | group_id bigint null, 10 | created_at datetime not null default current_timestamp, 11 | updated_at datetime not null default current_timestamp on update current_timestamp, 12 | primary key pk_member (member_id), 13 | key idx_groupid (group_id), 14 | unique unq_userid_groupid (users_id, group_id) 15 | ); 16 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/resources/sql/ddl/2023_06_13_CREATE_GAME.sql: -------------------------------------------------------------------------------- 1 | create table game 2 | ( 3 | game_id bigint NOT NULL AUTO_INCREMENT COMMENT 'PK', 4 | name varchar(50) NOT NULL COMMENT '게임 이름', 5 | min_member int NOT NULL COMMENT '게임 최소 인원수', 6 | max_member int NOT NULL COMMENT '게임 최대 인원수', 7 | rank_type varchar(20) NOT NULL COMMENT '게임 등수 정책', 8 | img_id bigint NOT NULL COMMENT '이미지 File id', 9 | created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 일시', 10 | updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '최근 수정 일시', 11 | PRIMARY KEY PK_game (game_id) 12 | ) COMMENT 'Game 메타 데이터'; 13 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/resources/sql/ddl/2023_07_01_CREATE_GAME_MEMBER.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE game_member 2 | ( 3 | game_member_id bigint NOT NULL AUTO_INCREMENT COMMENT 'PK', 4 | game_id bigint NOT NULL COMMENT '게임 ID', 5 | member_id bigint NOT NULL COMMENT '멤버 ID', 6 | season_id bigint NOT NULL COMMENT '시즌 ID', 7 | final_score int NOT NULL COMMENT '최종 점수', 8 | match_count int NOT NULL COMMENT '매치 수', 9 | winning_percentage DOUBLE NOT NULL COMMENT '승률', 10 | created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 일시', 11 | updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '최근 수정 일시', 12 | PRIMARY KEY PK_game_member (game_member_id) 13 | ) COMMENT '게임 멤버 테이블'; 14 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/resources/sql/ddl/2023_07_01_CREATE_MATCH.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE match_table 2 | ( 3 | match_id bigint NOT NULL AUTO_INCREMENT COMMENT 'PK', 4 | memo varchar(255) COMMENT '매치 기록', 5 | match_image_url varchar(255) COMMENT '매치 이미지 url', 6 | matched_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '매치 날짜', 7 | member_count int NOT NULL COMMENT '멤버 수', 8 | season_id bigint NOT NULL COMMENT '시즌 ID', 9 | game_id bigint NOT NULL COMMENT '게임 ID', 10 | group_id bigint NOT NULL COMMENT '그룹 ID', 11 | created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 일시', 12 | updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '최근 수정 일시', 13 | PRIMARY KEY PK_match (match_id) 14 | ) COMMENT '매치 테이블'; 15 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/resources/sql/ddl/2023_07_01_CREATE_MATCH_MEMBER.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE match_member 2 | ( 3 | match_member_id bigint NOT NULL AUTO_INCREMENT COMMENT 'PK', 4 | match_id bigint NOT NULL COMMENT '매치 id', 5 | member_id bigint NOT NULL COMMENT '멤버 id', 6 | score int NOT NULL COMMENT '매치 점수', 7 | previous_score int NOT NULL COMMENT '직전 점수', 8 | ranking int NOT NULL COMMENT '매치 순위', 9 | created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 일시', 10 | updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '최근 수정 일시', 11 | PRIMARY KEY PK_match_member (match_member_id), 12 | KEY IDX_matchId (match_id) 13 | ) COMMENT '매치 멤버 테이블'; 14 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/resources/sql/ddl/2023_07_01_CREATE_SEASON.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE season 2 | ( 3 | season_id bigint NOT NULL AUTO_INCREMENT COMMENT 'PK', 4 | group_id bigint NOT NULL COMMENT '그룹 ID', 5 | created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 일시', 6 | updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '최근 수정 일시', 7 | PRIMARY KEY PK_match (season_id) 8 | ) COMMENT '시즌 테이블'; 9 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/resources/sql/ddl/2023_07_10_CREATE_AGREED_TERMS.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE agreed_terms 2 | ( 3 | agreed_terms_id bigint NOT NULL AUTO_INCREMENT COMMENT 'PK', 4 | user_id bigint NOT NULL COMMENT '유저 ID', 5 | code varchar(20) NOT NULL COMMENT '동의한 약관 코드', 6 | is_agree bit NOT NULL COMMENT '동의 여부', 7 | created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 일시', 8 | updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '최근 수정 일시', 9 | PRIMARY KEY PK_match (agreed_terms_id) 10 | ) COMMENT '동의한 약관 정보'; 11 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/main/resources/sql/dml/2023_06_13_INSERT_GAMES.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `file` (`file_id`, `name`, `users_id`, `purpose`) 2 | VALUES (141, '6db3f834-5984-4d8c-b1db-4c79e838a45c', 4, 'GAME_IMAGE'), 3 | (140, 'b350d885-fd9c-4c77-908c-ef50e9c2dc65', 4, 'GAME_IMAGE'), 4 | (139, 'c30ca198-940c-48ab-8737-9eecca29dec3', 4, 'GAME_IMAGE'), 5 | (138, 'a53c8520-0a59-45c4-ad5a-39cfb9dd49db', 4, 'GAME_IMAGE'), 6 | (137, '3c354d19-42ff-4d39-a548-e34120ce30cb', 4, 'GAME_IMAGE'), 7 | (136, 'f3e5b9cf-f5f1-49ff-b948-b36f7a552bcc', 4, 'GAME_IMAGE'), 8 | (135, '35eca27a-b39b-4734-8cb8-3ab87af9488b', 4, 'GAME_IMAGE'), 9 | (134, '403b1539-cc60-44f5-95cd-641fb982a22d', 4, 'GAME_IMAGE'), 10 | (133, '7096fb3b-ff23-4296-a805-529374e5760c', 4, 'GAME_IMAGE'), 11 | (132, 'c6cf147a-b8f6-4a77-a533-633c6b0cb3c1', 4, 'GAME_IMAGE'), 12 | (131, '17dead0a-2de5-49d4-ad0a-11ec72fb0f5a', 4, 'GAME_IMAGE'), 13 | (130, '2aff27b8-656e-499c-a212-932ef8e52fcb', 4, 'GAME_IMAGE'); 14 | 15 | INSERT INTO `game` (`name`, `min_member`, `max_member`, `rank_type`, `img_id`) 16 | VALUES ('스플랜더', 2, 4, 'SCORE_HIGH', 130), 17 | ('테라포밍 마스', 2, 2, 'SCORE_HIGH', 131), 18 | ('세븐원더스 듀얼', 2, 4, 'SCORE_HIGH', 132), 19 | ('테라 미스티카', 2, 4, 'SCORE_HIGH', 134), 20 | ('아그리콜라', 2, 4, 'SCORE_HIGH', 135), 21 | ('윙스펜', 2, 5, 'SCORE_HIGH', 136), 22 | ('가이아 프로젝트', 2, 5, 'SCORE_HIGH', 133), 23 | ('푸에르토리코', 3, 5, 'SCORE_HIGH', 137), 24 | ('브라스: 버밍엄', 2, 4, 'SCORE_HIGH', 138), 25 | ('스컬킹', 2, 6, 'SCORE_HIGH', 139), 26 | ('듄: 임페리움', 2, 4, 'SCORE_HIGH', 140), 27 | ('메이지나이트', 1, 4, 'SCORE_HIGH', 141); 28 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/test/kotlin/com/yapp/bol/auth/AuthCommandRepositoryImplTest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | import com.yapp.bol.user.UserEntity 4 | import com.yapp.bol.user.UserRepository 5 | import io.kotest.core.spec.style.FunSpec 6 | import io.kotest.matchers.shouldBe 7 | import io.mockk.every 8 | import io.mockk.mockk 9 | import io.mockk.verify 10 | 11 | class AuthCommandRepositoryImplTest : FunSpec() { 12 | private val authSocialRepository: AuthSocialRepository = mockk() 13 | private val userRepository: UserRepository = mockk() 14 | 15 | private val sut = AuthCommandRepositoryImpl(authSocialRepository, userRepository) 16 | 17 | init { 18 | test("registerUserTest") { 19 | // given 20 | val loginType = LoginType.KAKAO_ACCESS_TOKEN 21 | val socialId = "kakaoID" 22 | val user = UserEntity.of(1234L) 23 | 24 | every { userRepository.save(any()) } returns user 25 | every { authSocialRepository.save(any()) } returns AuthSocialEntity( 26 | loginType.toSocialType(), 27 | socialId, 28 | user.id, 29 | ) 30 | 31 | // when 32 | val result = sut.registerUser(loginType, socialId) 33 | 34 | // then 35 | verify { userRepository.save(any()) } 36 | verify { authSocialRepository.save(any()) } 37 | result.id.value shouldBe user.id 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /adapter-out/rdb/src/test/kotlin/com/yapp/bol/auth/token/TokenCommandRepositoryImplTest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.token 2 | 3 | import com.yapp.bol.auth.Token 4 | import com.yapp.bol.auth.UserId 5 | import io.kotest.core.spec.style.FunSpec 6 | import io.mockk.every 7 | import io.mockk.mockk 8 | import io.mockk.verify 9 | import java.time.LocalDateTime 10 | 11 | class TokenCommandRepositoryImplTest : FunSpec() { 12 | private val accessTokenRepository: AccessTokenRepository = mockk() 13 | private val refreshTokenRepository: RefreshTokenRepository = mockk() 14 | private val sut = TokenCommandRepositoryImpl( 15 | accessTokenRepository, 16 | refreshTokenRepository, 17 | ) 18 | 19 | init { 20 | val token = Token( 21 | userId = UserId(123L), 22 | value = "123123asdfasdf", 23 | expiredAt = LocalDateTime.now().plusDays(1), 24 | ) 25 | 26 | test("saveAccessToken") { 27 | // given 28 | every { accessTokenRepository.save(any()) } returns AccessTokenEntity( 29 | userId = 123L, 30 | accessToken = ByteArray(20), 31 | expiredAt = token.expiredAt, 32 | ) 33 | 34 | // when 35 | sut.saveAccessToken(token) 36 | 37 | // then 38 | verify { accessTokenRepository.save(any()) } 39 | } 40 | 41 | test("saveRefreshToken") { 42 | // given 43 | every { refreshTokenRepository.save(any()) } returns RefreshTokenEntity( 44 | userId = 123L, 45 | refreshToken = ByteArray(20), 46 | expiredAt = token.expiredAt, 47 | ) 48 | 49 | // when 50 | sut.saveRefreshToken(token) 51 | 52 | // then 53 | verify { refreshTokenRepository.save(any()) } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /adapter-out/social/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Eclipse ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | 41 | ### Mac OS ### 42 | .DS_Store -------------------------------------------------------------------------------- /adapter-out/social/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | plugins { 4 | id("org.springframework.boot") 5 | kotlin("plugin.spring") 6 | } 7 | 8 | dependencies { 9 | val springVersion by properties 10 | val jjwtVersion by properties 11 | 12 | api(project(":domain")) 13 | api(project(":port-out")) 14 | 15 | implementation("org.springframework.boot:spring-boot-starter-webflux:$springVersion") 16 | implementation("com.google.api-client:google-api-client:1.32.1") 17 | } 18 | 19 | tasks { 20 | withType { enabled = true } 21 | withType { enabled = false } 22 | } 23 | -------------------------------------------------------------------------------- /adapter-out/social/src/main/kotlin/com/yapp/bol/social/SocialLoginClientFacade.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.social 2 | 3 | import com.yapp.bol.auth.LoginType 4 | import com.yapp.bol.auth.SocialUser 5 | import com.yapp.bol.auth.social.SocialLoginClient 6 | import com.yapp.bol.social.google.GoogleIdTokenLoginClient 7 | import com.yapp.bol.social.kakao.KakaoAccessTokenLoginClient 8 | import com.yapp.bol.social.naver.NaverAccessTokenLoginClient 9 | import org.springframework.stereotype.Component 10 | 11 | @Component 12 | internal class SocialLoginClientFacade( 13 | private val naverSocialLoginClient: NaverAccessTokenLoginClient, 14 | private val kakaoSocialLoginClient: KakaoAccessTokenLoginClient, 15 | private val googleIdTokenLoginClient: GoogleIdTokenLoginClient, 16 | ) : SocialLoginClient { 17 | 18 | override fun login(loginType: LoginType, token: String): SocialUser = 19 | when (loginType) { 20 | LoginType.NAVER_ACCESS_TOKEN -> naverSocialLoginClient.login(token) 21 | LoginType.KAKAO_ACCESS_TOKEN -> kakaoSocialLoginClient.login(token) 22 | LoginType.GOOGLE_ID_TOKEN -> googleIdTokenLoginClient.login(token) 23 | LoginType.REFRESH -> throw UnsupportedOperationException() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /adapter-out/social/src/main/kotlin/com/yapp/bol/social/google/GoogleApiConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.social.google 2 | 3 | import org.springframework.boot.context.properties.EnableConfigurationProperties 4 | import org.springframework.context.annotation.Configuration 5 | 6 | @Configuration 7 | @EnableConfigurationProperties(GoogleApiProperties::class) 8 | class GoogleApiConfiguration 9 | -------------------------------------------------------------------------------- /adapter-out/social/src/main/kotlin/com/yapp/bol/social/google/GoogleApiProperties.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.social.google 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties 4 | 5 | @ConfigurationProperties("social.google") 6 | data class GoogleApiProperties( 7 | val clientId: String, 8 | ) 9 | -------------------------------------------------------------------------------- /adapter-out/social/src/main/kotlin/com/yapp/bol/social/google/GoogleIdTokenLoginClient.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.social.google 2 | 3 | import com.yapp.bol.SocialLoginFailedException 4 | import com.yapp.bol.auth.SocialUser 5 | import org.springframework.stereotype.Component 6 | 7 | @Component 8 | internal class GoogleIdTokenLoginClient( 9 | private val googleIdTokenService: GoogleIdTokenService, 10 | ) { 11 | fun login(token: String): SocialUser { 12 | val googleUserId = googleIdTokenService.getUserId(token) ?: throw SocialLoginFailedException() 13 | 14 | return GoogleSocialUser(id = googleUserId) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /adapter-out/social/src/main/kotlin/com/yapp/bol/social/google/GoogleIdTokenService.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.social.google 2 | 3 | import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier 4 | import com.google.api.client.http.javanet.NetHttpTransport 5 | import com.google.api.client.json.gson.GsonFactory 6 | import com.yapp.bol.SocialLoginFailedException 7 | import java.security.GeneralSecurityException 8 | import org.springframework.stereotype.Component 9 | 10 | @Component 11 | internal class GoogleIdTokenService( 12 | private val googleApiProperties: GoogleApiProperties, 13 | ) { 14 | private val idTokenVerifier = 15 | GoogleIdTokenVerifier.Builder(NetHttpTransport(), GsonFactory.getDefaultInstance()) 16 | .setAudience(listOf(googleApiProperties.clientId)) 17 | .build() 18 | 19 | fun getUserId(token: String): String? { 20 | try { 21 | val idToken = idTokenVerifier.verify(token) 22 | return idToken?.payload?.subject 23 | } catch (e: GeneralSecurityException) { 24 | e.printStackTrace() 25 | throw SocialLoginFailedException(e) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /adapter-out/social/src/main/kotlin/com/yapp/bol/social/google/GoogleSocialUser.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.social.google 2 | 3 | import com.yapp.bol.auth.SocialUser 4 | 5 | data class GoogleSocialUser(override val id: String) : SocialUser 6 | -------------------------------------------------------------------------------- /adapter-out/social/src/main/kotlin/com/yapp/bol/social/kakao/KakaoAccessTokenLoginClient.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.social.kakao 2 | 3 | import com.yapp.bol.SocialLoginFailedException 4 | import com.yapp.bol.auth.SocialUser 5 | import org.springframework.stereotype.Component 6 | 7 | @Component 8 | internal class KakaoAccessTokenLoginClient( 9 | private val kakaoAuthClient: KakaoOpenApiClient, 10 | ) { 11 | fun login(token: String): SocialUser = 12 | kakaoAuthClient.getTokenInfo("Bearer $token").block() 13 | ?: throw SocialLoginFailedException() 14 | } 15 | -------------------------------------------------------------------------------- /adapter-out/social/src/main/kotlin/com/yapp/bol/social/kakao/KakaoConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.social.kakao 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.web.reactive.function.client.WebClient 6 | import org.springframework.web.reactive.function.client.support.WebClientAdapter 7 | import org.springframework.web.service.invoker.HttpServiceProxyFactory 8 | 9 | @Configuration 10 | class KakaoConfiguration { 11 | @Bean 12 | internal fun kakaoOpenApiClient(): KakaoOpenApiClient { 13 | val webClient = WebClient.builder().baseUrl(KAKAO_OPEN_API_URL).build() 14 | val factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient)).build() 15 | 16 | return factory.createClient(KakaoOpenApiClient::class.java) 17 | } 18 | 19 | companion object { 20 | private const val KAKAO_OPEN_API_URL = "https://kapi.kakao.com" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /adapter-out/social/src/main/kotlin/com/yapp/bol/social/kakao/KakaoOpenApiClient.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.social.kakao 2 | 3 | import com.yapp.bol.social.kakao.dto.KakaoUserResponse 4 | import org.springframework.web.bind.annotation.RequestHeader 5 | import org.springframework.web.service.annotation.GetExchange 6 | import reactor.core.publisher.Mono 7 | 8 | internal interface KakaoOpenApiClient { 9 | @GetExchange("/v2/user/me") 10 | fun getTokenInfo( 11 | @RequestHeader("Authorization") authorization: String, 12 | ): Mono 13 | } 14 | -------------------------------------------------------------------------------- /adapter-out/social/src/main/kotlin/com/yapp/bol/social/kakao/dto/KakaoUserResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.social.kakao.dto 2 | 3 | import com.yapp.bol.auth.SocialUser 4 | 5 | internal data class KakaoUserResponse( 6 | override val id: String, 7 | // @JsonProperty("kakao_account") val kakaoAccount: KakaoAccount, 동의 항목 필요 8 | ) : SocialUser 9 | -------------------------------------------------------------------------------- /adapter-out/social/src/main/kotlin/com/yapp/bol/social/naver/NaverAccessTokenLoginClient.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.social.naver 2 | 3 | import com.yapp.bol.SocialLoginFailedException 4 | import com.yapp.bol.auth.SocialUser 5 | import org.springframework.stereotype.Component 6 | 7 | @Component 8 | internal class NaverAccessTokenLoginClient( 9 | private val naverAuthClient: NaverOpenApiClient, 10 | ) { 11 | fun login(token: String): SocialUser { 12 | val userResponse = naverAuthClient.getUserProfile("Bearer $token").block() 13 | ?: throw SocialLoginFailedException() 14 | 15 | if (userResponse.resultCode == "00") return userResponse.userInfo 16 | 17 | throw SocialLoginFailedException() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /adapter-out/social/src/main/kotlin/com/yapp/bol/social/naver/NaverConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.social.naver 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.web.reactive.function.client.WebClient 6 | import org.springframework.web.reactive.function.client.support.WebClientAdapter 7 | import org.springframework.web.service.invoker.HttpServiceProxyFactory 8 | 9 | @Configuration 10 | class NaverConfiguration { 11 | @Bean 12 | internal fun naverOpenApiClient(): NaverOpenApiClient { 13 | val webClient = WebClient.builder().baseUrl(NAVER_OPEN_API_URL).build() 14 | val factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient)).build() 15 | return factory.createClient(NaverOpenApiClient::class.java) 16 | } 17 | 18 | companion object { 19 | private const val NAVER_OPEN_API_URL = "https://openapi.naver.com" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /adapter-out/social/src/main/kotlin/com/yapp/bol/social/naver/NaverOpenApiClient.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.social.naver 2 | 3 | import com.yapp.bol.social.naver.dto.NaverUserResponse 4 | import org.springframework.web.bind.annotation.RequestHeader 5 | import org.springframework.web.service.annotation.GetExchange 6 | import reactor.core.publisher.Mono 7 | 8 | internal interface NaverOpenApiClient { 9 | @GetExchange("/v1/nid/me") 10 | fun getUserProfile( 11 | @RequestHeader("Authorization") authorization: String, 12 | ): Mono 13 | } 14 | -------------------------------------------------------------------------------- /adapter-out/social/src/main/kotlin/com/yapp/bol/social/naver/dto/NaverAuthResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.social.naver.dto 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | import com.yapp.bol.auth.SocialUser 5 | 6 | internal data class NaverUserResponse( 7 | @JsonProperty("resultcode") val resultCode: String, 8 | val message: String, 9 | @JsonProperty("response") val userInfo: NaverUserInfoResponse, 10 | ) 11 | 12 | internal class NaverUserInfoResponse( 13 | override val id: String, 14 | // 네이버 콘솔에서 추가 필요 15 | // val nickname: String, 16 | // val email: String, 17 | ) : SocialUser 18 | -------------------------------------------------------------------------------- /adapter-out/social/src/main/resources/application-social.yml: -------------------------------------------------------------------------------- 1 | --- 2 | spring.config.activate.on-profile: local, sandbox 3 | social: 4 | google: 5 | client-id: 407255596267-oh8srvr5ddpv0qd53kdceeu8e7thjeld.apps.googleusercontent.com 6 | 7 | --- 8 | spring.config.activate.on-profile: production 9 | social: 10 | google: 11 | client-id: 499798963911-c1vtj0pvein1vel8e5v4li5ih634bh3r.apps.googleusercontent.com 12 | 13 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | import org.springframework.boot.gradle.tasks.bundling.BootJar 3 | 4 | plugins { 5 | kotlin("jvm") version "1.7.22" 6 | kotlin("kapt") version "1.7.22" 7 | id("org.jlleitschuh.gradle.ktlint") version "10.2.0" 8 | id("io.spring.dependency-management") version "1.1.0" 9 | id("org.springframework.boot") 10 | } 11 | 12 | repositories { 13 | mavenCentral() 14 | google() 15 | } 16 | 17 | tasks { 18 | withType { enabled = true } 19 | withType { enabled = false } 20 | } 21 | 22 | subprojects { 23 | apply(plugin = "kotlin") 24 | apply(plugin = "kotlin-kapt") 25 | apply(plugin = "org.jlleitschuh.gradle.ktlint") 26 | 27 | repositories { 28 | mavenCentral() 29 | google() 30 | } 31 | 32 | dependencies { 33 | val kotestVersion: String by properties 34 | 35 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.0") 36 | 37 | testImplementation("io.kotest:kotest-runner-junit5-jvm:$kotestVersion") 38 | testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") 39 | testImplementation("io.mockk:mockk:1.13.5") 40 | } 41 | 42 | tasks { 43 | withType { 44 | kotlinOptions { 45 | freeCompilerArgs = listOf("-Xjsr305=strict") 46 | jvmTarget = "17" 47 | } 48 | } 49 | test { 50 | useJUnitPlatform() 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | plugins { 4 | kotlin("plugin.spring") 5 | } 6 | 7 | dependencies { 8 | val springVersion by properties 9 | val jjwtVersion by properties 10 | 11 | api(project(":port-in")) 12 | implementation(project(":adapter-out:rdb")) 13 | implementation(project(":adapter-out:social")) 14 | 15 | implementation("org.springframework.boot:spring-boot-starter:$springVersion") 16 | implementation("io.jsonwebtoken:jjwt-api:$jjwtVersion") 17 | runtimeOnly("io.jsonwebtoken:jjwt-impl:$jjwtVersion") 18 | runtimeOnly("io.jsonwebtoken:jjwt-jackson:$jjwtVersion") 19 | 20 | testImplementation("org.springframework.boot:spring-boot-starter-test:$springVersion") 21 | } 22 | 23 | tasks { 24 | withType { enabled = true } 25 | withType { enabled = false } 26 | } 27 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/Extension.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol 2 | 3 | import java.time.LocalDateTime 4 | import java.time.ZoneId 5 | import java.util.Date 6 | 7 | fun LocalDateTime.toDate(): Date { 8 | val instant = this.atZone(ZoneId.systemDefault()).toInstant() 9 | return Date.from(instant) 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/auth/AuthConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | import com.yapp.bol.auth.token.OpaqueTokenUtils 4 | import com.yapp.bol.auth.token.TokenCommandRepository 5 | import com.yapp.bol.auth.token.TokenPolicy 6 | import java.time.Duration 7 | import org.springframework.beans.factory.annotation.Value 8 | import org.springframework.context.annotation.Bean 9 | import org.springframework.context.annotation.Configuration 10 | 11 | @Configuration 12 | class AuthConfiguration { 13 | 14 | @Value("\${auth.access-token.expire-duration}") 15 | lateinit var accessTokenExpireDuration: Duration 16 | 17 | @Value("\${auth.refresh-token.expire-duration}") 18 | lateinit var refreshTokenExpireDuration: Duration 19 | 20 | @Bean 21 | internal fun accessTokenPolicy(): TokenPolicy = TokenPolicy( 22 | OpaqueTokenUtils(40), 23 | accessTokenExpireDuration, 24 | ) 25 | 26 | @Bean 27 | internal fun refreshTokenPolicy(): TokenPolicy = TokenPolicy( 28 | OpaqueTokenUtils(60), 29 | refreshTokenExpireDuration, 30 | ) 31 | 32 | @Bean 33 | internal fun tokenService( 34 | tokenQueryRepository: TokenQueryRepository, 35 | tokenCommandRepository: TokenCommandRepository, 36 | ): TokenService = 37 | TokenService( 38 | accessTokenPolicy(), 39 | refreshTokenPolicy(), 40 | tokenQueryRepository, 41 | tokenCommandRepository, 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/auth/AuthServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | import com.yapp.bol.AuthException 4 | import com.yapp.bol.auth.social.SocialLoginClient 5 | import org.springframework.stereotype.Service 6 | 7 | @Service 8 | internal class AuthServiceImpl( 9 | private val socialAuthClient: SocialLoginClient, 10 | private val authCommandRepository: AuthCommandRepository, 11 | private val authQueryRepository: AuthQueryRepository, 12 | private val tokenService: TokenService, 13 | ) : AuthService { 14 | override fun login(loginType: LoginType, token: String): AuthToken { 15 | return if (loginType == LoginType.REFRESH) loginByRefreshToken(token) 16 | else socialLogin(loginType, token) 17 | } 18 | 19 | override fun getAuthUserByAccessToken(token: String): AuthUser? { 20 | return try { 21 | val accessToken = tokenService.validateAccessToken(token) 22 | AuthUser(accessToken.userId) 23 | } catch (e: AuthException) { 24 | null 25 | } 26 | } 27 | 28 | private fun socialLogin(loginType: LoginType, token: String): AuthToken { 29 | val socialUser = socialAuthClient.login(loginType, token) 30 | 31 | val authUser = getOrCreateAuthUser(loginType, socialUser.id) 32 | 33 | return AuthToken( 34 | accessToken = tokenService.generateAccessToken(authUser.id), 35 | refreshToken = tokenService.generateRefreshToken(authUser.id), 36 | ) 37 | } 38 | 39 | private fun loginByRefreshToken(tokenValue: String): AuthToken { 40 | val token = tokenService.validateRefreshToken(tokenValue) 41 | 42 | return AuthToken( 43 | accessToken = tokenService.generateAccessToken(token.userId), 44 | refreshToken = tokenService.renewRefreshToken(token), 45 | ) 46 | } 47 | 48 | private fun getOrCreateAuthUser(loginType: LoginType, socialUserId: String): AuthUser { 49 | val authUser = authQueryRepository.findAuthUser(loginType, socialUserId) 50 | if (authUser != null) return authUser 51 | 52 | return authCommandRepository.registerUser(loginType, socialUserId) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/auth/TokenService.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | import com.yapp.bol.auth.token.TokenCommandRepository 4 | import com.yapp.bol.auth.token.TokenPolicy 5 | import org.springframework.beans.factory.annotation.Qualifier 6 | 7 | internal class TokenService( 8 | @Qualifier("accessTokenPolicy") private val accessTokenPolicy: TokenPolicy, 9 | @Qualifier("refreshTokenPolicy") private val refreshTokenPolicy: TokenPolicy, 10 | private val tokenQueryRepository: TokenQueryRepository, 11 | private val tokenCommandRepository: TokenCommandRepository, 12 | ) { 13 | fun generateAccessToken(userId: UserId): Token { 14 | val token = accessTokenPolicy.generate(userId) 15 | tokenCommandRepository.saveAccessToken(token) 16 | return token 17 | } 18 | 19 | fun generateRefreshToken(userId: UserId): Token { 20 | val token = refreshTokenPolicy.generate(userId) 21 | tokenCommandRepository.saveRefreshToken(token) 22 | return token 23 | } 24 | 25 | fun renewRefreshToken(token: Token): Token { 26 | token.validate() 27 | tokenCommandRepository.removeRefreshToken(token) 28 | return generateRefreshToken(token.userId) 29 | } 30 | 31 | fun validateAccessToken(value: String): Token { 32 | accessTokenPolicy.assertValidate(value) 33 | return tokenQueryRepository.findAccessToken(value).validate() 34 | } 35 | 36 | fun validateRefreshToken(value: String): Token { 37 | refreshTokenPolicy.assertValidate(value) 38 | return tokenQueryRepository.findRefreshToken(value).validate() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/auth/token/JwtUtils.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.token 2 | 3 | import com.yapp.bol.auth.Token 4 | import com.yapp.bol.auth.UserId 5 | import com.yapp.bol.toDate 6 | import io.jsonwebtoken.JwtException 7 | import io.jsonwebtoken.Jwts 8 | import io.jsonwebtoken.security.Keys 9 | import java.security.Key 10 | import java.time.LocalDateTime 11 | 12 | internal class JwtUtils( 13 | private val secretKey: ByteArray, 14 | ) : TokenUtils { 15 | override fun generate(userId: UserId, expiredAt: LocalDateTime): Token { 16 | val token = Jwts.builder() 17 | .setId(userId.toString()) 18 | .setExpiration(expiredAt.toDate()) 19 | .signWith(getSigningKey()) 20 | .compact() 21 | 22 | return Token(token, userId, expiredAt) 23 | } 24 | 25 | override fun validate(token: String): Boolean = 26 | try { 27 | Jwts.parserBuilder() 28 | .setSigningKey(getSigningKey()) 29 | .build() 30 | .parseClaimsJws(token) 31 | true 32 | } catch (e: JwtException) { 33 | false 34 | } 35 | 36 | override fun getUserId(token: String): Long = 37 | Jwts.parserBuilder() 38 | .setSigningKey(getSigningKey()) 39 | .build() 40 | .parseClaimsJws(token).body.id.toLong() 41 | 42 | private fun getSigningKey(): Key = Keys.hmacShaKeyFor(secretKey) 43 | } 44 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/auth/token/OpaqueTokenUtils.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.token 2 | 3 | import com.yapp.bol.auth.Token 4 | import com.yapp.bol.auth.UserId 5 | import java.security.SecureRandom 6 | import java.time.LocalDateTime 7 | import java.util.Base64 8 | 9 | internal class OpaqueTokenUtils( 10 | private val tokenLength: Int, 11 | ) : TokenUtils { 12 | private val tokenByteSize = tokenLength * 6 / 8 13 | 14 | private val random = SecureRandom() 15 | 16 | override fun generate(userId: UserId, expiredAt: LocalDateTime): Token { 17 | val arr = ByteArray(tokenByteSize) 18 | random.nextBytes(arr) 19 | val value = Base64.getEncoder().encodeToString(arr) 20 | return Token(value, userId, expiredAt) 21 | } 22 | 23 | override fun validate(token: String): Boolean { 24 | return token.length == tokenLength 25 | } 26 | 27 | override fun getUserId(token: String): Long? = null 28 | } 29 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/auth/token/TokenPolicy.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.token 2 | 3 | import com.yapp.bol.auth.Token 4 | import com.yapp.bol.auth.UserId 5 | import java.time.Duration 6 | import java.time.LocalDateTime 7 | 8 | internal class TokenPolicy( 9 | private val tokenUtils: TokenUtils, 10 | private val expiredDuration: Duration, 11 | ) : TokenUtils by tokenUtils { 12 | fun generate(userId: UserId): Token = tokenUtils.generate(userId, LocalDateTime.now().plus(expiredDuration)) 13 | } 14 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/auth/token/TokenUtils.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.token 2 | 3 | import com.yapp.bol.InvalidTokenException 4 | import com.yapp.bol.auth.Token 5 | import com.yapp.bol.auth.UserId 6 | import java.time.LocalDateTime 7 | 8 | internal interface TokenUtils { 9 | fun generate(userId: UserId, expiredAt: LocalDateTime): Token 10 | fun validate(token: String): Boolean 11 | fun getUserId(token: String): Long? 12 | fun assertValidate(token: String) { 13 | if (validate(token).not()) throw InvalidTokenException 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/file/FileServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.file 2 | 3 | import com.yapp.bol.NotFoundFileException 4 | import com.yapp.bol.auth.UserId 5 | import com.yapp.bol.file.dto.RawFileData 6 | import org.springframework.stereotype.Service 7 | 8 | @Service 9 | class FileServiceImpl( 10 | private val fileQueryRepository: FileQueryRepository, 11 | private val fileCommandRepository: FileCommandRepository, 12 | ) : FileService { 13 | override fun uploadFile(request: RawFileData): FileInfo { 14 | return fileCommandRepository.saveFile(request) 15 | } 16 | 17 | override fun downloadFile(userId: UserId?, fileName: String): RawFileData { 18 | val fileData = fileQueryRepository.getFile(fileName) ?: throw NotFoundFileException 19 | 20 | if (fileData.canAccess(userId).not()) throw NotFoundFileException 21 | 22 | return fileData 23 | } 24 | 25 | override fun getDefaultGroupImageUrl(): String { 26 | return fileQueryRepository.getFiles(FilePurpose.GROUP_DEFAULT_IMAGE).random() 27 | } 28 | 29 | private fun RawFileData.canAccess(userId: UserId?): Boolean = 30 | when (this.purpose.accessLevel) { 31 | FileAccessLevel.PUBLIC -> true 32 | FileAccessLevel.PRIVATE -> this.userId == userId 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/game/GameServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game 2 | 3 | import com.yapp.bol.NotFoundGameException 4 | import com.yapp.bol.group.GroupId 5 | import org.springframework.stereotype.Service 6 | 7 | @Service 8 | internal class GameServiceImpl( 9 | private val gameQueryRepository: GameQueryRepository, 10 | ) : GameService { 11 | override fun getGameList(groupId: GroupId): List { 12 | return gameQueryRepository.getGameListByGroupId(groupId) 13 | } 14 | 15 | override fun validateMemberSize(gameId: GameId, memberCount: Int): Boolean { 16 | val game = gameQueryRepository.findById(gameId) ?: throw NotFoundGameException 17 | 18 | return memberCount in game.minMember..game.maxMember 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/game/member/GameMemberServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game.member 2 | 3 | import com.yapp.bol.InvalidMatchMemberException 4 | import com.yapp.bol.game.GameId 5 | import com.yapp.bol.game.GameService 6 | import com.yapp.bol.group.GroupId 7 | import com.yapp.bol.group.member.MemberId 8 | import com.yapp.bol.match.dto.CreateMatchDto 9 | import com.yapp.bol.season.SeasonService 10 | import org.springframework.stereotype.Service 11 | 12 | @Service 13 | class GameMemberServiceImpl( 14 | private val gameMemberQueryRepository: GameMemberQueryRepository, 15 | private val gameMemberCommandRepository: GameMemberCommandRepository, 16 | private val gameService: GameService, 17 | private val seasonService: SeasonService 18 | ) : GameMemberService { 19 | override fun processScore(createMatchDto: CreateMatchDto): List { 20 | val gameId = createMatchDto.gameId 21 | val group = createMatchDto.groupId 22 | val memberDtos = createMatchDto.createMatchMemberDtos 23 | val memberCount = createMatchDto.createMatchMemberDtos.size 24 | 25 | if (gameService.validateMemberSize(gameId = gameId, memberCount = memberCount).not()) { 26 | throw InvalidMatchMemberException 27 | } 28 | 29 | val gameMembers = memberDtos.map { createMatchMemberDto -> 30 | val gameMember = getOrCreateGameMember( 31 | memberId = createMatchMemberDto.memberId, 32 | gameId = gameId, 33 | groupId = group 34 | ) 35 | 36 | gameMember.generateWithNewMatch( 37 | rank = createMatchMemberDto.ranking, 38 | memberCount = memberCount 39 | ) 40 | } 41 | 42 | return gameMembers 43 | } 44 | 45 | private fun getOrCreateGameMember(memberId: MemberId, gameId: GameId, groupId: GroupId): GameMember { 46 | val gameMember = gameMemberQueryRepository.findGameMember(memberId = memberId, gameId = gameId, groupId = groupId) 47 | 48 | if (gameMember != null) { 49 | return gameMember 50 | } 51 | 52 | val season = seasonService.getOrCreateSeason(groupId = groupId) 53 | 54 | return gameMemberCommandRepository.createGameMember( 55 | GameMember.of( 56 | gameId = gameId, 57 | memberId = memberId, 58 | season = season 59 | ), 60 | groupId = groupId 61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/group/member/MemberServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member 2 | 3 | import com.yapp.bol.DuplicatedMemberNicknameException 4 | import com.yapp.bol.auth.UserId 5 | import com.yapp.bol.group.GroupId 6 | import com.yapp.bol.group.member.dto.PaginationCursorMemberRequest 7 | import com.yapp.bol.pagination.cursor.SimplePaginationCursorResponse 8 | import com.yapp.bol.validate.NicknameValidator 9 | import org.springframework.stereotype.Service 10 | 11 | @Service 12 | internal class MemberServiceImpl( 13 | private val memberQueryRepository: MemberQueryRepository, 14 | private val memberCommandRepository: MemberCommandRepository, 15 | ) : MemberService { 16 | override fun validateMemberNickname(groupId: GroupId, nickname: String): Boolean = 17 | when { 18 | validateUniqueNickname(groupId, nickname).not() -> false 19 | NicknameValidator.validate(nickname).not() -> false 20 | else -> true 21 | } 22 | 23 | override fun createHostMember(userId: UserId, groupId: GroupId, nickname: String): HostMember { 24 | if (validateUniqueNickname(groupId, nickname).not()) throw DuplicatedMemberNicknameException 25 | 26 | val member = HostMember( 27 | userId = userId, 28 | nickname = nickname 29 | ) 30 | 31 | return memberCommandRepository.createMember(groupId, member) as HostMember 32 | } 33 | 34 | override fun createGuestMember(groupId: GroupId, nickname: String): GuestMember { 35 | val member = GuestMember(nickname = nickname) 36 | 37 | return memberCommandRepository.createMember(groupId, member) as GuestMember 38 | } 39 | 40 | override fun getMembers(request: PaginationCursorMemberRequest): SimplePaginationCursorResponse = 41 | memberQueryRepository.getMemberListByCursor(request) 42 | 43 | override fun findMembersByGroupId(groupId: GroupId): List = 44 | memberQueryRepository.findByGroupId(groupId) 45 | 46 | private fun validateUniqueNickname(groupId: GroupId, nickname: String): Boolean = 47 | memberQueryRepository.findByNicknameAndGroupId(nickname, groupId) == null 48 | 49 | private fun validateNicknameLength(nickname: String): Boolean = 50 | nickname.length in Member.MIN_NICKNAME_LENGTH..Member.MAX_NICKNAME_LENGTH 51 | } 52 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/match/MatchServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.match 2 | 3 | import com.yapp.bol.game.member.GameMemberService 4 | import com.yapp.bol.match.dto.CreateMatchDto 5 | import com.yapp.bol.match.dto.toDomain 6 | import com.yapp.bol.season.SeasonService 7 | import org.springframework.stereotype.Service 8 | 9 | @Service 10 | class MatchServiceImpl( 11 | private val matchCommandRepository: MatchCommandRepository, 12 | private val gameMemberService: GameMemberService, 13 | private val seasonService: SeasonService 14 | ) : MatchService { 15 | // TODO: @Transactional 16 | override fun createMatch(createMatchDto: CreateMatchDto): Match { 17 | // TODO: 검증 로직 추가, member ID 랑 groupId 18 | // TODO: game id 검증 19 | val gameMembers = gameMemberService.processScore(createMatchDto = createMatchDto) 20 | 21 | val season = seasonService.getOrCreateSeason(groupId = createMatchDto.groupId) 22 | 23 | val match = createMatchDto.toDomain(season = season) 24 | 25 | return matchCommandRepository.createMatch(match = match, gameMembers = gameMembers) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/onboarding/OnboardingServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.onboarding 2 | 3 | import com.yapp.bol.NotFoundUserException 4 | import com.yapp.bol.auth.UserId 5 | import com.yapp.bol.terms.TermsQueryRepository 6 | import com.yapp.bol.terms.existUpdatedTerms 7 | import com.yapp.bol.user.UserQueryRepository 8 | import org.springframework.stereotype.Service 9 | 10 | @Service 11 | class OnboardingServiceImpl( 12 | private val termsQueryRepository: TermsQueryRepository, 13 | private val userQueryRepository: UserQueryRepository, 14 | ) : OnboardingService { 15 | override fun getRemainOnboarding(userId: UserId): List { 16 | val result = mutableListOf() 17 | 18 | // 약관 관련 19 | val termsList = termsQueryRepository.getSavedTermsByUserId(userId) 20 | if (termsList.isEmpty()) { 21 | result.add(OnboardingType.TERMS) 22 | } else if (termsList.existUpdatedTerms()) { 23 | result.add(OnboardingType.UPDATE_TERMS) 24 | } 25 | 26 | // 닉네임 입력 관련 27 | val user = userQueryRepository.getUser(userId) ?: throw NotFoundUserException 28 | if (user.nickname == null) result.add(OnboardingType.NICKNAME) 29 | 30 | return result 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/season/SeasonServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.season 2 | 3 | import com.yapp.bol.group.GroupId 4 | import org.springframework.stereotype.Service 5 | 6 | @Service 7 | class SeasonServiceImpl( 8 | private val seasonQueryRepository: SeasonQueryRepository, 9 | private val seasonCommandRepository: SeasonCommandRepository 10 | ) : SeasonService { 11 | override fun getOrCreateSeason(groupId: GroupId): Season { 12 | return seasonQueryRepository.getSeason(groupId) ?: seasonCommandRepository.createSeason(Season(groupId = groupId)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/terms/TermsServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.terms 2 | 3 | import com.yapp.bol.NotExistRequiredTermsException 4 | import com.yapp.bol.OldVersionTermsException 5 | import com.yapp.bol.auth.UserId 6 | import org.springframework.stereotype.Service 7 | 8 | @Service 9 | class TermsServiceImpl( 10 | private val termsQueryRepository: TermsQueryRepository, 11 | private val termsCommandRepository: TermsCommandRepository, 12 | ) : TermsService { 13 | 14 | private val wholeTerms: List = TermsCode.latestTerms() 15 | 16 | override fun getNeedTermsAgreeList(userId: UserId): List { 17 | val list = termsQueryRepository.getSavedTermsByUserId(userId) 18 | 19 | return wholeTerms 20 | .filter { baseTerms -> 21 | list.any { baseTerms == it.termsCode }.not() 22 | } 23 | .sortedBy { it.displayOrder } 24 | } 25 | 26 | override fun getWholeTerms(): List = wholeTerms 27 | 28 | override fun agreeTerms(userId: UserId, termsInfoList: List) { 29 | if (termsInfoList.any { it.termsCode.nextVersion != null }) throw OldVersionTermsException 30 | if (getNeedTermsAgreeList(userId) 31 | .filter { it.isRequired } 32 | .any { termsInfoList.existAgreed(it).not() } 33 | ) throw NotExistRequiredTermsException 34 | 35 | termsCommandRepository.saveTermsAgreeInfo(userId, termsInfoList) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/yapp/bol/user/UserServiceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.user 2 | 3 | import com.yapp.bol.InvalidNicknameException 4 | import com.yapp.bol.auth.UserId 5 | import com.yapp.bol.validate.NicknameValidator 6 | import org.springframework.stereotype.Service 7 | 8 | @Service 9 | class UserServiceImpl( 10 | private val userQueryRepository: UserQueryRepository, 11 | private val userCommandRepository: UserCommandRepository, 12 | ) : UserService { 13 | 14 | override fun getUser(userId: UserId): User? { 15 | return userQueryRepository.getUser(userId) 16 | } 17 | 18 | override fun putUser(user: User) { 19 | if (NicknameValidator.validate(user.nickname ?: throw InvalidNicknameException(null)).not()) { 20 | throw InvalidNicknameException(user.nickname) 21 | } 22 | 23 | userCommandRepository.updateUser(user) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/resources/application-core.yml: -------------------------------------------------------------------------------- 1 | auth: 2 | access-token: 3 | expire-duration: P3D 4 | refresh-token: 5 | expire-duration: P7D 6 | 7 | --- 8 | spring.config.activate.on-profile: local 9 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/yapp/bol/ExtensionTest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.data.forAll 5 | import io.kotest.data.row 6 | import io.kotest.matchers.shouldBe 7 | import java.time.LocalDateTime 8 | import java.util.Calendar 9 | import java.util.GregorianCalendar 10 | 11 | class ExtensionTest : FunSpec() { 12 | init { 13 | context("LocalDateTime.toDate() 테스트") { 14 | forAll( 15 | row(2023, 5, 6, 8, 28), 16 | row(2023, 1, 1, 0, 0), 17 | row(2023, 12, 31, 0, 0), 18 | row(2023, 12, 31, 23, 59), 19 | ) { year, month, day, hour, minute -> 20 | test("$year-$month-$day $hour:$minute") { 21 | val localDateTime = LocalDateTime.of(year, month, day, hour, minute) 22 | 23 | val date = localDateTime.toDate() 24 | val calendar: Calendar = GregorianCalendar() 25 | calendar.time = date 26 | 27 | calendar.get(Calendar.YEAR) shouldBe year 28 | calendar.get(Calendar.MONTH) + 1 shouldBe month 29 | calendar.get(Calendar.DAY_OF_MONTH) shouldBe day 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/yapp/bol/auth/TokenServiceTest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | import com.yapp.bol.auth.token.TokenCommandRepository 4 | import com.yapp.bol.auth.token.TokenPolicy 5 | import io.kotest.core.spec.style.FunSpec 6 | import io.kotest.matchers.shouldBe 7 | import io.mockk.every 8 | import io.mockk.justRun 9 | import io.mockk.mockk 10 | import io.mockk.verify 11 | import java.time.LocalDateTime 12 | 13 | class TokenServiceTest : FunSpec() { 14 | private val tokenPolicy: TokenPolicy = mockk() 15 | private val tokenQueryRepository: TokenQueryRepository = mockk() 16 | private val tokenCommandRepository: TokenCommandRepository = mockk() 17 | 18 | private val sut = TokenService(tokenPolicy, tokenPolicy, tokenQueryRepository, tokenCommandRepository) 19 | 20 | init { 21 | context("Access Token") { 22 | test("generate - Success") { 23 | // given 24 | val userId = UserId(123L) 25 | val token = Token("value", userId, LocalDateTime.now()) 26 | every { tokenPolicy.generate(userId) } returns token 27 | justRun { tokenCommandRepository.saveAccessToken(any()) } 28 | 29 | // when 30 | val result = sut.generateAccessToken(userId) 31 | 32 | // then 33 | result shouldBe token 34 | verify(exactly = 1) { tokenCommandRepository.saveAccessToken(token) } 35 | } 36 | } 37 | 38 | context("Refresh Token") { 39 | test("generate - Success") { 40 | // given 41 | val userId = UserId(123L) 42 | val token = Token("value", userId, LocalDateTime.now()) 43 | every { tokenPolicy.generate(userId) } returns token 44 | justRun { tokenCommandRepository.saveRefreshToken(any()) } 45 | 46 | // when 47 | val result = sut.generateRefreshToken(userId) 48 | 49 | // then 50 | result shouldBe token 51 | verify(exactly = 1) { tokenCommandRepository.saveRefreshToken(token) } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/yapp/bol/auth/token/OpaqueTokenUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.token 2 | 3 | import com.yapp.bol.auth.UserId 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.matchers.shouldBe 6 | import java.time.LocalDateTime 7 | 8 | class OpaqueTokenUtilsTest : FunSpec() { 9 | init { 10 | val userId = UserId(1L) 11 | val expiredTime = LocalDateTime.now().plusDays(3) 12 | for (length in 20..200 step 4) { 13 | context("길이 : $length") { 14 | val sut = OpaqueTokenUtils(length) 15 | val token = getRandomString(length) 16 | test("generate") { 17 | // when 18 | val result = sut.generate(userId, expiredTime) 19 | 20 | // then 21 | result.value.length shouldBe length 22 | result.userId shouldBe userId 23 | result.expiredAt shouldBe expiredTime 24 | } 25 | test("validate") { 26 | // when 27 | val result = sut.validate(token) 28 | 29 | // then 30 | result shouldBe true 31 | } 32 | test("validate Fail length : ${length + 1}") { 33 | // when 34 | val result = sut.validate("$token=") 35 | 36 | // then 37 | result shouldBe false 38 | } 39 | test("getUserId") { 40 | // when 41 | val result = sut.getUserId(token) 42 | 43 | // then 44 | result shouldBe null 45 | } 46 | } 47 | } 48 | } 49 | 50 | private fun getRandomString(length: Int): String { 51 | val charset = "ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz0123456789" 52 | return (1..length) 53 | .map { charset.random() } 54 | .joinToString("") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/yapp/bol/auth/token/TokenPolicyTest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.token 2 | 3 | import com.yapp.bol.auth.Token 4 | import com.yapp.bol.auth.UserId 5 | import io.kotest.core.spec.style.FunSpec 6 | import io.kotest.matchers.shouldBe 7 | import io.mockk.every 8 | import io.mockk.mockk 9 | import io.mockk.mockkStatic 10 | import io.mockk.verify 11 | import java.time.Duration 12 | import java.time.LocalDateTime 13 | 14 | class TokenPolicyTest : FunSpec() { 15 | private val tokenUtils: TokenUtils = mockk() 16 | 17 | init { 18 | test("TokenPolicy.Generate 테스트") { 19 | // given 20 | val userId = UserId(123L) 21 | val now = LocalDateTime.of(1970, 1, 1, 0, 0, 0) 22 | val expireDuration = Duration.ofDays(7) 23 | val expiredAt = now.plus(expireDuration) 24 | val mockToken = Token("value", userId, expiredAt) 25 | 26 | mockkStatic(LocalDateTime::class) 27 | every { LocalDateTime.now() } returns now 28 | every { tokenUtils.generate(userId, expiredAt) } returns mockToken 29 | 30 | // when 31 | val policy = TokenPolicy(tokenUtils, expireDuration) 32 | val result = policy.generate(userId) 33 | 34 | // then 35 | verify(exactly = 1) { tokenUtils.generate(userId, now.plus(expireDuration)) } 36 | result shouldBe mockToken 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/yapp/bol/group/member/MemberServiceImplTest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member 2 | 3 | import com.yapp.bol.auth.UserId 4 | import com.yapp.bol.group.GroupId 5 | import io.kotest.core.spec.style.FunSpec 6 | import io.kotest.matchers.shouldBe 7 | import io.mockk.every 8 | import io.mockk.mockk 9 | 10 | class MemberServiceImplTest : FunSpec() { 11 | private val memberQueryRepository: MemberQueryRepository = mockk() 12 | private val memberCommandRepository: MemberCommandRepository = mockk() 13 | private val sut = MemberServiceImpl(memberQueryRepository, memberCommandRepository) 14 | 15 | init { 16 | context("validateMemberNickname") { 17 | val groupId = GroupId(0) 18 | val nickname = "닉네임" 19 | 20 | test("Success") { 21 | val groupId = GroupId(0) 22 | val nickname = "닉네임" 23 | 24 | every { memberQueryRepository.findByNicknameAndGroupId(nickname, groupId) } returns null 25 | 26 | sut.validateMemberNickname(groupId, nickname) shouldBe true 27 | } 28 | 29 | test("닉네임 중복") { 30 | val mockMember = HostMember( 31 | userId = UserId(0), 32 | nickname = nickname, 33 | ) 34 | 35 | every { memberQueryRepository.findByNicknameAndGroupId(nickname, groupId) } returns mockMember 36 | 37 | sut.validateMemberNickname(groupId, nickname) shouldBe false 38 | } 39 | 40 | test("닉네임 길이 초과") { 41 | val nickname = "x".repeat(Member.MAX_NICKNAME_LENGTH + 1) 42 | 43 | every { memberQueryRepository.findByNicknameAndGroupId(nickname, groupId) } returns null 44 | 45 | sut.validateMemberNickname(groupId, nickname) shouldBe false 46 | } 47 | 48 | test("닉네임 길이 부족") { 49 | val nickname = "x".repeat(Member.MIN_NICKNAME_LENGTH - 1) 50 | 51 | every { memberQueryRepository.findByNicknameAndGroupId(nickname, groupId) } returns null 52 | 53 | sut.validateMemberNickname(groupId, nickname) shouldBe false 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/yapp/bol/validate/NicknameValidatorTest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.validate 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | class NicknameValidatorTest : FunSpec() { 7 | init { 8 | test("길이 SUCCESS") { 9 | for (i in 1..10) { 10 | val nickname = "x".repeat(i) 11 | 12 | NicknameValidator.validate(nickname) shouldBe true 13 | } 14 | } 15 | 16 | test("특수문자 SUCCESS") { 17 | val nickname = "s12345_" 18 | 19 | NicknameValidator.validate(nickname) shouldBe true 20 | } 21 | 22 | test("길이 FAIL") { 23 | for (i in listOf(0, 11)) { 24 | val nickname = "x".repeat(i) 25 | 26 | NicknameValidator.validate(nickname) shouldBe false 27 | } 28 | } 29 | 30 | test("특수문자 FAIL") { 31 | val nickname = "s12345!@#" 32 | 33 | NicknameValidator.validate(nickname) shouldBe false 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /docs/image/hexagonal-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YAPP-Github/onboard-server/cd2951562e26d0d016672dc27e6d6bf2c6658be2/docs/image/hexagonal-architecture.png -------------------------------------------------------------------------------- /domain/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | tasks { 4 | withType { enabled = true } 5 | withType { enabled = false } 6 | } 7 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/auth/AuthToken.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | data class AuthToken( 4 | val accessToken: Token, 5 | val refreshToken: Token? = null, 6 | ) 7 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/auth/AuthUser.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | @JvmInline 4 | value class UserId(val value: Long) 5 | 6 | data class AuthUser( 7 | val id: UserId, 8 | ) 9 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/auth/LoginType.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | enum class LoginType { 4 | KAKAO_ACCESS_TOKEN, 5 | NAVER_ACCESS_TOKEN, 6 | GOOGLE_ID_TOKEN, 7 | REFRESH, 8 | } 9 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/auth/SocialUser.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | interface SocialUser { 4 | val id: String 5 | } 6 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/auth/Token.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | import com.yapp.bol.ExpiredTokenException 4 | import com.yapp.bol.InvalidTokenException 5 | import java.time.LocalDateTime 6 | import java.util.Base64 7 | 8 | data class Token( 9 | val value: String, 10 | val userId: UserId, 11 | val expiredAt: LocalDateTime, 12 | ) { 13 | constructor(value: ByteArray, userId: UserId, expiredAt: LocalDateTime) : this( 14 | Base64.getEncoder().encodeToString(value), 15 | userId, 16 | expiredAt, 17 | ) 18 | 19 | val isExpired: Boolean 20 | get() = expiredAt.isBefore(LocalDateTime.now()) 21 | 22 | fun toBinary(): ByteArray = value.toBinary() 23 | } 24 | 25 | fun Token?.validate(): Token { 26 | if (this == null) throw InvalidTokenException 27 | if (this.isExpired) throw ExpiredTokenException 28 | return this 29 | } 30 | 31 | fun String.toBinary(): ByteArray { 32 | return Base64.getDecoder().decode(this) 33 | } 34 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/date/DateTimeUtils.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.date 2 | 3 | import com.yapp.bol.InvalidDateTimeException 4 | import java.time.LocalDateTime 5 | import java.time.format.DateTimeFormatter 6 | import java.time.format.DateTimeParseException 7 | 8 | object DateTimeUtils { 9 | private const val DATE_TIME_FORMAT = "yy/MM/dd HH:mm" 10 | private val formatter = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT) 11 | 12 | fun parseString(date: String): LocalDateTime { 13 | return try { 14 | LocalDateTime.parse(date, formatter) 15 | } catch (e: DateTimeParseException) { 16 | throw InvalidDateTimeException 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/file/FileAccessLevel.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.file 2 | 3 | enum class FileAccessLevel { 4 | PUBLIC, 5 | PRIVATE 6 | } 7 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/file/FileInfo.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.file 2 | 3 | data class FileInfo( 4 | val name: String, 5 | val contentType: String, 6 | ) 7 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/file/FilePurpose.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.file 2 | 3 | enum class FilePurpose(val accessLevel: FileAccessLevel) { 4 | GAME_IMAGE(FileAccessLevel.PUBLIC), 5 | GROUP_DEFAULT_IMAGE(FileAccessLevel.PUBLIC), 6 | GROUP_IMAGE(FileAccessLevel.PUBLIC), 7 | MATCH_IMAGE(FileAccessLevel.PUBLIC), 8 | } 9 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/file/dto/RawFileData.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.file.dto 2 | 3 | import com.yapp.bol.auth.UserId 4 | import com.yapp.bol.file.FilePurpose 5 | import java.io.InputStream 6 | 7 | data class RawFileData( 8 | val userId: UserId, 9 | val contentType: String, 10 | val content: InputStream, 11 | val purpose: FilePurpose, 12 | ) 13 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/game/Game.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game 2 | 3 | @JvmInline 4 | value class GameId(val value: Long) 5 | 6 | data class GameWithMatchCount( 7 | val game: Game, 8 | val matchCount: Int, 9 | ) : GameBasicInfo by game 10 | 11 | data class Game( 12 | override val id: GameId, 13 | override val name: String, 14 | override val minMember: Int, 15 | override val maxMember: Int, 16 | override val rankType: GameRankType, 17 | override val img: String, 18 | ) : GameBasicInfo 19 | 20 | interface GameBasicInfo { 21 | val id: GameId 22 | val name: String 23 | val minMember: Int 24 | val maxMember: Int 25 | val rankType: GameRankType 26 | val img: String 27 | } 28 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/game/GameRankType.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game 2 | 3 | enum class GameRankType(val description: String) { 4 | SCORE_LOW("점수가 낮은 사람이 1등"), 5 | SCORE_HIGH("점수가 높은 사람이 1등"), 6 | } 7 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/game/member/GameMember.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game.member 2 | 3 | import com.yapp.bol.InvalidMatchMemberException 4 | import com.yapp.bol.game.GameId 5 | import com.yapp.bol.group.member.MemberId 6 | import com.yapp.bol.season.Season 7 | import kotlin.math.floor 8 | import kotlin.math.sqrt 9 | 10 | @JvmInline 11 | value class GameMemberId(val value: Long) 12 | 13 | data class GameMember( 14 | val id: GameMemberId = GameMemberId(0), 15 | val gameId: GameId, 16 | val memberId: MemberId, 17 | val season: Season, 18 | val finalScore: Int, 19 | val matchCount: Int, 20 | val winningPercentage: Double 21 | ) { 22 | fun generateWithNewMatch( 23 | rank: Int, 24 | memberCount: Int 25 | ): GameMember { 26 | val finalScore = this.finalScore + this.calculateUpdatedScore( 27 | rank = rank, 28 | memberCount = memberCount 29 | ) 30 | val matchCount = this.matchCount + 1 31 | 32 | return this.copy( 33 | finalScore = finalScore, 34 | matchCount = matchCount, 35 | ) 36 | } 37 | 38 | private fun calculateUpdatedScore(rank: Int, memberCount: Int): Int { 39 | if (rank > memberCount) { 40 | throw InvalidMatchMemberException 41 | } 42 | 43 | val threshold = floor((memberCount / 2).toDouble()) 44 | 45 | if (rank > threshold) { 46 | return 0 47 | } 48 | 49 | return floor((MAX_SCORE / rank).toDouble() * sqrt(memberCount.toDouble())).toInt() 50 | } 51 | 52 | companion object { 53 | private const val DEFAULT_SCORE = 0 54 | private const val DEFAULT_MATCH_COUNT = 0 55 | private const val DEFAULT_WINNING_PERCENTAGE = 0.0 56 | private const val MAX_SCORE = 100 57 | 58 | fun of( 59 | gameId: GameId, 60 | memberId: MemberId, 61 | season: Season, 62 | ): GameMember { 63 | return GameMember( 64 | gameId = gameId, 65 | memberId = memberId, 66 | season = season, 67 | finalScore = DEFAULT_SCORE, 68 | matchCount = DEFAULT_MATCH_COUNT, 69 | winningPercentage = DEFAULT_WINNING_PERCENTAGE 70 | ) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/group/Group.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group 2 | 3 | import com.yapp.bol.InvalidGroupDescriptionException 4 | import com.yapp.bol.InvalidGroupNameException 5 | import com.yapp.bol.InvalidGroupOrganizationException 6 | 7 | @JvmInline 8 | value class GroupId(val value: Long) 9 | 10 | data class Group( 11 | override val id: GroupId = GroupId(0), 12 | override val name: String, 13 | override val description: String, 14 | override val organization: String?, 15 | override val profileImageUrl: String = DEFAULT_PROFILE_IMAGE_URL, 16 | override val accessCode: String = generateAccessCode(), 17 | ) : GroupBasicInfo { 18 | init { 19 | if (name.length > MAX_NAME_LENGTH) { 20 | throw InvalidGroupNameException 21 | } 22 | if (description.length > MAX_DESCRIPTION_LENGTH) { 23 | throw InvalidGroupDescriptionException 24 | } 25 | 26 | if (organization?.length ?: 0 > MAX_ORGANIZATION_LENGTH) { 27 | throw InvalidGroupOrganizationException 28 | } 29 | } 30 | 31 | companion object { 32 | const val MAX_NAME_LENGTH = 14 33 | const val MAX_DESCRIPTION_LENGTH = 72 34 | const val MAX_ORGANIZATION_LENGTH = 15 35 | 36 | const val ACCESS_CODE_LENGTH = 6 37 | 38 | const val DEFAULT_PROFILE_IMAGE_URL = 39 | "https://github.com/YAPP-Github" + 40 | "/22nd-Android-Team-2-BE" + 41 | "/assets/46865281/2a3caefb-e0ab-4f60-b745-273a889f0c96" // FIXME: 임시 이미지 42 | 43 | private fun generateAccessCode(): String { 44 | val chars = ('A'..'Z') + ('0'..'9') 45 | 46 | return (1..ACCESS_CODE_LENGTH) 47 | .map { chars.random() } 48 | .joinToString("") 49 | } 50 | } 51 | } 52 | 53 | interface GroupBasicInfo { 54 | val id: GroupId 55 | val name: String 56 | val description: String 57 | val organization: String? 58 | val profileImageUrl: String 59 | val accessCode: String 60 | } 61 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/group/LeaderBoardMember.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group 2 | 3 | import com.yapp.bol.group.member.Member 4 | 5 | data class LeaderBoardMember( 6 | val member: Member, 7 | val score: Int?, 8 | val matchCount: Int?, 9 | val isChangeRecent: Boolean, 10 | ) 11 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/group/dto/GroupMemberList.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.dto 2 | 3 | import com.yapp.bol.group.Group 4 | import com.yapp.bol.group.member.MemberList 5 | 6 | data class GroupMemberList( 7 | val group: Group, 8 | val members: MemberList, 9 | ) 10 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/group/dto/GroupWithMemberCount.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.dto 2 | 3 | import com.yapp.bol.group.Group 4 | import com.yapp.bol.group.GroupBasicInfo 5 | import com.yapp.bol.group.member.MemberList 6 | 7 | data class GroupWithMemberCount( 8 | val group: GroupBasicInfo, 9 | val memberCount: Int 10 | ) : GroupBasicInfo by group { 11 | companion object { 12 | fun of(group: Group, members: MemberList) = GroupWithMemberCount( 13 | group = group, 14 | memberCount = members.getSize() 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/group/member/GuestMember.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member 2 | 3 | class GuestMember( 4 | id: MemberId = MemberId(0), 5 | nickname: String, 6 | level: Int = 0, 7 | ) : ParticipantMember( 8 | id = id, 9 | nickname = nickname, 10 | userId = null, 11 | level = level, 12 | ) 13 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/group/member/HostMember.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member 2 | 3 | import com.yapp.bol.auth.UserId 4 | 5 | class HostMember( 6 | id: MemberId = MemberId(0), 7 | userId: UserId, 8 | nickname: String, 9 | level: Int = 0, 10 | ) : ParticipantMember( 11 | id = id, 12 | userId = userId, 13 | nickname = nickname, 14 | level = level, 15 | ) 16 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/group/member/Member.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member 2 | 3 | import com.yapp.bol.InvalidMemberRoleException 4 | import com.yapp.bol.InvalidNicknameException 5 | import com.yapp.bol.auth.UserId 6 | import com.yapp.bol.validate.NicknameValidator 7 | 8 | @JvmInline 9 | value class MemberId(val value: Long) 10 | 11 | abstract class Member internal constructor( 12 | val id: MemberId, 13 | val userId: UserId?, 14 | val nickname: String, 15 | val level: Int, 16 | ) { 17 | val role: MemberRole = when { 18 | isOwner() -> MemberRole.OWNER 19 | isGuest() -> MemberRole.GUEST 20 | isHost() -> MemberRole.HOST 21 | else -> throw InvalidMemberRoleException 22 | } 23 | 24 | init { 25 | if (NicknameValidator.validate(nickname).not()) throw InvalidNicknameException(nickname) 26 | 27 | if (userId == null && isGuest().not()) throw InvalidMemberRoleException 28 | } 29 | 30 | fun isOwner(): Boolean = this is OwnerMember 31 | fun isGuest(): Boolean = userId == null || this is GuestMember 32 | fun isHost(): Boolean = this is HostMember 33 | 34 | companion object { 35 | const val MAX_NICKNAME_LENGTH = 10 36 | const val MIN_NICKNAME_LENGTH = 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/group/member/MemberList.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member 2 | 3 | import com.yapp.bol.DuplicatedMembersNicknameException 4 | 5 | class MemberList( 6 | val owner: OwnerMember, 7 | val participant: List = emptyList(), 8 | ) { 9 | private val totalList: List 10 | get() = participant + owner 11 | 12 | init { 13 | if (validateDistinctNicknames(totalList).not()) { 14 | throw DuplicatedMembersNicknameException 15 | } 16 | } 17 | 18 | fun toList(): List { 19 | return totalList 20 | } 21 | 22 | fun getSize(): Int { 23 | return totalList.size 24 | } 25 | 26 | private fun validateDistinctNicknames(members: List): Boolean = 27 | members.size == members 28 | .map { it.nickname } 29 | .distinct() 30 | .size 31 | 32 | private fun validateDistinctNickname(hostMember: Member): Boolean = 33 | this.participant 34 | .all { it.nickname != hostMember.nickname } 35 | } 36 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/group/member/MemberRole.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member 2 | 3 | enum class MemberRole { 4 | OWNER, 5 | HOST, 6 | GUEST, 7 | } 8 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/group/member/OwnerMember.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member 2 | 3 | import com.yapp.bol.auth.UserId 4 | 5 | class OwnerMember( 6 | id: MemberId = MemberId(0), 7 | userId: UserId, 8 | nickname: String, 9 | level: Int = 0, 10 | ) : Member( 11 | id = id, 12 | userId = userId, 13 | nickname = nickname, 14 | level = level 15 | ) 16 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/group/member/ParticipantMember.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member 2 | 3 | import com.yapp.bol.auth.UserId 4 | 5 | abstract class ParticipantMember internal constructor( 6 | id: MemberId, 7 | userId: UserId?, 8 | nickname: String, 9 | level: Int, 10 | ) : Member( 11 | id, 12 | userId, 13 | nickname, 14 | level = level, 15 | ) 16 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/group/member/dto/PaginationCursorMemberRequest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member.dto 2 | 3 | import com.yapp.bol.group.GroupId 4 | import com.yapp.bol.group.member.MemberRole 5 | import com.yapp.bol.pagination.cursor.PaginationCursorRequest 6 | 7 | data class PaginationCursorMemberRequest( 8 | val groupId: GroupId, 9 | val nickname: String?, 10 | val role: MemberRole?, 11 | override val size: Int, 12 | override val cursor: String?, 13 | ) : PaginationCursorRequest 14 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/match/Match.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.match 2 | 3 | import com.yapp.bol.game.GameId 4 | import com.yapp.bol.group.GroupId 5 | import com.yapp.bol.match.member.MatchMember 6 | import com.yapp.bol.season.Season 7 | import java.time.LocalDateTime 8 | 9 | @JvmInline 10 | value class MatchId(val value: Long) 11 | 12 | data class Match( 13 | val id: MatchId = MatchId(0), 14 | val gameId: GameId, 15 | val groupId: GroupId, 16 | val matchedDate: LocalDateTime, 17 | val memberCount: Int, 18 | val season: Season, 19 | val matchMembers: List 20 | ) 21 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/match/member/MatchMember.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.match.member 2 | 3 | import com.yapp.bol.group.member.MemberId 4 | 5 | @JvmInline 6 | value class MatchMemberId(val value: Long) 7 | 8 | data class MatchMember( 9 | val id: MatchMemberId, 10 | val memberId: MemberId, 11 | val score: Int, 12 | val ranking: Int, 13 | ) 14 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/onboarding/OnboardingType.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.onboarding 2 | 3 | enum class OnboardingType { 4 | TERMS, 5 | UPDATE_TERMS, 6 | NICKNAME, 7 | } 8 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/pagination/cursor/PaginationCursorRequest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.pagination.cursor 2 | 3 | interface PaginationCursorRequest { 4 | val size: Int 5 | val cursor: CURSOR? 6 | } 7 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/pagination/cursor/PaginationCursorResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.pagination.cursor 2 | 3 | interface PaginationCursorResponse { 4 | val contents: List 5 | val cursor: CURSOR 6 | val hasNext: Boolean 7 | } 8 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/pagination/cursor/SimplePaginationCursorRequest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.pagination.cursor 2 | 3 | data class SimplePaginationCursorRequest( 4 | override val size: Int, 5 | override val cursor: CURSOR?, 6 | ) : PaginationCursorRequest 7 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/pagination/cursor/SimplePaginationCursorResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.pagination.cursor 2 | 3 | data class SimplePaginationCursorResponse( 4 | override val contents: List, 5 | override val cursor: CURSOR, 6 | override val hasNext: Boolean, 7 | ) : PaginationCursorResponse { 8 | 9 | fun mapContents(func: (T) -> U): SimplePaginationCursorResponse { 10 | return SimplePaginationCursorResponse( 11 | contents = contents.map(func), 12 | cursor = cursor, 13 | hasNext = hasNext, 14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/pagination/offset/PaginationOffsetResponse.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.pagination.offset 2 | 3 | data class PaginationOffsetResponse( 4 | val content: List, 5 | val hasNext: Boolean 6 | ) { 7 | fun mapContents(func: (T) -> U): PaginationOffsetResponse { 8 | return PaginationOffsetResponse( 9 | content = content.map(func), 10 | hasNext = hasNext, 11 | ) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/season/Season.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.season 2 | 3 | import com.yapp.bol.group.GroupId 4 | 5 | @JvmInline 6 | value class SeasonId(val value: Long) 7 | 8 | data class Season( 9 | val id: SeasonId = SeasonId(0), 10 | val groupId: GroupId, 11 | ) 12 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/terms/TermsAgreeInfo.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.terms 2 | 3 | data class TermsAgreeInfo( 4 | val termsCode: TermsCode, 5 | val isAgree: Boolean, 6 | ) 7 | 8 | operator fun List.get(termsCode: TermsCode): TermsAgreeInfo? { 9 | return find { it.termsCode == termsCode } 10 | } 11 | 12 | fun List.existAgreed(termsCode: TermsCode): Boolean { 13 | return get(termsCode)?.isAgree ?: false 14 | } 15 | 16 | fun List.existUpdatedTerms(): Boolean { 17 | return any { it.isAgree && it.termsCode.nextVersion != null } 18 | } 19 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/terms/TermsCode.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.terms 2 | 3 | enum class TermsCode( 4 | val title: String, 5 | val path: String, 6 | val isRequired: Boolean, 7 | val displayOrder: Int, 8 | val nextVersion: TermsCode?, 9 | val isUse: Boolean = true, 10 | ) { 11 | SERVICE_V1( 12 | title = "서비스 이용약관", 13 | path = "terms.html", 14 | isRequired = true, 15 | displayOrder = 1, 16 | nextVersion = null, 17 | ), 18 | PRIVACY_V1( 19 | title = "개인정보 처리방침", 20 | path = "privacy.html", 21 | isRequired = true, 22 | displayOrder = 2, 23 | nextVersion = null, 24 | ), 25 | ; 26 | 27 | companion object { 28 | fun latestTerms(): List { 29 | return TermsCode.values() 30 | .filter { it.path != null || it.nextVersion != null } 31 | .sortedBy { it.displayOrder } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/user/User.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.user 2 | 3 | import com.yapp.bol.auth.UserId 4 | 5 | data class User( 6 | val id: UserId, 7 | val nickname: String?, 8 | ) 9 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/yapp/bol/validate/NicknameValidator.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.validate 2 | 3 | object NicknameValidator { 4 | private const val MIN_NICKNAME_LENGTH = 1 5 | private const val MAX_NICKNAME_LENGTH = 10 6 | private val regex = Regex("^[a-zA-Z0-9_가-힣 ]*$") // 한글, 영어, 숫자, 언더 스코어, 스페이스 7 | 8 | fun validate(nickname: String): Boolean { 9 | return validateLength(nickname) && validateRegex(nickname) 10 | } 11 | 12 | private fun validateRegex(nickname: String): Boolean { 13 | return regex.matches(nickname) 14 | } 15 | 16 | private fun validateLength(nickname: String): Boolean { 17 | return nickname.length in MIN_NICKNAME_LENGTH..MAX_NICKNAME_LENGTH && nickname.isNotBlank() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/yapp/bol/date/DateTimeUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.date 2 | 3 | import io.kotest.core.spec.style.FunSpec 4 | import io.kotest.matchers.shouldBe 5 | 6 | class DateTimeUtilsTest : FunSpec() { 7 | init { 8 | val dateTimeUtils = DateTimeUtils 9 | val dateTimeString = "21/10/10 10:10" 10 | 11 | test("날짜 문자열을 LocalDateTime 으로 변환한다") { 12 | val dateTime = dateTimeUtils.parseString(dateTimeString) 13 | 14 | dateTime.year shouldBe 2021 15 | dateTime.monthValue shouldBe 10 16 | dateTime.dayOfMonth shouldBe 10 17 | dateTime.hour shouldBe 10 18 | dateTime.minute shouldBe 10 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/yapp/bol/group/GroupTest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group 2 | 3 | import com.yapp.bol.InvalidGroupDescriptionException 4 | import com.yapp.bol.InvalidGroupNameException 5 | import com.yapp.bol.InvalidGroupOrganizationException 6 | import io.kotest.assertions.throwables.shouldThrow 7 | import io.kotest.core.spec.style.FunSpec 8 | import io.kotest.matchers.shouldBe 9 | import io.kotest.matchers.types.shouldBeInstanceOf 10 | 11 | class GroupTest : FunSpec() { 12 | init { 13 | val group = Group( 14 | name = "name", 15 | description = "description", 16 | organization = "organization", 17 | ) 18 | 19 | test("그룹 생성") { 20 | group.shouldBeInstanceOf() 21 | } 22 | 23 | test("그룹 이름 길이 제한") { 24 | val name = "x".repeat(Group.MAX_NAME_LENGTH + 1) 25 | 26 | shouldThrow { 27 | Group(name = name, description = "description", organization = "organization") 28 | } 29 | } 30 | 31 | test("그룹 설명 길이 제한") { 32 | val description = "x".repeat(Group.MAX_DESCRIPTION_LENGTH + 1) 33 | 34 | shouldThrow { 35 | Group(name = "name", description = description, organization = "organization") 36 | } 37 | } 38 | 39 | test("그룹 소속 길이 제한") { 40 | val organization = "x".repeat(Group.MAX_ORGANIZATION_LENGTH + 1) 41 | 42 | shouldThrow { 43 | Group(name = "name", description = "description", organization = organization) 44 | } 45 | } 46 | 47 | test("그룹 프로필 이미지 URL 이 비었다면 디폴트 이미지로 설정되어야 한다") { 48 | group.profileImageUrl shouldBe Group.DEFAULT_PROFILE_IMAGE_URL 49 | } 50 | 51 | test("accessCode 가 자동으로 생성된다") { 52 | group.accessCode.shouldBeInstanceOf() 53 | group.accessCode.length shouldBe Group.ACCESS_CODE_LENGTH 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/yapp/bol/member/MemberListTest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.member 2 | 3 | import com.yapp.bol.DuplicatedMembersNicknameException 4 | import com.yapp.bol.auth.UserId 5 | import com.yapp.bol.group.member.HostMember 6 | import com.yapp.bol.group.member.MemberList 7 | import com.yapp.bol.group.member.OwnerMember 8 | import io.kotest.assertions.throwables.shouldThrow 9 | import io.kotest.core.spec.style.FunSpec 10 | import io.kotest.matchers.types.shouldBeInstanceOf 11 | class MemberListTest : FunSpec() { 12 | init { 13 | test("멤버 리스트 생성") { 14 | val members = MemberList(MEMBER_OWNER) 15 | members.shouldBeInstanceOf() 16 | } 17 | 18 | test("멤버 리스트 생성시 멤버의 닉네임은 중복될 수 없다.") { 19 | val nickname = "holden" 20 | val owner = OwnerMember(userId = UserId(1), nickname = nickname) 21 | val member = HostMember(userId = UserId(2), nickname = nickname) 22 | 23 | shouldThrow { 24 | MemberList(owner, mutableListOf(member)) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/yapp/bol/member/MemberTest.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.member 2 | 3 | import com.yapp.bol.InvalidNicknameException 4 | import com.yapp.bol.auth.UserId 5 | import com.yapp.bol.group.member.GuestMember 6 | import com.yapp.bol.group.member.HostMember 7 | import com.yapp.bol.group.member.Member 8 | import com.yapp.bol.group.member.OwnerMember 9 | import io.kotest.assertions.throwables.shouldThrow 10 | import io.kotest.core.spec.style.FunSpec 11 | import io.kotest.matchers.types.shouldBeInstanceOf 12 | 13 | val MEMBER_OWNER = OwnerMember(userId = UserId(0), nickname = "nick") 14 | 15 | class MemberTest : FunSpec() { 16 | init { 17 | test("멤버 생성") { 18 | MEMBER_OWNER.shouldBeInstanceOf() 19 | } 20 | 21 | test("멤버 닉네임 최대 길이 제한") { 22 | val nickname = "x".repeat(11) 23 | 24 | shouldThrow { 25 | OwnerMember(userId = UserId(0), nickname = nickname) 26 | } 27 | shouldThrow { 28 | HostMember(userId = UserId(0), nickname = nickname) 29 | } 30 | shouldThrow { 31 | GuestMember(nickname = nickname) 32 | } 33 | } 34 | 35 | test("멤버 닉네임 최소 길이 제한") { 36 | val nickname = "x".repeat(0) 37 | 38 | shouldThrow { 39 | OwnerMember(userId = UserId(0), nickname = nickname) 40 | } 41 | shouldThrow { 42 | HostMember(userId = UserId(0), nickname = nickname) 43 | } 44 | shouldThrow { 45 | GuestMember(nickname = nickname) 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kapt.use.worker.api=false 3 | 4 | springVersion=3.0.6 5 | kotestVersion=5.6.1 6 | kotlinVersion=1.8.21 7 | 8 | jjwtVersion=0.11.5 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YAPP-Github/onboard-server/cd2951562e26d0d016672dc27e6d6bf2c6658be2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /port-in/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | dependencies { 4 | api(project(":domain")) 5 | } 6 | 7 | tasks { 8 | withType { enabled = true } 9 | withType { enabled = false } 10 | } 11 | -------------------------------------------------------------------------------- /port-in/src/main/kotlin/com/yapp/bol/auth/AuthService.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | interface AuthService { 4 | fun login(loginType: LoginType, token: String): AuthToken 5 | fun getAuthUserByAccessToken(token: String): AuthUser? 6 | } 7 | -------------------------------------------------------------------------------- /port-in/src/main/kotlin/com/yapp/bol/file/FileService.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.file 2 | 3 | import com.yapp.bol.auth.UserId 4 | import com.yapp.bol.file.dto.RawFileData 5 | 6 | interface FileService { 7 | fun uploadFile(request: RawFileData): FileInfo 8 | fun downloadFile(userId: UserId?, fileName: String): RawFileData 9 | 10 | /** 11 | * @return Default Image URL 12 | */ 13 | fun getDefaultGroupImageUrl(): String 14 | } 15 | -------------------------------------------------------------------------------- /port-in/src/main/kotlin/com/yapp/bol/game/GameService.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game 2 | 3 | import com.yapp.bol.group.GroupId 4 | 5 | interface GameService { 6 | fun getGameList(groupId: GroupId): List 7 | 8 | fun validateMemberSize(gameId: GameId, memberCount: Int): Boolean 9 | } 10 | -------------------------------------------------------------------------------- /port-in/src/main/kotlin/com/yapp/bol/game/member/GameMemberService.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game.member 2 | 3 | import com.yapp.bol.match.dto.CreateMatchDto 4 | 5 | interface GameMemberService { 6 | fun processScore(createMatchDto: CreateMatchDto): List 7 | } 8 | -------------------------------------------------------------------------------- /port-in/src/main/kotlin/com/yapp/bol/group/GroupService.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group 2 | 3 | import com.yapp.bol.auth.UserId 4 | import com.yapp.bol.game.GameId 5 | import com.yapp.bol.group.dto.AddGuestDto 6 | import com.yapp.bol.group.dto.CreateGroupDto 7 | import com.yapp.bol.group.dto.GroupMemberList 8 | import com.yapp.bol.group.dto.GroupWithMemberCount 9 | import com.yapp.bol.group.dto.JoinGroupDto 10 | import com.yapp.bol.group.member.OwnerMember 11 | import com.yapp.bol.pagination.offset.PaginationOffsetResponse 12 | 13 | interface GroupService { 14 | fun createGroup( 15 | createGroupDto: CreateGroupDto 16 | ): GroupMemberList 17 | 18 | fun joinGroup(request: JoinGroupDto) 19 | 20 | fun searchGroup( 21 | keyword: String?, 22 | pageNumber: Int, 23 | pageSize: Int 24 | ): PaginationOffsetResponse 25 | 26 | fun addGuest(request: AddGuestDto) 27 | 28 | fun getLeaderBoard(groupId: GroupId, gameId: GameId): List 29 | 30 | fun getGroupsByUserId(userId: UserId): List 31 | 32 | fun checkAccessToken(groupId: GroupId, accessToken: String): Boolean 33 | 34 | fun getGroupWithMemberCount(groupId: GroupId): GroupWithMemberCount 35 | 36 | fun getOwner(groupId: GroupId): OwnerMember 37 | 38 | fun isRegisterGroup(userId: UserId, groupId: GroupId): Boolean 39 | } 40 | -------------------------------------------------------------------------------- /port-in/src/main/kotlin/com/yapp/bol/group/dto/AddGuestDto.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.dto 2 | 3 | import com.yapp.bol.auth.UserId 4 | import com.yapp.bol.group.GroupId 5 | 6 | data class AddGuestDto( 7 | val groupId: GroupId, 8 | val requestUserId: UserId, 9 | val nickname: String, 10 | ) 11 | -------------------------------------------------------------------------------- /port-in/src/main/kotlin/com/yapp/bol/group/dto/CreateGroupDto.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.dto 2 | 3 | import com.yapp.bol.auth.UserId 4 | 5 | data class CreateGroupDto( 6 | val name: String, 7 | val description: String, 8 | val organization: String?, 9 | val profileImageUrl: String?, 10 | val ownerId: UserId, 11 | val nickname: String?, 12 | ) 13 | -------------------------------------------------------------------------------- /port-in/src/main/kotlin/com/yapp/bol/group/dto/JoinGroupDto.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.dto 2 | 3 | import com.yapp.bol.auth.UserId 4 | import com.yapp.bol.group.GroupId 5 | import com.yapp.bol.group.member.MemberId 6 | 7 | data class JoinGroupDto( 8 | val groupId: GroupId, 9 | val userId: UserId, 10 | val nickname: String?, 11 | val accessCode: String, 12 | val guestId: MemberId?, 13 | ) 14 | -------------------------------------------------------------------------------- /port-in/src/main/kotlin/com/yapp/bol/group/member/MemberService.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member 2 | 3 | import com.yapp.bol.auth.UserId 4 | import com.yapp.bol.group.GroupId 5 | import com.yapp.bol.group.member.dto.PaginationCursorMemberRequest 6 | import com.yapp.bol.pagination.cursor.SimplePaginationCursorResponse 7 | 8 | interface MemberService { 9 | fun validateMemberNickname(groupId: GroupId, nickname: String): Boolean 10 | 11 | fun createHostMember(userId: UserId, groupId: GroupId, nickname: String): HostMember 12 | 13 | fun createGuestMember(groupId: GroupId, nickname: String): GuestMember 14 | 15 | fun getMembers(request: PaginationCursorMemberRequest): SimplePaginationCursorResponse 16 | 17 | fun findMembersByGroupId(groupId: GroupId): List 18 | } 19 | -------------------------------------------------------------------------------- /port-in/src/main/kotlin/com/yapp/bol/group/member/dto/CreateMemberDto.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member.dto 2 | 3 | import com.yapp.bol.auth.UserId 4 | import com.yapp.bol.group.GroupId 5 | import com.yapp.bol.group.member.MemberRole 6 | 7 | data class CreateMemberDto( 8 | val userId: UserId? = null, 9 | val role: MemberRole, 10 | val nickname: String, 11 | val groupId: GroupId, 12 | ) 13 | -------------------------------------------------------------------------------- /port-in/src/main/kotlin/com/yapp/bol/match/MatchService.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.match 2 | 3 | import com.yapp.bol.match.dto.CreateMatchDto 4 | 5 | interface MatchService { 6 | fun createMatch(createMatchDto: CreateMatchDto): Match 7 | } 8 | -------------------------------------------------------------------------------- /port-in/src/main/kotlin/com/yapp/bol/match/dto/CreateMatchDto.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.match.dto 2 | 3 | import com.yapp.bol.game.GameId 4 | import com.yapp.bol.group.GroupId 5 | import com.yapp.bol.group.member.MemberId 6 | import com.yapp.bol.match.Match 7 | import com.yapp.bol.match.member.MatchMember 8 | import com.yapp.bol.match.member.MatchMemberId 9 | import com.yapp.bol.season.Season 10 | import java.time.LocalDateTime 11 | 12 | data class CreateMatchMemberDto( 13 | val memberId: MemberId, 14 | val score: Int, 15 | val ranking: Int, 16 | ) 17 | 18 | data class CreateMatchDto( 19 | val gameId: GameId, 20 | val groupId: GroupId, 21 | val matchedDate: LocalDateTime, 22 | val createMatchMemberDtos: List 23 | ) 24 | 25 | fun CreateMatchDto.toDomain(season: Season): Match = Match( 26 | gameId = this.gameId, 27 | groupId = this.groupId, 28 | matchedDate = this.matchedDate, 29 | memberCount = this.createMatchMemberDtos.size, 30 | season = season, 31 | matchMembers = this.createMatchMemberDtos.map { it.toDomain() } 32 | ) 33 | 34 | fun CreateMatchMemberDto.toDomain(): MatchMember = MatchMember( 35 | id = MatchMemberId(0), 36 | memberId = this.memberId, 37 | score = this.score, 38 | ranking = this.ranking 39 | ) 40 | -------------------------------------------------------------------------------- /port-in/src/main/kotlin/com/yapp/bol/onboarding/OnboardingService.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.onboarding 2 | 3 | import com.yapp.bol.auth.UserId 4 | 5 | interface OnboardingService { 6 | fun getRemainOnboarding(userId: UserId): List 7 | } 8 | -------------------------------------------------------------------------------- /port-in/src/main/kotlin/com/yapp/bol/season/SeasonService.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.season 2 | 3 | import com.yapp.bol.group.GroupId 4 | 5 | interface SeasonService { 6 | fun getOrCreateSeason(groupId: GroupId): Season 7 | } 8 | -------------------------------------------------------------------------------- /port-in/src/main/kotlin/com/yapp/bol/terms/TermsService.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.terms 2 | 3 | import com.yapp.bol.auth.UserId 4 | 5 | interface TermsService { 6 | fun getNeedTermsAgreeList(userId: UserId): List 7 | fun getWholeTerms(): List 8 | fun agreeTerms(userId: UserId, termsInfoList: List) 9 | } 10 | -------------------------------------------------------------------------------- /port-in/src/main/kotlin/com/yapp/bol/user/UserService.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.user 2 | 3 | import com.yapp.bol.auth.UserId 4 | 5 | interface UserService { 6 | fun getUser(userId: UserId): User? 7 | 8 | fun putUser(user: User) 9 | } 10 | -------------------------------------------------------------------------------- /port-out/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | dependencies { 4 | api(project(":domain")) 5 | } 6 | 7 | tasks { 8 | withType { enabled = true } 9 | withType { enabled = false } 10 | } 11 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/auth/AuthCommandRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | interface AuthCommandRepository { 4 | fun registerUser(loginType: LoginType, socialId: String): AuthUser 5 | } 6 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/auth/AuthQueryRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | interface AuthQueryRepository { 4 | fun findAuthUser(id: UserId): AuthUser? 5 | fun findAuthUser(socialType: LoginType, socialId: String): AuthUser? 6 | } 7 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/auth/TokenQueryRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth 2 | 3 | interface TokenQueryRepository { 4 | fun findAccessToken(value: String): Token? 5 | fun findRefreshToken(token: String): Token? 6 | } 7 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/auth/social/SocialLoginClient.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.social 2 | 3 | import com.yapp.bol.auth.LoginType 4 | import com.yapp.bol.auth.SocialUser 5 | 6 | interface SocialLoginClient { 7 | fun login(loginType: LoginType, token: String): SocialUser 8 | } 9 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/auth/token/TokenCommandRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.auth.token 2 | 3 | import com.yapp.bol.auth.Token 4 | 5 | interface TokenCommandRepository { 6 | fun saveAccessToken(token: Token) 7 | fun saveRefreshToken(token: Token) 8 | 9 | fun removeRefreshToken(token: Token) 10 | } 11 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/file/FileCommandRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.file 2 | 3 | import com.yapp.bol.file.dto.RawFileData 4 | 5 | interface FileCommandRepository { 6 | fun saveFile(file: RawFileData): FileInfo 7 | } 8 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/file/FileQueryRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.file 2 | 3 | import com.yapp.bol.file.dto.RawFileData 4 | 5 | interface FileQueryRepository { 6 | fun getFile(name: String): RawFileData? 7 | 8 | /** 9 | * @return url 목록 10 | */ 11 | fun getFiles(filePurpose: FilePurpose): List 12 | } 13 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/game/GameQueryRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game 2 | 3 | import com.yapp.bol.group.GroupId 4 | 5 | interface GameQueryRepository { 6 | fun getGameListByGroupId(groupId: GroupId): List 7 | 8 | fun findById(id: GameId): Game? 9 | } 10 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/game/member/GameMemberCommandRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game.member 2 | 3 | import com.yapp.bol.group.GroupId 4 | 5 | interface GameMemberCommandRepository { 6 | fun createGameMember(gameMember: GameMember, groupId: GroupId): GameMember 7 | } 8 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/game/member/GameMemberQueryRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.game.member 2 | 3 | import com.yapp.bol.game.GameId 4 | import com.yapp.bol.group.GroupId 5 | import com.yapp.bol.group.member.MemberId 6 | 7 | interface GameMemberQueryRepository { 8 | fun findGameMember(memberId: MemberId, gameId: GameId, groupId: GroupId): GameMember? 9 | } 10 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/group/GroupCommandRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group 2 | 3 | import com.yapp.bol.group.dto.GroupMemberList 4 | import com.yapp.bol.group.member.OwnerMember 5 | 6 | interface GroupCommandRepository { 7 | fun createGroup(group: Group, owner: OwnerMember): GroupMemberList 8 | } 9 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/group/GroupQueryRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group 2 | 3 | import com.yapp.bol.auth.UserId 4 | import com.yapp.bol.game.GameId 5 | import com.yapp.bol.pagination.offset.PaginationOffsetResponse 6 | 7 | interface GroupQueryRepository { 8 | fun findById(id: GroupId): Group? 9 | 10 | fun getLeaderBoardList(groupId: GroupId, gameId: GameId): List 11 | 12 | fun search(keyword: String?, pageNumber: Int, pageSize: Int): PaginationOffsetResponse 13 | 14 | fun getGroupsByUserId(userId: UserId): List 15 | } 16 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/group/member/MemberCommandRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member 2 | 3 | import com.yapp.bol.auth.UserId 4 | import com.yapp.bol.group.GroupId 5 | 6 | interface MemberCommandRepository { 7 | fun createMember(groupId: GroupId, member: Member): Member 8 | fun updateGuestToHost(groupId: GroupId, memberId: MemberId, userId: UserId) 9 | } 10 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/group/member/MemberQueryRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.group.member 2 | 3 | import com.yapp.bol.auth.UserId 4 | import com.yapp.bol.group.GroupId 5 | import com.yapp.bol.group.member.dto.PaginationCursorMemberRequest 6 | import com.yapp.bol.pagination.cursor.SimplePaginationCursorResponse 7 | 8 | interface MemberQueryRepository { 9 | fun findByNicknameAndGroupId(nickname: String, groupId: GroupId): Member? 10 | fun findByGroupId(groupId: GroupId): List 11 | fun findByGroupIdAndUserId(groupId: GroupId, userId: UserId): Member? 12 | fun getMemberListByCursor(request: PaginationCursorMemberRequest): SimplePaginationCursorResponse 13 | fun findOwner(groupId: GroupId): OwnerMember 14 | fun getCount(groupId: GroupId): Int 15 | } 16 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/match/MatchCommandRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.match 2 | 3 | import com.yapp.bol.game.member.GameMember 4 | 5 | interface MatchCommandRepository { 6 | fun createMatch(match: Match, gameMembers: List): Match 7 | } 8 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/season/SeasonCommandRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.season 2 | 3 | interface SeasonCommandRepository { 4 | fun createSeason(season: Season): Season 5 | } 6 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/season/SeasonQueryRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.season 2 | 3 | import com.yapp.bol.group.GroupId 4 | 5 | interface SeasonQueryRepository { 6 | fun getSeason(groupId: GroupId): Season? 7 | } 8 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/terms/TermsCommandRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.terms 2 | 3 | import com.yapp.bol.auth.UserId 4 | 5 | interface TermsCommandRepository { 6 | fun saveTermsAgreeInfo(userId: UserId, termsCode: List) 7 | } 8 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/terms/TermsQueryRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.terms 2 | 3 | import com.yapp.bol.auth.UserId 4 | 5 | interface TermsQueryRepository { 6 | fun getSavedTermsByUserId(userId: UserId): List 7 | } 8 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/user/UserCommandRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.user 2 | 3 | interface UserCommandRepository { 4 | fun updateUser(user: User) 5 | } 6 | -------------------------------------------------------------------------------- /port-out/src/main/kotlin/com/yapp/bol/user/UserQueryRepository.kt: -------------------------------------------------------------------------------- 1 | package com.yapp.bol.user 2 | 3 | import com.yapp.bol.auth.UserId 4 | 5 | interface UserQueryRepository { 6 | fun getUser(userId: UserId): User? 7 | } 8 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "BoL" 2 | 3 | pluginManagement { 4 | val springVersion: String by settings 5 | val kotlinVersion: String by settings 6 | plugins { 7 | id("org.springframework.boot") version springVersion 8 | kotlin("plugin.spring") version kotlinVersion 9 | kotlin("plugin.jpa") version kotlinVersion 10 | kotlin("plugin.allopen") version kotlinVersion 11 | kotlin("plugin.noarg") version kotlinVersion 12 | kotlin("kapt") version "1.6.21" 13 | } 14 | } 15 | 16 | include( 17 | "domain", 18 | "core", 19 | "port-in", 20 | "port-out", 21 | "adapter-in:web", 22 | "adapter-out:rdb", 23 | "adapter-out:social", 24 | "support:yaml", 25 | "support:logging", 26 | ) 27 | -------------------------------------------------------------------------------- /support/logging/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | dependencies { 4 | val springVersion by properties 5 | implementation(project(":support:yaml")) 6 | 7 | implementation("org.springframework.boot:spring-boot-starter:$springVersion") 8 | implementation("ca.pjer:logback-awslogs-appender:1.6.0") 9 | } 10 | 11 | tasks { 12 | withType { enabled = true } 13 | withType { enabled = false } 14 | } 15 | -------------------------------------------------------------------------------- /support/logging/src/main/resources/application-logging.yml: -------------------------------------------------------------------------------- 1 | --- 2 | spring.config.activate.on-profile: local 3 | logging.config: classpath:logback-local.xml 4 | --- 5 | spring.config.activate.on-profile: sandbox 6 | logging.config: classpath:logback-sandbox.xml 7 | --- 8 | spring.config.activate.on-profile: production 9 | logging.config: classpath:logback-production.xml 10 | -------------------------------------------------------------------------------- /support/logging/src/main/resources/logback-appender.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | ${LOG_PATTERN} 11 | 12 | 13 | 14 | 15 | log/console.txt 16 | 17 | log/console-%d{yyyy-MM-dd}.%i.txt 18 | 100MB 19 | 60 20 | 1GB 21 | 22 | 23 | ${LOG_PATTERN} 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /support/logging/src/main/resources/logback-local.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /support/logging/src/main/resources/logback-production.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | prod-api-server 8 | prod-api-console 9 | ap-northeast-2 10 | 50 11 | 30000 12 | 5000 13 | 0 14 | 15 | ${LOG_PATTERN} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /support/logging/src/main/resources/logback-sandbox.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /support/yaml/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.springframework.boot.gradle.tasks.bundling.BootJar 2 | 3 | dependencies { 4 | val springVersion by properties 5 | 6 | implementation("org.springframework.boot:spring-boot-starter:$springVersion") 7 | } 8 | 9 | tasks { 10 | withType { enabled = true } 11 | withType { enabled = false } 12 | } 13 | -------------------------------------------------------------------------------- /support/yaml/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.env.EnvironmentPostProcessor=\ 2 | com.yapp.bol.support.yaml.YamlEnvironmentPostProcessor 3 | --------------------------------------------------------------------------------