diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml deleted file mode 100644 index 560e1a5a..00000000 --- a/.github/workflows/CICD.yml +++ /dev/null @@ -1,96 +0,0 @@ -name: Backend CI - -on: - push: - branches: [ dev ] - pull_request: - branches: [ dev ] - pull_request_target: - branches: [ dev ] # fork PR secrets μ‚¬μš© κ°€λŠ₯ - types: [opened, synchronize, reopened] - -jobs: - build-and-test: - runs-on: ubuntu-latest - if: | - (github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository) || - (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || - github.event_name == 'push' - - services: - mysql: - image: mysql:latest - env: - MYSQL_DATABASE: ${{ secrets.DB_NAME }} - MYSQL_ROOT_PASSWORD: root - MYSQL_USER: homeaid_user - MYSQL_PASSWORD: ${{ secrets.DB_PASSWORD }} - ports: - - 3306:3306 - options: >- - --health-cmd="mysqladmin ping -h localhost --silent" - --health-interval=10s - --health-timeout=5s - --health-retries=3 - - steps: - - name: Checkout PR code (fork PR only) - if: github.event_name == 'pull_request_target' - uses: actions/checkout@v3 - with: - ref: ${{ github.event.pull_request.head.ref }} - repository: ${{ github.event.pull_request.head.repo.full_name }} - - - name: Checkout code (for push or internal pull_request) - if: github.event_name != 'pull_request_target' - uses: actions/checkout@v3 - - - name: Set up JDK 17 (corretto) - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'corretto' - cache: gradle # πŸ‘‰ μΊμ‹œ μΆ”κ°€: λΉŒλ“œ 속도 ν–₯상 - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Build and Test - env: - DB_DRIVER: ${{ secrets.DB_DRIVER }} - DB_HOST: ${{ secrets.DB_HOST }} - DB_PORT: ${{ secrets.DB_PORT }} - DB_NAME: ${{ secrets.DB_NAME }} - DB_USERNAME: ${{ secrets.DB_USERNAME }} - DB_PASSWORD: ${{ secrets.DB_PASSWORD }} - run: ./gradlew clean build - - - name: Notify success on Discord - if: success() - run: | - curl -H "Content-Type: application/json" \ - -X POST \ - -d '{ - "embeds": [{ - "title": "βœ… CI 성곡", - "description": "**πŸ“¦ Repository:** `${{ github.repository }}`\n**🌿 Branch:** `${{ github.ref_name }}`\n**πŸ§ͺ Workflow:** `${{ github.workflow }}`\n**🎯 Event:** `${{ github.event_name }}`\n**πŸ‘€ μž‘μ„±μž:** `${{ github.actor }}`\n\n[πŸ”— GitHub Actions 둜그 ν™•μΈν•˜κΈ°](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\n\n${{ github.event_name == 'pull_request' && format('[πŸ” PR 보기](https://github.com/{0}/pull/{1})', github.repository, github.event.pull_request.number) || '' }}", - "color": 5763719 - }], - "content": "βœ… CI 톡과: `${{ github.ref_name }}` λΈŒλžœμΉ˜μž…λ‹ˆλ‹€!" - }' \ - ${{ secrets.DISCORD_WEBHOOK }} - - - name: Notify failure on Discord - if: failure() - run: | - curl -H "Content-Type: application/json" \ - -X POST \ - -d '{ - "embeds": [{ - "title": "❌ CI μ‹€νŒ¨", - "description": "**πŸ“¦ Repository:** `${{ github.repository }}`\n**🌿 Branch:** `${{ github.ref_name }}`\n**πŸ§ͺ Workflow:** `${{ github.workflow }}`\n**🎯 Event:** `${{ github.event_name }}`\n**πŸ‘€ μž‘μ„±μž:** `${{ github.actor }}`\n\n[πŸ”— GitHub Actions 둜그 ν™•μΈν•˜κΈ°](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\n\n${{ github.event_name == 'pull_request' && format('[πŸ” PR 보기](https://github.com/{0}/pull/{1})', github.repository, github.event.pull_request.number) || '' }}", - "color": 16711680 - }], - "content": "❗ CI μ‹€νŒ¨ λ°œμƒ: `${{ github.ref_name }}` 브랜치 ν™•μΈν•΄μ£Όμ„Έμš”!" - }' \ - ${{ secrets.DISCORD_WEBHOOK }} diff --git a/Jenkinsfile b/Jenkinsfile index a67d7c27..e58b4092 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -3,6 +3,12 @@ pipeline { environment { DISCORD_WEBHOOK = credentials('discord-webhook') + DB_DRIVER = 'mysql' + DB_HOST = 'mysql-ci' + DB_PORT = '3306' + DB_NAME = 'homeaid_db' + DB_USERNAME = 'homeaid_user' + DB_PASSWORD = 'root' } tools { @@ -16,34 +22,13 @@ pipeline { } } - stage('Build MySQL Service') { + stage('Set Variables') { steps { - script { - // Docker둜 MySQL μ»¨ν…Œμ΄λ„ˆ λ„μš°κΈ° (jenkins μ„œλ²„μ— Dockerκ°€ μ„€μΉ˜λ˜μ–΄ μžˆμ–΄μ•Ό 함) - sh ''' - docker run -d \ - --name mysql-ci \ - -e MYSQL_DATABASE=${DB_NAME} \ - -e MYSQL_ROOT_PASSWORD=root \ - -e MYSQL_USER=homeaid_user \ - -e MYSQL_PASSWORD=${DB_PASSWORD} \ - -p 3306:3306 \ - --health-cmd="mysqladmin ping -h localhost --silent" \ - --health-interval=10s \ - --health-timeout=5s \ - --health-retries=3 \ - mysql:latest - - # DB μ€€λΉ„ λŒ€κΈ° (μ΅œλŒ€ 60초) - for i in {1..12}; do - if docker exec mysql-ci mysqladmin ping -h localhost --silent; then - echo "MySQL is ready!" - break - fi - echo "Waiting for MySQL..." - sleep 5 - done - ''' + wrap([$class: 'BuildUser']) { + script { + env.BUILD_USER = "${env.BUILD_USER}" + echo "Triggered by: ${env.BUILD_USER}" + } } } } @@ -67,47 +52,44 @@ pipeline { post { success { - script { - def message = """{ - "embeds": [{ - "title": "βœ… CI 성곡", - "description": "**πŸ“¦ Repository:** `${env.JOB_NAME}`\\n**🌿 Branch:** `${env.BRANCH_NAME}`\\n**πŸ‘€ Triggered by:** `${env.BUILD_USER}`\\n[πŸ”— Jenkins 둜그 ν™•μΈν•˜κΈ°](${env.BUILD_URL})", - "color": 5763719 - }], - "content": "βœ… CI 톡과: `${env.BRANCH_NAME}` λΈŒλžœμΉ˜μž…λ‹ˆλ‹€!" - }""" - sh """ - curl -H "Content-Type: application/json" \ - -X POST \ - -d '${message}' \ - ${DISCORD_WEBHOOK} - """ + wrap([$class: 'BuildUser']) { + script { + def message = """{ + "embeds": [{ + "title": "βœ… CI 성곡", + "description": "**πŸ“¦ Repository:** `${env.JOB_NAME}`\\n**🌿 Branch:** `${env.BRANCH_NAME}`\\n**πŸ‘€ Triggered by:** `${env.BUILD_USER}`\\n[πŸ”— Jenkins 둜그 ν™•μΈν•˜κΈ°](${env.BUILD_URL})", + "color": 5763719 + }], + "content": "βœ… CI 톡과: `${env.BRANCH_NAME}` λΈŒλžœμΉ˜μž…λ‹ˆλ‹€!" + }""" + sh """ + curl -H "Content-Type: application/json" \ + -X POST \ + -d '${message}' \ + ${DISCORD_WEBHOOK} + """ + } } } failure { - script { - def message = """{ - "embeds": [{ - "title": "❌ CI μ‹€νŒ¨", - "description": "**πŸ“¦ Repository:** `${env.JOB_NAME}`\\n**🌿 Branch:** `${env.BRANCH_NAME}`\\n**πŸ‘€ Triggered by:** `${env.BUILD_USER}`\\n[πŸ”— Jenkins 둜그 ν™•μΈν•˜κΈ°](${env.BUILD_URL})", - "color": 16711680 - }], - "content": "❗ CI μ‹€νŒ¨ λ°œμƒ: `${env.BRANCH_NAME}` 브랜치 ν™•μΈν•΄μ£Όμ„Έμš”!" - }""" - sh """ - curl -H "Content-Type: application/json" \ - -X POST \ - -d '${message}' \ - ${DISCORD_WEBHOOK} - """ + wrap([$class: 'BuildUser']) { + script { + def message = """{ + "embeds": [{ + "title": "❌ CI μ‹€νŒ¨", + "description": "**πŸ“¦ Repository:** `${env.JOB_NAME}`\\n**🌿 Branch:** `${env.BRANCH_NAME}`\\n**πŸ‘€ Triggered by:** `${env.BUILD_USER}`\\n[πŸ”— Jenkins 둜그 ν™•μΈν•˜κΈ°](${env.BUILD_URL})", + "color": 16711680 + }], + "content": "❗ CI μ‹€νŒ¨ λ°œμƒ: `${env.BRANCH_NAME}` 브랜치 ν™•μΈν•΄μ£Όμ„Έμš”!" + }""" + sh """ + curl -H "Content-Type: application/json" \ + -X POST \ + -d '${message}' \ + ${DISCORD_WEBHOOK} + """ + } } } - always { - // Clean up MySQL container - sh ''' - docker stop mysql-ci || true - docker rm mysql-ci || true - ''' - } } } diff --git a/board/src/main/java/com/homeaid/controller/UserBoardController.java b/board/src/main/java/com/homeaid/controller/UserBoardController.java index cb2af47e..38f21ab5 100644 --- a/board/src/main/java/com/homeaid/controller/UserBoardController.java +++ b/board/src/main/java/com/homeaid/controller/UserBoardController.java @@ -39,7 +39,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/api/v1/board") +@RequestMapping("/api/v1/boards") @Tag(name = "1:1 λ¬Έμ˜κΈ€", description = "μ‚¬μš©μžμ™€ κ΄€λ¦¬μž κ°„ 1:1 λ¬Έμ˜κΈ€ API") @SecurityRequirement(name = "Bearer Authentication") public class UserBoardController { @@ -155,7 +155,7 @@ public ResponseEntity> getBoard( content = @Content(schema = @Schema(implementation = CommonApiResponse.class))) }) public ResponseEntity>> searchBoard( - @Parameter(description = "검색 ν‚€μ›Œλ“œ", example = "문의") + @Parameter(description = "검색 ν‚€μ›Œλ“œ") @RequestParam(name = "keyword", required = false) String keyword, @Parameter(description = "νŽ˜μ΄μ§€ 번호 (0λΆ€ν„° μ‹œμž‘)", example = "0") @RequestParam(name = "page", defaultValue = "0") int page, diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..db8873c0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,38 @@ +version: '3.8' + +services: + jenkins: + image: jenkins/jenkins:lts + container_name: jenkins + ports: + - "8080:8080" + - "50000:50000" + volumes: + - jenkins_home:/var/jenkins_home + networks: + - ci-network + + mysql-ci: + image: mysql:latest + container_name: mysql-ci + ports: + - "3306:3306" + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: homeaid_db + MYSQL_USER: homeaid_user + MYSQL_PASSWORD: root + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 30s + timeout: 10s + retries: 5 + networks: + - ci-network + +volumes: + jenkins_home: + +networks: + ci-network: + driver: bridge diff --git a/global/src/main/java/com/homeaid/config/SwaggerConfig.java b/global/src/main/java/com/homeaid/config/SwaggerConfig.java index fb4fe0d0..0b0d8f92 100644 --- a/global/src/main/java/com/homeaid/config/SwaggerConfig.java +++ b/global/src/main/java/com/homeaid/config/SwaggerConfig.java @@ -1,7 +1,9 @@ package com.homeaid.config; +import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; import org.springdoc.core.models.GroupedOpenApi; import org.springframework.context.annotation.Bean; @@ -12,17 +14,16 @@ public class SwaggerConfig { @Bean public OpenAPI springOpenAPI() { - return new OpenAPI() .info(new Info() .title("HomeAid API λͺ…μ„Έμ„œ") .version("v1") - .description("HomeAidλŠ” λ§€μΉ­μ‹œμŠ€ν…œμ„ ν™œμš©ν•˜μ—¬ κ³ κ°μ—κ²Œ λ§žμΆ€ν˜• λ§€λ‹ˆμ €λ₯Ό λ§€μΉ­μ‹œμΌœμ£Όκ³  가사 및 μ²­μ†Œ μ„œλΉ„μŠ€λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.")); - /*// μ „μ—­ λ³΄μ•ˆ μš”κ΅¬μ‚¬ν•­ μΆ”κ°€ + .description("HomeAidλŠ” λ§€μΉ­μ‹œμŠ€ν…œμ„ ν™œμš©ν•˜μ—¬ κ³ κ°μ—κ²Œ λ§žμΆ€ν˜• λ§€λ‹ˆμ €λ₯Ό λ§€μΉ­μ‹œμΌœμ£Όκ³  가사 및 μ²­μ†Œ μ„œλΉ„μŠ€λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.")) + // μ „μ—­ λ³΄μ•ˆ μš”κ΅¬μ‚¬ν•­ μΆ”κ°€ .addSecurityItem(new SecurityRequirement().addList("Bearer Authentication")) // λ³΄μ•ˆ μŠ€ν‚€λ§ˆ μ •μ˜ .components(new Components() - .addSecuritySchemes("Bearer Authentication", createBearerTokenScheme()));*/ + .addSecuritySchemes("Bearer Authentication", createBearerTokenScheme())); } /** @@ -42,6 +43,7 @@ private SecurityScheme createBearerTokenScheme() { public GroupedOpenApi reservationAPI() { return GroupedOpenApi.builder() .group("reservations") + .displayName("μ˜ˆμ•½") .pathsToMatch("/api/v1/customer/reservations/**") .build(); } @@ -49,7 +51,8 @@ public GroupedOpenApi reservationAPI() { @Bean public GroupedOpenApi serviceOptionAPI() { return GroupedOpenApi.builder() - .group("serviceOption") + .group("serviceOptions") + .displayName("μ„œλΉ„μŠ€ μ˜΅μ…˜") .pathsToMatch("/api/v1/admin/service-option/**") .build(); } @@ -59,6 +62,7 @@ public GroupedOpenApi serviceOptionAPI() { public GroupedOpenApi matchingAPI() { return GroupedOpenApi.builder() .group("matchings") + .displayName("λ§€μΉ­") .pathsToMatch("/api/v1/admin/matchings/**") .build(); } @@ -69,7 +73,8 @@ public GroupedOpenApi userAPI() { return GroupedOpenApi.builder() .group("Users") - .pathsToMatch("/api/v1/user/**") + .displayName("둜그인/νšŒμ›κ°€μž…") + .pathsToMatch("/api/v1/swagger/auth/**") .build(); } @@ -77,18 +82,18 @@ public GroupedOpenApi userAPI() { public GroupedOpenApi worklogAPI() { return GroupedOpenApi.builder() - .group("worklog") + .group("workLogs") + .displayName("μž‘μ—…κΈ°λ‘") .pathsToMatch("/api/v1/manager/work-logs/**") .build(); } @Bean - public GroupedOpenApi boardAPI() { return GroupedOpenApi.builder() .group("Boards") - .displayName("πŸ’¬ 1:1 λ¬Έμ˜κΈ€") - .pathsToMatch("/api/v1/board/**") + .displayName("λ¬Έμ˜κΈ€") + .pathsToMatch("/api/v1/boards/**") .build(); } @@ -98,7 +103,7 @@ public GroupedOpenApi reviewAPI() { return GroupedOpenApi.builder() .group("reviews") .displayName("리뷰") - .pathsToMatch("/api/v1/review/**") + .pathsToMatch("/api/v1/reviews/**") .build(); } } diff --git a/reservation/src/main/java/com/homeaid/domain/Reservation.java b/reservation/src/main/java/com/homeaid/domain/Reservation.java index 521de097..4f6392b9 100644 --- a/reservation/src/main/java/com/homeaid/domain/Reservation.java +++ b/reservation/src/main/java/com/homeaid/domain/Reservation.java @@ -48,6 +48,8 @@ public class Reservation { private Long customerId; + private Long managerId; + private Long finalMatchingId; private Double latitude; @@ -93,7 +95,6 @@ public void softDelete() { } - public void updateStatusCompleted() { this.status = ReservationStatus.COMPLETED; } diff --git a/review/src/main/java/com/homeaid/controller/ReviewController.java b/review/src/main/java/com/homeaid/controller/ReviewController.java index 24205eb5..48669ef8 100644 --- a/review/src/main/java/com/homeaid/controller/ReviewController.java +++ b/review/src/main/java/com/homeaid/controller/ReviewController.java @@ -7,6 +7,7 @@ import com.homeaid.security.CustomUserDetails; import com.homeaid.service.ReviewService; import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -14,7 +15,7 @@ @RequiredArgsConstructor @RestController -@RequestMapping("/api/v1/review") +@RequestMapping("/api/v1/reviews") public class ReviewController { private final ReviewService reviewService; @@ -23,7 +24,7 @@ public class ReviewController { @PostMapping public ResponseEntity> createReview( @AuthenticationPrincipal CustomUserDetails user, - @RequestBody CustomerReviewRequestDto customerReviewRequestDto) { + @RequestBody @Valid CustomerReviewRequestDto customerReviewRequestDto) { Review requestReview = CustomerReviewRequestDto.toEntity(customerReviewRequestDto, user.getUserRole(), user.getUserId()); diff --git a/review/src/main/java/com/homeaid/domain/Review.java b/review/src/main/java/com/homeaid/domain/Review.java index d62e8b56..d76e31bc 100644 --- a/review/src/main/java/com/homeaid/domain/Review.java +++ b/review/src/main/java/com/homeaid/domain/Review.java @@ -23,7 +23,7 @@ public class Review { private Long targetId; - private int rating; //별점 + private Integer rating; //별점 private String comment; diff --git a/review/src/main/java/com/homeaid/dto/request/CustomerReviewRequestDto.java b/review/src/main/java/com/homeaid/dto/request/CustomerReviewRequestDto.java index 8e63bcbc..51401a6b 100644 --- a/review/src/main/java/com/homeaid/dto/request/CustomerReviewRequestDto.java +++ b/review/src/main/java/com/homeaid/dto/request/CustomerReviewRequestDto.java @@ -3,21 +3,32 @@ import com.homeaid.domain.Review; import com.homeaid.domain.enumerate.UserRole; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import lombok.Getter; @Getter public class CustomerReviewRequestDto { + @NotNull(message = "리뷰 λŒ€μƒμžλŠ” ν•„μˆ˜μž…λ‹ˆλ‹€") private Long targetId; - private Integer rating; + @NotNull(message = "평점은 ν•„μˆ˜μž…λ‹ˆλ‹€") + @Min(value = 1, message = "평점은 1점 이상이어야 ν•©λ‹ˆλ‹€") + @Max(value = 5, message = "평점은 5점 μ΄ν•˜μ—¬μ•Ό ν•©λ‹ˆλ‹€") + private int rating; + @NotBlank(message = "리뷰 λ‚΄μš©μ€ ν•„μˆ˜μž…λ‹ˆλ‹€") + @Size(min = 10, message = "λ¦¬λ·°λŠ” 10자 이상 μž‘μ„±ν•΄μ£Όμ„Έμš”") private String comment; + @NotNull(message = "μ˜ˆμ•½ IDλŠ” ν•„μˆ˜μž…λ‹ˆλ‹€") private Long reservationId; public static Review toEntity(CustomerReviewRequestDto customerReviewRequestDto, UserRole userRole, Long userId) { - return Review.builder() .writerId(userId) .targetId(customerReviewRequestDto.targetId) diff --git a/review/src/main/java/com/homeaid/repository/ReviewRepository.java b/review/src/main/java/com/homeaid/repository/ReviewRepository.java index 1a999afc..ef9b056f 100644 --- a/review/src/main/java/com/homeaid/repository/ReviewRepository.java +++ b/review/src/main/java/com/homeaid/repository/ReviewRepository.java @@ -3,8 +3,14 @@ import com.homeaid.domain.Review; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository public interface ReviewRepository extends JpaRepository { + + @Query("SELECT COUNT(r), AVG(r.rating) FROM Review r WHERE r.targetId = :targetId") + Object[] getReviewStatisticsByTargetId(@Param("targetId") Long targetId); + } diff --git a/review/src/main/java/com/homeaid/service/ReviewServiceImpl.java b/review/src/main/java/com/homeaid/service/ReviewServiceImpl.java index 9cf3b58e..61582d80 100644 --- a/review/src/main/java/com/homeaid/service/ReviewServiceImpl.java +++ b/review/src/main/java/com/homeaid/service/ReviewServiceImpl.java @@ -17,81 +17,95 @@ @RequiredArgsConstructor @Service public class ReviewServiceImpl implements ReviewService { - private final ReviewRepository reviewRepository; - private final ReservationRepository reservationRepository; - private final MatchingRepository matchingRepository; - @Transactional - @Override - public Review createReviewByCustomer(Review requestReview) { - Reservation validatedReservation = validateReview(requestReview); + private final ReviewRepository reviewRepository; + private final ReservationRepository reservationRepository; + private final MatchingRepository matchingRepository; + private final UserRatingUpdateService userRatingUpdateService; - //μ˜ˆμ•½κ±΄μ˜ 고객아이디와 μš”μ²­ 고객의 아이디 검증 - if (!validatedReservation.getCustomerId().equals(requestReview.getWriterId())) { - throw new CustomException(ReviewErrorCode.UNAUTHORIZED_REVIEW_ACCESS); - } + @Transactional + @Override + public Review createReviewByCustomer(Review requestReview) { + Reservation validatedReservation = validateReview(requestReview); - //타켓 λ§€λ‹ˆμ € 와 μ˜ˆμ•½ 건의 λ§€λ‹ˆμ € 검증 - Long reservationManagerId = getFinalMatchingOfManagerId(validatedReservation.getFinalMatchingId()); - if (!reservationManagerId.equals(requestReview.getTargetId())) { - throw new CustomException(ReviewErrorCode.UNAUTHORIZED_REVIEW_TARGET); - } - - //Todo λ§€λ‹ˆμ € 찜 κΈ°λŠ₯ + //μ˜ˆμ•½κ±΄μ˜ 고객아이디와 μš”μ²­ 고객의 아이디 검증 + if (!validatedReservation.getCustomerId().equals(requestReview.getWriterId())) { + throw new CustomException(ReviewErrorCode.UNAUTHORIZED_REVIEW_ACCESS); + } - return reviewRepository.save(requestReview); + //타켓 λ§€λ‹ˆμ € 와 μ˜ˆμ•½ 건의 λ§€λ‹ˆμ € 검증 + Long reservationManagerId = getFinalMatchingOfManagerId( + validatedReservation.getFinalMatchingId()); + if (!reservationManagerId.equals(requestReview.getTargetId())) { + throw new CustomException(ReviewErrorCode.UNAUTHORIZED_REVIEW_TARGET); } - @Transactional - @Override - public Review createReviewByManager(Review requestReview) { - Reservation validatedReservation = validateReview(requestReview); + Review savedReview = reviewRepository.save(requestReview); + userRatingUpdateService.updateRating(requestReview.getTargetId(), + requestReview.getWriterRole()); + + //Todo λ§€λ‹ˆμ € 찜 κΈ°λŠ₯ + + return savedReview; + } - //μ˜ˆμ•½ 건의 λ§€λ‹ˆμ €μ™€ 와 μš”μ²­μžμ˜ λ§€λ‹ˆμ € 아이디 검증 - Long reservationManagerId = getFinalMatchingOfManagerId(validatedReservation.getFinalMatchingId()); - if (!reservationManagerId.equals(requestReview.getWriterId())) { - throw new CustomException(ReviewErrorCode.UNAUTHORIZED_REVIEW_ACCESS); - } + @Transactional + @Override + public Review createReviewByManager(Review requestReview) { + Reservation validatedReservation = validateReview(requestReview); - //μ˜ˆμ•½ 건의 고객아이디와 μš”μ²­ 받은 고객아이디 검증 - if (!validatedReservation.getCustomerId().equals(requestReview.getTargetId())) { - throw new CustomException(ReviewErrorCode.UNAUTHORIZED_REVIEW_TARGET); - } + //μ˜ˆμ•½ 건의 λ§€λ‹ˆμ €μ™€ 와 μš”μ²­μžμ˜ λ§€λ‹ˆμ € 아이디 검증 + Long reservationManagerId = getFinalMatchingOfManagerId( + validatedReservation.getFinalMatchingId()); + if (!reservationManagerId.equals(requestReview.getWriterId())) { + throw new CustomException(ReviewErrorCode.UNAUTHORIZED_REVIEW_ACCESS); + } - return reviewRepository.save(requestReview); + //μ˜ˆμ•½ 건의 고객아이디와 μš”μ²­ 받은 고객아이디 검증 + if (!validatedReservation.getCustomerId().equals(requestReview.getTargetId())) { + throw new CustomException(ReviewErrorCode.UNAUTHORIZED_REVIEW_TARGET); } - public void deleteReview(Long reviewId, Long userId) { - Review review = reviewRepository.findById(reviewId).orElseThrow(() -> - new CustomException(ReviewErrorCode.REVIEW_NOT_FOUND)); + Review savedReview = reviewRepository.save(requestReview); + userRatingUpdateService.updateRating(requestReview.getTargetId(), + requestReview.getWriterRole()); + + return savedReview; + } - if (!review.getWriterId().equals(userId)) { - throw new CustomException(ReviewErrorCode.UNAUTHORIZED_REVIEW_ACCESS); - } + public void deleteReview(Long reviewId, Long userId) { + Review review = reviewRepository.findById(reviewId).orElseThrow(() -> + new CustomException(ReviewErrorCode.REVIEW_NOT_FOUND)); - reservationRepository.deleteById(reviewId); + if (!review.getWriterId().equals(userId)) { + throw new CustomException(ReviewErrorCode.UNAUTHORIZED_REVIEW_ACCESS); } + reviewRepository.deleteById(reviewId); + userRatingUpdateService.updateRating(review.getTargetId(), review.getWriterRole()); + } - /** - * μš”μ²­ 받은 μ˜ˆμ•½μ΄ μœ μš”ν•œ μ˜ˆμ•½μ΄κ³  μ™„λ£Œμƒνƒœ 검증 - * - * @param requestReview μš”μ²­ν•œμ˜ˆμ•½ 아이디 - * @return 쑰회된 μ˜ˆμ•½ - */ - private Reservation validateReview(Review requestReview) { - Reservation reservation = reservationRepository.findById(requestReview.getReservationId()).orElseThrow(() -> - new CustomException(ReservationErrorCode.RESERVATION_NOT_FOUND)); - - if (reservation.getStatus() != ReservationStatus.COMPLETED) { - throw new CustomException(ReviewErrorCode.REVIEW_NOT_ALLOWED); - } - return reservation; - } - private Long getFinalMatchingOfManagerId(Long finalMatchingId) { - return matchingRepository.findById(finalMatchingId).orElseThrow( - () -> new CustomException(MatchingErrorCode.MATCHING_NOT_FOUND) - ).getManager().getId(); + /** + * μš”μ²­ 받은 μ˜ˆμ•½μ΄ μœ μš”ν•œ μ˜ˆμ•½μ΄κ³  μ™„λ£Œμƒνƒœ 검증 + * + * @param requestReview μš”μ²­ν•œμ˜ˆμ•½ 아이디 + * @return 쑰회된 μ˜ˆμ•½ + */ + private Reservation validateReview(Review requestReview) { + Reservation reservation = reservationRepository.findById(requestReview.getReservationId()) + .orElseThrow(() -> + new CustomException(ReservationErrorCode.RESERVATION_NOT_FOUND)); + + if (reservation.getStatus() != ReservationStatus.COMPLETED) { + throw new CustomException(ReviewErrorCode.REVIEW_NOT_ALLOWED); } + return reservation; + } + + private Long getFinalMatchingOfManagerId(Long finalMatchingId) { + return matchingRepository.findById(finalMatchingId).orElseThrow( + () -> new CustomException(MatchingErrorCode.MATCHING_NOT_FOUND) + ).getManager().getId(); + } } diff --git a/review/src/main/java/com/homeaid/service/UserRatingUpdateService.java b/review/src/main/java/com/homeaid/service/UserRatingUpdateService.java new file mode 100644 index 00000000..996f882b --- /dev/null +++ b/review/src/main/java/com/homeaid/service/UserRatingUpdateService.java @@ -0,0 +1,9 @@ +package com.homeaid.service; + +import com.homeaid.domain.enumerate.UserRole; + +public interface UserRatingUpdateService { + + void updateRating(Long targetId, UserRole writerRole); + +} diff --git a/review/src/main/java/com/homeaid/service/UserRatingUpdateServiceImpl.java b/review/src/main/java/com/homeaid/service/UserRatingUpdateServiceImpl.java new file mode 100644 index 00000000..ccdcf48d --- /dev/null +++ b/review/src/main/java/com/homeaid/service/UserRatingUpdateServiceImpl.java @@ -0,0 +1,58 @@ +package com.homeaid.service; + +import com.homeaid.domain.Customer; +import com.homeaid.domain.CustomerRating; +import com.homeaid.domain.Manager; +import com.homeaid.domain.ManagerRating; +import com.homeaid.domain.enumerate.UserRole; +import com.homeaid.repository.CustomerRatingRepository; +import com.homeaid.repository.CustomerRepository; +import com.homeaid.repository.ManagerRatingRepository; +import com.homeaid.repository.ManagerRepository; +import com.homeaid.repository.ReviewRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class UserRatingUpdateServiceImpl implements UserRatingUpdateService { + + private final ReviewRepository reviewRepository; + private final CustomerRepository customerRepository; + private final ManagerRepository managerRepository; + private final CustomerRatingRepository customerRatingRepository; + private final ManagerRatingRepository managerRatingRepository; + + @Transactional + @Override + public void updateRating(Long targetId, UserRole writerRole) { + Object[] rating = reviewRepository.getReviewStatisticsByTargetId(targetId); + + int count = ((Number) rating[0]).intValue(); + double average = ((Number) rating[1]).doubleValue(); + + // μž‘μ„±μž: CUSTOMER -> MANAGER update / μž‘μ„±μž: MANAGER -> CUSTOMER update + if (writerRole == UserRole.CUSTOMER) { + updateManagerRating(targetId, count, average); + } else if (writerRole == UserRole.MANAGER) { + updateCustomerRating(targetId, count, average); + } + } + + private void updateManagerRating(Long managerId, int count, double average) { + Manager manager = managerRepository.getReferenceById(managerId); + ManagerRating rating = managerRatingRepository.findById(managerId) + .orElseGet(() -> new ManagerRating(manager)); + rating.updateRating(count, average); + managerRatingRepository.save(rating); + } + + private void updateCustomerRating(Long customerId, int count, double average) { + Customer customer = customerRepository.getReferenceById(customerId); + CustomerRating rating = customerRatingRepository.findById(customerId) + .orElseGet(() -> new CustomerRating(customer)); + rating.updateRating(count, average); + customerRatingRepository.save(rating); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index cfecd4fb..e47e5999 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -22,6 +22,9 @@ spring: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect + jwt: + secret: ${JWT_SECRET} + env: secure-cookie: false diff --git a/user/src/main/java/com/homeaid/controller/SwaggerAuthController.java b/user/src/main/java/com/homeaid/controller/SwaggerAuthController.java new file mode 100644 index 00000000..aa1960c2 --- /dev/null +++ b/user/src/main/java/com/homeaid/controller/SwaggerAuthController.java @@ -0,0 +1,97 @@ +package com.homeaid.controller; + +import com.homeaid.common.response.CommonApiResponse; +import com.homeaid.domain.Customer; +import com.homeaid.domain.Manager; +import com.homeaid.dto.request.CustomerSignUpRequestDto; +import com.homeaid.dto.request.ManagerSignUpRequestDto; +import com.homeaid.dto.request.SignInRequestDto; +import com.homeaid.dto.response.SignUpResponseDto; +import com.homeaid.dto.response.SignInResponseDto; +import com.homeaid.service.UserServiceImpl; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/swagger/auth") +@RequiredArgsConstructor +@Tag(name = "SignUp/SignIn", description = "μ‚¬μš©μž νšŒμ›κ°€μž… API (λ§€λ‹ˆμ €/고객)") +//@Profile("swagger") // swagger ν”„λ‘œνŒŒμΌμ—μ„œλ§Œ ν™œμ„±ν™” κ°€λŠ₯ (선택) +public class SwaggerAuthController { + + private final UserServiceImpl userServiceImpl; + private final BCryptPasswordEncoder passwordEncoder; + + @PostMapping("/signup/manager") + @Operation(summary = "λ§€λ‹ˆμ € νšŒμ›κ°€μž…", description = "λ§€λ‹ˆμ € μ‚¬μš©μž 정보λ₯Ό μž…λ ₯λ°›μ•„ νšŒμ›κ°€μž…μ„ μ²˜λ¦¬ν•©λ‹ˆλ‹€.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "νšŒμ›κ°€μž… 성곡" + , content = @Content(schema = @Schema(implementation = SignUpResponseDto.class))), + @ApiResponse(responseCode = "400", description = "잘λͺ»λœ μž…λ ₯κ°’" + , content = @Content(schema = @Schema(implementation = CommonApiResponse.class))), + @ApiResponse(responseCode = "409", description = "이미 μ‘΄μž¬ν•˜λŠ” 이메일" + , content = @Content(schema = @Schema(implementation = CommonApiResponse.class))) + }) + public ResponseEntity> signUpManager( + @RequestBody @Valid ManagerSignUpRequestDto managerSignUpRequestDto + ) { + + String password = passwordEncoder.encode(managerSignUpRequestDto.getPassword()); + Manager manager = userServiceImpl.signUpManager( + ManagerSignUpRequestDto.toEntity(managerSignUpRequestDto, password) // λΉ„λ°€λ²ˆν˜Έ μΆ”κ°€ μˆ˜μ • ν•„μš” + ); + return ResponseEntity.status(HttpStatus.CREATED) + .body(CommonApiResponse.success(SignUpResponseDto.toManagerDto(manager))); + } + + @Operation(summary = "고객 νšŒμ›κ°€μž…", description = "고객 μ‚¬μš©μž 정보λ₯Ό μž…λ ₯λ°›μ•„ νšŒμ›κ°€μž…μ„ μ²˜λ¦¬ν•©λ‹ˆλ‹€.") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", description = "νšŒμ›κ°€μž… 성곡" + , content = @Content(schema = @Schema(implementation = SignUpResponseDto.class))), + @ApiResponse(responseCode = "400", description = "잘λͺ»λœ μž…λ ₯κ°’" + , content = @Content(schema = @Schema(implementation = CommonApiResponse.class))), + @ApiResponse(responseCode = "409", description = "이미 μ‘΄μž¬ν•˜λŠ” 이메일" + , content = @Content(schema = @Schema(implementation = CommonApiResponse.class))) + }) + @PostMapping("/signup/customer") + public ResponseEntity> signUpCustomer( + @RequestBody @Valid CustomerSignUpRequestDto customerSignUpRequestDto + ) { + + String password = passwordEncoder.encode(customerSignUpRequestDto.getPassword()); + + Customer customer = userServiceImpl.signUpCustomer( + CustomerSignUpRequestDto.toEntity(customerSignUpRequestDto, password) // λΉ„λ°€λ²ˆν˜Έ μΆ”κ°€ μˆ˜μ • ν•„μš” + ); + return ResponseEntity.status(HttpStatus.CREATED) + .body(CommonApiResponse.success(SignUpResponseDto.toCustomerDto(customer))); + } + + @Operation(summary = "둜그인", description = "이메일과 λΉ„λ°€λ²ˆν˜Έλ₯Ό λ°›μ•„ λ‘œκ·ΈμΈμ„ μ²˜λ¦¬ν•©λ‹ˆλ‹€.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "둜그인 성곡" + , content = @Content(schema = @Schema(implementation = SignUpResponseDto.class))), + @ApiResponse(responseCode = "400", description = "잘λͺ»λœ μž…λ ₯κ°’" + , content = @Content(schema = @Schema(implementation = CommonApiResponse.class))) + }) + @PostMapping("/signin") + public ResponseEntity swaggerSignIn( + @RequestBody SignInRequestDto swaggerRequest) { + String token = userServiceImpl.loginAndGetToken(swaggerRequest); + return ResponseEntity.ok(new SignInResponseDto(token)); + } + +} diff --git a/user/src/main/java/com/homeaid/controller/UserController.java b/user/src/main/java/com/homeaid/controller/UserController.java index 664cf563..bbe94491 100644 --- a/user/src/main/java/com/homeaid/controller/UserController.java +++ b/user/src/main/java/com/homeaid/controller/UserController.java @@ -5,7 +5,7 @@ import com.homeaid.domain.Manager; import com.homeaid.dto.request.CustomerSignUpRequestDto; import com.homeaid.dto.request.ManagerSignUpRequestDto; -import com.homeaid.dto.response.UserSignUpResponseDto; +import com.homeaid.dto.response.SignUpResponseDto; import com.homeaid.service.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -36,13 +36,13 @@ public class UserController { @Operation(summary = "λ§€λ‹ˆμ € νšŒμ›κ°€μž…", description = "λ§€λ‹ˆμ € μ‚¬μš©μž 정보λ₯Ό μž…λ ₯λ°›μ•„ νšŒμ›κ°€μž…μ„ μ²˜λ¦¬ν•©λ‹ˆλ‹€.") @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "νšŒμ›κ°€μž… 성곡" - , content = @Content(schema = @Schema(implementation = UserSignUpResponseDto.class))), + , content = @Content(schema = @Schema(implementation = SignUpResponseDto.class))), @ApiResponse(responseCode = "400", description = "잘λͺ»λœ μž…λ ₯κ°’" , content = @Content(schema = @Schema(implementation = CommonApiResponse.class))), @ApiResponse(responseCode = "409", description = "이미 μ‘΄μž¬ν•˜λŠ” 이메일" , content = @Content(schema = @Schema(implementation = CommonApiResponse.class))) }) - public ResponseEntity> signUpManager( + public ResponseEntity> signUpManager( @RequestBody @Valid ManagerSignUpRequestDto managerSignUpRequestDto ) { @@ -51,20 +51,20 @@ public ResponseEntity> signUpManager( ManagerSignUpRequestDto.toEntity(managerSignUpRequestDto, password) // λΉ„λ°€λ²ˆν˜Έ μΆ”κ°€ μˆ˜μ • ν•„μš” ); return ResponseEntity.status(HttpStatus.CREATED) - .body(CommonApiResponse.success(UserSignUpResponseDto.toManagerDto(manager))); + .body(CommonApiResponse.success(SignUpResponseDto.toManagerDto(manager))); } @Operation(summary = "고객 νšŒμ›κ°€μž…", description = "고객 μ‚¬μš©μž 정보λ₯Ό μž…λ ₯λ°›μ•„ νšŒμ›κ°€μž…μ„ μ²˜λ¦¬ν•©λ‹ˆλ‹€.") @ApiResponses(value = { @ApiResponse(responseCode = "201", description = "νšŒμ›κ°€μž… 성곡" - , content = @Content(schema = @Schema(implementation = UserSignUpResponseDto.class))), + , content = @Content(schema = @Schema(implementation = SignUpResponseDto.class))), @ApiResponse(responseCode = "400", description = "잘λͺ»λœ μž…λ ₯κ°’" , content = @Content(schema = @Schema(implementation = CommonApiResponse.class))), @ApiResponse(responseCode = "409", description = "이미 μ‘΄μž¬ν•˜λŠ” 이메일" , content = @Content(schema = @Schema(implementation = CommonApiResponse.class))) }) @PostMapping("/auth/signup/customer") - public ResponseEntity> signUpCustomer( + public ResponseEntity> signUpCustomer( @RequestBody @Valid CustomerSignUpRequestDto customerSignUpRequestDto ) { @@ -74,7 +74,7 @@ public ResponseEntity> signUpCustomer( CustomerSignUpRequestDto.toEntity(customerSignUpRequestDto, password) // λΉ„λ°€λ²ˆν˜Έ μΆ”κ°€ μˆ˜μ • ν•„μš” ); return ResponseEntity.status(HttpStatus.CREATED) - .body(CommonApiResponse.success(UserSignUpResponseDto.toCustomerDto(customer))); + .body(CommonApiResponse.success(SignUpResponseDto.toCustomerDto(customer))); } } diff --git a/user/src/main/java/com/homeaid/domain/CustomerRating.java b/user/src/main/java/com/homeaid/domain/CustomerRating.java new file mode 100644 index 00000000..a45657b5 --- /dev/null +++ b/user/src/main/java/com/homeaid/domain/CustomerRating.java @@ -0,0 +1,53 @@ +package com.homeaid.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.LastModifiedDate; + +@Entity +@Getter +@Table(name = "customer_rating") +@NoArgsConstructor +public class CustomerRating { + + @Id + @Column(name = "customer_id") + private Long customerId; + + @Column(name = "review_count", nullable = false) + private Integer reviewCount; + + @Column(name = "average_rating", nullable = false) + private Double averageRating; + + @Column(name = "last_updated", nullable = false) + @LastModifiedDate + private LocalDateTime lastUpdated; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "customer_id") + @MapsId // customer_id λ₯Ό PK둜 μ‚¬μš© + private Customer customer; + + public CustomerRating(Customer customer){ + this.customerId = customer.getId(); + this.reviewCount = 0; + this.averageRating = 0.0; + this.customer = customer; + } + + public void updateRating(int reviewCount, double averageRating){ + this.averageRating = averageRating; + this.reviewCount = reviewCount; + } + +} diff --git a/user/src/main/java/com/homeaid/domain/ManagerRating.java b/user/src/main/java/com/homeaid/domain/ManagerRating.java new file mode 100644 index 00000000..2d52c288 --- /dev/null +++ b/user/src/main/java/com/homeaid/domain/ManagerRating.java @@ -0,0 +1,53 @@ +package com.homeaid.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.LastModifiedDate; + +@Entity +@Getter +@Table(name = "manager_rating") +@NoArgsConstructor +public class ManagerRating { + + @Id + @Column(name = "manager_id") + private Long managerId; + + @Column(name = "review_count", nullable = false) + private Integer reviewCount; + + @Column(name = "average_rating", nullable = false) + private Double averageRating; + + @Column(name = "last_updated", nullable = false) + @LastModifiedDate + private LocalDateTime lastUpdated; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "manager_id") + @MapsId // manager_id λ₯Ό PK둜 μ‚¬μš© + private Manager manager; + + public ManagerRating(Manager manager){ + this.managerId = manager.getId(); + this.reviewCount = 0 ; + this.averageRating = 0.0; + this.manager = manager; + } + + public void updateRating(Integer reviewCount, double averageRating) { + this.averageRating = averageRating; + this.reviewCount = reviewCount; + } + +} diff --git a/user/src/main/java/com/homeaid/dto/request/UserSignInRequestDto.java b/user/src/main/java/com/homeaid/dto/request/SignInRequestDto.java similarity index 60% rename from user/src/main/java/com/homeaid/dto/request/UserSignInRequestDto.java rename to user/src/main/java/com/homeaid/dto/request/SignInRequestDto.java index ff2ae5d5..11ceb22c 100644 --- a/user/src/main/java/com/homeaid/dto/request/UserSignInRequestDto.java +++ b/user/src/main/java/com/homeaid/dto/request/SignInRequestDto.java @@ -1,17 +1,20 @@ package com.homeaid.dto.request; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import lombok.Getter; @Getter -public class UserSignInRequestDto { +public class SignInRequestDto { @Email(message = "μ˜¬λ°”λ₯Έ 이메일 ν˜•μ‹μ΄ μ•„λ‹™λ‹ˆλ‹€.") @NotBlank(message = "이메일은 ν•„μˆ˜ μž…λ ₯κ°’μž…λ‹ˆλ‹€.") + @Schema(description = "이메일", example = "customer@example.com") private String email; @NotBlank(message = "λΉ„λ°€λ²ˆν˜ΈλŠ” ν•„μˆ˜ μž…λ ₯κ°’μž…λ‹ˆλ‹€.") + @Schema(description = "λΉ„λ°€λ²ˆν˜Έ (8자 이상, 영문+숫자+특수문자 포함)", example = "Password1!") private String password; } diff --git a/user/src/main/java/com/homeaid/dto/response/SignInResponseDto.java b/user/src/main/java/com/homeaid/dto/response/SignInResponseDto.java new file mode 100644 index 00000000..a868d89b --- /dev/null +++ b/user/src/main/java/com/homeaid/dto/response/SignInResponseDto.java @@ -0,0 +1,16 @@ +package com.homeaid.dto.response; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class SignInResponseDto { + + private String token; + + public SignInResponseDto(String token) { + this.token = token; + } + +} diff --git a/user/src/main/java/com/homeaid/dto/response/UserSignUpResponseDto.java b/user/src/main/java/com/homeaid/dto/response/SignUpResponseDto.java similarity index 73% rename from user/src/main/java/com/homeaid/dto/response/UserSignUpResponseDto.java rename to user/src/main/java/com/homeaid/dto/response/SignUpResponseDto.java index 24e754ca..e4d743f4 100644 --- a/user/src/main/java/com/homeaid/dto/response/UserSignUpResponseDto.java +++ b/user/src/main/java/com/homeaid/dto/response/SignUpResponseDto.java @@ -10,7 +10,7 @@ @Getter @Builder @AllArgsConstructor -public class UserSignUpResponseDto { +public class SignUpResponseDto { @Schema(description = "κ°€μž…λœ 이메일", example = "user@example.com") private String email; @@ -19,15 +19,15 @@ public class UserSignUpResponseDto { private String message; - public static UserSignUpResponseDto toManagerDto(Manager manager) { - return UserSignUpResponseDto.builder() + public static SignUpResponseDto toManagerDto(Manager manager) { + return SignUpResponseDto.builder() .email(manager.getEmail()) .message("νšŒμ›κ°€μž…μ΄ μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.") .build(); } - public static UserSignUpResponseDto toCustomerDto(Customer customer) { - return UserSignUpResponseDto.builder() + public static SignUpResponseDto toCustomerDto(Customer customer) { + return SignUpResponseDto.builder() .email(customer.getEmail()) .message("νšŒμ›κ°€μž…μ΄ μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.") .build(); diff --git a/user/src/main/java/com/homeaid/exception/UserErrorCode.java b/user/src/main/java/com/homeaid/exception/UserErrorCode.java index c2ca5079..01431a4f 100644 --- a/user/src/main/java/com/homeaid/exception/UserErrorCode.java +++ b/user/src/main/java/com/homeaid/exception/UserErrorCode.java @@ -16,10 +16,10 @@ public enum UserErrorCode implements BaseErrorCode { // 401 UNAUTHORIZED UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "UNAUTHORIZED", "둜그인이 ν•„μš”ν•©λ‹ˆλ‹€."), - INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "INVALID_CREDENTIALS", "아이디 λ˜λŠ” λΉ„λ°€λ²ˆν˜Έκ°€ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), + LOGIN_FAILED(HttpStatus.UNAUTHORIZED, "INVALID_CREDENTIALS", "아이디 λ˜λŠ” λΉ„λ°€λ²ˆν˜Έκ°€ μ˜¬λ°”λ₯΄μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."), INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "INVALID_TOKEN", "μœ νš¨ν•˜μ§€ μ•Šμ€ ν† ν°μž…λ‹ˆλ‹€."), TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED, "TOKEN_EXPIRED", "토큰이 λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€."), - USER_NOT_AUTHENTICATED(HttpStatus.UNAUTHORIZED, "USER_NOT_AUTHENTICATED", "μΈμ¦λ˜μ§€ μ•Šμ€ μ‚¬μš©μžμž…λ‹ˆλ‹€."), + USER_NOT_AUTHENTICATED(HttpStatus.UNAUTHORIZED, "USER_NOT_AUTHENTICATED", "μΈμ¦λ˜μ§€ μ•Šμ€ μ‚¬μš©μžμž…λ‹ˆλ‹€."), // 403 FORBIDDEN FORBIDDEN_USER_ACCESS(HttpStatus.FORBIDDEN, "FORBIDDEN_USER_ACCESS", "μ ‘κ·Ό κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€."), @@ -29,7 +29,6 @@ public enum UserErrorCode implements BaseErrorCode { MANAGER_NOT_FOUND(HttpStatus.NOT_FOUND, "MANAGER_NOT_FOUND", "λ§€λ‹ˆμ €λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."), CUSTOMER_NOT_FOUND(HttpStatus.NOT_FOUND, "CUSTOMER_NOT_FOUND", "고객을 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."), - // 409 CONFLICT USER_ALREADY_EXISTS(HttpStatus.CONFLICT, "USER_ALREADY_EXISTS", "이미 κ°€μž…λœ νšŒμ›μž…λ‹ˆλ‹€."), SOCIAL_ACCOUNT_ALREADY_LINKED(HttpStatus.CONFLICT, "SOCIAL_ACCOUNT_ALREADY_LINKED", "이미 μ—°λ™λœ μ†Œμ…œ κ³„μ •μž…λ‹ˆλ‹€."), diff --git a/user/src/main/java/com/homeaid/repository/CustomerRatingRepository.java b/user/src/main/java/com/homeaid/repository/CustomerRatingRepository.java new file mode 100644 index 00000000..961301ea --- /dev/null +++ b/user/src/main/java/com/homeaid/repository/CustomerRatingRepository.java @@ -0,0 +1,10 @@ +package com.homeaid.repository; + +import com.homeaid.domain.CustomerRating; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CustomerRatingRepository extends JpaRepository { + +} diff --git a/user/src/main/java/com/homeaid/repository/ManagerRatingRepository.java b/user/src/main/java/com/homeaid/repository/ManagerRatingRepository.java new file mode 100644 index 00000000..83e1b5c6 --- /dev/null +++ b/user/src/main/java/com/homeaid/repository/ManagerRatingRepository.java @@ -0,0 +1,10 @@ +package com.homeaid.repository; + +import com.homeaid.domain.ManagerRating; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ManagerRatingRepository extends JpaRepository { + +} diff --git a/user/src/main/java/com/homeaid/security/SecurityAuthenticationFilter.java b/user/src/main/java/com/homeaid/security/SecurityAuthenticationFilter.java index a9bc5f53..e4f2bbe5 100644 --- a/user/src/main/java/com/homeaid/security/SecurityAuthenticationFilter.java +++ b/user/src/main/java/com/homeaid/security/SecurityAuthenticationFilter.java @@ -1,7 +1,7 @@ package com.homeaid.security; import com.fasterxml.jackson.databind.ObjectMapper; -import com.homeaid.dto.request.UserSignInRequestDto; +import com.homeaid.dto.request.SignInRequestDto; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -25,8 +25,8 @@ public class SecurityAuthenticationFilter extends UsernamePasswordAuthentication public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { try { - UserSignInRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), - UserSignInRequestDto.class); + SignInRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), + SignInRequestDto.class); UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(requestDto.getEmail(), requestDto.getPassword()); diff --git a/user/src/main/java/com/homeaid/security/SecurityConfig.java b/user/src/main/java/com/homeaid/security/SecurityConfig.java index 6f989d0c..f9e0d3f6 100644 --- a/user/src/main/java/com/homeaid/security/SecurityConfig.java +++ b/user/src/main/java/com/homeaid/security/SecurityConfig.java @@ -36,7 +36,7 @@ SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationManager http .authorizeHttpRequests((auth) -> auth - .requestMatchers("/api/v1/user/auth/signup/**").permitAll() + .requestMatchers("/api/v1/user/auth/signup/**", "/api/v1/swagger/auth/**").permitAll() .requestMatchers( "/swagger-ui/**", "/v3/api-docs/**", diff --git a/user/src/main/java/com/homeaid/service/UserServiceImpl.java b/user/src/main/java/com/homeaid/service/UserServiceImpl.java index c942ee7a..f2b9d4ec 100644 --- a/user/src/main/java/com/homeaid/service/UserServiceImpl.java +++ b/user/src/main/java/com/homeaid/service/UserServiceImpl.java @@ -2,11 +2,14 @@ import com.homeaid.domain.Customer; import com.homeaid.domain.Manager; +import com.homeaid.dto.request.SignInRequestDto; import com.homeaid.exception.CustomException; import com.homeaid.exception.UserErrorCode; import com.homeaid.repository.UserRepository; +import com.homeaid.security.JwtUtil; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; @Service @@ -14,6 +17,8 @@ public class UserServiceImpl implements UserService { private final UserRepository userRepository; + private final BCryptPasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; public Manager signUpManager(@Valid Manager manager) { @@ -36,4 +41,15 @@ public Customer signUpCustomer(Customer customer) { return customer; } + public String loginAndGetToken(SignInRequestDto request) { + var user = userRepository.findByEmail(request.getEmail()) + .orElseThrow(() -> new CustomException(UserErrorCode.LOGIN_FAILED)); + + if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { + throw new CustomException(UserErrorCode.LOGIN_FAILED); + } + + return jwtUtil.createJwt(user.getId(), user.getEmail(), user.getRole().name(), 1800000L); + } + }