├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ └── 🎯-feature-request.md └── workflows │ ├── CI-CD.yaml │ ├── Test.yaml │ └── sonarcloud-analyze.yml ├── .gitignore ├── README.md ├── build.gradle ├── config ├── checkstyle │ └── checkstyle.xml └── formatter │ └── intellij-java-google-style.xml ├── docker ├── app.Dockerfile ├── database │ ├── config │ │ └── mysql.cnf │ └── db.Dockerfile └── docker-compose.yaml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main ├── java │ └── com │ │ └── flab │ │ └── infrun │ │ ├── InfrunApplication.java │ │ ├── cart │ │ ├── application │ │ │ ├── AddCartItemProcessor.java │ │ │ ├── CartsFacade.java │ │ │ ├── CartsReader.java │ │ │ ├── DeleteCartItemProcessor.java │ │ │ ├── command │ │ │ │ ├── AddCartItemCommand.java │ │ │ │ └── DeleteCartItemCommand.java │ │ │ └── result │ │ │ │ ├── AddedCartItemResult.java │ │ │ │ ├── CartsResult.java │ │ │ │ └── DeletedCartItemResult.java │ │ ├── domain │ │ │ ├── Cart.java │ │ │ ├── CartItem.java │ │ │ ├── CartItems.java │ │ │ ├── CartRepository.java │ │ │ └── exception │ │ │ │ ├── CartException.java │ │ │ │ ├── NotFoundCartException.java │ │ │ │ └── NotFoundCartItemException.java │ │ ├── infrastructure │ │ │ ├── config │ │ │ │ └── CartPersistenceConfig.java │ │ │ └── persistence │ │ │ │ ├── CartJpaRepository.java │ │ │ │ └── CartRepositoryAdapter.java │ │ └── presentation │ │ │ ├── CartsController.java │ │ │ ├── request │ │ │ └── AddCartItemRequest.java │ │ │ └── response │ │ │ ├── AddedCartItemResponse.java │ │ │ ├── CartsResponse.java │ │ │ └── DeletedCartItemResponse.java │ │ ├── common │ │ ├── CommonExceptionHandler.java │ │ ├── annotation │ │ │ └── ApplicationService.kt │ │ ├── config │ │ │ ├── AuditingConfig.java │ │ │ ├── jwt │ │ │ │ ├── JwtFilter.java │ │ │ │ └── JwtSecurityConfig.java │ │ │ ├── queryDsl │ │ │ │ └── QueryDSLConfig.java │ │ │ └── security │ │ │ │ ├── CurrentUser.java │ │ │ │ ├── CustomAccessDeniedHandler.java │ │ │ │ ├── CustomAuthenticationEntryPoint.java │ │ │ │ ├── CustomUserDetailsService.java │ │ │ │ ├── SecurityConfig.java │ │ │ │ ├── UserAdapter.java │ │ │ │ └── WebSecurityCustomizerByProfile.java │ │ ├── entity │ │ │ └── BaseEntity.java │ │ ├── exception │ │ │ ├── ErrorCode.java │ │ │ └── SystemException.java │ │ ├── response │ │ │ └── Response.java │ │ └── validator │ │ │ ├── EnumValid.java │ │ │ └── EnumValidator.java │ │ ├── coupon │ │ ├── application │ │ │ ├── CouponFacade.java │ │ │ ├── CouponReader.java │ │ │ ├── CreateCouponProcessor.java │ │ │ ├── EnrollCouponProcessor.java │ │ │ ├── command │ │ │ │ ├── CouponRegisterCommand.java │ │ │ │ └── CreateCouponCommand.java │ │ │ └── result │ │ │ │ ├── CouponView.java │ │ │ │ ├── CreatedCouponResult.java │ │ │ │ └── EnrolledCouponResult.java │ │ ├── domain │ │ │ ├── Coupon.java │ │ │ ├── CouponCodeGenerator.java │ │ │ ├── CouponRepository.java │ │ │ ├── CouponStatus.java │ │ │ ├── CouponValidator.java │ │ │ ├── DiscountInfo.java │ │ │ ├── DiscountType.java │ │ │ └── exception │ │ │ │ ├── AlreadyRegisteredCouponException.java │ │ │ │ ├── AlreadyUsedCouponException.java │ │ │ │ ├── ExpiredCouponException.java │ │ │ │ ├── InvalidCouponDiscountAmountException.java │ │ │ │ ├── InvalidCouponDiscountTypeException.java │ │ │ │ ├── InvalidCouponExpirationAtException.java │ │ │ │ ├── InvalidCouponOwnerException.java │ │ │ │ ├── InvalidCouponQuantityException.java │ │ │ │ └── NotFoundCouponException.java │ │ ├── infrastructure │ │ │ ├── CouponValidatorImpl.java │ │ │ ├── UUIDCouponCodeGenerator.java │ │ │ ├── config │ │ │ │ ├── CouponCodeGeneratorConfig.java │ │ │ │ ├── CouponPersistenceConfig.java │ │ │ │ └── CouponValidatorConfig.java │ │ │ └── persistence │ │ │ │ ├── CouponJpaRepository.java │ │ │ │ └── CouponRepositoryAdapter.java │ │ └── presentation │ │ │ ├── CouponController.java │ │ │ ├── request │ │ │ ├── CreateCouponRequest.java │ │ │ └── EnrollCouponRequest.java │ │ │ └── response │ │ │ ├── CouponViewResponse.java │ │ │ ├── CreatedCouponResponse.java │ │ │ └── EnrolledCouponResponse.java │ │ ├── lecture │ │ ├── application │ │ │ ├── LectureCommandProcessor.java │ │ │ ├── LectureExceptionHandler.java │ │ │ ├── LectureFacade.java │ │ │ ├── LectureQueryProcessor.java │ │ │ ├── command │ │ │ │ ├── LectureDetailCommand.java │ │ │ │ ├── LectureRegisterCommand.java │ │ │ │ ├── LectureReviewDeleteCommand.java │ │ │ │ ├── LectureReviewModifyCommand.java │ │ │ │ ├── LectureReviewRegisterCommand.java │ │ │ │ └── PublishPreSignedUrlCommand.java │ │ │ ├── exception │ │ │ │ └── DuplicateLectureFileNameException.java │ │ │ ├── query │ │ │ │ └── LectureSearchQuery.java │ │ │ └── result │ │ │ │ ├── LectureSearchResult.java │ │ │ │ └── PreSignedUrlResult.java │ │ ├── domain │ │ │ ├── Lecture.java │ │ │ ├── LectureDetail.java │ │ │ ├── LectureDetailRepository.java │ │ │ ├── LectureFileRepository.java │ │ │ ├── LectureRepository.java │ │ │ ├── LectureReview.java │ │ │ ├── LectureReviewRepository.java │ │ │ └── exception │ │ │ │ ├── InvalidAuthorizationLectureReviewException.java │ │ │ │ ├── NotFoundLectureException.java │ │ │ │ └── NotFoundLectureReviewException.java │ │ ├── infrastructure │ │ │ ├── config │ │ │ │ └── LecturePersistenceConfig.java │ │ │ ├── data │ │ │ │ └── LectureDTO.java │ │ │ ├── persistence │ │ │ │ ├── LectureDetailRepositoryAdapter.java │ │ │ │ ├── LectureFileRepositoryAdapter.java │ │ │ │ ├── LectureRepositoryAdapter.java │ │ │ │ ├── LectureReviewRepositoryAdapter.java │ │ │ │ ├── jpa │ │ │ │ │ ├── LectureDetailJpaRepository.java │ │ │ │ │ ├── LectureJpaRepository.java │ │ │ │ │ └── LectureReviewJpaRepository.java │ │ │ │ └── query │ │ │ │ │ ├── LectureQueryRepository.java │ │ │ │ │ └── condition │ │ │ │ │ └── LectureSearchCondition.java │ │ │ └── storage │ │ │ │ └── aws │ │ │ │ ├── SimpleStorageService.java │ │ │ │ └── config │ │ │ │ └── S3Config.java │ │ └── presentation │ │ │ ├── LectureController.java │ │ │ ├── request │ │ │ ├── LectureDetailRequest.java │ │ │ ├── LectureRegisterRequest.java │ │ │ ├── LectureReviewDeleteRequest.java │ │ │ ├── LectureReviewModifyRequest.java │ │ │ ├── LectureReviewRegisterRequest.java │ │ │ ├── LectureSearchRequest.java │ │ │ └── PreSingedUrlRequest.java │ │ │ └── response │ │ │ ├── LectureQueryResponse.java │ │ │ └── PreSignedUrlResponse.java │ │ ├── member │ │ ├── application │ │ │ ├── MemberFacade.java │ │ │ ├── MemberLoginProcessor.java │ │ │ ├── MemberSignupProcessor.java │ │ │ ├── command │ │ │ │ ├── LoginCommand.java │ │ │ │ └── SignupCommand.java │ │ │ └── result │ │ │ │ └── LoginResult.java │ │ ├── domain │ │ │ ├── Member.java │ │ │ ├── MemberRepository.java │ │ │ ├── MemberVerifier.java │ │ │ ├── Role.java │ │ │ └── exception │ │ │ │ ├── DuplicatedEmailException.java │ │ │ │ ├── DuplicatedNicknameException.java │ │ │ │ ├── InvalidPasswordException.java │ │ │ │ ├── NotFoundMemberException.java │ │ │ │ └── NotMatchPasswordException.java │ │ ├── infrastructure │ │ │ ├── MemberExceptionHandler.java │ │ │ ├── MemberVerifierImpl.java │ │ │ ├── config │ │ │ │ └── MemberConfig.java │ │ │ ├── jwt │ │ │ │ └── TokenProvider.java │ │ │ └── persistence │ │ │ │ ├── MemberJpaRepository.java │ │ │ │ └── MemberRepositoryAdapter.java │ │ └── presentation │ │ │ ├── LoginRequest.java │ │ │ ├── MemberController.java │ │ │ └── SignupRequest.java │ │ ├── order │ │ ├── application │ │ │ ├── CreateOrderProcessor.java │ │ │ ├── OrderFacade.java │ │ │ ├── PayOrderProcessor.java │ │ │ ├── command │ │ │ │ ├── CreateOrderCommand.java │ │ │ │ └── PayOrderCommand.java │ │ │ └── result │ │ │ │ ├── CreatedOrderResult.java │ │ │ │ └── PayedOrderResult.java │ │ ├── domain │ │ │ ├── Order.java │ │ │ ├── OrderItem.java │ │ │ ├── OrderRepository.java │ │ │ ├── OrderStatus.java │ │ │ ├── Price.java │ │ │ └── exception │ │ │ │ ├── AlreadyCanceledOrderException.java │ │ │ │ ├── AlreadyCompletedOrderException.java │ │ │ │ ├── InvalidCreateOrderException.java │ │ │ │ ├── InvalidOrderItemPriceException.java │ │ │ │ ├── InvalidPriceValueException.java │ │ │ │ ├── NotFoundOrderException.java │ │ │ │ └── OrderPayAmountNotMatchException.java │ │ ├── infrastructure │ │ │ ├── config │ │ │ │ └── OrderPersistenceConfig.java │ │ │ └── persistence │ │ │ │ ├── OrderJpaRepository.java │ │ │ │ └── OrderRepositoryAdapter.java │ │ └── presentation │ │ │ ├── OrderController.java │ │ │ ├── request │ │ │ ├── CreateOrderRequest.java │ │ │ └── PayOrderRequest.java │ │ │ └── response │ │ │ ├── CreatedOrderResponse.java │ │ │ └── PayedOrderResponse.java │ │ └── payment │ │ ├── domain │ │ ├── PayMethod.java │ │ ├── PayStatus.java │ │ ├── PayType.java │ │ ├── Payment.java │ │ ├── PaymentRepository.java │ │ └── exception │ │ │ └── NotAllowedInstallmentMonthException.java │ │ └── infrastructure │ │ ├── config │ │ └── PaymentPersistenceConfig.java │ │ └── persistence │ │ ├── PaymentJpaRepository.java │ │ └── PaymentRepositoryAdapter.java ├── kotlin │ └── com │ │ └── flab │ │ └── infrun │ │ └── member │ │ ├── application │ │ ├── data │ │ │ └── LoginData.kt │ │ ├── facade │ │ │ └── MemberManager.kt │ │ └── processor │ │ │ ├── MemberLoginProcessorKt.kt │ │ │ └── MemberSignupProcessorKt.kt │ │ ├── domain │ │ ├── MemberKt.kt │ │ ├── MemberRepositoryKt.kt │ │ ├── MemberVerifierKt.kt │ │ └── exception │ │ │ ├── DuplicatedEmailExceptionKt.kt │ │ │ ├── DuplicatedNicknameExceptionKt.kt │ │ │ ├── InvalidPasswordExceptionKt.kt │ │ │ ├── NotFoundMemberExceptionKt.kt │ │ │ └── NotMatchPasswordExceptionKt.kt │ │ ├── infra │ │ ├── MemberVerifierKtImpl.kt │ │ ├── config │ │ │ ├── MemberApplicationServiceConfiguration.kt │ │ │ ├── MemberDomainServiceConfiguration.kt │ │ │ ├── MemberFacadeConfiguration.kt │ │ │ └── MemberRepositoryConfiguration.kt │ │ ├── jwt │ │ │ └── TokenProviderKt.kt │ │ └── persistence │ │ │ ├── MemberRepositoryImpl.kt │ │ │ └── jpa │ │ │ └── MemberKtJpaRepository.kt │ │ └── interface │ │ ├── MemberControllerKt.kt │ │ └── request │ │ ├── MemberLoginRequest.kt │ │ └── MemberSignupRequest.kt └── resources │ ├── application-local.yaml │ ├── application-prod.yaml │ └── application.yaml └── test ├── java └── com │ └── flab │ └── infrun │ ├── cart │ ├── application │ │ ├── AddCartItemProcessorTest.java │ │ ├── CartsReaderTest.java │ │ ├── DeleteCartItemProcessorTest.java │ │ ├── FakeCartRepository.java │ │ └── FakeLectureRepository.java │ ├── domain │ │ ├── CartFixture.java │ │ ├── CartItemFixture.java │ │ └── CartItemsFixture.java │ └── presentation │ │ └── CartsControllerTest.java │ ├── common │ ├── DatabaseCleaner.java │ └── IntegrationTest.java │ ├── coupon │ ├── application │ │ ├── ConcurrencyRegisterCouponTest.java │ │ ├── CouponReaderTest.java │ │ ├── CreateCouponProcessorTest.java │ │ ├── EnrollCouponProcessorTest.java │ │ └── FakeCouponRepository.java │ ├── domain │ │ └── CouponFixture.java │ └── presentation │ │ └── CouponControllerTest.java │ ├── lecture │ ├── application │ │ ├── LectureCommandProcessorTest.java │ │ └── LectureReviewProcessorTest.java │ ├── domain │ │ ├── LectureFixture.java │ │ ├── StubLectureRepository.java │ │ └── StubLectureReviewRepository.java │ ├── infrastructure │ │ └── persistence │ │ │ ├── LectureDetailRepositoryTest.java │ │ │ ├── LectureRepositoryTest.java │ │ │ └── query │ │ │ └── LectureQueryRepositoryTest.java │ └── presentation │ │ └── LectureControllerApiTest.java │ ├── member │ ├── application │ │ ├── FakePasswordEncoder.java │ │ ├── MemberLoginProcessorTest.java │ │ └── MemberSignupProcessorTest.java │ ├── domain │ │ ├── MemberFixture.java │ │ └── StubMemberRepository.java │ └── presentation │ │ └── MemberControllerTest.java │ ├── order │ ├── application │ │ ├── CreateOrderProcessorTest.java │ │ └── PayOrderProcessorTest.java │ ├── domain │ │ ├── OrderFixture.java │ │ └── OrderItemFixture.java │ └── presentation │ │ └── OrderControllerTest.java │ └── utils │ └── persistence │ ├── StubCouponRepository.java │ └── StubLectureRepository.java └── resources └── db └── h2 └── data.sql /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | charset=utf-8 5 | indent_style=space 6 | indent_size=4 7 | insert_final_newline=true 8 | disabled_rules=no-wildcard-imports,import-ordering 9 | 10 | [*.{kt,kts}] 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/🎯-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F3AF Feature request" 3 | about: Suggest an idea for this project 4 | title: "\U0001F3AF Feature Request" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## 목표 11 | 12 | 구현할 목표를 작성하세요. 13 | 14 | ## 할 일 15 | 16 | - [ ] 할 일을 작성하세요. 17 | 18 | ## 고려사항 19 | 20 | - 고려사항이 있다면 작성하세요. 21 | -------------------------------------------------------------------------------- /.github/workflows/CI-CD.yaml: -------------------------------------------------------------------------------- 1 | name: CI-CD 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | - main 8 | 9 | jobs: 10 | build-and-push: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Set up JDK 17 15 | uses: actions/setup-java@v3 16 | with: 17 | java-version: '17' 18 | distribution: 'adopt' 19 | 20 | - name: Grant execute permission for gradlew 21 | run: chmod +x gradlew 22 | 23 | - name: Build with Gradle 24 | run: ./gradlew build -x test 25 | 26 | - name: Docker build and push 27 | run: | 28 | docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} 29 | docker build -f docker/app.Dockerfile --platform linux/amd64 -t infrun-app . 30 | docker tag infrun-app ${{ secrets.DOCKER_USERNAME }}/infrun-app 31 | docker push ${{ secrets.DOCKER_USERNAME }}/infrun-app 32 | 33 | - name: Deploy prod 34 | uses: appleboy/ssh-action@master 35 | with: 36 | host: ${{ secrets.SSH_HOST }} 37 | username: ${{ secrets.SSH_USERNAME }} 38 | password: ${{ secrets.SSH_PASSWORD }} 39 | port: ${{ secrets.SSH_PORT }} 40 | script: | 41 | docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }} 42 | docker pull ${{ secrets.DOCKER_USERNAME }}/infrun-app 43 | docker rmi -f ${{ secrets.DOCKER_USERNAME }}/infrun-app 44 | docker-compose up --build -d infrun-app -------------------------------------------------------------------------------- /.github/workflows/Test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | types: [ opened, reopened ] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up JDK 17 13 | uses: actions/setup-java@v3 14 | with: 15 | java-version: '17' 16 | distribution: 'adopt' 17 | 18 | - name: Grant execute permission for gradlew 19 | run: chmod +x gradlew 20 | 21 | - name: Test with Gradle 22 | run: ./gradlew test jacocoTestReport 23 | 24 | - name: Add coverage to PR 25 | uses: madrapps/jacoco-report@v1.3 26 | with: 27 | paths: ${{ github.workspace }}/build/reports/jacoco/test/jacocoTestReport.xml 28 | token: ${{ secrets.GITHUB_TOKEN }} 29 | min-coverage-overall: 50 30 | min-coverage-changed-files: 50 31 | update-comment: true 32 | title: "Coverage Report" -------------------------------------------------------------------------------- /.github/workflows/sonarcloud-analyze.yml: -------------------------------------------------------------------------------- 1 | name: F-Lab SonarCloud Code Analyze 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | workflow_dispatch: 7 | 8 | env: 9 | CACHED_DEPENDENCIES_PATHS: '**/node_modules' 10 | 11 | jobs: 12 | CodeAnalyze: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set SonarCloud Project Key 21 | run: | 22 | REPO_NAME=$(echo $GITHUB_REPOSITORY | cut -d '/' -f 2) 23 | ORG_NAME=$(echo $GITHUB_REPOSITORY | cut -d '/' -f 1) 24 | SONAR_PROJECT_KEY="${ORG_NAME}_${REPO_NAME}" 25 | echo "SONAR_PROJECT_KEY=$SONAR_PROJECT_KEY" >> $GITHUB_ENV 26 | 27 | - name: Set up JDK 28 | uses: actions/setup-java@v2 29 | with: 30 | java-version: '19' 31 | distribution: 'adopt' 32 | 33 | - name: Create Sonar Gradle File 34 | run: | 35 | insert_string="plugins { id 'org.sonarqube' version '4.4.1.3373' }" 36 | if [ -f "build.gradle" ]; then 37 | echo "$insert_string" > temp.gradle 38 | cat build.gradle >> temp.gradle 39 | echo "" >> temp.gradle 40 | echo "sonarqube {" >> temp.gradle 41 | echo " properties {" >> temp.gradle 42 | echo " property 'sonar.java.binaries', '**'" >> temp.gradle 43 | echo " }" >> temp.gradle 44 | echo "}" >> temp.gradle 45 | mv temp.gradle build.gradle 46 | else 47 | echo "$insert_string" > build.gradle 48 | echo "" >> build.gradle 49 | echo "sonarqube {" >> build.gradle 50 | echo " properties {" >> build.gradle 51 | echo " property 'sonar.java.binaries', '**'" >> build.gradle 52 | echo " }" >> build.gradle 53 | echo "}" >> build.gradle 54 | fi 55 | 56 | 57 | - name: Analyze 58 | run: ./gradlew sonar -Dsonar.projectKey=${{ env.SONAR_PROJECT_KEY }} -Dsonar.organization=f-lab-edu-1 -Dsonar.host.url=https://sonarcloud.io -Dsonar.token=${{ secrets.SECRET_SONARQUBE }} -Dsonar.gradle.skipCompile=true 59 | env: 60 | SONAR_TOKEN: ${{ secrets.SECRET_SONARQUBE }} 61 | 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## infrun 2 | 3 | 교육 강의 플랫폼인 인프런을 클론하여 그동안 학습한 것을 적용하고 점진적으로 개선해나가는 토이 프로젝트입니다. 4 | 5 | ## 기술 및 환경 6 | 7 | ![stacks](https://github.com/f-lab-edu/infrun/assets/40778768/37f8fbbf-9c06-482a-bba3-40bb10195ff8) 8 | 9 | **BACKEND** 10 | Spring Boot 3.1.0, Java 17, Spring Data JPA, Gradle 11 | 12 | **DATABASE** 13 | H2, MySQL 8.0 14 | 15 | **INFRA** 16 | Docker, Github Actions, NAVER Cloud Platform 17 | 18 | ## 프로젝트 구조 19 | 20 | ![architecture](https://github.com/f-lab-edu/infrun/assets/40778768/42cc2d5f-11b3-4718-ab7e-a433d344aa4f) 21 | -------------------------------------------------------------------------------- /docker/app.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:17 2 | ARG JAR_FILE=build/libs/app.jar 3 | COPY ${JAR_FILE} app.jar 4 | ENV TZ=Asia/Seoul 5 | ENTRYPOINT ["java","-jar","/app.jar"] -------------------------------------------------------------------------------- /docker/database/config/mysql.cnf: -------------------------------------------------------------------------------- 1 | [client] 2 | default-character-set=utf8mb4 3 | 4 | [mysql] 5 | default-character-set=utf8mb4 6 | 7 | [mysqld] 8 | character-set-server=utf8mb4 9 | collation-server=utf8mb4_unicode_ci 10 | skip-character-set-client-handshake 11 | 12 | [mysqldump] 13 | default-character-set=utf8mb4 -------------------------------------------------------------------------------- /docker/database/db.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mysql:8.0.30 2 | ENV TZ=Asia/Seoul 3 | -------------------------------------------------------------------------------- /docker/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | name: infrun 4 | 5 | services: 6 | infrun-database: 7 | restart: on-failure 8 | platform: linux/amd64 9 | build: 10 | dockerfile: db.Dockerfile 11 | context: ./database 12 | ports: 13 | - "3306:3306" 14 | volumes: 15 | - ./database/config:/etc/mysql/conf.d 16 | environment: 17 | MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}" 18 | MYSQL_DATABASE: "${MYSQL_DATABASE}" 19 | MYSQL_USER: "${MYSQL_USER}" 20 | MYSQL_PASSWORD: "${MYSQL_PASSWORD}" 21 | infrun-app: 22 | restart: always 23 | image: chicori3/infrun-app 24 | platform: linux/amd64 25 | depends_on: 26 | - infrun-database 27 | ports: 28 | - "8081:8081" 29 | environment: 30 | SPRING_PROFILES_ACTIVE: prod 31 | SPRING_DATASOURCE_USER: "${MYSQL_USER}" 32 | SPRING_DATASOURCE_PASSWORD: "${MYSQL_PASSWORD}" 33 | JWT_SECRET: "${JWT_SECRET}" 34 | JWT_TOKEN_VALIDITY_IN_SECONDS: "${JWT_TOKEN_VALIDITY_IN_SECONDS}" -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-lab-edu/infrun/04f82aa664ebc1055312f79e90e2c3f6eb5cbe0c/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.6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'infrun' 2 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/InfrunApplication.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class InfrunApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(InfrunApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/application/AddCartItemProcessor.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.application; 2 | 3 | import com.flab.infrun.cart.application.command.AddCartItemCommand; 4 | import com.flab.infrun.cart.application.result.AddedCartItemResult; 5 | import com.flab.infrun.cart.domain.Cart; 6 | import com.flab.infrun.cart.domain.CartRepository; 7 | import com.flab.infrun.lecture.domain.Lecture; 8 | import com.flab.infrun.lecture.domain.LectureRepository; 9 | import java.math.BigDecimal; 10 | import lombok.RequiredArgsConstructor; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.transaction.annotation.Transactional; 13 | 14 | @RequiredArgsConstructor 15 | @Component 16 | public class AddCartItemProcessor { 17 | 18 | private final CartRepository cartRepository; 19 | private final LectureRepository lectureRepository; 20 | 21 | @Transactional 22 | public AddedCartItemResult execute(final AddCartItemCommand command) { 23 | final Lecture lecture = lectureRepository.findById(command.lectureId()) 24 | .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 강의입니다.")); 25 | final Cart cart = cartRepository.findByOwnerId(command.ownerId()) 26 | .orElse(Cart.create(command.ownerId())); 27 | 28 | cart.addCartItem(lecture.getId(), BigDecimal.valueOf(lecture.getPrice())); 29 | 30 | return AddedCartItemResult.from(cart.getLectureIds()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/application/CartsFacade.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.application; 2 | 3 | import com.flab.infrun.cart.application.command.AddCartItemCommand; 4 | import com.flab.infrun.cart.application.command.DeleteCartItemCommand; 5 | import com.flab.infrun.cart.application.result.AddedCartItemResult; 6 | import com.flab.infrun.cart.application.result.CartsResult; 7 | import com.flab.infrun.cart.application.result.DeletedCartItemResult; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.stereotype.Service; 10 | 11 | @RequiredArgsConstructor 12 | @Service 13 | public class CartsFacade { 14 | 15 | private final AddCartItemProcessor addCartItemProcessor; 16 | private final DeleteCartItemProcessor deleteCartItemProcessor; 17 | private final CartsReader cartsReader; 18 | 19 | public CartsResult readCartItems(final Long memberId) { 20 | return cartsReader.read(memberId); 21 | } 22 | 23 | public AddedCartItemResult addCartItem(final AddCartItemCommand command) { 24 | return addCartItemProcessor.execute(command); 25 | } 26 | 27 | public DeletedCartItemResult deleteCartItem(final DeleteCartItemCommand command) { 28 | return deleteCartItemProcessor.execute(command); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/application/CartsReader.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.application; 2 | 3 | import com.flab.infrun.cart.application.result.CartsResult; 4 | import com.flab.infrun.cart.application.result.CartsResult.CartItemResult; 5 | import com.flab.infrun.cart.domain.Cart; 6 | import com.flab.infrun.cart.domain.CartRepository; 7 | import com.flab.infrun.lecture.domain.Lecture; 8 | import com.flab.infrun.lecture.domain.LectureRepository; 9 | import com.flab.infrun.member.domain.Member; 10 | import com.flab.infrun.member.domain.MemberRepository; 11 | import java.math.BigDecimal; 12 | import java.util.List; 13 | import java.util.Optional; 14 | import lombok.RequiredArgsConstructor; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.transaction.annotation.Transactional; 17 | 18 | @RequiredArgsConstructor 19 | @Component 20 | public class CartsReader { 21 | 22 | private final CartRepository cartRepository; 23 | private final LectureRepository lectureRepository; 24 | private final MemberRepository memberRepository; 25 | 26 | @Transactional(readOnly = true) 27 | public CartsResult read(final Long ownerId) { 28 | final Optional optionalCart = cartRepository.findByOwnerId(ownerId); 29 | 30 | return optionalCart.map(this::mapToCartItemsResult) 31 | .orElseGet(() -> new CartsResult(List.of(), BigDecimal.ZERO)); 32 | } 33 | 34 | private CartsResult mapToCartItemsResult(final Cart cart) { 35 | final List lectureIds = cart.getLectureIds(); 36 | final List lectures = lectureRepository.findAllByIdIn(lectureIds); 37 | 38 | return new CartsResult(lectures.stream() 39 | .map(lecture -> { 40 | final Member teacher = memberRepository.findById(lecture.getMember().getId()); 41 | return new CartItemResult( 42 | lecture.getId(), 43 | lecture.getName(), 44 | teacher.getNickname(), 45 | BigDecimal.valueOf(lecture.getPrice()) 46 | ); 47 | }) 48 | .toList(), cart.getTotalPrice()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/application/DeleteCartItemProcessor.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.application; 2 | 3 | import com.flab.infrun.cart.application.command.DeleteCartItemCommand; 4 | import com.flab.infrun.cart.application.result.DeletedCartItemResult; 5 | import com.flab.infrun.cart.domain.Cart; 6 | import com.flab.infrun.cart.domain.CartRepository; 7 | import com.flab.infrun.cart.domain.exception.NotFoundCartException; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | @RequiredArgsConstructor 13 | @Component 14 | public class DeleteCartItemProcessor { 15 | 16 | private final CartRepository cartRepository; 17 | 18 | @Transactional 19 | public DeletedCartItemResult execute(final DeleteCartItemCommand command) { 20 | final Cart cart = cartRepository.findByOwnerId(command.ownerId()) 21 | .orElseThrow(NotFoundCartException::new); 22 | 23 | cart.deleteCartItem(command.lectureId()); 24 | 25 | return DeletedCartItemResult.from(cart.getLectureIds()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/application/command/AddCartItemCommand.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.application.command; 2 | 3 | public record AddCartItemCommand( 4 | Long ownerId, 5 | Long lectureId 6 | ) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/application/command/DeleteCartItemCommand.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.application.command; 2 | 3 | public record DeleteCartItemCommand( 4 | Long ownerId, 5 | Long lectureId 6 | ) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/application/result/AddedCartItemResult.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.application.result; 2 | 3 | import java.util.List; 4 | 5 | public record AddedCartItemResult( 6 | List lectureIds 7 | ) { 8 | 9 | public static AddedCartItemResult from( 10 | final List lectureIds 11 | ) { 12 | return new AddedCartItemResult(lectureIds); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/application/result/CartsResult.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.application.result; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.List; 5 | 6 | public record CartsResult( 7 | List cartItemsResults, 8 | BigDecimal totalPrice 9 | ) { 10 | 11 | public record CartItemResult( 12 | Long lectureId, 13 | String lectureTitle, 14 | String teacherName, 15 | BigDecimal price 16 | ) { 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/application/result/DeletedCartItemResult.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.application.result; 2 | 3 | import java.util.List; 4 | 5 | public record DeletedCartItemResult( 6 | List lectureIds 7 | ) { 8 | 9 | public static DeletedCartItemResult from( 10 | final List lectureIds 11 | ) { 12 | return new DeletedCartItemResult(lectureIds); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/domain/Cart.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.domain; 2 | 3 | import com.flab.infrun.cart.domain.exception.NotFoundCartItemException; 4 | import com.google.common.annotations.VisibleForTesting; 5 | import jakarta.persistence.Embedded; 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 | import java.math.BigDecimal; 12 | import java.util.List; 13 | import lombok.AccessLevel; 14 | import lombok.NoArgsConstructor; 15 | 16 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 17 | @Table(name = "carts") 18 | @Entity 19 | public class Cart { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | private Long id; 24 | private Long ownerId; 25 | @Embedded 26 | private CartItems cartItems = new CartItems(); 27 | private BigDecimal totalPrice; 28 | 29 | private Cart(final Long ownerId) { 30 | this.ownerId = ownerId; 31 | } 32 | 33 | public static Cart create(final Long ownerId) { 34 | return new Cart(ownerId); 35 | } 36 | 37 | private void calculateTotalPrice() { 38 | totalPrice = cartItems.calculateTotalPrice(); 39 | } 40 | 41 | public void addCartItem(final Long lectureId, final BigDecimal price) { 42 | cartItems.add(new CartItem(this, lectureId, price)); 43 | calculateTotalPrice(); 44 | } 45 | 46 | public void deleteCartItem(final Long lectureId) { 47 | if (!cartItems.delete(lectureId)) { 48 | throw new NotFoundCartItemException(); 49 | } 50 | calculateTotalPrice(); 51 | } 52 | 53 | public void hasCartItem(final List itemIds) { 54 | if (!cartItems.hasCartItem(itemIds)) { 55 | throw new NotFoundCartItemException(); 56 | } 57 | } 58 | 59 | public Long getOwnerId() { 60 | return ownerId; 61 | } 62 | 63 | public BigDecimal getTotalPrice() { 64 | return totalPrice; 65 | } 66 | 67 | public List getLectureIds() { 68 | return cartItems.getLectureIds(); 69 | } 70 | 71 | @VisibleForTesting 72 | public void assignId(final Long id) { 73 | this.id = id; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/domain/CartItem.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.domain; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.FetchType; 5 | import jakarta.persistence.GeneratedValue; 6 | import jakarta.persistence.GenerationType; 7 | import jakarta.persistence.Id; 8 | import jakarta.persistence.JoinColumn; 9 | import jakarta.persistence.ManyToOne; 10 | import jakarta.persistence.Table; 11 | import java.math.BigDecimal; 12 | import java.util.Objects; 13 | import lombok.AccessLevel; 14 | import lombok.NoArgsConstructor; 15 | 16 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 17 | @Table(name = "cart_items") 18 | @Entity 19 | public class CartItem { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | private Long id; 24 | private Long lectureId; 25 | private BigDecimal price; 26 | @ManyToOne(fetch = FetchType.LAZY) 27 | @JoinColumn(name = "cart_id") 28 | private Cart cart; 29 | 30 | public CartItem(final Cart cart, final Long lectureId, final BigDecimal price) { 31 | this.cart = cart; 32 | this.lectureId = lectureId; 33 | this.price = price; 34 | } 35 | 36 | Long getLectureId() { 37 | return lectureId; 38 | } 39 | 40 | BigDecimal getPrice() { 41 | return price; 42 | } 43 | 44 | @Override 45 | public boolean equals(final Object o) { 46 | if (this == o) { 47 | return true; 48 | } 49 | if (o == null || getClass() != o.getClass()) { 50 | return false; 51 | } 52 | final CartItem cartItem = (CartItem) o; 53 | return Objects.equals(id, cartItem.id); 54 | } 55 | 56 | @Override 57 | public int hashCode() { 58 | return Objects.hash(lectureId); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/domain/CartItems.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.domain; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Embeddable; 5 | import jakarta.persistence.FetchType; 6 | import jakarta.persistence.OneToMany; 7 | import java.math.BigDecimal; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Objects; 11 | 12 | @Embeddable 13 | public class CartItems { 14 | 15 | @OneToMany(mappedBy = "cart", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) 16 | private List cartItems = new ArrayList<>(); 17 | 18 | List getLectureIds() { 19 | return cartItems.stream() 20 | .map(CartItem::getLectureId) 21 | .toList(); 22 | } 23 | 24 | void add(final CartItem cartItem) { 25 | cartItems.add(cartItem); 26 | } 27 | 28 | boolean delete(final Long lectureId) { 29 | return cartItems.removeIf(cartItem -> Objects.equals(cartItem.getLectureId(), lectureId)); 30 | } 31 | 32 | BigDecimal calculateTotalPrice() { 33 | return cartItems.stream() 34 | .map(CartItem::getPrice) 35 | .reduce(BigDecimal.ZERO, BigDecimal::add); 36 | } 37 | 38 | public boolean hasCartItem(final List itemIds) { 39 | return cartItems.stream() 40 | .map(CartItem::getLectureId) 41 | .allMatch(itemIds::contains); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/domain/CartRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.domain; 2 | 3 | import java.util.Optional; 4 | 5 | public interface CartRepository { 6 | 7 | Cart save(final Cart cart); 8 | 9 | Optional findByOwnerId(final Long ownerId); 10 | 11 | Cart findWithCartItemsByOwnerId(final Long ownerId); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/domain/exception/CartException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public class CartException extends SystemException { 7 | 8 | public CartException(final ErrorCode errorCode) { 9 | super(errorCode); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/domain/exception/NotFoundCartException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | 5 | public final class NotFoundCartException extends CartException { 6 | 7 | public NotFoundCartException() { 8 | super(ErrorCode.NOT_FOUND_CART); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/domain/exception/NotFoundCartItemException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | 5 | public final class NotFoundCartItemException extends CartException { 6 | 7 | public NotFoundCartItemException() { 8 | super(ErrorCode.NOT_FOUND_CART_ITEM); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/infrastructure/config/CartPersistenceConfig.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.infrastructure.config; 2 | 3 | import com.flab.infrun.cart.domain.CartRepository; 4 | import com.flab.infrun.cart.infrastructure.persistence.CartJpaRepository; 5 | import com.flab.infrun.cart.infrastructure.persistence.CartRepositoryAdapter; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class CartPersistenceConfig { 11 | 12 | @Bean 13 | public CartRepository cartRepository(final CartJpaRepository cartJpaRepository) { 14 | return new CartRepositoryAdapter(cartJpaRepository); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/infrastructure/persistence/CartJpaRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.infrastructure.persistence; 2 | 3 | import com.flab.infrun.cart.domain.Cart; 4 | import java.util.Optional; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.Query; 7 | import org.springframework.data.repository.query.Param; 8 | 9 | public interface CartJpaRepository extends JpaRepository { 10 | 11 | Optional findByOwnerId(final Long ownerId); 12 | 13 | @Query("select distinct c from Cart c join fetch c.cartItems.cartItems where c.ownerId = :ownerId") 14 | Optional findWithCartItemsByOwnerId(@Param("ownerId") final Long ownerId); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/infrastructure/persistence/CartRepositoryAdapter.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.infrastructure.persistence; 2 | 3 | import com.flab.infrun.cart.domain.Cart; 4 | import com.flab.infrun.cart.domain.CartRepository; 5 | import com.flab.infrun.cart.domain.exception.NotFoundCartException; 6 | import java.util.Optional; 7 | import lombok.RequiredArgsConstructor; 8 | 9 | @RequiredArgsConstructor 10 | public class CartRepositoryAdapter implements CartRepository { 11 | 12 | private final CartJpaRepository cartJpaRepository; 13 | 14 | @Override 15 | public Cart save(final Cart cart) { 16 | return cartJpaRepository.save(cart); 17 | } 18 | 19 | @Override 20 | public Optional findByOwnerId(final Long ownerId) { 21 | return cartJpaRepository.findByOwnerId(ownerId); 22 | } 23 | 24 | @Override 25 | public Cart findWithCartItemsByOwnerId(final Long ownerId) { 26 | return cartJpaRepository.findWithCartItemsByOwnerId(ownerId) 27 | .orElseThrow(NotFoundCartException::new); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/presentation/request/AddCartItemRequest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.presentation.request; 2 | 3 | import com.flab.infrun.cart.application.command.AddCartItemCommand; 4 | import jakarta.validation.constraints.Min; 5 | import jakarta.validation.constraints.NotNull; 6 | 7 | public record AddCartItemRequest( 8 | 9 | @NotNull(message = "강의 ID는 필수입니다.") 10 | @Min(value = 1, message = "강의 ID는 1 이상이어야 합니다.") 11 | Long lectureId 12 | ) { 13 | 14 | public AddCartItemCommand toCommand(final Long memberId) { 15 | return new AddCartItemCommand(memberId, lectureId); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/presentation/response/AddedCartItemResponse.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.presentation.response; 2 | 3 | import com.flab.infrun.cart.application.result.AddedCartItemResult; 4 | import java.util.List; 5 | 6 | public record AddedCartItemResponse( 7 | List lectureIds 8 | ) { 9 | 10 | public static AddedCartItemResponse from(final AddedCartItemResult result) { 11 | return new AddedCartItemResponse(result.lectureIds()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/presentation/response/CartsResponse.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.presentation.response; 2 | 3 | import com.flab.infrun.cart.application.result.CartsResult; 4 | import java.math.BigDecimal; 5 | import java.util.List; 6 | 7 | public record CartsResponse( 8 | List carts, 9 | BigDecimal totalPrice 10 | ) { 11 | 12 | public static CartsResponse from(final CartsResult result) { 13 | return new CartsResponse( 14 | result.cartItemsResults().stream() 15 | .map(CartItem::from) 16 | .toList(), result.totalPrice() 17 | ); 18 | } 19 | 20 | record CartItem( 21 | Long lectureId, 22 | String lectureTitle, 23 | String teacherName, 24 | BigDecimal price 25 | ) { 26 | 27 | public static CartItem from(final CartsResult.CartItemResult result) { 28 | return new CartItem( 29 | result.lectureId(), 30 | result.lectureTitle(), 31 | result.teacherName(), 32 | result.price() 33 | ); 34 | } 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/cart/presentation/response/DeletedCartItemResponse.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.presentation.response; 2 | 3 | import com.flab.infrun.cart.application.result.DeletedCartItemResult; 4 | import java.util.List; 5 | 6 | public record DeletedCartItemResponse( 7 | List lectureIds 8 | ) { 9 | 10 | public static DeletedCartItemResponse from(final DeletedCartItemResult result) { 11 | return new DeletedCartItemResponse(result.lectureIds()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/annotation/ApplicationService.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.annotation 2 | 3 | annotation class ApplicationService 4 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/config/AuditingConfig.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 5 | 6 | @Configuration 7 | @EnableJpaAuditing 8 | public class AuditingConfig { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/config/jwt/JwtFilter.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.config.jwt; 2 | 3 | import com.flab.infrun.member.infrastructure.jwt.TokenProvider; 4 | import jakarta.servlet.FilterChain; 5 | import jakarta.servlet.ServletException; 6 | import jakarta.servlet.ServletRequest; 7 | import jakarta.servlet.ServletResponse; 8 | import jakarta.servlet.http.HttpServletRequest; 9 | import java.io.IOException; 10 | import org.apache.http.HttpHeaders; 11 | import org.springframework.security.core.Authentication; 12 | import org.springframework.security.core.context.SecurityContextHolder; 13 | import org.springframework.util.StringUtils; 14 | import org.springframework.web.filter.GenericFilterBean; 15 | 16 | public class JwtFilter extends GenericFilterBean { 17 | 18 | private final TokenProvider tokenProvider; 19 | 20 | public JwtFilter(final TokenProvider tokenProvider) { 21 | this.tokenProvider = tokenProvider; 22 | } 23 | 24 | @Override 25 | public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, 26 | final FilterChain chain) 27 | throws IOException, ServletException { 28 | final HttpServletRequest request = (HttpServletRequest) servletRequest; 29 | final String jwt = resolveToken(request); 30 | 31 | if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) { 32 | final Authentication authentication = tokenProvider.getAuthentication(jwt); 33 | SecurityContextHolder.getContext().setAuthentication(authentication); 34 | } 35 | 36 | chain.doFilter(servletRequest, servletResponse); 37 | } 38 | 39 | private String resolveToken(HttpServletRequest request) { 40 | final String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION); 41 | 42 | if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { 43 | return bearerToken.substring(7); 44 | } 45 | 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/config/jwt/JwtSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.config.jwt; 2 | 3 | import com.flab.infrun.member.infrastructure.jwt.TokenProvider; 4 | import org.springframework.security.config.annotation.SecurityConfigurerAdapter; 5 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6 | import org.springframework.security.web.DefaultSecurityFilterChain; 7 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 8 | 9 | public class JwtSecurityConfig extends 10 | SecurityConfigurerAdapter { 11 | 12 | private final TokenProvider tokenProvider; 13 | 14 | public JwtSecurityConfig(final TokenProvider tokenProvider) { 15 | this.tokenProvider = tokenProvider; 16 | } 17 | 18 | @Override 19 | public void configure(final HttpSecurity builder) { 20 | final JwtFilter filter = new JwtFilter(tokenProvider); 21 | 22 | builder.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/config/queryDsl/QueryDSLConfig.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.config.queryDsl; 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory; 4 | import jakarta.persistence.EntityManager; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class QueryDSLConfig { 10 | 11 | @Bean 12 | public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) { 13 | return new JPAQueryFactory(entityManager); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/config/security/CurrentUser.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.config.security; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 8 | 9 | @Target(ElementType.PARAMETER) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") 12 | public @interface CurrentUser { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/config/security/CustomAccessDeniedHandler.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.config.security; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import java.io.IOException; 6 | import org.springframework.beans.factory.annotation.Qualifier; 7 | import org.springframework.security.access.AccessDeniedException; 8 | import org.springframework.security.web.access.AccessDeniedHandler; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.servlet.HandlerExceptionResolver; 11 | 12 | @Component 13 | public class CustomAccessDeniedHandler implements AccessDeniedHandler { 14 | 15 | private final HandlerExceptionResolver resolver; 16 | 17 | public CustomAccessDeniedHandler( 18 | @Qualifier("handlerExceptionResolver") final HandlerExceptionResolver resolver) { 19 | this.resolver = resolver; 20 | } 21 | 22 | @Override 23 | public void handle(final HttpServletRequest request, final HttpServletResponse response, 24 | final AccessDeniedException accessDeniedException) throws IOException { 25 | resolver.resolveException(request, response, null, accessDeniedException); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/config/security/CustomAuthenticationEntryPoint.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.config.security; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import java.io.IOException; 6 | import org.springframework.beans.factory.annotation.Qualifier; 7 | import org.springframework.security.core.AuthenticationException; 8 | import org.springframework.security.web.AuthenticationEntryPoint; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.servlet.HandlerExceptionResolver; 11 | 12 | @Component 13 | public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { 14 | 15 | private final HandlerExceptionResolver resolver; 16 | 17 | public CustomAuthenticationEntryPoint( 18 | @Qualifier("handlerExceptionResolver") final HandlerExceptionResolver resolver) { 19 | this.resolver = resolver; 20 | } 21 | 22 | @Override 23 | public void commence(final HttpServletRequest request, final HttpServletResponse response, 24 | final AuthenticationException authException) throws IOException { 25 | resolver.resolveException(request, response, null, authException); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/config/security/CustomUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.config.security; 2 | 3 | import com.flab.infrun.member.domain.Member; 4 | import com.flab.infrun.member.domain.MemberRepository; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | import org.springframework.security.core.userdetails.UserDetailsService; 8 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component("userDetailsService") 12 | @RequiredArgsConstructor 13 | public class CustomUserDetailsService implements UserDetailsService { 14 | 15 | private final MemberRepository memberRepository; 16 | 17 | @Override 18 | public UserDetails loadUserByUsername(final String email) throws UsernameNotFoundException { 19 | final Member member = memberRepository.findByEmail(email); 20 | 21 | return new UserAdapter(member); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/config/security/UserAdapter.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.config.security; 2 | 3 | import com.flab.infrun.member.domain.Member; 4 | import com.flab.infrun.member.domain.Role; 5 | import java.util.Collection; 6 | import java.util.Set; 7 | import org.springframework.security.core.GrantedAuthority; 8 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 9 | import org.springframework.security.core.userdetails.User; 10 | 11 | public class UserAdapter extends User { 12 | 13 | private final Member member; 14 | 15 | public UserAdapter(final Member member) { 16 | super(member.getEmail(), member.getPassword(), authorities(member.getRole())); 17 | this.member = member; 18 | } 19 | 20 | private static Collection authorities(final Role role) { 21 | return Set.of(new SimpleGrantedAuthority(role.getValue())); 22 | } 23 | 24 | public Member getMember() { 25 | return member; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/config/security/WebSecurityCustomizerByProfile.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.config.security; 2 | 3 | import org.springframework.boot.autoconfigure.security.servlet.PathRequest; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.annotation.Profile; 7 | import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; 8 | 9 | @Configuration 10 | public class WebSecurityCustomizerByProfile { 11 | 12 | @Bean 13 | @Profile("local") 14 | public WebSecurityCustomizer localWebSecurityCustomizer() { 15 | return web -> web.ignoring() 16 | .requestMatchers(PathRequest.toStaticResources().atCommonLocations()) 17 | .requestMatchers(PathRequest.toH2Console()); 18 | } 19 | 20 | @Bean 21 | @Profile("prod") 22 | public WebSecurityCustomizer webSecurityCustomizer() { 23 | return web -> web.ignoring() 24 | .requestMatchers(PathRequest.toStaticResources().atCommonLocations()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/entity/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.entity; 2 | 3 | 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 | public abstract class BaseEntity { 14 | 15 | @CreatedDate 16 | private LocalDateTime createdAt; 17 | 18 | @LastModifiedDate 19 | private LocalDateTime modifiedAt; 20 | 21 | public LocalDateTime getCreatedAt() { 22 | return createdAt; 23 | } 24 | 25 | public LocalDateTime getModifiedAt() { 26 | return modifiedAt; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/exception/ErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.exception; 2 | 3 | public enum ErrorCode { 4 | 5 | // COMMON 6 | SYSTEM_ERROR("시스템 에러가 발생하였습니다."), 7 | INVALID_PARAMETER("요청값이 올바르지 않습니다."), 8 | 9 | // AUTH 10 | UN_AUTHORIZATION("유효하지 않은 인증정보입니다."), 11 | ACCESS_DENIED("접근 권한이 없습니다."), 12 | 13 | // MEMBER 14 | DUPLICATED_NICKNAME("이미 존재하는 닉네임입니다."), 15 | DUPLICATED_EMAIL("이미 존재하는 이메일입니다."), 16 | NOT_FOUND_MEMBER("존재하지 않는 회원입니다."), 17 | INVALID_PASSWORD("비밀번호 형식이 올바르지 않습니다."), 18 | NOT_MATCH_PASSWORD("비밀번호가 일치하지 않습니다."), 19 | 20 | //LECTURE 21 | DUPLICATED_FILE_NAME("중복되는 파일명이 존재합니다."), 22 | NOT_FOUND_LECTURE("존재하지 않는 강의입니다."), 23 | 24 | //LECTURE_REVIEW 25 | NOT_FOUND_LECTURE_REVIEW("존재하지 않는 강의 리뷰입니다."), 26 | INVALID_AUTHORIZATION_LECTURE_REVIEW("리뷰 수정 권한이 없습니다."), 27 | 28 | // COUPON 29 | INVALID_COUPON_EXPIRATION_AT("쿠폰 만료일이 유효하지 않습니다."), 30 | INVALID_COUPON_QUANTITY("쿠폰 수량이 유효하지 않습니다."), 31 | INVALID_COUPON_DISCOUNT_TYPE("쿠폰 할인 타입이 유효하지 않습니다."), 32 | INVALID_COUPON_DISCOUNT_AMOUNT("쿠폰 할인 금액 또는 할인율이 유효하지 않습니다."), 33 | NOT_FOUND_COUPON("존재하지 않는 쿠폰입니다."), 34 | ALREADY_USED_COUPON("이미 사용된 쿠폰입니다."), 35 | ALREADY_REGISTERED_COUPON("이미 등록된 쿠폰입니다."), 36 | EXPIRED_COUPON("만료된 쿠폰입니다."), 37 | INVALID_COUPON_OWNER("쿠폰 소유자가 아닙니다."), 38 | 39 | // CART 40 | NOT_FOUND_CART("수강바구니를 찾을 수 없습니다."), 41 | NOT_FOUND_CART_ITEM("수강바구니에 해당 상품을 찾을 수 없습니다."), 42 | 43 | // ORDER 44 | INVALID_CREATE_ORDER("유효하지 않은 주문 정보입니다."), 45 | INVALID_PRICE_VALUE("주문 금액은 0원보다 적을 수 없습니다."), 46 | INVALID_ORDER_ITEM_PRICE_VALUE("주문 상품 가격이 올바르지 않습니다."), 47 | NOT_FOUND_ORDER("주문을 찾을 수 없습니다."), 48 | ORDER_PAY_AMOUNT_NOT_MATCH("주문 결제 금액이 일치하지 않습니다."), 49 | ALREADY_CANCELED_ORDER("이미 취소된 주문입니다."), 50 | ALREADY_COMPLETED_ORDER("이미 완료된 주문입니다."), 51 | 52 | // PAYMENT 53 | NOT_ALLOWED_INSTALLMENT_MONTH("허용되지 않은 할부 개월입니다."); 54 | 55 | private final String message; 56 | 57 | ErrorCode(final String message) { 58 | this.message = message; 59 | } 60 | 61 | public String getMessage() { 62 | return message; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/exception/SystemException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.exception; 2 | 3 | public class SystemException extends RuntimeException { 4 | 5 | private final ErrorCode errorCode; 6 | 7 | public SystemException(final ErrorCode errorCode) { 8 | super(errorCode.getMessage()); 9 | this.errorCode = errorCode; 10 | } 11 | 12 | public SystemException(final String message, final ErrorCode errorCode) { 13 | super(message); 14 | this.errorCode = errorCode; 15 | } 16 | 17 | public ErrorCode getErrorCode() { 18 | return errorCode; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/response/Response.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.response; 2 | 3 | 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.fasterxml.jackson.annotation.JsonInclude.Include; 6 | import com.flab.infrun.common.exception.ErrorCode; 7 | 8 | public record Response( 9 | boolean isSuccess, 10 | @JsonInclude(Include.NON_NULL) 11 | T data, 12 | @JsonInclude(Include.NON_NULL) 13 | ErrorCode errorCode, 14 | @JsonInclude(Include.NON_NULL) 15 | String message) { 16 | 17 | public static Response success() { 18 | return new Response<>(true, null, null, null); 19 | } 20 | 21 | public static Response success(final T data) { 22 | return new Response<>(true, data, null, null); 23 | } 24 | 25 | public static Response fail(final ErrorCode errorCode) { 26 | return new Response<>(false, null, errorCode, errorCode.getMessage()); 27 | } 28 | 29 | public static Response fail(final ErrorCode errorCode, final String message) { 30 | return new Response<>(false, null, errorCode, message); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/validator/EnumValid.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.validator; 2 | 3 | import jakarta.validation.Constraint; 4 | import jakarta.validation.Payload; 5 | import java.lang.annotation.Documented; 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | @Documented 12 | @Constraint(validatedBy = EnumValidator.class) 13 | @Target({ElementType.METHOD, ElementType.FIELD}) 14 | @Retention(RetentionPolicy.RUNTIME) 15 | public @interface EnumValid { 16 | 17 | Class> type(); 18 | 19 | String message() default ""; 20 | 21 | Class[] groups() default {}; 22 | 23 | Class[] payload() default {}; 24 | 25 | boolean ignoreCase() default false; 26 | } -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/common/validator/EnumValidator.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common.validator; 2 | 3 | import jakarta.validation.ConstraintValidator; 4 | import jakarta.validation.ConstraintValidatorContext; 5 | 6 | public class EnumValidator implements ConstraintValidator { 7 | 8 | private EnumValid enumValid; 9 | 10 | @Override 11 | public void initialize(final EnumValid constraintAnnotation) { 12 | this.enumValid = constraintAnnotation; 13 | } 14 | 15 | @Override 16 | public boolean isValid(final String value, final ConstraintValidatorContext context) { 17 | if (value == null) { 18 | return false; 19 | } 20 | 21 | Enum[] enumValues = this.enumValid.type().getEnumConstants(); 22 | 23 | for (Enum enumValue : enumValues) { 24 | if (value.equals(enumValue.toString()) 25 | || this.enumValid.ignoreCase() && value.equalsIgnoreCase(enumValue.toString())) { 26 | return true; 27 | } 28 | } 29 | 30 | return false; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/application/CouponFacade.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.application; 2 | 3 | import com.flab.infrun.coupon.application.command.CouponRegisterCommand; 4 | import com.flab.infrun.coupon.application.command.CreateCouponCommand; 5 | import com.flab.infrun.coupon.application.result.CouponView; 6 | import com.flab.infrun.coupon.application.result.CreatedCouponResult; 7 | import com.flab.infrun.coupon.application.result.EnrolledCouponResult; 8 | import java.time.LocalDateTime; 9 | import java.util.List; 10 | import lombok.RequiredArgsConstructor; 11 | import org.springframework.stereotype.Service; 12 | 13 | @RequiredArgsConstructor 14 | @Service 15 | public class CouponFacade { 16 | 17 | private final CreateCouponProcessor createCouponProcessor; 18 | private final EnrollCouponProcessor enrollCouponProcessor; 19 | private final CouponReader couponReader; 20 | 21 | public List getCoupons( 22 | final Long ownerId, 23 | final LocalDateTime currentTime 24 | ) { 25 | return couponReader.read(ownerId, currentTime); 26 | } 27 | 28 | public CreatedCouponResult createCoupons( 29 | final CreateCouponCommand command, 30 | final LocalDateTime currentTime 31 | ) { 32 | return createCouponProcessor.execute(command, currentTime); 33 | } 34 | 35 | public EnrolledCouponResult enrollCoupon( 36 | final CouponRegisterCommand command, 37 | final LocalDateTime currentTime 38 | ) { 39 | return enrollCouponProcessor.execute(command, currentTime); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/application/CouponReader.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.application; 2 | 3 | import com.flab.infrun.coupon.application.result.CouponView; 4 | import com.flab.infrun.coupon.domain.Coupon; 5 | import com.flab.infrun.coupon.domain.CouponRepository; 6 | import java.time.Duration; 7 | import java.time.LocalDateTime; 8 | import java.util.List; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.stereotype.Component; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | @RequiredArgsConstructor 14 | @Component 15 | public class CouponReader { 16 | 17 | private final CouponRepository couponRepository; 18 | 19 | @Transactional(readOnly = true) 20 | public List read(final Long ownerId, final LocalDateTime currentTime) { 21 | final List coupons = couponRepository.findAllByOwnerId(ownerId); 22 | 23 | return mapToCouponView(currentTime, coupons); 24 | } 25 | 26 | private List mapToCouponView( 27 | final LocalDateTime currentTime, 28 | final List coupons 29 | ) { 30 | return coupons.stream() 31 | .filter(coupon -> coupon.getExpirationAt().isBefore(currentTime)) 32 | .map(coupon -> new CouponView( 33 | Duration.between(coupon.getExpirationAt(), currentTime).toDays(), 34 | coupon.getDiscountInfo().getDiscountValue(), 35 | coupon.getDiscountInfo().getDiscountType().name() 36 | )) 37 | .toList(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/application/CreateCouponProcessor.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.application; 2 | 3 | import com.flab.infrun.coupon.application.command.CreateCouponCommand; 4 | import com.flab.infrun.coupon.application.result.CreatedCouponResult; 5 | import com.flab.infrun.coupon.domain.Coupon; 6 | import com.flab.infrun.coupon.domain.CouponCodeGenerator; 7 | import com.flab.infrun.coupon.domain.CouponRepository; 8 | import com.flab.infrun.coupon.domain.CouponValidator; 9 | import java.time.LocalDateTime; 10 | import java.util.List; 11 | import java.util.stream.IntStream; 12 | import lombok.AllArgsConstructor; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.transaction.annotation.Transactional; 15 | 16 | @AllArgsConstructor 17 | @Component 18 | public class CreateCouponProcessor { 19 | 20 | private final CouponRepository couponRepository; 21 | private final CouponCodeGenerator couponCodeGenerator; 22 | private final CouponValidator couponValidator; 23 | 24 | @Transactional 25 | public CreatedCouponResult execute( 26 | final CreateCouponCommand command, 27 | final LocalDateTime currentTime 28 | ) { 29 | verifyCouponCommand(command, currentTime); 30 | 31 | final List coupons = generateCoupons(command); 32 | final List savedCoupons = couponRepository.saveAll(coupons); 33 | 34 | return CreatedCouponResult.from( 35 | savedCoupons.size(), 36 | savedCoupons.stream().map(Coupon::getCode).toList(), 37 | command.expirationAt() 38 | ); 39 | } 40 | 41 | private List generateCoupons(final CreateCouponCommand command) { 42 | final int quantity = command.quantity(); 43 | 44 | return IntStream.range(0, quantity) 45 | .mapToObj(i -> command.toEntity(couponCodeGenerator.generate())) 46 | .toList(); 47 | } 48 | 49 | private void verifyCouponCommand(final CreateCouponCommand command, 50 | final LocalDateTime currentTime) { 51 | couponValidator.verifyDiscountType(command.discountType()); 52 | couponValidator.verifyDiscountValue(command.discountValue()); 53 | couponValidator.verifyExpirationAt(command.expirationAt(), currentTime); 54 | couponValidator.verifyQuantity(command.quantity()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/application/EnrollCouponProcessor.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.application; 2 | 3 | import com.flab.infrun.coupon.application.command.CouponRegisterCommand; 4 | import com.flab.infrun.coupon.application.result.EnrolledCouponResult; 5 | import com.flab.infrun.coupon.domain.Coupon; 6 | import com.flab.infrun.coupon.domain.CouponRepository; 7 | import com.flab.infrun.coupon.domain.exception.NotFoundCouponException; 8 | import java.time.LocalDateTime; 9 | import lombok.AllArgsConstructor; 10 | import org.springframework.stereotype.Component; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | @AllArgsConstructor 14 | @Component 15 | public class EnrollCouponProcessor { 16 | 17 | private final CouponRepository couponRepository; 18 | 19 | @Transactional 20 | public EnrolledCouponResult execute( 21 | final CouponRegisterCommand command, 22 | final LocalDateTime currentTime 23 | ) { 24 | final Coupon coupon = couponRepository.findByCouponCodeWithLock(command.couponCode()) 25 | .orElseThrow(NotFoundCouponException::new); 26 | 27 | coupon.enroll(command.member().getId(), currentTime); 28 | 29 | return EnrolledCouponResult.from(command.member().getEmail(), coupon); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/application/command/CouponRegisterCommand.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.application.command; 2 | 3 | import com.flab.infrun.member.domain.Member; 4 | 5 | public record CouponRegisterCommand( 6 | Member member, 7 | String couponCode 8 | ) { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/application/command/CreateCouponCommand.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.application.command; 2 | 3 | import com.flab.infrun.coupon.domain.Coupon; 4 | import com.flab.infrun.coupon.domain.DiscountInfo; 5 | import com.flab.infrun.coupon.domain.DiscountType; 6 | import java.time.LocalDateTime; 7 | 8 | public record CreateCouponCommand( 9 | String discountType, 10 | int discountValue, 11 | LocalDateTime expirationAt, 12 | int quantity 13 | ) { 14 | 15 | public Coupon toEntity(final String code) { 16 | return Coupon.create( 17 | code, 18 | DiscountInfo.of( 19 | DiscountType.valueOf(discountType), 20 | discountValue 21 | ), 22 | expirationAt 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/application/result/CouponView.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.application.result; 2 | 3 | public record CouponView( 4 | long dDay, 5 | int discountValue, 6 | String discountType 7 | ) { 8 | 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/application/result/CreatedCouponResult.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.application.result; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.List; 5 | 6 | public record CreatedCouponResult( 7 | int quantity, 8 | List couponCodes, 9 | LocalDateTime expirationAt 10 | ) { 11 | 12 | public static CreatedCouponResult from( 13 | final int quantity, 14 | final List couponCodes, 15 | final LocalDateTime expirationAt 16 | ) { 17 | return new CreatedCouponResult(quantity, couponCodes, expirationAt); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/application/result/EnrolledCouponResult.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.application.result; 2 | 3 | import com.flab.infrun.coupon.domain.Coupon; 4 | import com.flab.infrun.coupon.domain.DiscountInfo; 5 | import java.time.LocalDateTime; 6 | 7 | public record EnrolledCouponResult( 8 | String ownerEmail, 9 | DiscountInfo discountInfo, 10 | LocalDateTime expirationAt 11 | ) { 12 | 13 | public static EnrolledCouponResult from( 14 | final String ownerEmail, 15 | final Coupon coupon 16 | ) { 17 | return new EnrolledCouponResult( 18 | ownerEmail, 19 | coupon.getDiscountInfo(), 20 | coupon.getExpirationAt() 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/domain/CouponCodeGenerator.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.domain; 2 | 3 | public interface CouponCodeGenerator { 4 | 5 | String generate(); 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/domain/CouponRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | public interface CouponRepository { 7 | 8 | Coupon save(final Coupon coupon); 9 | 10 | List saveAll(final List coupons); 11 | 12 | Coupon findByCouponCode(final String couponCode); 13 | 14 | Optional findByCouponCodeWithLock(final String couponCode); 15 | 16 | List findAllByOwnerId(final Long ownerId); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/domain/CouponStatus.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.domain; 2 | 3 | public enum CouponStatus { 4 | UNREGISTERED, REGISTERED, USED, EXPIRED 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/domain/CouponValidator.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.domain; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public interface CouponValidator { 6 | 7 | void verifyDiscountType(final String discountType); 8 | 9 | void verifyDiscountValue(final int discountValue); 10 | 11 | void verifyExpirationAt(final LocalDateTime expirationAt, final LocalDateTime currentTime); 12 | 13 | void verifyQuantity(final int quantity); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/domain/DiscountInfo.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.domain; 2 | 3 | import jakarta.persistence.Embeddable; 4 | import java.math.BigDecimal; 5 | import lombok.AccessLevel; 6 | import lombok.NoArgsConstructor; 7 | 8 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 9 | @Embeddable 10 | public class DiscountInfo { 11 | 12 | private DiscountType discountType; 13 | private int discountValue; 14 | 15 | private DiscountInfo(final DiscountType discountType, final int discountValue) { 16 | this.discountType = discountType; 17 | this.discountValue = discountValue; 18 | } 19 | 20 | public static DiscountInfo of( 21 | final DiscountType discountType, 22 | final int discountValue 23 | ) { 24 | return new DiscountInfo(discountType, discountValue); 25 | } 26 | 27 | public DiscountType getDiscountType() { 28 | return discountType; 29 | } 30 | 31 | public int getDiscountValue() { 32 | return discountValue; 33 | } 34 | 35 | public BigDecimal discount(final BigDecimal price) { 36 | return BigDecimal.valueOf( 37 | this.discountType.calculateDiscountPrice(price.intValue(), discountValue)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/domain/DiscountType.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.domain; 2 | 3 | import java.util.function.BiFunction; 4 | 5 | public enum DiscountType { 6 | 7 | 8 | FIX(DiscountType::calculateDiscountByFix), 9 | RATE(DiscountType::calculateDiscountByRate); 10 | 11 | private final BiFunction expression; 12 | 13 | DiscountType(final BiFunction expression) { 14 | this.expression = expression; 15 | } 16 | 17 | private static int calculateDiscountByRate(final int lecturePrice, final int discountAmount) { 18 | final int discountedPrice = lecturePrice * (100 - discountAmount) / 100; 19 | return (int) (double) ((discountedPrice / 10) * 10); 20 | } 21 | 22 | private static int calculateDiscountByFix(final int lecturePrice, final int discountAmount) { 23 | return lecturePrice - discountAmount; 24 | } 25 | 26 | public int calculateDiscountPrice(final int lecturePrice, final int discountValue) { 27 | return expression.apply(lecturePrice, discountValue); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/domain/exception/AlreadyRegisteredCouponException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class AlreadyRegisteredCouponException extends SystemException { 7 | 8 | public AlreadyRegisteredCouponException() { 9 | super(ErrorCode.ALREADY_REGISTERED_COUPON); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/domain/exception/AlreadyUsedCouponException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public class AlreadyUsedCouponException extends SystemException { 7 | 8 | public AlreadyUsedCouponException() { 9 | super(ErrorCode.ALREADY_USED_COUPON); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/domain/exception/ExpiredCouponException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class ExpiredCouponException extends SystemException { 7 | 8 | public ExpiredCouponException() { 9 | super(ErrorCode.EXPIRED_COUPON); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/domain/exception/InvalidCouponDiscountAmountException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class InvalidCouponDiscountAmountException extends SystemException { 7 | 8 | public InvalidCouponDiscountAmountException() { 9 | super(ErrorCode.INVALID_COUPON_DISCOUNT_AMOUNT); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/domain/exception/InvalidCouponDiscountTypeException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class InvalidCouponDiscountTypeException extends SystemException { 7 | 8 | public InvalidCouponDiscountTypeException() { 9 | super(ErrorCode.INVALID_COUPON_DISCOUNT_TYPE); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/domain/exception/InvalidCouponExpirationAtException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class InvalidCouponExpirationAtException extends SystemException { 7 | 8 | public InvalidCouponExpirationAtException() { 9 | super(ErrorCode.INVALID_COUPON_EXPIRATION_AT); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/domain/exception/InvalidCouponOwnerException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public class InvalidCouponOwnerException extends SystemException { 7 | 8 | public InvalidCouponOwnerException() { 9 | super(ErrorCode.INVALID_COUPON_OWNER); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/domain/exception/InvalidCouponQuantityException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class InvalidCouponQuantityException extends SystemException { 7 | 8 | public InvalidCouponQuantityException() { 9 | super(ErrorCode.INVALID_COUPON_QUANTITY); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/domain/exception/NotFoundCouponException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class NotFoundCouponException extends SystemException { 7 | 8 | public NotFoundCouponException() { 9 | super(ErrorCode.NOT_FOUND_COUPON); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/infrastructure/CouponValidatorImpl.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.infrastructure; 2 | 3 | import com.flab.infrun.coupon.domain.CouponValidator; 4 | import com.flab.infrun.coupon.domain.exception.InvalidCouponDiscountAmountException; 5 | import com.flab.infrun.coupon.domain.exception.InvalidCouponDiscountTypeException; 6 | import com.flab.infrun.coupon.domain.exception.InvalidCouponExpirationAtException; 7 | import com.flab.infrun.coupon.domain.exception.InvalidCouponQuantityException; 8 | import java.time.LocalDateTime; 9 | 10 | public class CouponValidatorImpl implements CouponValidator { 11 | 12 | @Override 13 | public void verifyDiscountType(final String discountType) { 14 | if (discountType == null) { 15 | throw new InvalidCouponDiscountTypeException(); 16 | } 17 | } 18 | 19 | @Override 20 | public void verifyDiscountValue(final int discountAmount) { 21 | if (discountAmount <= 0) { 22 | throw new InvalidCouponDiscountAmountException(); 23 | } 24 | } 25 | 26 | @Override 27 | public void verifyExpirationAt( 28 | final LocalDateTime expirationAt, 29 | final LocalDateTime currentTime 30 | ) { 31 | if (expirationAt.isBefore(currentTime)) { 32 | throw new InvalidCouponExpirationAtException(); 33 | } 34 | } 35 | 36 | @Override 37 | public void verifyQuantity(final int quantity) { 38 | if (quantity <= 0) { 39 | throw new InvalidCouponQuantityException(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/infrastructure/UUIDCouponCodeGenerator.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.infrastructure; 2 | 3 | import com.flab.infrun.coupon.domain.CouponCodeGenerator; 4 | import java.util.UUID; 5 | 6 | 7 | public class UUIDCouponCodeGenerator implements CouponCodeGenerator { 8 | 9 | @Override 10 | public String generate() { 11 | return UUID.randomUUID().toString(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/infrastructure/config/CouponCodeGeneratorConfig.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.infrastructure.config; 2 | 3 | import com.flab.infrun.coupon.domain.CouponCodeGenerator; 4 | import com.flab.infrun.coupon.infrastructure.UUIDCouponCodeGenerator; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class CouponCodeGeneratorConfig { 10 | 11 | @Bean 12 | public CouponCodeGenerator couponCodeGenerator() { 13 | return new UUIDCouponCodeGenerator(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/infrastructure/config/CouponPersistenceConfig.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.infrastructure.config; 2 | 3 | import com.flab.infrun.coupon.infrastructure.persistence.CouponJpaRepository; 4 | import com.flab.infrun.coupon.infrastructure.persistence.CouponRepositoryAdapter; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class CouponPersistenceConfig { 10 | 11 | @Bean 12 | public CouponRepositoryAdapter couponRepositoryAdapter( 13 | final CouponJpaRepository couponJpaRepository 14 | ) { 15 | return new CouponRepositoryAdapter(couponJpaRepository); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/infrastructure/config/CouponValidatorConfig.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.infrastructure.config; 2 | 3 | import com.flab.infrun.coupon.domain.CouponValidator; 4 | import com.flab.infrun.coupon.infrastructure.CouponValidatorImpl; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class CouponValidatorConfig { 10 | 11 | @Bean 12 | public CouponValidator couponValidator() { 13 | return new CouponValidatorImpl(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/infrastructure/persistence/CouponJpaRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.infrastructure.persistence; 2 | 3 | import com.flab.infrun.coupon.domain.Coupon; 4 | import jakarta.persistence.LockModeType; 5 | import java.util.List; 6 | import java.util.Optional; 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.data.jpa.repository.Lock; 9 | import org.springframework.data.jpa.repository.Query; 10 | import org.springframework.data.repository.query.Param; 11 | 12 | public interface CouponJpaRepository extends JpaRepository { 13 | 14 | Optional findByCode(final String couponCode); 15 | 16 | @Lock(LockModeType.PESSIMISTIC_WRITE) 17 | @Query("select c from Coupon c where c.code = :couponCode") 18 | Optional findByCodeWithLock(@Param("couponCode") final String couponCode); 19 | 20 | List findAllByOwnerId(final Long ownerId); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/infrastructure/persistence/CouponRepositoryAdapter.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.infrastructure.persistence; 2 | 3 | import com.flab.infrun.coupon.domain.Coupon; 4 | import com.flab.infrun.coupon.domain.CouponRepository; 5 | import com.flab.infrun.coupon.domain.exception.NotFoundCouponException; 6 | import java.util.List; 7 | import java.util.Optional; 8 | import lombok.RequiredArgsConstructor; 9 | 10 | @RequiredArgsConstructor 11 | public class CouponRepositoryAdapter implements CouponRepository { 12 | 13 | private final CouponJpaRepository couponJpaRepository; 14 | 15 | @Override 16 | public Coupon save(final Coupon coupon) { 17 | return couponJpaRepository.save(coupon); 18 | } 19 | 20 | @Override 21 | public List saveAll(final List coupons) { 22 | return couponJpaRepository.saveAll(coupons); 23 | } 24 | 25 | @Override 26 | public Coupon findByCouponCode(final String couponCode) { 27 | return couponJpaRepository.findByCode(couponCode) 28 | .orElseThrow(NotFoundCouponException::new); 29 | } 30 | 31 | @Override 32 | public Optional findByCouponCodeWithLock(final String couponCode) { 33 | return couponJpaRepository.findByCodeWithLock(couponCode); 34 | } 35 | 36 | @Override 37 | public List findAllByOwnerId(final Long ownerId) { 38 | return couponJpaRepository.findAllByOwnerId(ownerId); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/presentation/request/CreateCouponRequest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.presentation.request; 2 | 3 | import com.flab.infrun.coupon.application.command.CreateCouponCommand; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | import jakarta.validation.constraints.Positive; 7 | import java.time.LocalDateTime; 8 | 9 | public record CreateCouponRequest( 10 | @NotBlank(message = "할인 타입은 필수 입력입니다.") 11 | String discountType, 12 | @Positive(message = "할인 금액 또는 할인율은 필수 입력입니다.") 13 | int discountAmount, 14 | @NotNull(message = "쿠폰 만료일은 필수 입력입니다.") 15 | LocalDateTime expirationAt, 16 | @Positive(message = "쿠폰 수량은 1개 이상이어야 합니다.") 17 | int quantity 18 | ) { 19 | 20 | public CreateCouponCommand toCommand() { 21 | return new CreateCouponCommand( 22 | discountType, 23 | discountAmount, 24 | expirationAt, 25 | quantity 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/presentation/request/EnrollCouponRequest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.presentation.request; 2 | 3 | import com.flab.infrun.coupon.application.command.CouponRegisterCommand; 4 | import com.flab.infrun.member.domain.Member; 5 | import jakarta.validation.constraints.NotBlank; 6 | 7 | public record EnrollCouponRequest( 8 | @NotBlank(message = "쿠폰 코드는 필수입니다.") 9 | String couponCode 10 | ) { 11 | 12 | public CouponRegisterCommand toCommand(final Member member) { 13 | return new CouponRegisterCommand(member, couponCode); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/presentation/response/CouponViewResponse.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.presentation.response; 2 | 3 | import com.flab.infrun.coupon.application.result.CouponView; 4 | import java.util.List; 5 | 6 | public record CouponViewResponse( 7 | int quantity, 8 | List coupons 9 | ) { 10 | 11 | public static CouponViewResponse from(final List couponViews) { 12 | return new CouponViewResponse( 13 | couponViews.size(), 14 | couponViews.stream() 15 | .map(couponView -> new CouponResponse( 16 | couponView.dDay(), 17 | couponView.discountValue(), 18 | couponView.discountType() 19 | )) 20 | .toList() 21 | ); 22 | } 23 | 24 | public record CouponResponse( 25 | long dDay, 26 | int discountValue, 27 | String discountType 28 | ) { 29 | 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/presentation/response/CreatedCouponResponse.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.presentation.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import com.flab.infrun.coupon.application.result.CreatedCouponResult; 5 | import java.time.LocalDateTime; 6 | import java.util.List; 7 | 8 | public record CreatedCouponResponse( 9 | int quantity, 10 | List couponCodes, 11 | 12 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 13 | LocalDateTime expirationAt 14 | ) { 15 | 16 | public static CreatedCouponResponse from(final CreatedCouponResult createdCouponResult) { 17 | return new CreatedCouponResponse( 18 | createdCouponResult.quantity(), 19 | createdCouponResult.couponCodes(), 20 | createdCouponResult.expirationAt() 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/coupon/presentation/response/EnrolledCouponResponse.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.presentation.response; 2 | 3 | import com.flab.infrun.coupon.application.result.EnrolledCouponResult; 4 | import java.time.LocalDateTime; 5 | 6 | public record EnrolledCouponResponse( 7 | String ownerEmail, 8 | String discountType, 9 | int discountValue, 10 | LocalDateTime expirationAt 11 | ) { 12 | 13 | public static EnrolledCouponResponse from(final EnrolledCouponResult result) { 14 | return new EnrolledCouponResponse( 15 | result.ownerEmail(), 16 | result.discountInfo() 17 | .getDiscountType() 18 | .name(), 19 | result.discountInfo() 20 | .getDiscountValue(), 21 | result.expirationAt() 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/application/LectureExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.application; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | import com.flab.infrun.common.response.Response; 6 | import com.flab.infrun.lecture.application.exception.DuplicateLectureFileNameException; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | import org.springframework.web.bind.annotation.ResponseStatus; 10 | import org.springframework.web.bind.annotation.RestControllerAdvice; 11 | 12 | @RestControllerAdvice 13 | public class LectureExceptionHandler { 14 | 15 | @ExceptionHandler(DuplicateLectureFileNameException.class) 16 | @ResponseStatus(HttpStatus.BAD_REQUEST) 17 | public Response handleException(SystemException e) { 18 | return Response.fail(e.getErrorCode()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/application/LectureFacade.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.application; 2 | 3 | import com.flab.infrun.lecture.application.command.LectureRegisterCommand; 4 | import com.flab.infrun.lecture.application.command.LectureReviewDeleteCommand; 5 | import com.flab.infrun.lecture.application.command.LectureReviewModifyCommand; 6 | import com.flab.infrun.lecture.application.command.LectureReviewRegisterCommand; 7 | import com.flab.infrun.lecture.application.command.PublishPreSignedUrlCommand; 8 | import com.flab.infrun.lecture.application.query.LectureSearchQuery; 9 | import com.flab.infrun.lecture.application.result.LectureSearchResult; 10 | import com.flab.infrun.lecture.application.result.PreSignedUrlResult; 11 | import com.flab.infrun.member.domain.Member; 12 | import lombok.RequiredArgsConstructor; 13 | import org.springframework.stereotype.Service; 14 | 15 | @RequiredArgsConstructor 16 | @Service 17 | public class LectureFacade { 18 | 19 | private final LectureCommandProcessor lectureCommandProcessor; 20 | private final LectureQueryProcessor lectureQueryProcessor; 21 | 22 | public PreSignedUrlResult publishPreSignedUrl(PublishPreSignedUrlCommand command) { 23 | return lectureCommandProcessor.publishPreSignedUrl(command); 24 | } 25 | 26 | public long registerLecture(LectureRegisterCommand command, Member member) { 27 | return lectureCommandProcessor.registerLecture(command, member); 28 | } 29 | 30 | public LectureSearchResult searchLecture(LectureSearchQuery query) { 31 | return lectureQueryProcessor.searchLecture(query); 32 | } 33 | 34 | public Long registerLectureReview(LectureReviewRegisterCommand command, Member member) { 35 | return lectureCommandProcessor.registerLectureReview(command, member); 36 | } 37 | 38 | public Long modifyLectureReview(LectureReviewModifyCommand command, Member member) { 39 | return lectureCommandProcessor.modifyLectureReview(command, member); 40 | } 41 | 42 | public Long deleteLectureReview(LectureReviewDeleteCommand command, Member member) { 43 | return lectureCommandProcessor.deleteLectureReview(command, member); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/application/LectureQueryProcessor.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.application; 2 | 3 | import com.flab.infrun.lecture.application.query.LectureSearchQuery; 4 | import com.flab.infrun.lecture.application.result.LectureSearchResult; 5 | import com.flab.infrun.lecture.application.result.LectureSearchResult.LectureInfoResult; 6 | import com.flab.infrun.lecture.infrastructure.persistence.query.LectureQueryRepository; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.stereotype.Component; 9 | 10 | @RequiredArgsConstructor 11 | @Component 12 | public class LectureQueryProcessor { 13 | 14 | private final LectureQueryRepository lectureQueryRepository; 15 | 16 | public LectureSearchResult searchLecture(LectureSearchQuery lectureSearchQuery) { 17 | return new LectureSearchResult(lectureQueryRepository.search( 18 | lectureSearchQuery.toCondition()).stream() 19 | .map(dto -> new LectureInfoResult(dto.id(), dto.name(), dto.price(), 20 | dto.lectureLevel(), dto.skill(), dto.uploadUserId(), 21 | dto.uploadUserName())) 22 | .toList()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/application/command/LectureDetailCommand.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.application.command; 2 | 3 | import java.util.List; 4 | 5 | public record LectureDetailCommand( 6 | String chapter, 7 | String name, 8 | String objectKey, 9 | String uploadId, 10 | List etagList 11 | ) { 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/application/command/LectureRegisterCommand.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.application.command; 2 | 3 | import java.util.List; 4 | 5 | public record LectureRegisterCommand( 6 | String name, 7 | int price, 8 | int level, 9 | String skill, 10 | String introduce, 11 | List lectureDetailCommandList 12 | ) { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/application/command/LectureReviewDeleteCommand.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.application.command; 2 | 3 | public record LectureReviewDeleteCommand( 4 | Long lectureReviewId 5 | ) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/application/command/LectureReviewModifyCommand.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.application.command; 2 | 3 | public record LectureReviewModifyCommand( 4 | Long lectureReviewId, 5 | String content 6 | ) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/application/command/LectureReviewRegisterCommand.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.application.command; 2 | 3 | public record LectureReviewRegisterCommand( 4 | Long lectureId, 5 | String content 6 | ) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/application/command/PublishPreSignedUrlCommand.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.application.command; 2 | 3 | public record PublishPreSignedUrlCommand( 4 | String objectKey, 5 | int partCnt 6 | ) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/application/exception/DuplicateLectureFileNameException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.application.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public class DuplicateLectureFileNameException extends SystemException { 7 | 8 | public DuplicateLectureFileNameException() { 9 | super(ErrorCode.DUPLICATED_FILE_NAME); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/application/query/LectureSearchQuery.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.application.query; 2 | 3 | import com.flab.infrun.lecture.infrastructure.persistence.query.condition.LectureSearchCondition; 4 | 5 | public record LectureSearchQuery( 6 | String name, 7 | Integer price, 8 | Integer lectureLevel, 9 | String skill, 10 | String uploadUserName 11 | 12 | ) { 13 | 14 | public LectureSearchCondition toCondition() { 15 | return new LectureSearchCondition(name, price, lectureLevel, skill, uploadUserName); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/application/result/LectureSearchResult.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.application.result; 2 | 3 | import java.util.List; 4 | 5 | public record LectureSearchResult( 6 | List lecturesList 7 | ) { 8 | 9 | public record LectureInfoResult( 10 | Long id, 11 | String name, 12 | int price, 13 | int lectureLevel, 14 | String skill, 15 | Long uploadUserId, 16 | String uploadUserName) { 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/application/result/PreSignedUrlResult.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.application.result; 2 | 3 | import java.util.List; 4 | 5 | public record PreSignedUrlResult( 6 | String uploadId, 7 | List preSignedUrlList 8 | ) { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/domain/LectureDetail.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.domain; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.FetchType; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.ManyToOne; 10 | import lombok.AccessLevel; 11 | import lombok.NoArgsConstructor; 12 | 13 | @Entity 14 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 15 | public class LectureDetail { 16 | 17 | @Id 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | private Long id; 20 | private String chapter; 21 | private String name; 22 | @ManyToOne(fetch = FetchType.LAZY) 23 | private Lecture lecture; 24 | 25 | private String objectKey; 26 | 27 | private LectureDetail(String chapter, String name, String objectKey) { 28 | this.chapter = chapter; 29 | this.name = name; 30 | this.objectKey = objectKey; 31 | } 32 | 33 | public static LectureDetail of(String chapter, String name, String objectKey) { 34 | return new LectureDetail(chapter, name, objectKey); 35 | } 36 | 37 | void setLecture(Lecture lecture) { 38 | this.lecture = lecture; 39 | } 40 | 41 | public Long getId() { 42 | return id; 43 | } 44 | 45 | public Lecture getLecture() { 46 | return lecture; 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return "LectureDetail{" + 52 | "id=" + id + 53 | ", chapter='" + chapter + '\'' + 54 | ", name='" + name + '\'' + 55 | ", lecture=" + lecture + 56 | '}'; 57 | } 58 | 59 | @VisibleForTesting 60 | void assignId(final Long id) { 61 | this.id = id; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/domain/LectureDetailRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.domain; 2 | 3 | import java.util.Optional; 4 | 5 | public interface LectureDetailRepository { 6 | 7 | LectureDetail save(LectureDetail entity); 8 | 9 | Optional findById(Long id); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/domain/LectureFileRepository.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-lab-edu/infrun/04f82aa664ebc1055312f79e90e2c3f6eb5cbe0c/src/main/java/com/flab/infrun/lecture/domain/LectureFileRepository.java -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/domain/LectureRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | public interface LectureRepository { 7 | 8 | Lecture save(Lecture entity); 9 | 10 | Optional findById(Long id); 11 | 12 | List findAllByIdIn(List lectureIds); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/domain/LectureReview.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.domain; 2 | 3 | import com.flab.infrun.lecture.domain.exception.InvalidAuthorizationLectureReviewException; 4 | import com.flab.infrun.member.domain.Member; 5 | import com.google.common.annotations.VisibleForTesting; 6 | import jakarta.persistence.Entity; 7 | import jakarta.persistence.FetchType; 8 | import jakarta.persistence.GeneratedValue; 9 | import jakarta.persistence.GenerationType; 10 | import jakarta.persistence.Id; 11 | import jakarta.persistence.ManyToOne; 12 | import lombok.AccessLevel; 13 | import lombok.NoArgsConstructor; 14 | 15 | @Entity 16 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 17 | public class LectureReview { 18 | 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | private Long id; 22 | 23 | private String content; 24 | 25 | @ManyToOne(fetch = FetchType.LAZY) 26 | private Lecture lecture; 27 | 28 | @ManyToOne(fetch = FetchType.LAZY) 29 | private Member member; 30 | 31 | public Long getId() { 32 | return id; 33 | } 34 | 35 | public Lecture getLecture() { 36 | return lecture; 37 | } 38 | 39 | public String getContent() { 40 | return content; 41 | } 42 | 43 | public Member getMember() { 44 | return member; 45 | } 46 | 47 | private LectureReview(String content, Lecture lecture, Member member) { 48 | this.content = content; 49 | this.lecture = lecture; 50 | this.member = member; 51 | } 52 | 53 | public static LectureReview of(String content, Lecture lecture, Member member) { 54 | return new LectureReview(content, lecture, member); 55 | } 56 | 57 | public void checkReviewAuthorization(Member member) { 58 | if (!this.member.getId().equals(member.getId())) { 59 | throw new InvalidAuthorizationLectureReviewException(); 60 | } 61 | } 62 | 63 | public void changeContent(String content) { 64 | this.content = content; 65 | } 66 | 67 | @VisibleForTesting 68 | void assignId(final Long id) { 69 | this.id = id; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/domain/LectureReviewRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.domain; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | public interface LectureReviewRepository { 7 | 8 | List findByLectureId(Long lectureId); 9 | 10 | Optional findById(Long id); 11 | 12 | LectureReview save(LectureReview entity); 13 | 14 | Long deleteById(Long id); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/domain/exception/InvalidAuthorizationLectureReviewException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public class InvalidAuthorizationLectureReviewException extends SystemException { 7 | 8 | public InvalidAuthorizationLectureReviewException() { 9 | super(ErrorCode.INVALID_AUTHORIZATION_LECTURE_REVIEW); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/domain/exception/NotFoundLectureException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public class NotFoundLectureException extends SystemException { 7 | 8 | public NotFoundLectureException() { 9 | super(ErrorCode.NOT_FOUND_LECTURE); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/domain/exception/NotFoundLectureReviewException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public class NotFoundLectureReviewException extends SystemException { 7 | 8 | public NotFoundLectureReviewException() { 9 | super(ErrorCode.NOT_FOUND_LECTURE_REVIEW); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/infrastructure/config/LecturePersistenceConfig.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.infrastructure.config; 2 | 3 | import com.flab.infrun.lecture.infrastructure.persistence.LectureDetailRepositoryAdapter; 4 | import com.flab.infrun.lecture.infrastructure.persistence.LectureRepositoryAdapter; 5 | import com.flab.infrun.lecture.infrastructure.persistence.LectureReviewRepositoryAdapter; 6 | import com.flab.infrun.lecture.infrastructure.persistence.jpa.LectureDetailJpaRepository; 7 | import com.flab.infrun.lecture.infrastructure.persistence.jpa.LectureJpaRepository; 8 | import com.flab.infrun.lecture.infrastructure.persistence.jpa.LectureReviewJpaRepository; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | @Configuration 13 | public class LecturePersistenceConfig { 14 | 15 | @Bean 16 | public LectureRepositoryAdapter lectureRepository(final LectureJpaRepository jpaRepository) { 17 | return new LectureRepositoryAdapter(jpaRepository); 18 | } 19 | 20 | @Bean 21 | public LectureDetailRepositoryAdapter lectureDetailRepository( 22 | final LectureDetailJpaRepository jpaRepository) { 23 | return new LectureDetailRepositoryAdapter(jpaRepository); 24 | } 25 | 26 | @Bean 27 | public LectureReviewRepositoryAdapter lectureReviewRepositoryAdapter( 28 | final LectureReviewJpaRepository jpaRepository) { 29 | return new LectureReviewRepositoryAdapter(jpaRepository); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/infrastructure/data/LectureDTO.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.infrastructure.data; 2 | 3 | public record LectureDTO( 4 | Long id, 5 | String name, 6 | int price, 7 | int lectureLevel, 8 | String skill, 9 | Long uploadUserId, 10 | String uploadUserName) { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/infrastructure/persistence/LectureDetailRepositoryAdapter.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.infrastructure.persistence; 2 | 3 | import com.flab.infrun.lecture.domain.LectureDetail; 4 | import com.flab.infrun.lecture.domain.LectureDetailRepository; 5 | import com.flab.infrun.lecture.infrastructure.persistence.jpa.LectureDetailJpaRepository; 6 | import java.util.Optional; 7 | import lombok.RequiredArgsConstructor; 8 | 9 | 10 | @RequiredArgsConstructor 11 | public class LectureDetailRepositoryAdapter implements 12 | LectureDetailRepository { 13 | 14 | private final LectureDetailJpaRepository lectureDetailJpaRepository; 15 | 16 | @Override 17 | public LectureDetail save(LectureDetail entity) { 18 | return lectureDetailJpaRepository.save(entity); 19 | } 20 | 21 | @Override 22 | public Optional findById(Long id) { 23 | return lectureDetailJpaRepository.findById(id); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/infrastructure/persistence/LectureFileRepositoryAdapter.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f-lab-edu/infrun/04f82aa664ebc1055312f79e90e2c3f6eb5cbe0c/src/main/java/com/flab/infrun/lecture/infrastructure/persistence/LectureFileRepositoryAdapter.java -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/infrastructure/persistence/LectureRepositoryAdapter.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.infrastructure.persistence; 2 | 3 | import com.flab.infrun.lecture.domain.Lecture; 4 | import com.flab.infrun.lecture.domain.LectureRepository; 5 | import com.flab.infrun.lecture.infrastructure.persistence.jpa.LectureJpaRepository; 6 | import java.util.List; 7 | import java.util.Optional; 8 | import lombok.RequiredArgsConstructor; 9 | 10 | @RequiredArgsConstructor 11 | public class LectureRepositoryAdapter implements LectureRepository { 12 | 13 | private final LectureJpaRepository lectureJpaRepository; 14 | 15 | @Override 16 | public Lecture save(Lecture entity) { 17 | return lectureJpaRepository.save(entity); 18 | } 19 | 20 | @Override 21 | public Optional findById(Long id) { 22 | return lectureJpaRepository.findById(id); 23 | } 24 | 25 | @Override 26 | public List findAllByIdIn(final List lectureIds) { 27 | return lectureJpaRepository.findAllByIdIn(lectureIds); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/infrastructure/persistence/LectureReviewRepositoryAdapter.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.infrastructure.persistence; 2 | 3 | import com.flab.infrun.lecture.domain.LectureReview; 4 | import com.flab.infrun.lecture.domain.LectureReviewRepository; 5 | import com.flab.infrun.lecture.infrastructure.persistence.jpa.LectureReviewJpaRepository; 6 | import java.util.List; 7 | import java.util.Optional; 8 | import lombok.RequiredArgsConstructor; 9 | 10 | @RequiredArgsConstructor 11 | public class LectureReviewRepositoryAdapter implements LectureReviewRepository { 12 | 13 | private final LectureReviewJpaRepository jpaRepository; 14 | 15 | @Override 16 | public List findByLectureId(Long lectureId) { 17 | return jpaRepository.findByLectureId(lectureId); 18 | } 19 | 20 | @Override 21 | public Optional findById(Long id) { 22 | return jpaRepository.findById(id); 23 | } 24 | 25 | @Override 26 | public LectureReview save(LectureReview entity) { 27 | return jpaRepository.save(entity); 28 | } 29 | 30 | @Override 31 | public Long deleteById(Long id) { 32 | return jpaRepository.deleteCountById(id); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/infrastructure/persistence/jpa/LectureDetailJpaRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.infrastructure.persistence.jpa; 2 | 3 | import com.flab.infrun.lecture.domain.LectureDetail; 4 | import java.util.Optional; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface LectureDetailJpaRepository extends JpaRepository { 8 | 9 | @Override 10 | LectureDetail save(LectureDetail entity); 11 | 12 | @Override 13 | Optional findById(Long id); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/infrastructure/persistence/jpa/LectureJpaRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.infrastructure.persistence.jpa; 2 | 3 | import com.flab.infrun.lecture.domain.Lecture; 4 | import java.util.List; 5 | import java.util.Optional; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | 8 | 9 | public interface LectureJpaRepository extends JpaRepository { 10 | 11 | @Override 12 | Optional findById(Long aLong); 13 | 14 | @Override 15 | Lecture save(Lecture entity); 16 | 17 | List findAllByIdIn(List lectureIds); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/infrastructure/persistence/jpa/LectureReviewJpaRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.infrastructure.persistence.jpa; 2 | 3 | import com.flab.infrun.lecture.domain.LectureReview; 4 | import java.util.List; 5 | import java.util.Optional; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | 8 | public interface LectureReviewJpaRepository extends JpaRepository { 9 | 10 | @Override 11 | LectureReview save(LectureReview entity); 12 | 13 | @Override 14 | Optional findById(Long id); 15 | 16 | List findByLectureId(Long id); 17 | 18 | Long deleteCountById(Long id); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/infrastructure/persistence/query/condition/LectureSearchCondition.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.infrastructure.persistence.query.condition; 2 | 3 | public record LectureSearchCondition( 4 | String name, 5 | Integer price, 6 | Integer lectureLevel, 7 | String skill, 8 | String uploadUserName 9 | ) { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/infrastructure/storage/aws/config/S3Config.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.infrastructure.storage.aws.config; 2 | 3 | import java.time.Duration; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.stereotype.Component; 6 | import software.amazon.awssdk.regions.Region; 7 | 8 | @Component 9 | public class S3Config { 10 | 11 | @Value("${aws.s3.bucketName}") 12 | String bucketName; 13 | 14 | @Value("${aws.s3.profileName}") 15 | String profileName; 16 | 17 | @Value("${aws.s3.region}") 18 | String region; 19 | 20 | @Value("${aws.s3.duration}") 21 | String duration; 22 | 23 | public String getBucketName() { 24 | return bucketName; 25 | } 26 | 27 | public String getProfileName() { 28 | return profileName; 29 | } 30 | 31 | public Region getRegion() { 32 | return Region.of(region); 33 | } 34 | 35 | public Duration getDuration() { 36 | return Duration.ofMinutes(Long.parseLong(duration)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/presentation/request/LectureDetailRequest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.presentation.request; 2 | 3 | import com.flab.infrun.lecture.application.command.LectureDetailCommand; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.Size; 6 | import java.util.List; 7 | 8 | public record LectureDetailRequest( 9 | 10 | @NotBlank @Size(max = 30) 11 | String chapter, 12 | @NotBlank @Size(max = 30) 13 | String name, 14 | @NotBlank 15 | String objectKey, 16 | @NotBlank 17 | String uploadId, 18 | List etagList 19 | ) { 20 | 21 | public LectureDetailCommand toCommand() { 22 | return new LectureDetailCommand(chapter, name, objectKey, uploadId, etagList); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/presentation/request/LectureRegisterRequest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.presentation.request; 2 | 3 | import com.flab.infrun.lecture.application.command.LectureRegisterCommand; 4 | import jakarta.validation.Valid; 5 | import jakarta.validation.constraints.NotBlank; 6 | import jakarta.validation.constraints.NotNull; 7 | import jakarta.validation.constraints.Positive; 8 | import jakarta.validation.constraints.PositiveOrZero; 9 | import jakarta.validation.constraints.Size; 10 | import java.util.List; 11 | 12 | public record LectureRegisterRequest( 13 | @NotBlank @Size(max = 30) 14 | String name, 15 | @PositiveOrZero 16 | int price, 17 | @Positive 18 | int lectureLevel, 19 | @NotBlank @Size(max = 30) 20 | String skill, 21 | @NotBlank @Size(max = 200) 22 | String introduce, 23 | @NotNull @Valid 24 | List lectureDetailRequest 25 | ) { 26 | 27 | public LectureRegisterCommand toCommand() { 28 | return new LectureRegisterCommand(name, price, lectureLevel, skill, introduce, 29 | lectureDetailRequest.stream().map(LectureDetailRequest::toCommand).toList()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/presentation/request/LectureReviewDeleteRequest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.presentation.request; 2 | 3 | import com.flab.infrun.lecture.application.command.LectureReviewDeleteCommand; 4 | import jakarta.validation.constraints.NotNull; 5 | 6 | public record LectureReviewDeleteRequest( 7 | @NotNull 8 | Long lectureReviewId 9 | ) { 10 | 11 | public LectureReviewDeleteCommand toCommand() { 12 | return new LectureReviewDeleteCommand(lectureReviewId); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/presentation/request/LectureReviewModifyRequest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.presentation.request; 2 | 3 | import com.flab.infrun.lecture.application.command.LectureReviewModifyCommand; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | import jakarta.validation.constraints.Size; 7 | 8 | public record LectureReviewModifyRequest( 9 | @NotNull 10 | Long lectureReviewId, 11 | @NotBlank 12 | @Size(max = 400) 13 | String content 14 | ) { 15 | 16 | public LectureReviewModifyCommand toCommand() { 17 | return new LectureReviewModifyCommand(lectureReviewId, content); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/presentation/request/LectureReviewRegisterRequest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.presentation.request; 2 | 3 | import com.flab.infrun.lecture.application.command.LectureReviewRegisterCommand; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | import jakarta.validation.constraints.Size; 7 | 8 | public record LectureReviewRegisterRequest( 9 | @NotNull 10 | Long lectureId, 11 | 12 | @NotBlank 13 | @Size(max = 400) 14 | String content 15 | ) { 16 | 17 | public LectureReviewRegisterCommand toCommand() { 18 | return new LectureReviewRegisterCommand(lectureId, content); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/presentation/request/LectureSearchRequest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.presentation.request; 2 | 3 | import com.flab.infrun.lecture.application.query.LectureSearchQuery; 4 | import jakarta.validation.constraints.Positive; 5 | import jakarta.validation.constraints.Size; 6 | 7 | public record LectureSearchRequest( 8 | @Size(max = 30) 9 | String name, 10 | Integer price, 11 | @Positive 12 | Integer lectureLevel, 13 | @Size(max = 30) 14 | String skill, 15 | @Size(max = 30) 16 | String uploadUserName 17 | 18 | ) { 19 | 20 | public LectureSearchQuery toQuery() { 21 | return new LectureSearchQuery(name, price, lectureLevel, skill, uploadUserName); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/presentation/request/PreSingedUrlRequest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.presentation.request; 2 | 3 | import com.flab.infrun.lecture.application.command.PublishPreSignedUrlCommand; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.Positive; 6 | 7 | public record PreSingedUrlRequest( 8 | @NotBlank 9 | String objectKey, 10 | @Positive 11 | Integer partCnt 12 | ) { 13 | 14 | public PublishPreSignedUrlCommand toCommand() { 15 | return new PublishPreSignedUrlCommand(objectKey, partCnt); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/presentation/response/LectureQueryResponse.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.presentation.response; 2 | 3 | import com.flab.infrun.lecture.application.result.LectureSearchResult; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | 7 | public record LectureQueryResponse( 8 | List lectureList 9 | ) { 10 | 11 | public static LectureQueryResponse from(LectureSearchResult lectureSearchResult) { 12 | return new LectureQueryResponse(lectureSearchResult.lecturesList().stream() 13 | .map(lectureInfoResult -> new LectureInfo( 14 | lectureInfoResult.id(), 15 | lectureInfoResult.name(), 16 | lectureInfoResult.price(), 17 | lectureInfoResult.lectureLevel(), 18 | lectureInfoResult.skill(), 19 | lectureInfoResult.uploadUserId(), 20 | lectureInfoResult.uploadUserName() 21 | )) 22 | .collect(Collectors.toList())); 23 | } 24 | 25 | record LectureInfo( 26 | Long id, 27 | String name, 28 | int price, 29 | int lectureLevel, 30 | String skill, 31 | Long uploadUserId, 32 | String uploadUserName) { 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/lecture/presentation/response/PreSignedUrlResponse.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.presentation.response; 2 | 3 | import com.flab.infrun.lecture.application.result.PreSignedUrlResult; 4 | import java.util.List; 5 | 6 | public record PreSignedUrlResponse( 7 | String uploadId, 8 | List preSignedUrlList 9 | ) { 10 | 11 | public static PreSignedUrlResponse from(PreSignedUrlResult preSignedUrlResult) { 12 | return new PreSignedUrlResponse(preSignedUrlResult.uploadId(), 13 | preSignedUrlResult.preSignedUrlList()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/application/MemberFacade.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.application; 2 | 3 | import com.flab.infrun.member.application.command.LoginCommand; 4 | import com.flab.infrun.member.application.command.SignupCommand; 5 | import com.flab.infrun.member.application.result.LoginResult; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | @Service 10 | @RequiredArgsConstructor 11 | public class MemberFacade { 12 | 13 | private final MemberSignupProcessor signupProcessor; 14 | private final MemberLoginProcessor loginProcessor; 15 | 16 | public void signup(final SignupCommand command) { 17 | signupProcessor.execute(command); 18 | } 19 | 20 | public LoginResult login(final LoginCommand command) { 21 | return loginProcessor.execute(command); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/application/MemberLoginProcessor.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.application; 2 | 3 | import com.flab.infrun.member.application.command.LoginCommand; 4 | import com.flab.infrun.member.application.result.LoginResult; 5 | import com.flab.infrun.member.domain.MemberRepository; 6 | import com.flab.infrun.member.domain.exception.NotFoundMemberException; 7 | import com.flab.infrun.member.domain.exception.NotMatchPasswordException; 8 | import com.flab.infrun.member.infrastructure.jwt.TokenProvider; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 11 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 12 | import org.springframework.security.core.context.SecurityContextHolder; 13 | import org.springframework.security.crypto.password.PasswordEncoder; 14 | import org.springframework.stereotype.Component; 15 | import org.springframework.util.StringUtils; 16 | 17 | @Component 18 | @RequiredArgsConstructor 19 | public class MemberLoginProcessor { 20 | 21 | private final TokenProvider tokenProvider; 22 | private final AuthenticationManagerBuilder authenticationManagerBuilder; 23 | private final MemberRepository memberRepository; 24 | private final PasswordEncoder passwordEncoder; 25 | 26 | public LoginResult execute(final LoginCommand command) { 27 | verifyCommand(command); 28 | 29 | final var authenticationToken = new UsernamePasswordAuthenticationToken( 30 | command.email(), command.password()); 31 | final var authentication = authenticationManagerBuilder.getObject() 32 | .authenticate(authenticationToken); 33 | SecurityContextHolder.getContext().setAuthentication(authentication); 34 | 35 | return new LoginResult(tokenProvider.generateToken(authentication)); 36 | } 37 | 38 | private void verifyCommand(final LoginCommand command) { 39 | if (!StringUtils.hasText(command.email())) { 40 | throw new NotFoundMemberException(); 41 | } 42 | 43 | final var member = memberRepository.findByEmail(command.email()); 44 | 45 | if (!StringUtils.hasText(command.password()) 46 | || !passwordEncoder.matches(command.password(), member.getPassword())) { 47 | throw new NotMatchPasswordException(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/application/MemberSignupProcessor.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.application; 2 | 3 | import com.flab.infrun.member.application.command.SignupCommand; 4 | import com.flab.infrun.member.domain.Member; 5 | import com.flab.infrun.member.domain.MemberRepository; 6 | import com.flab.infrun.member.domain.MemberVerifier; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.security.crypto.password.PasswordEncoder; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | @Component 13 | @RequiredArgsConstructor 14 | public class MemberSignupProcessor { 15 | 16 | private final MemberRepository memberRepository; 17 | private final PasswordEncoder passwordEncoder; 18 | private final MemberVerifier memberVerifier; 19 | 20 | @Transactional 21 | public void execute(final SignupCommand command) { 22 | verifyCommand(command); 23 | 24 | memberRepository.save( 25 | Member.of(command.nickname(), command.email(), 26 | passwordEncoder.encode(command.password()))); 27 | } 28 | 29 | private void verifyCommand(final SignupCommand command) { 30 | memberVerifier.verifyNickname(command.nickname()); 31 | memberVerifier.verifyEmail(command.email()); 32 | memberVerifier.verifyPassword(command.password()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/application/command/LoginCommand.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.application.command; 2 | 3 | public record LoginCommand( 4 | String email, 5 | String password 6 | ) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/application/command/SignupCommand.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.application.command; 2 | 3 | public record SignupCommand( 4 | String nickname, 5 | String email, 6 | String password 7 | ) { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/application/result/LoginResult.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.application.result; 2 | 3 | public record LoginResult( 4 | String token 5 | ) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/domain/MemberRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain; 2 | 3 | public interface MemberRepository { 4 | 5 | Member save(Member member); 6 | 7 | Member findByEmail(String email); 8 | 9 | Member findById(Long id); 10 | 11 | boolean existsByEmail(String email); 12 | 13 | boolean existsByNickname(String nickname); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/domain/MemberVerifier.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain; 2 | 3 | public interface MemberVerifier { 4 | 5 | void verifyNickname(final String nickname); 6 | 7 | void verifyEmail(final String email); 8 | 9 | void verifyPassword(final String password); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/domain/Role.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain; 2 | 3 | public enum Role { 4 | 5 | USER("ROLE_USER"), 6 | TEACHER("ROLE_TEACHER"); 7 | 8 | private final String value; 9 | 10 | Role(final String value) { 11 | this.value = value; 12 | } 13 | 14 | public String getValue() { 15 | return value; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/domain/exception/DuplicatedEmailException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class DuplicatedEmailException extends SystemException { 7 | 8 | public DuplicatedEmailException() { 9 | super(ErrorCode.DUPLICATED_EMAIL); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/domain/exception/DuplicatedNicknameException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class DuplicatedNicknameException extends SystemException { 7 | 8 | public DuplicatedNicknameException() { 9 | super(ErrorCode.DUPLICATED_NICKNAME); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/domain/exception/InvalidPasswordException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public class InvalidPasswordException extends SystemException { 7 | 8 | public InvalidPasswordException() { 9 | super(ErrorCode.INVALID_PASSWORD); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/domain/exception/NotFoundMemberException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public class NotFoundMemberException extends SystemException { 7 | 8 | public NotFoundMemberException() { 9 | super(ErrorCode.NOT_FOUND_MEMBER); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/domain/exception/NotMatchPasswordException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public class NotMatchPasswordException extends SystemException { 7 | 8 | public NotMatchPasswordException() { 9 | super(ErrorCode.NOT_MATCH_PASSWORD); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/infrastructure/MemberExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.infrastructure; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | import com.flab.infrun.common.response.Response; 6 | import com.flab.infrun.member.domain.exception.DuplicatedEmailException; 7 | import com.flab.infrun.member.domain.exception.DuplicatedNicknameException; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.web.bind.annotation.ExceptionHandler; 11 | import org.springframework.web.bind.annotation.ResponseStatus; 12 | import org.springframework.web.bind.annotation.RestControllerAdvice; 13 | 14 | @Slf4j 15 | @RestControllerAdvice 16 | public class MemberExceptionHandler { 17 | 18 | @ExceptionHandler({ 19 | DuplicatedNicknameException.class, 20 | DuplicatedEmailException.class 21 | }) 22 | @ResponseStatus(HttpStatus.BAD_REQUEST) 23 | public Response handleException(final SystemException e) { 24 | return Response.fail(e.getErrorCode()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/infrastructure/MemberVerifierImpl.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.infrastructure; 2 | 3 | import static java.util.regex.Pattern.matches; 4 | 5 | import com.flab.infrun.member.domain.MemberVerifier; 6 | import com.flab.infrun.member.domain.exception.DuplicatedEmailException; 7 | import com.flab.infrun.member.domain.exception.DuplicatedNicknameException; 8 | import com.flab.infrun.member.domain.exception.InvalidPasswordException; 9 | import com.flab.infrun.member.infrastructure.persistence.MemberJpaRepository; 10 | import lombok.RequiredArgsConstructor; 11 | import org.springframework.util.StringUtils; 12 | 13 | @RequiredArgsConstructor 14 | public class MemberVerifierImpl implements MemberVerifier { 15 | 16 | private final MemberJpaRepository memberJpaRepository; 17 | 18 | @Override 19 | public void verifyNickname(final String nickname) { 20 | if (!StringUtils.hasText(nickname) || memberJpaRepository.existsByNickname(nickname)) { 21 | throw new DuplicatedNicknameException(); 22 | } 23 | } 24 | 25 | @Override 26 | public void verifyEmail(final String email) { 27 | if (!StringUtils.hasText(email) || memberJpaRepository.existsByEmail(email)) { 28 | throw new DuplicatedEmailException(); 29 | } 30 | } 31 | 32 | @Override 33 | public void verifyPassword(final String password) { 34 | if (!StringUtils.hasText(password) 35 | || !matches("^(?=.*[A-Z])(?=.*\\d)(?=.*[a-z])(?=.*[\\W_])[A-Za-z\\d\\W_]{8,16}$", 36 | password) 37 | ) { 38 | throw new InvalidPasswordException(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/infrastructure/config/MemberConfig.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.infrastructure.config; 2 | 3 | import com.flab.infrun.member.domain.MemberVerifier; 4 | import com.flab.infrun.member.infrastructure.MemberVerifierImpl; 5 | import com.flab.infrun.member.infrastructure.persistence.MemberJpaRepository; 6 | import com.flab.infrun.member.infrastructure.persistence.MemberRepositoryAdapter; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | @Configuration 11 | public class MemberConfig { 12 | 13 | @Bean 14 | public MemberRepositoryAdapter memberRepositoryAdapter( 15 | final MemberJpaRepository memberJpaRepository 16 | ) { 17 | return new MemberRepositoryAdapter(memberJpaRepository); 18 | } 19 | 20 | @Bean 21 | public MemberVerifier memberVerifier( 22 | final MemberJpaRepository memberJpaRepository 23 | ) { 24 | return new MemberVerifierImpl(memberJpaRepository); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/infrastructure/persistence/MemberJpaRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.infrastructure.persistence; 2 | 3 | import com.flab.infrun.member.domain.Member; 4 | import java.util.Optional; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | public interface MemberJpaRepository extends JpaRepository { 8 | 9 | Member save(final Member member); 10 | 11 | boolean existsByEmail(final String email); 12 | 13 | boolean existsByNickname(final String nickname); 14 | 15 | Optional findByEmail(final String email); 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/infrastructure/persistence/MemberRepositoryAdapter.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.infrastructure.persistence; 2 | 3 | import com.flab.infrun.member.domain.Member; 4 | import com.flab.infrun.member.domain.MemberRepository; 5 | import com.flab.infrun.member.domain.exception.NotFoundMemberException; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | @RequiredArgsConstructor 9 | public class MemberRepositoryAdapter implements MemberRepository { 10 | 11 | private final MemberJpaRepository jpaRepository; 12 | 13 | @Override 14 | public Member save(final Member member) { 15 | return jpaRepository.save(member); 16 | } 17 | 18 | @Override 19 | public boolean existsByEmail(final String email) { 20 | return jpaRepository.existsByEmail(email); 21 | } 22 | 23 | @Override 24 | public boolean existsByNickname(final String nickname) { 25 | return jpaRepository.existsByNickname(nickname); 26 | } 27 | 28 | @Override 29 | public Member findByEmail(final String email) { 30 | return jpaRepository.findByEmail(email) 31 | .orElseThrow(NotFoundMemberException::new); 32 | } 33 | 34 | @Override 35 | public Member findById(final Long id) { 36 | return jpaRepository.findById(id) 37 | .orElseThrow(NotFoundMemberException::new); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/presentation/LoginRequest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.presentation; 2 | 3 | import com.flab.infrun.member.application.command.LoginCommand; 4 | import jakarta.validation.constraints.Email; 5 | import jakarta.validation.constraints.NotBlank; 6 | import jakarta.validation.constraints.Pattern; 7 | 8 | public record LoginRequest( 9 | @NotBlank(message = "이메일은 필수 입력입니다.") 10 | @Email(message = "이메일 형식이 올바르지 않습니다.") 11 | String email, 12 | 13 | @NotBlank(message = "비밀번호는 필수 입력입니다.") 14 | @Pattern( 15 | regexp = "^(?=.*[A-Z])(?=.*\\d)(?=.*[a-z])(?=.*[\\W_])[A-Za-z\\d\\W_]{8,16}$", 16 | message = "비밀번호는 8자리 ~ 16자리의 영문 대소문자, 숫자, 특수문자를 포함해야 합니다.") 17 | String password 18 | ) { 19 | 20 | public LoginCommand toCommand() { 21 | return new LoginCommand(email, password); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/presentation/MemberController.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.presentation; 2 | 3 | 4 | import com.flab.infrun.common.response.Response; 5 | import com.flab.infrun.member.application.MemberFacade; 6 | import com.flab.infrun.member.application.result.LoginResult; 7 | import jakarta.validation.Valid; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.RequestBody; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.ResponseStatus; 14 | import org.springframework.web.bind.annotation.RestController; 15 | 16 | @RestController 17 | @RequiredArgsConstructor 18 | @RequestMapping("/members") 19 | public class MemberController { 20 | 21 | private final MemberFacade facade; 22 | 23 | @ResponseStatus(HttpStatus.CREATED) 24 | @PostMapping 25 | public Response signup(@RequestBody @Valid final SignupRequest request) { 26 | facade.signup(request.toCommand()); 27 | 28 | return Response.success(1L); 29 | } 30 | 31 | @PostMapping("/login") 32 | public Response login(@RequestBody @Valid final LoginRequest request) { 33 | var result = facade.login(request.toCommand()); 34 | 35 | return Response.success(result); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/member/presentation/SignupRequest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.presentation; 2 | 3 | import com.flab.infrun.member.application.command.SignupCommand; 4 | import jakarta.validation.constraints.Email; 5 | import jakarta.validation.constraints.NotBlank; 6 | import jakarta.validation.constraints.Pattern; 7 | 8 | public record SignupRequest( 9 | 10 | @NotBlank(message = "닉네임은 필수 입력입니다.") 11 | String nickname, 12 | 13 | @NotBlank(message = "이메일은 필수 입력입니다.") 14 | @Email(message = "이메일 형식이 올바르지 않습니다.") 15 | String email, 16 | 17 | @NotBlank(message = "비밀번호는 필수 입력입니다.") 18 | @Pattern( 19 | regexp = "^(?=.*[A-Z])(?=.*\\d)(?=.*[a-z])(?=.*[\\W_])[A-Za-z\\d\\W_]{8,16}$", 20 | message = "비밀번호는 8자리 ~ 16자리의 영문 대소문자, 숫자, 특수문자를 포함해야 합니다.") 21 | String password 22 | ) { 23 | 24 | public SignupCommand toCommand() { 25 | return new SignupCommand(nickname, email, password); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/application/OrderFacade.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.application; 2 | 3 | import com.flab.infrun.order.application.command.CreateOrderCommand; 4 | import com.flab.infrun.order.application.command.PayOrderCommand; 5 | import com.flab.infrun.order.application.result.CreatedOrderResult; 6 | import com.flab.infrun.order.application.result.PayedOrderResult; 7 | import java.time.LocalDateTime; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.stereotype.Service; 10 | 11 | @RequiredArgsConstructor 12 | @Service 13 | public class OrderFacade { 14 | 15 | private final CreateOrderProcessor createOrderProcessor; 16 | private final PayOrderProcessor payOrderProcessor; 17 | 18 | public CreatedOrderResult createOrder( 19 | final CreateOrderCommand command, final LocalDateTime currentTime 20 | ) { 21 | return createOrderProcessor.execute(command, currentTime); 22 | } 23 | 24 | public PayedOrderResult payOrder(final PayOrderCommand command) { 25 | return payOrderProcessor.execute(command); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/application/command/CreateOrderCommand.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.application.command; 2 | 3 | import com.flab.infrun.member.domain.Member; 4 | import java.util.List; 5 | 6 | public record CreateOrderCommand( 7 | Member customer, 8 | List lectureIds, 9 | String couponCode 10 | ) { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/application/command/PayOrderCommand.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.application.command; 2 | 3 | import com.flab.infrun.payment.domain.PayMethod; 4 | import com.flab.infrun.payment.domain.PayType; 5 | import java.math.BigDecimal; 6 | 7 | public record PayOrderCommand( 8 | Long userId, 9 | Long orderId, 10 | BigDecimal amount, 11 | PayMethod payMethod, 12 | PayType payType, 13 | Integer installmentMonths 14 | ) { 15 | 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/application/result/CreatedOrderResult.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.application.result; 2 | 3 | import com.flab.infrun.order.domain.Order; 4 | import com.flab.infrun.order.domain.OrderItem; 5 | import java.math.BigDecimal; 6 | import java.time.LocalDateTime; 7 | import java.util.List; 8 | 9 | public record CreatedOrderResult( 10 | Long orderId, 11 | BigDecimal totalPrice, 12 | BigDecimal discountPrice, 13 | LocalDateTime orderedAt, 14 | String orderStatus, 15 | List orderItems, 16 | boolean isCouponApplied 17 | ) { 18 | 19 | public static CreatedOrderResult from( 20 | final Order order 21 | ) { 22 | return new CreatedOrderResult( 23 | order.getId(), 24 | order.getTotalPrice(), 25 | order.getDiscountPrice(), 26 | order.getCreatedAt(), 27 | order.getOrderStatus().getDescription(), 28 | order.getOrderItems().stream() 29 | .map(OrderItemResult::from) 30 | .toList(), 31 | order.isCouponApplied() 32 | ); 33 | } 34 | 35 | public record OrderItemResult( 36 | String teacherName, 37 | String lectureName, 38 | BigDecimal basePrice, 39 | BigDecimal salesPrice 40 | ) { 41 | 42 | public static OrderItemResult from( 43 | final OrderItem orderItem 44 | ) { 45 | return new OrderItemResult( 46 | orderItem.getProviderName(), 47 | orderItem.getItemName(), 48 | orderItem.getBasePrice(), 49 | orderItem.getSalesPrice()); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/application/result/PayedOrderResult.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.application.result; 2 | 3 | import com.flab.infrun.order.domain.OrderStatus; 4 | import com.flab.infrun.payment.domain.PayMethod; 5 | import com.flab.infrun.payment.domain.PayType; 6 | import com.flab.infrun.payment.domain.Payment; 7 | import java.math.BigDecimal; 8 | import java.time.LocalDateTime; 9 | 10 | public record PayedOrderResult( 11 | BigDecimal amount, 12 | PayType payType, 13 | PayMethod payMethod, 14 | String orderStatus, 15 | String payStatus, 16 | LocalDateTime payedAt 17 | ) { 18 | 19 | public static PayedOrderResult from(final Payment payment, final OrderStatus orderStatus) { 20 | return new PayedOrderResult( 21 | payment.getAmount(), 22 | payment.getPayType(), 23 | payment.getPayMethod(), 24 | orderStatus.getDescription(), 25 | payment.getPayStatus().getDescription(), 26 | payment.getCreatedAt()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/domain/OrderItem.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.domain; 2 | 3 | import com.flab.infrun.order.domain.exception.InvalidOrderItemPriceException; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.GeneratedValue; 6 | import jakarta.persistence.GenerationType; 7 | import jakarta.persistence.Id; 8 | import jakarta.persistence.Table; 9 | import java.math.BigDecimal; 10 | import lombok.AccessLevel; 11 | import lombok.NoArgsConstructor; 12 | 13 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 14 | @Table(name = "order_items") 15 | @Entity 16 | public class OrderItem { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | private Long id; 21 | private Long itemId; 22 | private String providerName; 23 | private String itemName; 24 | private BigDecimal basePrice; 25 | private BigDecimal salesPrice; 26 | 27 | private OrderItem( 28 | final Long itemId, 29 | final String providerName, 30 | final String itemName, 31 | final BigDecimal basePrice, 32 | final BigDecimal salesPrice 33 | ) { 34 | verifyOrderItem(basePrice); 35 | this.itemId = itemId; 36 | this.providerName = providerName; 37 | this.itemName = itemName; 38 | this.basePrice = basePrice; 39 | this.salesPrice = salesPrice; 40 | } 41 | 42 | public static OrderItem create( 43 | final Long itemId, 44 | final String provider, 45 | final String itemName, 46 | final BigDecimal basePrice, 47 | final BigDecimal salesPrice 48 | ) { 49 | return new OrderItem(itemId, provider, itemName, basePrice, salesPrice); 50 | } 51 | 52 | private void verifyOrderItem(final BigDecimal basePrice) { 53 | if (basePrice.compareTo(BigDecimal.ZERO) < 0) { 54 | throw new InvalidOrderItemPriceException(); 55 | } 56 | } 57 | 58 | public String getProviderName() { 59 | return providerName; 60 | } 61 | 62 | public String getItemName() { 63 | return itemName; 64 | } 65 | 66 | public BigDecimal getBasePrice() { 67 | return basePrice; 68 | } 69 | 70 | public BigDecimal getSalesPrice() { 71 | return salesPrice; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/domain/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.domain; 2 | 3 | public interface OrderRepository { 4 | 5 | Order save(final Order order); 6 | 7 | Order findById(final Long id); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/domain/OrderStatus.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.domain; 2 | 3 | public enum OrderStatus { 4 | 5 | ORDER_CREATED("주문 생성"), 6 | ORDER_COMPLETED("주문 완료"), 7 | ORDER_CANCELED("주문 취소"); 8 | 9 | private final String description; 10 | 11 | OrderStatus(String description) { 12 | this.description = description; 13 | } 14 | 15 | public String getDescription() { 16 | return description; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/domain/Price.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.domain; 2 | 3 | import com.flab.infrun.order.domain.exception.InvalidPriceValueException; 4 | import jakarta.persistence.Embeddable; 5 | import java.math.BigDecimal; 6 | import lombok.AccessLevel; 7 | import lombok.NoArgsConstructor; 8 | 9 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 10 | @Embeddable 11 | public class Price { 12 | 13 | private BigDecimal totalPrice; 14 | private BigDecimal discountPrice; 15 | 16 | private Price(final BigDecimal totalPrice, final BigDecimal discountPrice) { 17 | verifyPrice(totalPrice, discountPrice); 18 | this.totalPrice = totalPrice; 19 | this.discountPrice = discountPrice; 20 | } 21 | 22 | public static Price create(final BigDecimal totalPrice) { 23 | return new Price(totalPrice, BigDecimal.ZERO); 24 | } 25 | 26 | public static Price create(final BigDecimal totalPrice, final BigDecimal basePrice) { 27 | return new Price(totalPrice, basePrice.subtract(totalPrice)); 28 | } 29 | 30 | private void verifyPrice(final BigDecimal totalPrice, final BigDecimal discountPrice) { 31 | if (totalPrice.compareTo(BigDecimal.ZERO) < 0 32 | || discountPrice.compareTo(BigDecimal.ZERO) < 0) { 33 | throw new InvalidPriceValueException(); 34 | } 35 | } 36 | 37 | public boolean comparePrice(final BigDecimal price) { 38 | return totalPrice.compareTo(price) == 0; 39 | } 40 | 41 | public BigDecimal getTotalPrice() { 42 | return totalPrice; 43 | } 44 | 45 | public BigDecimal getDiscountPrice() { 46 | return discountPrice; 47 | } 48 | 49 | public BigDecimal calculateAmount() { 50 | return totalPrice.subtract(discountPrice); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/domain/exception/AlreadyCanceledOrderException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class AlreadyCanceledOrderException extends SystemException { 7 | 8 | public AlreadyCanceledOrderException() { 9 | super(ErrorCode.ALREADY_CANCELED_ORDER); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/domain/exception/AlreadyCompletedOrderException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class AlreadyCompletedOrderException extends SystemException { 7 | 8 | public AlreadyCompletedOrderException() { 9 | super(ErrorCode.ALREADY_COMPLETED_ORDER); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/domain/exception/InvalidCreateOrderException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class InvalidCreateOrderException extends SystemException { 7 | 8 | public InvalidCreateOrderException() { 9 | super(ErrorCode.INVALID_CREATE_ORDER); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/domain/exception/InvalidOrderItemPriceException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class InvalidOrderItemPriceException extends SystemException { 7 | 8 | public InvalidOrderItemPriceException() { 9 | super(ErrorCode.INVALID_ORDER_ITEM_PRICE_VALUE); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/domain/exception/InvalidPriceValueException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class InvalidPriceValueException extends SystemException { 7 | 8 | public InvalidPriceValueException() { 9 | super(ErrorCode.INVALID_PRICE_VALUE); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/domain/exception/NotFoundOrderException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class NotFoundOrderException extends SystemException { 7 | 8 | public NotFoundOrderException() { 9 | super(ErrorCode.NOT_FOUND_ORDER); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/domain/exception/OrderPayAmountNotMatchException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class OrderPayAmountNotMatchException extends SystemException { 7 | 8 | public OrderPayAmountNotMatchException() { 9 | super(ErrorCode.ORDER_PAY_AMOUNT_NOT_MATCH); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/infrastructure/config/OrderPersistenceConfig.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.infrastructure.config; 2 | 3 | import com.flab.infrun.order.domain.OrderRepository; 4 | import com.flab.infrun.order.infrastructure.persistence.OrderJpaRepository; 5 | import com.flab.infrun.order.infrastructure.persistence.OrderRepositoryAdapter; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class OrderPersistenceConfig { 11 | 12 | @Bean 13 | public OrderRepository orderRepository(final OrderJpaRepository orderJpaRepository) { 14 | return new OrderRepositoryAdapter(orderJpaRepository); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/infrastructure/persistence/OrderJpaRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.infrastructure.persistence; 2 | 3 | import com.flab.infrun.order.domain.Order; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface OrderJpaRepository extends JpaRepository { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/infrastructure/persistence/OrderRepositoryAdapter.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.infrastructure.persistence; 2 | 3 | import com.flab.infrun.order.domain.Order; 4 | import com.flab.infrun.order.domain.OrderRepository; 5 | import com.flab.infrun.order.domain.exception.NotFoundOrderException; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | @RequiredArgsConstructor 9 | public class OrderRepositoryAdapter implements OrderRepository { 10 | 11 | private final OrderJpaRepository orderJpaRepository; 12 | 13 | @Override 14 | public Order save(final Order order) { 15 | return orderJpaRepository.save(order); 16 | } 17 | 18 | @Override 19 | public Order findById(final Long id) { 20 | return orderJpaRepository.findById(id) 21 | .orElseThrow(NotFoundOrderException::new); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/presentation/OrderController.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.presentation; 2 | 3 | import com.flab.infrun.common.config.security.CurrentUser; 4 | import com.flab.infrun.common.response.Response; 5 | import com.flab.infrun.member.domain.Member; 6 | import com.flab.infrun.order.application.OrderFacade; 7 | import com.flab.infrun.order.presentation.request.CreateOrderRequest; 8 | import com.flab.infrun.order.presentation.request.PayOrderRequest; 9 | import com.flab.infrun.order.presentation.response.CreatedOrderResponse; 10 | import com.flab.infrun.order.presentation.response.PayedOrderResponse; 11 | import jakarta.validation.Valid; 12 | import java.time.LocalDateTime; 13 | import lombok.RequiredArgsConstructor; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.security.access.prepost.PreAuthorize; 16 | import org.springframework.web.bind.annotation.PostMapping; 17 | import org.springframework.web.bind.annotation.RequestBody; 18 | import org.springframework.web.bind.annotation.RequestMapping; 19 | import org.springframework.web.bind.annotation.ResponseStatus; 20 | import org.springframework.web.bind.annotation.RestController; 21 | 22 | @RequiredArgsConstructor 23 | @RestController 24 | @RequestMapping("/orders") 25 | public class OrderController { 26 | 27 | private final OrderFacade orderFacade; 28 | 29 | @PostMapping 30 | @ResponseStatus(HttpStatus.CREATED) 31 | @PreAuthorize("hasRole('ROLE_USER')") 32 | public Response createOrder( 33 | @RequestBody final @Valid CreateOrderRequest request, 34 | @CurrentUser final Member member 35 | ) { 36 | final var result = orderFacade.createOrder(request.toCommand(member), LocalDateTime.now()); 37 | 38 | return Response.success(CreatedOrderResponse.from(result)); 39 | } 40 | 41 | @PostMapping("/pay") 42 | @ResponseStatus(HttpStatus.OK) 43 | @PreAuthorize("hasRole('ROLE_USER')") 44 | public Response payOrder( 45 | @RequestBody @Valid final PayOrderRequest request, 46 | @CurrentUser final Member member 47 | ) { 48 | final var result = orderFacade.payOrder(request.toCommand(member)); 49 | 50 | return Response.success(PayedOrderResponse.from(result)); 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/presentation/request/CreateOrderRequest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.presentation.request; 2 | 3 | import com.flab.infrun.member.domain.Member; 4 | import com.flab.infrun.order.application.command.CreateOrderCommand; 5 | import jakarta.validation.constraints.NotEmpty; 6 | import jakarta.validation.constraints.NotNull; 7 | import java.util.List; 8 | 9 | public record CreateOrderRequest( 10 | @NotEmpty(message = "강의 ID는 필수입니다.") 11 | @NotNull(message = "강의 ID는 필수입니다.") 12 | List lectureIds, 13 | String couponCode 14 | ) { 15 | 16 | public CreateOrderCommand toCommand(final Member customer) { 17 | return new CreateOrderCommand( 18 | customer, 19 | lectureIds, 20 | couponCode 21 | ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/presentation/request/PayOrderRequest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.presentation.request; 2 | 3 | import com.flab.infrun.common.validator.EnumValid; 4 | import com.flab.infrun.member.domain.Member; 5 | import com.flab.infrun.order.application.command.PayOrderCommand; 6 | import com.flab.infrun.payment.domain.PayMethod; 7 | import com.flab.infrun.payment.domain.PayType; 8 | import jakarta.validation.constraints.Min; 9 | import jakarta.validation.constraints.NotBlank; 10 | import jakarta.validation.constraints.NotNull; 11 | import java.math.BigDecimal; 12 | 13 | public record PayOrderRequest( 14 | 15 | @NotNull(message = "주문 ID는 필수입니다.") 16 | @Min(value = 1, message = "주문 ID는 1 이상이어야 합니다.") 17 | Long orderId, 18 | @NotNull(message = "결제 금액은 필수입니다.") 19 | BigDecimal amount, 20 | @NotBlank(message = "결제 방법은 필수입니다.") 21 | @EnumValid(type = PayMethod.class, message = "결제 방법은 CARD 또는 CASH만 가능합니다.", ignoreCase = true) 22 | String payMethod, 23 | 24 | @NotBlank(message = "결제 타입은 필수입니다.") 25 | @EnumValid(type = PayType.class, message = "결제 타입은 LUMP_SUM 또는 INSTALLMENT만 가능합니다.", ignoreCase = true) 26 | String payType, 27 | 28 | @Min(value = 1, message = "할부 개월 수는 1 이상이어야 합니다.") 29 | Integer installmentMonths 30 | ) { 31 | 32 | public PayOrderCommand toCommand(final Member member) { 33 | return new PayOrderCommand( 34 | member.getId(), 35 | 1L, 36 | BigDecimal.valueOf(40_000), 37 | PayMethod.valueOf(payMethod), 38 | PayType.valueOf(payType), 39 | installmentMonths 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/presentation/response/CreatedOrderResponse.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.presentation.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import com.flab.infrun.order.application.result.CreatedOrderResult; 5 | import java.text.DecimalFormat; 6 | import java.time.LocalDateTime; 7 | import java.util.List; 8 | 9 | public record CreatedOrderResponse( 10 | 11 | Long orderId, 12 | String totalPrice, 13 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 14 | LocalDateTime orderDate, 15 | String orderStatus, 16 | List orderItems, 17 | boolean isCouponApplied 18 | ) { 19 | 20 | public static CreatedOrderResponse from(final CreatedOrderResult result) { 21 | return new CreatedOrderResponse( 22 | result.orderId(), 23 | new DecimalFormat("###,###").format(result.totalPrice()), 24 | result.orderedAt(), 25 | result.orderStatus(), 26 | result.orderItems().stream() 27 | .map(CreatedOrderItemResponse::from) 28 | .toList(), 29 | result.isCouponApplied() 30 | ); 31 | } 32 | 33 | public record CreatedOrderItemResponse( 34 | String lectureName, 35 | String basePrice, 36 | String salesPrice 37 | ) { 38 | 39 | public static CreatedOrderItemResponse from( 40 | final CreatedOrderResult.OrderItemResult orderItem) { 41 | return new CreatedOrderItemResponse( 42 | orderItem.lectureName(), 43 | new DecimalFormat("###,###").format(orderItem.basePrice()), 44 | new DecimalFormat("###,###").format(orderItem.salesPrice()) 45 | ); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/order/presentation/response/PayedOrderResponse.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.presentation.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import com.flab.infrun.order.application.result.PayedOrderResult; 5 | import java.math.BigDecimal; 6 | import java.time.LocalDateTime; 7 | 8 | public record PayedOrderResponse( 9 | BigDecimal amount, 10 | String payType, 11 | String payMethod, 12 | String orderStatus, 13 | String payStatus, 14 | @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 15 | LocalDateTime payedAt 16 | ) { 17 | 18 | public static PayedOrderResponse from(final PayedOrderResult result) { 19 | return new PayedOrderResponse( 20 | result.amount(), 21 | result.payType().getDescription(), 22 | result.payMethod().getDescription(), 23 | result.orderStatus(), 24 | result.payStatus(), 25 | result.payedAt() 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/payment/domain/PayMethod.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.payment.domain; 2 | 3 | public enum PayMethod { 4 | 5 | CARD("카드"), 6 | CASH("현금"); 7 | 8 | private String description; 9 | 10 | PayMethod(final String description) { 11 | this.description = description; 12 | } 13 | 14 | public String getDescription() { 15 | return description; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/payment/domain/PayStatus.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.payment.domain; 2 | 3 | public enum PayStatus { 4 | PAYMENT_SUCCESS("결제 성공"), 5 | PAYMENT_CANCEL("결제 취소"), 6 | PAYMENT_FAIL("결제 실패"); 7 | 8 | private final String description; 9 | 10 | PayStatus(String description) { 11 | this.description = description; 12 | } 13 | 14 | public String getDescription() { 15 | return description; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/payment/domain/PayType.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.payment.domain; 2 | 3 | public enum PayType { 4 | 5 | LUMP_SUM("일시불"), 6 | INSTALLMENT("할부"); 7 | 8 | private String description; 9 | 10 | PayType(final String description) { 11 | this.description = description; 12 | } 13 | 14 | public String getDescription() { 15 | return description; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/payment/domain/PaymentRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.payment.domain; 2 | 3 | public interface PaymentRepository { 4 | 5 | Payment save(final Payment payment); 6 | 7 | Payment findByOrderId(final Long orderId); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/payment/domain/exception/NotAllowedInstallmentMonthException.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.payment.domain.exception; 2 | 3 | import com.flab.infrun.common.exception.ErrorCode; 4 | import com.flab.infrun.common.exception.SystemException; 5 | 6 | public final class NotAllowedInstallmentMonthException extends SystemException { 7 | 8 | public NotAllowedInstallmentMonthException() { 9 | super(ErrorCode.NOT_ALLOWED_INSTALLMENT_MONTH); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/payment/infrastructure/config/PaymentPersistenceConfig.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.payment.infrastructure.config; 2 | 3 | import com.flab.infrun.payment.infrastructure.persistence.PaymentJpaRepository; 4 | import com.flab.infrun.payment.infrastructure.persistence.PaymentRepositoryAdapter; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class PaymentPersistenceConfig { 10 | 11 | @Bean 12 | public PaymentRepositoryAdapter paymentRepositoryAdapter( 13 | final PaymentJpaRepository paymentJpaRepository 14 | ) { 15 | return new PaymentRepositoryAdapter(paymentJpaRepository); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/payment/infrastructure/persistence/PaymentJpaRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.payment.infrastructure.persistence; 2 | 3 | import com.flab.infrun.payment.domain.Payment; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | public interface PaymentJpaRepository extends JpaRepository { 7 | 8 | Payment findByOrderId(final Long orderId); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/flab/infrun/payment/infrastructure/persistence/PaymentRepositoryAdapter.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.payment.infrastructure.persistence; 2 | 3 | import com.flab.infrun.payment.domain.Payment; 4 | import com.flab.infrun.payment.domain.PaymentRepository; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | @RequiredArgsConstructor 8 | public class PaymentRepositoryAdapter implements PaymentRepository { 9 | 10 | private final PaymentJpaRepository paymentJpaRepository; 11 | 12 | @Override 13 | public Payment save(final Payment payment) { 14 | return paymentJpaRepository.save(payment); 15 | } 16 | 17 | @Override 18 | public Payment findByOrderId(final Long orderId) { 19 | return paymentJpaRepository.findByOrderId(orderId); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/application/data/LoginData.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.application.data 2 | 3 | data class LoginData(val token: String) 4 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/application/facade/MemberManager.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.application.facade 2 | 3 | import com.flab.infrun.common.annotation.ApplicationService 4 | import com.flab.infrun.member.application.data.LoginData 5 | import com.flab.infrun.member.application.processor.MemberLoginProcessorKt 6 | import com.flab.infrun.member.application.processor.MemberSignupProcessorKt 7 | 8 | @ApplicationService 9 | class MemberManager( 10 | private val memberSignupProcessor: MemberSignupProcessorKt, 11 | private val memberLoginProcessor: MemberLoginProcessorKt, 12 | ) { 13 | 14 | fun signup(command: MemberSignupProcessorKt.Command) { 15 | memberSignupProcessor.execute(command) 16 | } 17 | 18 | fun login(command: MemberLoginProcessorKt.Command): LoginData { 19 | return memberLoginProcessor.execute(command) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/application/processor/MemberSignupProcessorKt.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.application.processor 2 | 3 | import com.flab.infrun.common.annotation.ApplicationService 4 | import com.flab.infrun.member.domain.MemberKt 5 | import com.flab.infrun.member.domain.MemberRepositoryKt 6 | import com.flab.infrun.member.domain.MemberVerifierKt 7 | import org.springframework.security.crypto.password.PasswordEncoder 8 | import org.springframework.transaction.annotation.Transactional 9 | 10 | @ApplicationService 11 | class MemberSignupProcessorKt( 12 | private val memberRepositoryKt: MemberRepositoryKt, 13 | private val passwordEncoder: PasswordEncoder, 14 | private val memberVerifierKt: MemberVerifierKt, 15 | ) { 16 | 17 | @Transactional 18 | fun execute(command: Command) { 19 | verifyCommand(command) 20 | 21 | val member = MemberKt( 22 | nickname = command.nickname, 23 | email = command.email, 24 | password = passwordEncoder.encode(command.password), 25 | ) 26 | 27 | memberRepositoryKt.save(member) 28 | } 29 | 30 | private fun verifyCommand(command: Command) { 31 | memberVerifierKt.verifyNickname(command.nickname) 32 | memberVerifierKt.verifyEmail(command.email) 33 | memberVerifierKt.verifyPassword(command.password) 34 | } 35 | 36 | data class Command( 37 | val nickname: String, 38 | val email: String, 39 | val password: String, 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/domain/MemberKt.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain 2 | 3 | import com.flab.infrun.common.entity.BaseEntity 4 | import jakarta.persistence.* 5 | 6 | @Table(name = "members_kt") 7 | @Entity 8 | class MemberKt( 9 | id: Long = 0, 10 | nickname: String, 11 | email: String, 12 | password: String, 13 | role: Role = Role.USER 14 | ) : BaseEntity() { 15 | 16 | @Id 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | var id: Long? = null 19 | protected set 20 | 21 | @Column(name = "nickname") 22 | var nickname = nickname 23 | protected set 24 | 25 | @Column(name = "email") 26 | var email = email 27 | protected set 28 | 29 | @Column(name = "password") 30 | var password = password 31 | protected set 32 | 33 | @Enumerated(value = EnumType.STRING) 34 | var role = role 35 | protected set 36 | 37 | enum class Role { 38 | USER, TEACHER 39 | } 40 | 41 | fun promoteToTeacher() { 42 | role = Role.TEACHER 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/domain/MemberRepositoryKt.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain 2 | 3 | interface MemberRepositoryKt { 4 | fun save(member: MemberKt): MemberKt 5 | fun findById(id: Long): MemberKt? 6 | fun findByEmail(email: String): MemberKt? 7 | fun existsByEmail(email: String): Boolean 8 | fun existsByNickname(nickname: String): Boolean 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/domain/MemberVerifierKt.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain 2 | 3 | interface MemberVerifierKt { 4 | fun verifyNickname(nickname: String) 5 | fun verifyEmail(email: String) 6 | fun verifyPassword(password: String) 7 | } 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/domain/exception/DuplicatedEmailExceptionKt.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain.exception 2 | 3 | import com.flab.infrun.common.exception.ErrorCode 4 | import com.flab.infrun.common.exception.SystemException 5 | 6 | class DuplicatedEmailExceptionKt : SystemException(ErrorCode.DUPLICATED_EMAIL) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/domain/exception/DuplicatedNicknameExceptionKt.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain.exception 2 | 3 | import com.flab.infrun.common.exception.ErrorCode 4 | import com.flab.infrun.common.exception.SystemException 5 | 6 | class DuplicatedNicknameExceptionKt : SystemException(ErrorCode.DUPLICATED_NICKNAME) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/domain/exception/InvalidPasswordExceptionKt.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain.exception 2 | 3 | import com.flab.infrun.common.exception.ErrorCode 4 | import com.flab.infrun.common.exception.SystemException 5 | 6 | class InvalidPasswordExceptionKt : SystemException(ErrorCode.INVALID_PASSWORD) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/domain/exception/NotFoundMemberExceptionKt.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain.exception 2 | 3 | import com.flab.infrun.common.exception.ErrorCode 4 | import com.flab.infrun.common.exception.SystemException 5 | 6 | class NotFoundMemberExceptionKt : SystemException(ErrorCode.NOT_FOUND_MEMBER) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/domain/exception/NotMatchPasswordExceptionKt.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain.exception 2 | 3 | import com.flab.infrun.common.exception.ErrorCode 4 | import com.flab.infrun.common.exception.SystemException 5 | 6 | class NotMatchPasswordExceptionKt : SystemException(ErrorCode.NOT_MATCH_PASSWORD) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/infra/MemberVerifierKtImpl.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.infra 2 | 3 | import com.flab.infrun.member.domain.MemberVerifierKt 4 | import com.flab.infrun.member.domain.exception.DuplicatedEmailException 5 | import com.flab.infrun.member.domain.exception.DuplicatedNicknameException 6 | import com.flab.infrun.member.domain.exception.InvalidPasswordException 7 | import com.flab.infrun.member.infra.persistence.jpa.MemberKtJpaRepository 8 | import org.springframework.util.StringUtils 9 | import java.util.regex.Pattern 10 | 11 | class MemberVerifierKtImpl( 12 | private val memberJpaRepository: MemberKtJpaRepository 13 | ) : MemberVerifierKt { 14 | 15 | override fun verifyNickname(nickname: String) { 16 | if (!StringUtils.hasText(nickname) || memberJpaRepository.existsByNickname(nickname)) { 17 | throw DuplicatedNicknameException() 18 | } 19 | } 20 | 21 | override fun verifyEmail(email: String) { 22 | if (!StringUtils.hasText(email) || memberJpaRepository.existsByEmail(email)) { 23 | throw DuplicatedEmailException() 24 | } 25 | } 26 | 27 | override fun verifyPassword(password: String) { 28 | if (!StringUtils.hasText(password) || !Pattern.matches( 29 | "^(?=.*[A-Z])(?=.*\\d)(?=.*[a-z])(?=.*[\\W_])[A-Za-z\\d\\W_]{8,16}$", 30 | password 31 | ) 32 | ) { 33 | throw InvalidPasswordException() 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/infra/config/MemberApplicationServiceConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.infra.config 2 | 3 | import com.flab.infrun.member.application.processor.MemberLoginProcessorKt 4 | import com.flab.infrun.member.application.processor.MemberSignupProcessorKt 5 | import com.flab.infrun.member.domain.MemberRepositoryKt 6 | import com.flab.infrun.member.domain.MemberVerifierKt 7 | import com.flab.infrun.member.infra.jwt.TokenProviderKt 8 | import org.springframework.context.annotation.Bean 9 | import org.springframework.context.annotation.Configuration 10 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder 11 | import org.springframework.security.crypto.password.PasswordEncoder 12 | 13 | @Configuration 14 | class MemberApplicationServiceConfiguration { 15 | 16 | @Bean 17 | fun memberLoginProcessorKt( 18 | memberRepository: MemberRepositoryKt, 19 | passwordEncoder: PasswordEncoder, 20 | tokenProvider: TokenProviderKt, 21 | authenticationManagerBuilder: AuthenticationManagerBuilder, 22 | ): MemberLoginProcessorKt { 23 | return MemberLoginProcessorKt( 24 | memberRepository = memberRepository, 25 | passwordEncoder = passwordEncoder, 26 | tokenProvider = tokenProvider, 27 | authenticationMangerBuilder = authenticationManagerBuilder, 28 | ) 29 | } 30 | 31 | @Bean 32 | fun memberSignupProcessorKt( 33 | memberRepositoryKt: MemberRepositoryKt, 34 | passwordEncoder: PasswordEncoder, 35 | memberVerifierKt: MemberVerifierKt, 36 | ): MemberSignupProcessorKt { 37 | return MemberSignupProcessorKt( 38 | memberRepositoryKt = memberRepositoryKt, 39 | passwordEncoder = passwordEncoder, 40 | memberVerifierKt = memberVerifierKt, 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/infra/config/MemberDomainServiceConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.infra.config 2 | 3 | import com.flab.infrun.member.domain.MemberVerifierKt 4 | import com.flab.infrun.member.infra.MemberVerifierKtImpl 5 | import com.flab.infrun.member.infra.persistence.jpa.MemberKtJpaRepository 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | 9 | @Configuration 10 | class MemberDomainServiceConfiguration { 11 | 12 | @Bean 13 | fun memberVerifierKt(memberJpaRepository: MemberKtJpaRepository): MemberVerifierKt { 14 | return MemberVerifierKtImpl(memberJpaRepository) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/infra/config/MemberFacadeConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.infra.config 2 | 3 | import com.flab.infrun.member.application.facade.MemberManager 4 | import com.flab.infrun.member.application.processor.MemberLoginProcessorKt 5 | import com.flab.infrun.member.application.processor.MemberSignupProcessorKt 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | 9 | @Configuration 10 | class MemberFacadeConfiguration { 11 | 12 | @Bean 13 | fun memberManager( 14 | memberSignupProcessor: MemberSignupProcessorKt, 15 | memberLoginProcessor: MemberLoginProcessorKt, 16 | ): MemberManager { 17 | return MemberManager( 18 | memberSignupProcessor = memberSignupProcessor, 19 | memberLoginProcessor = memberLoginProcessor, 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/infra/config/MemberRepositoryConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.infra.config 2 | 3 | import com.flab.infrun.member.domain.MemberRepositoryKt 4 | import com.flab.infrun.member.infra.persistence.MemberRepositoryImpl 5 | import com.flab.infrun.member.infra.persistence.jpa.MemberKtJpaRepository 6 | import org.springframework.context.annotation.Bean 7 | import org.springframework.context.annotation.Configuration 8 | 9 | @Configuration 10 | class MemberRepositoryConfiguration { 11 | 12 | @Bean 13 | fun memberKtRepository(memberJpaRepository: MemberKtJpaRepository): MemberRepositoryKt { 14 | return MemberRepositoryImpl(memberJpaRepository) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/infra/persistence/MemberRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.infra.persistence 2 | 3 | import com.flab.infrun.member.domain.MemberKt 4 | import com.flab.infrun.member.domain.MemberRepositoryKt 5 | import com.flab.infrun.member.infra.persistence.jpa.MemberKtJpaRepository 6 | import org.springframework.data.repository.findByIdOrNull 7 | 8 | class MemberRepositoryImpl( 9 | private val memberJpaRepository: MemberKtJpaRepository 10 | ) : MemberRepositoryKt { 11 | 12 | override fun save(member: MemberKt): MemberKt { 13 | return memberJpaRepository.save(member) 14 | } 15 | 16 | override fun findById(id: Long): MemberKt? { 17 | return memberJpaRepository.findByIdOrNull(id) 18 | } 19 | 20 | override fun findByEmail(email: String): MemberKt? { 21 | return memberJpaRepository.findByEmail(email) 22 | } 23 | 24 | override fun existsByEmail(email: String): Boolean { 25 | return memberJpaRepository.existsByEmail(email) 26 | } 27 | 28 | override fun existsByNickname(nickname: String): Boolean { 29 | return memberJpaRepository.existsByNickname(nickname) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/infra/persistence/jpa/MemberKtJpaRepository.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.infra.persistence.jpa 2 | 3 | import com.flab.infrun.member.domain.MemberKt 4 | import org.springframework.data.jpa.repository.JpaRepository 5 | 6 | interface MemberKtJpaRepository : JpaRepository { 7 | 8 | fun save(member: MemberKt): MemberKt 9 | fun findByEmail(email: String): MemberKt? 10 | fun existsByEmail(email: String): Boolean 11 | fun existsByNickname(nickname: String): Boolean 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/interface/MemberControllerKt.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.`interface` 2 | 3 | import com.flab.infrun.member.application.facade.MemberManager 4 | import com.flab.infrun.member.`interface`.request.MemberLoginRequest 5 | import com.flab.infrun.member.`interface`.request.MemberSignupRequest 6 | import org.springframework.http.HttpStatus 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.ResponseStatus 11 | import org.springframework.web.bind.annotation.RestController 12 | 13 | @RestController 14 | @RequestMapping("/api/v1/members") 15 | class MemberControllerKt( 16 | private val memberManager: MemberManager, 17 | ) { 18 | 19 | @ResponseStatus(HttpStatus.CREATED) 20 | @PostMapping("/signup") 21 | fun signup(@RequestBody request: MemberSignupRequest) { 22 | memberManager.signup(request.toCommand()) 23 | } 24 | 25 | @ResponseStatus(HttpStatus.OK) 26 | @PostMapping("/login") 27 | fun login(@RequestBody request: MemberLoginRequest) { 28 | memberManager.login(request.toCommand()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/interface/request/MemberLoginRequest.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.`interface`.request 2 | 3 | import com.flab.infrun.member.application.processor.MemberLoginProcessorKt 4 | import jakarta.validation.constraints.Email 5 | import jakarta.validation.constraints.NotBlank 6 | import jakarta.validation.constraints.Pattern 7 | 8 | data class MemberLoginRequest( 9 | @NotBlank(message = "이메일은 필수 입력입니다.") 10 | @Email(message = "이메일 형식이 올바르지 않습니다.") 11 | val email: String, 12 | 13 | @NotBlank(message = "비밀번호는 필수 입력입니다.") 14 | @Pattern( 15 | regexp = "^(?=.*[A-Z])(?=.*\\d)(?=.*[a-z])(?=.*[\\W_])[A-Za-z\\d\\W_]{8,16}$", 16 | message = "비밀번호는 8자리 ~ 16자리의 영문 대소문자, 숫자, 특수문자를 포함해야 합니다." 17 | ) 18 | val password: String, 19 | ) { 20 | fun toCommand(): MemberLoginProcessorKt.Command { 21 | return MemberLoginProcessorKt.Command( 22 | email = this.email, 23 | password = this.password, 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/com/flab/infrun/member/interface/request/MemberSignupRequest.kt: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.`interface`.request 2 | 3 | import com.flab.infrun.member.application.processor.MemberSignupProcessorKt 4 | import jakarta.validation.constraints.Email 5 | import jakarta.validation.constraints.NotBlank 6 | import jakarta.validation.constraints.Pattern 7 | 8 | data class MemberSignupRequest( 9 | @NotBlank(message = "닉네임은 필수 입력입니다.") 10 | val nickname: String, 11 | 12 | @NotBlank(message = "이메일은 필수 입력입니다.") 13 | @Email(message = "이메일 형식이 올바르지 않습니다.") 14 | val email: String, 15 | 16 | @NotBlank(message = "비밀번호는 필수 입력입니다.") 17 | @Pattern( 18 | regexp = "^(?=.*[A-Z])(?=.*\\d)(?=.*[a-z])(?=.*[\\W_])[A-Za-z\\d\\W_]{8,16}$", 19 | message = "비밀번호는 8자리 ~ 16자리의 영문 대소문자, 숫자, 특수문자를 포함해야 합니다." 20 | ) 21 | val password: String, 22 | ) { 23 | fun toCommand(): MemberSignupProcessorKt.Command { 24 | return MemberSignupProcessorKt.Command( 25 | nickname = this.nickname, 26 | email = this.email, 27 | password = this.password, 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/resources/application-local.yaml: -------------------------------------------------------------------------------- 1 | spring: 2 | h2: 3 | console: 4 | path: /h2-console 5 | enabled: true 6 | datasource: 7 | url: jdbc:h2:mem:~/local 8 | username: sa 9 | password: 10 | driver-class-name: org.h2.Driver 11 | servlet: 12 | multipart: 13 | max-file-size: 50MB 14 | max-request-size: 50MB 15 | jpa: 16 | properties: 17 | hibernate: 18 | show_sql: true 19 | format_sql: true 20 | use_sql_comments: true 21 | hibernate: 22 | ddl-auto: create 23 | 24 | jwt: 25 | secret: Zi1sYWItZWR1LWluZnJ1bi1zZWNyZXQtand0LWtleS1oczUxMgo= 26 | token-validity-in-seconds: 86400 27 | aws: 28 | s3: 29 | bucketName: infrun 30 | profileName: InfrunManager 31 | region: ap-northeast-2 32 | duration: 30 -------------------------------------------------------------------------------- /src/main/resources/application-prod.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8081 3 | 4 | spring: 5 | datasource: 6 | url: jdbc:mysql://infrun-database:3306/infrun 7 | username: ${SPRING_DATASOURCE_USER} 8 | password: ${SPRING_DATASOURCE_PASSWORD} 9 | driver-class-name: com.mysql.cj.jdbc.Driver 10 | servlet: 11 | multipart: 12 | max-file-size: 50MB 13 | max-request-size: 50MB 14 | jpa: 15 | properties: 16 | hibernate: 17 | show_sql: true 18 | format_sql: true 19 | use_sql_comments: true 20 | hibernate: 21 | ddl-auto: update 22 | 23 | jwt: 24 | secret: ${JWT_SECRET} 25 | token-validity-in-seconds: ${JWT_TOKEN_VALIDITY_IN_SECONDS} -------------------------------------------------------------------------------- /src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | spring: 5 | application: 6 | name: infrun 7 | profiles: 8 | active: local 9 | 10 | logging.level: 11 | org.hibernate.SQL: debug 12 | 13 | aws: 14 | s3: 15 | bucketName: infrun 16 | profileName: InfrunManager 17 | region: ap-northeast-2 18 | duration: 30 19 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/cart/application/DeleteCartItemProcessorTest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.application; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 5 | 6 | import com.flab.infrun.cart.application.command.DeleteCartItemCommand; 7 | import com.flab.infrun.cart.domain.Cart; 8 | import com.flab.infrun.cart.domain.CartRepository; 9 | import com.flab.infrun.cart.domain.exception.NotFoundCartException; 10 | import com.flab.infrun.cart.domain.exception.NotFoundCartItemException; 11 | import java.math.BigDecimal; 12 | import org.junit.jupiter.api.BeforeEach; 13 | import org.junit.jupiter.api.DisplayName; 14 | import org.junit.jupiter.api.Test; 15 | 16 | final class DeleteCartItemProcessorTest { 17 | 18 | private DeleteCartItemProcessor sut; 19 | private CartRepository cartRepository; 20 | 21 | @BeforeEach 22 | void setup() { 23 | cartRepository = new FakeCartRepository(); 24 | sut = new DeleteCartItemProcessor(cartRepository); 25 | } 26 | 27 | @Test 28 | @DisplayName("수강바구니에서 선택한 강의를 삭제한다") 29 | void deleteCartItem() { 30 | createCartItem(); 31 | final var command = createDeleteCartCommand(); 32 | 33 | final var result = sut.execute(command); 34 | 35 | assertThat(result.lectureIds()).hasSize(0); 36 | } 37 | 38 | @Test 39 | @DisplayName("수강바구니가 없다면 예외가 발생한다") 40 | void deleteCartItem_notFoundCart() { 41 | final var command = createDeleteCartCommand(); 42 | 43 | assertThatThrownBy(() -> sut.execute(command)) 44 | .isInstanceOf(NotFoundCartException.class); 45 | } 46 | 47 | @Test 48 | @DisplayName("수강바구니에 해당 강의가 없다면 예외가 발생한다") 49 | void deleteCartItem_notFoundCartItem() { 50 | createCart(); 51 | final var command = createDeleteCartCommand(); 52 | 53 | assertThatThrownBy(() -> sut.execute(command)) 54 | .isInstanceOf(NotFoundCartItemException.class); 55 | } 56 | 57 | private DeleteCartItemCommand createDeleteCartCommand() { 58 | return new DeleteCartItemCommand(1L, 1L); 59 | } 60 | 61 | private Cart createCartItem() { 62 | final Cart cart = cartRepository.save(Cart.create(1L)); 63 | cart.addCartItem(1L, BigDecimal.valueOf(63_000)); 64 | 65 | return cart; 66 | } 67 | 68 | private void createCart() { 69 | cartRepository.save(Cart.create(1L)); 70 | } 71 | } -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/cart/application/FakeCartRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.application; 2 | 3 | import com.flab.infrun.cart.domain.Cart; 4 | import com.flab.infrun.cart.domain.CartRepository; 5 | import java.util.Map; 6 | import java.util.Optional; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | 9 | public class FakeCartRepository implements CartRepository { 10 | 11 | private final Map persistence = new ConcurrentHashMap<>(); 12 | private Long sequence = 0L; 13 | 14 | @Override 15 | public Cart save(final Cart cart) { 16 | persistence.put(++sequence, cart); 17 | cart.assignId(sequence); 18 | return cart; 19 | } 20 | 21 | @Override 22 | public Optional findByOwnerId(final Long ownerId) { 23 | return persistence.values().stream() 24 | .filter(cart -> cart.getOwnerId().equals(ownerId)) 25 | .findFirst(); 26 | } 27 | 28 | @Override 29 | public Cart findWithCartItemsByOwnerId(final Long ownerId) { 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/cart/application/FakeLectureRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.application; 2 | 3 | import com.flab.infrun.lecture.domain.Lecture; 4 | import com.flab.infrun.lecture.domain.LectureRepository; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Optional; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | 10 | public class FakeLectureRepository implements LectureRepository { 11 | 12 | private final Map persistence = new ConcurrentHashMap<>(); 13 | private Long sequence = 0L; 14 | 15 | @Override 16 | public Lecture save(final Lecture lecture) { 17 | persistence.put(++sequence, lecture); 18 | lecture.assignId(sequence); 19 | return lecture; 20 | } 21 | 22 | public void saveAll(final List lectures) { 23 | for (Lecture lecture : lectures) { 24 | persistence.put(++sequence, lecture); 25 | lecture.assignId(sequence); 26 | } 27 | } 28 | 29 | @Override 30 | public Optional findById(final Long id) { 31 | return persistence.values().stream() 32 | .filter(lecture -> lecture.getId().equals(id)) 33 | .findFirst(); 34 | } 35 | 36 | @Override 37 | public List findAllByIdIn(final List lectureIds) { 38 | return null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/cart/domain/CartFixture.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.domain; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | public class CartFixture { 9 | 10 | private Long id = 1L; 11 | private Long ownerId = 1L; 12 | private List cartItems = new ArrayList<>(); 13 | private BigDecimal totalPrice = BigDecimal.valueOf(100_000); 14 | 15 | 16 | public static CartFixture aCartFixture() { 17 | return new CartFixture(); 18 | } 19 | 20 | public CartFixture id(final Long id) { 21 | this.id = id; 22 | return this; 23 | } 24 | 25 | public CartFixture ownerId(final Long ownerId) { 26 | this.ownerId = ownerId; 27 | return this; 28 | } 29 | 30 | public CartFixture cartItems(final CartItemFixture... cartItems) { 31 | this.cartItems = Arrays.asList(cartItems); 32 | return this; 33 | } 34 | 35 | public CartFixture totalPrice(final BigDecimal totalPrice) { 36 | this.totalPrice = totalPrice; 37 | return this; 38 | } 39 | 40 | public Cart build() { 41 | final Cart cart = Cart.create(this.ownerId); 42 | 43 | this.cartItems.stream() 44 | .map(CartItemFixture::build) 45 | .forEach(cartItem -> cart.addCartItem(cartItem.getLectureId(), cartItem.getPrice())); 46 | 47 | return cart; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/cart/domain/CartItemFixture.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.domain; 2 | 3 | import static com.flab.infrun.cart.domain.CartFixture.aCartFixture; 4 | 5 | import java.math.BigDecimal; 6 | 7 | public class CartItemFixture { 8 | 9 | private Long lectureId = 1L; 10 | private BigDecimal price = BigDecimal.valueOf(30_000); 11 | private CartFixture cart = aCartFixture(); 12 | 13 | public static CartItemFixture aCartItemFixture() { 14 | return new CartItemFixture(); 15 | } 16 | 17 | public Long getLectureId() { 18 | return lectureId; 19 | } 20 | 21 | public BigDecimal getPrice() { 22 | return price; 23 | } 24 | 25 | public CartItemFixture lectureId(final Long lectureId) { 26 | this.lectureId = lectureId; 27 | return this; 28 | } 29 | 30 | public CartItemFixture price(final BigDecimal price) { 31 | this.price = price; 32 | return this; 33 | } 34 | 35 | public CartItem build() { 36 | return new CartItem(this.cart.build(), this.lectureId, this.price); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/cart/domain/CartItemsFixture.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.cart.domain; 2 | 3 | import static com.flab.infrun.cart.domain.CartItemFixture.aCartItemFixture; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | public class CartItemsFixture { 9 | 10 | private List cartItems = List.of(aCartItemFixture()); 11 | 12 | public static CartItemsFixture aCartItemsFixture() { 13 | return new CartItemsFixture(); 14 | } 15 | 16 | public CartItemsFixture cartItems(final CartItemFixture... cartItems) { 17 | this.cartItems = Arrays.asList(cartItems); 18 | return this; 19 | } 20 | 21 | public List build() { 22 | return cartItems.stream() 23 | .map(CartItemFixture::build) 24 | .toList(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/common/DatabaseCleaner.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common; 2 | 3 | import jakarta.persistence.EntityManager; 4 | import jakarta.persistence.PersistenceContext; 5 | import jakarta.persistence.Table; 6 | import java.util.Set; 7 | import java.util.stream.Collectors; 8 | import org.springframework.beans.factory.InitializingBean; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | @Component 13 | public class DatabaseCleaner implements InitializingBean { 14 | @PersistenceContext 15 | private EntityManager entityManager; 16 | private Set tableNames; 17 | 18 | @Override 19 | public void afterPropertiesSet() { 20 | tableNames = entityManager.getMetamodel().getEntities().stream() 21 | .filter(e -> e.getJavaType().isAnnotationPresent(Table.class)) 22 | .map(e -> e.getJavaType().getAnnotation(Table.class).name()) 23 | .collect(Collectors.toUnmodifiableSet()); 24 | } 25 | 26 | @Transactional 27 | public void execute() { 28 | entityManager.flush(); 29 | entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); 30 | 31 | for (final String tableName : tableNames) { 32 | entityManager.createNativeQuery("TRUNCATE TABLE " + tableName + " RESTART IDENTITY ").executeUpdate(); 33 | } 34 | entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/common/IntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.common; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @SpringBootTest 8 | public class IntegrationTest { 9 | 10 | @Autowired 11 | private DatabaseCleaner databaseCleaner; 12 | 13 | @BeforeEach 14 | void setupIntegrationTest() { 15 | databaseCleaner.afterPropertiesSet(); 16 | databaseCleaner.execute(); 17 | } 18 | } 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/coupon/application/CouponReaderTest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.application; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.tuple; 5 | import static org.mockito.BDDMockito.given; 6 | 7 | import com.flab.infrun.coupon.application.result.CouponView; 8 | import java.time.LocalDateTime; 9 | import java.util.List; 10 | import org.junit.jupiter.api.DisplayName; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.jupiter.MockitoExtension; 15 | 16 | @ExtendWith(MockitoExtension.class) 17 | final class CouponReaderTest { 18 | 19 | @Mock 20 | private CouponReader sut; 21 | 22 | @Test 23 | @DisplayName("쿠폰 목록을 조회한다") 24 | void readCoupons() { 25 | final LocalDateTime currentTime = LocalDateTime.of(2023, 12, 31, 0, 0); 26 | final Long ownerId = 1L; 27 | given(sut.read(ownerId, currentTime)) 28 | .willReturn(List.of( 29 | new CouponView(1, 10, "FIX"), 30 | new CouponView(1, 10000, "RATE") 31 | )); 32 | 33 | final var result = sut.read(ownerId, currentTime); 34 | 35 | assertThat(result).isNotEmpty(); 36 | assertThat(result.size()).isEqualTo(2); 37 | assertThat(result).extracting("dDay", "discountValue", "discountType") 38 | .containsExactly( 39 | tuple(1L, 10, "FIX"), 40 | tuple(1L, 10000, "RATE") 41 | ); 42 | } 43 | } -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/coupon/application/FakeCouponRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.application; 2 | 3 | import com.flab.infrun.coupon.domain.Coupon; 4 | import com.flab.infrun.coupon.domain.CouponRepository; 5 | import com.flab.infrun.coupon.domain.exception.NotFoundCouponException; 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Objects; 10 | import java.util.Optional; 11 | 12 | public class FakeCouponRepository implements CouponRepository { 13 | 14 | private final Map persistence = new HashMap<>(); 15 | private Long sequence = 0L; 16 | 17 | @Override 18 | public Coupon save(final Coupon coupon) { 19 | persistence.put(++sequence, coupon); 20 | coupon.assignId(sequence); 21 | 22 | return coupon; 23 | } 24 | 25 | @Override 26 | public List saveAll(final List coupons) { 27 | coupons.forEach(coupon -> { 28 | persistence.put(++sequence, coupon); 29 | coupon.assignId(sequence); 30 | }); 31 | 32 | return coupons; 33 | } 34 | 35 | @Override 36 | public Coupon findByCouponCode(final String couponCode) { 37 | return persistence.values().stream() 38 | .filter(coupon -> Objects.equals(coupon.getCode(), couponCode)) 39 | .findFirst() 40 | .orElseThrow(NotFoundCouponException::new); 41 | } 42 | 43 | @Override 44 | public Optional findByCouponCodeWithLock(final String couponCode) { 45 | return persistence.values().stream() 46 | .filter(coupon -> Objects.equals(coupon.getCode(), couponCode)) 47 | .findFirst(); 48 | } 49 | 50 | @Override 51 | public List findAllByOwnerId(final Long ownerId) { 52 | return persistence.values().stream() 53 | .filter(coupon -> Objects.equals(coupon.getOwnerId(), ownerId)) 54 | .toList(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/coupon/domain/CouponFixture.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.coupon.domain; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public class CouponFixture { 6 | 7 | private Long id = 1L; 8 | private String code = "coupon-code"; 9 | private CouponStatus status = CouponStatus.REGISTERED; 10 | private DiscountInfo discountInfo = DiscountInfo.of(DiscountType.FIX, 1_000); 11 | private LocalDateTime expirationAt = LocalDateTime.of(2099, 12, 31, 0, 0); 12 | private Long ownerId = 1L; 13 | 14 | public static CouponFixture aCouponFixture() { 15 | return new CouponFixture(); 16 | } 17 | 18 | public CouponFixture id(final Long id) { 19 | this.id = id; 20 | return this; 21 | } 22 | 23 | public CouponFixture code(final String code) { 24 | this.code = code; 25 | return this; 26 | } 27 | 28 | public CouponFixture status(final CouponStatus status) { 29 | this.status = status; 30 | return this; 31 | } 32 | 33 | public CouponFixture discountInfo(final DiscountInfo discountInfo) { 34 | this.discountInfo = discountInfo; 35 | return this; 36 | } 37 | 38 | public CouponFixture expirationAt(final LocalDateTime expirationAt) { 39 | this.expirationAt = expirationAt; 40 | return this; 41 | } 42 | 43 | public CouponFixture ownerId(final Long ownerId) { 44 | this.ownerId = ownerId; 45 | return this; 46 | } 47 | 48 | public Coupon build() { 49 | final Coupon coupon = Coupon.create( 50 | code, 51 | discountInfo, 52 | expirationAt 53 | ); 54 | 55 | coupon.assignId(id); 56 | 57 | return coupon; 58 | } 59 | 60 | public Coupon buildWithEnrolled() { 61 | final Coupon coupon = Coupon.create( 62 | code, 63 | discountInfo, 64 | expirationAt 65 | ); 66 | 67 | coupon.assignId(id); 68 | coupon.enroll(ownerId, LocalDateTime.now()); 69 | 70 | return coupon; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/lecture/application/LectureCommandProcessorTest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.application; 2 | 3 | import com.flab.infrun.lecture.domain.StubLectureRepository; 4 | import com.flab.infrun.member.domain.StubMemberRepository; 5 | import java.nio.charset.StandardCharsets; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.springframework.mock.web.MockMultipartFile; 8 | 9 | 10 | class LectureCommandProcessorTest { 11 | 12 | private LectureCommandProcessor processor; 13 | private final StubLectureRepository lectureRepository = new StubLectureRepository(); 14 | private final StubMemberRepository memberRepository = new StubMemberRepository(); 15 | 16 | @BeforeEach 17 | void setup() { 18 | processor = new LectureCommandProcessor( 19 | //todo- utils stub refactor 20 | lectureRepository, 21 | null, 22 | null, 23 | null 24 | ); 25 | } 26 | 27 | private MockMultipartFile setupMultiPartTextFile(String fileName, String originFileName, 28 | String content) { 29 | return new MockMultipartFile(fileName, originFileName, 30 | "text/plain", content.getBytes( 31 | StandardCharsets.UTF_8)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/lecture/domain/LectureFixture.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.domain; 2 | 3 | import com.flab.infrun.member.domain.Member; 4 | import com.flab.infrun.member.domain.MemberFixture; 5 | 6 | public class LectureFixture { 7 | 8 | private Long id = 1L; 9 | private String name = "객체지향의 사실과 오해"; 10 | private int price = 30_000; 11 | private int lectureLevel = 1; 12 | private String skill = "java"; 13 | private String introduce = "객체지향"; 14 | private Member member = MemberFixture.aMemberFixture().build(); 15 | 16 | public static LectureFixture aLectureFixture() { 17 | return new LectureFixture(); 18 | } 19 | 20 | public LectureFixture id(final Long id) { 21 | this.id = id; 22 | return this; 23 | } 24 | 25 | public LectureFixture name(final String name) { 26 | this.name = name; 27 | return this; 28 | } 29 | 30 | public LectureFixture price(final int price) { 31 | this.price = price; 32 | return this; 33 | } 34 | 35 | public LectureFixture lectureLevel(final int lectureLevel) { 36 | this.lectureLevel = lectureLevel; 37 | return this; 38 | } 39 | 40 | public LectureFixture skill(final String skill) { 41 | this.skill = skill; 42 | return this; 43 | } 44 | 45 | public LectureFixture introduce(final String introduce) { 46 | this.introduce = introduce; 47 | return this; 48 | } 49 | 50 | public LectureFixture member(final Member member) { 51 | this.member = member; 52 | return this; 53 | } 54 | 55 | public Lecture build() { 56 | final Lecture lecture = Lecture.of( 57 | this.name, 58 | this.price, 59 | this.lectureLevel, 60 | this.skill, 61 | this.introduce, 62 | this.member); 63 | lecture.assignId(this.id); 64 | return lecture; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/lecture/domain/StubLectureRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.domain; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.Optional; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | 8 | public final class StubLectureRepository implements LectureRepository { 9 | 10 | private final Map persistence = new ConcurrentHashMap<>(); 11 | private Long sequence = 0L; 12 | 13 | @Override 14 | public Lecture save(Lecture entity) { 15 | persistence.put(++sequence, entity); 16 | entity.assignId(sequence); 17 | return entity; 18 | } 19 | 20 | @Override 21 | public Optional findById(Long id) { 22 | return Optional.ofNullable(persistence.get(id)); 23 | } 24 | 25 | @Override 26 | public List findAllByIdIn(List lectureIds) { 27 | return null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/lecture/domain/StubLectureReviewRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.domain; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.Optional; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | 8 | public final class StubLectureReviewRepository implements LectureReviewRepository { 9 | 10 | private final Map persistence = new ConcurrentHashMap<>(); 11 | private Long sequence = 0L; 12 | 13 | @Override 14 | public LectureReview save(final LectureReview entity) { 15 | persistence.put(++sequence, entity); 16 | entity.assignId(sequence); 17 | return entity; 18 | } 19 | 20 | @Override 21 | public Long deleteById(Long id) { 22 | int size = persistence.size(); 23 | persistence.remove(id); 24 | 25 | return (long) (size - persistence.size()); 26 | } 27 | 28 | @Override 29 | public List findByLectureId(Long lectureId) { 30 | return persistence.values().stream() 31 | .filter(r -> r.getLecture().getId().equals(lectureId)) 32 | .toList(); 33 | } 34 | 35 | @Override 36 | public Optional findById(Long id) { 37 | return Optional.ofNullable(persistence.get(id)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/lecture/infrastructure/persistence/LectureDetailRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.infrastructure.persistence; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import com.flab.infrun.lecture.domain.LectureDetail; 6 | import com.flab.infrun.lecture.infrastructure.persistence.jpa.LectureDetailJpaRepository; 7 | import java.util.Optional; 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 12 | 13 | @DataJpaTest 14 | class LectureDetailRepositoryTest { 15 | 16 | @Autowired 17 | private LectureDetailJpaRepository repository; 18 | 19 | @Test 20 | @DisplayName("강의상세 저장 테스트") 21 | void save() { 22 | LectureDetail lectureDetail = LectureDetail.of("1", "강의 A의 챕터1", null); 23 | 24 | LectureDetail saved = repository.save(lectureDetail); 25 | Optional lecture1 = repository.findById(saved.getId()); 26 | 27 | assertThat(lecture1.get()).isEqualTo(lectureDetail); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/lecture/infrastructure/persistence/LectureRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.lecture.infrastructure.persistence; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import com.flab.infrun.lecture.domain.Lecture; 6 | import com.flab.infrun.lecture.infrastructure.persistence.jpa.LectureJpaRepository; 7 | import java.util.Optional; 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 12 | 13 | @DataJpaTest 14 | class LectureRepositoryTest { 15 | 16 | @Autowired 17 | private LectureJpaRepository lectureJpaRepository; 18 | 19 | @Test 20 | @DisplayName("강의 저장 테스트") 21 | void save() { 22 | Lecture lecture = Lecture.of("Lecture1", 20000, 1, "JAVA", "이것은 강의 A", 23 | null); 24 | 25 | Lecture saved = lectureJpaRepository.save(lecture); 26 | Optional lecture1 = lectureJpaRepository.findById(saved.getId()); 27 | 28 | assertThat(lecture1.get()).isEqualTo(lecture); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/member/application/FakePasswordEncoder.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.application; 2 | 3 | import java.util.Objects; 4 | import org.springframework.security.crypto.password.PasswordEncoder; 5 | 6 | class FakePasswordEncoder implements PasswordEncoder { 7 | 8 | @Override 9 | public String encode(final CharSequence rawPassword) { 10 | return rawPassword.toString(); 11 | } 12 | 13 | @Override 14 | public boolean matches(final CharSequence rawPassword, final String encodedPassword) { 15 | return Objects.equals(rawPassword.toString(), encodedPassword); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/member/domain/MemberFixture.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain; 2 | 3 | public class MemberFixture { 4 | 5 | private Long id = 1L; 6 | private String nickname = "test"; 7 | private String email = "test@test.com"; 8 | private String password = "test1234"; 9 | private Role role = Role.USER; 10 | 11 | public static MemberFixture aMemberFixture() { 12 | return new MemberFixture(); 13 | } 14 | 15 | public MemberFixture id(final Long id) { 16 | this.id = id; 17 | return this; 18 | } 19 | 20 | public MemberFixture nickname(final String nickname) { 21 | this.nickname = nickname; 22 | return this; 23 | } 24 | 25 | public MemberFixture email(final String email) { 26 | this.email = email; 27 | return this; 28 | } 29 | 30 | public MemberFixture password(final String password) { 31 | this.password = password; 32 | return this; 33 | } 34 | 35 | public MemberFixture role(final Role role) { 36 | this.role = role; 37 | return this; 38 | } 39 | 40 | public Member build() { 41 | final Member member = Member.of(this.nickname, this.email, this.password); 42 | member.assignId(this.id); 43 | 44 | if (this.role == Role.TEACHER) { 45 | member.promoteToTeacher(); 46 | } 47 | 48 | return member; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/member/domain/StubMemberRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.member.domain; 2 | 3 | import java.util.Map; 4 | import java.util.Objects; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | 7 | public final class StubMemberRepository implements MemberRepository { 8 | 9 | private final Map persistence = new ConcurrentHashMap<>(); 10 | private Long sequence = 0L; 11 | 12 | @Override 13 | public Member save(final Member member) { 14 | persistence.put(++sequence, member); 15 | member.assignId(sequence); 16 | return member; 17 | } 18 | 19 | @Override 20 | public boolean existsByEmail(final String email) { 21 | return persistence.values().stream() 22 | .anyMatch(member -> member.getEmail().equals(email)); 23 | } 24 | 25 | @Override 26 | public boolean existsByNickname(final String nickname) { 27 | return persistence.values().stream() 28 | .anyMatch(member -> member.getNickname().equals(nickname)); 29 | } 30 | 31 | @Override 32 | public Member findByEmail(final String email) { 33 | return persistence.values().stream() 34 | .filter(member -> Objects.equals(member.getEmail(), email)) 35 | .findAny() 36 | .get(); 37 | } 38 | 39 | @Override 40 | public Member findById(Long id) { 41 | return null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/order/domain/OrderItemFixture.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.order.domain; 2 | 3 | import java.math.BigDecimal; 4 | 5 | public class OrderItemFixture { 6 | 7 | private Long id = 1L; 8 | private Long itemId = 1L; 9 | private String providerName = "조영호님"; 10 | private String itemName = "객체지향의 사실과 오해"; 11 | private BigDecimal basePrice = BigDecimal.valueOf(30_000); 12 | private BigDecimal salesPrice = BigDecimal.ZERO; 13 | 14 | public static OrderItemFixture anOrderItemsFixture() { 15 | return new OrderItemFixture(); 16 | } 17 | 18 | public OrderItemFixture id(Long id) { 19 | this.id = id; 20 | return this; 21 | } 22 | 23 | public OrderItemFixture itemId(Long itemId) { 24 | this.itemId = itemId; 25 | return this; 26 | } 27 | 28 | public OrderItemFixture itemName(String itemName) { 29 | this.itemName = itemName; 30 | return this; 31 | } 32 | 33 | public OrderItemFixture basePrice(BigDecimal basePrice) { 34 | this.basePrice = basePrice; 35 | return this; 36 | } 37 | 38 | public OrderItemFixture salesPrice(BigDecimal salesPrice) { 39 | this.salesPrice = salesPrice; 40 | return this; 41 | } 42 | 43 | public OrderItem build() { 44 | return OrderItem.create(itemId, itemName, providerName, basePrice, salesPrice); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/utils/persistence/StubCouponRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.utils.persistence; 2 | 3 | import com.flab.infrun.coupon.domain.Coupon; 4 | import com.flab.infrun.coupon.domain.CouponRepository; 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Objects; 9 | import java.util.Optional; 10 | 11 | class StubCouponRepository implements CouponRepository { 12 | 13 | private final Map persistence = new HashMap<>(); 14 | private Long sequence = 0L; 15 | 16 | @Override 17 | public Coupon save(final Coupon coupon) { 18 | persistence.put(++sequence, coupon); 19 | coupon.assignId(sequence); 20 | 21 | return coupon; 22 | } 23 | 24 | @Override 25 | public List saveAll(final List coupons) { 26 | coupons.forEach(coupon -> { 27 | persistence.put(++sequence, coupon); 28 | coupon.assignId(sequence); 29 | }); 30 | 31 | return coupons; 32 | } 33 | 34 | @Override 35 | public Coupon findByCouponCode(final String couponCode) { 36 | return null; 37 | } 38 | 39 | @Override 40 | public Optional findByCouponCodeWithLock(final String couponCode) { 41 | return persistence.values().stream() 42 | .filter(coupon -> Objects.equals(coupon.getCode(), couponCode)) 43 | .findFirst(); 44 | } 45 | 46 | @Override 47 | public List findAllByOwnerId(Long ownerId) { 48 | return null; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/com/flab/infrun/utils/persistence/StubLectureRepository.java: -------------------------------------------------------------------------------- 1 | package com.flab.infrun.utils.persistence; 2 | 3 | import com.flab.infrun.lecture.domain.Lecture; 4 | import com.flab.infrun.lecture.domain.LectureRepository; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Optional; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | 10 | final class StubLectureRepository implements LectureRepository { 11 | 12 | private final Map persistence = new ConcurrentHashMap<>(); 13 | private Long sequence = 0L; 14 | 15 | @Override 16 | public Lecture save(final Lecture entity) { 17 | persistence.put(++sequence, entity); 18 | return entity; 19 | } 20 | 21 | @Override 22 | public Optional findById(Long id) { 23 | return Optional.ofNullable(persistence.get(id)); 24 | } 25 | 26 | @Override 27 | public List findAllByIdIn(List lectureIds) { 28 | return null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/resources/db/h2/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO MEMBER 2 | VALUES (1L, 'teacher1@test.com', '테스통Teacher', 3 | '$2a$10$4DrCHP6cxV7vH/HiwbAtce2XGmZSosvqTVLMYp0AkDkMWj7X2ILXK', 'TEACHER'), 4 | (2L, 'member1@test.com', '테스통Member', 5 | '$2a$10$4DrCHP6cxV7vH/HiwbAtce2XGmZSosvqTVLMYp0AkDkMWj7X2ILXK', 'USER'); 6 | --------------------------------------------------------------------------------