diff --git a/src/main/java/com/somemore/domains/Review.java b/src/main/java/com/somemore/domains/Review.java deleted file mode 100644 index ee4720bd6..000000000 --- a/src/main/java/com/somemore/domains/Review.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.somemore.domains; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; - -import java.util.UUID; - -@Getter -@Setter -@Entity -public class Review { - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - - @Column(name = "center_id", nullable = false, length = 16) - private String centerId; - - @Lob - @Column(name = "content", nullable = false) - private String content; - - @Column(name = "img_url") - private String imgUrl; - -} \ No newline at end of file diff --git a/src/main/java/com/somemore/global/exception/ExceptionMessage.java b/src/main/java/com/somemore/global/exception/ExceptionMessage.java index 257147885..27fca6ff3 100644 --- a/src/main/java/com/somemore/global/exception/ExceptionMessage.java +++ b/src/main/java/com/somemore/global/exception/ExceptionMessage.java @@ -24,7 +24,10 @@ public enum ExceptionMessage { NOT_EXISTS_VOLUNTEER("존재하지 않는 봉사자입니다."), UNAUTHORIZED_VOLUNTEER_DETAIL("해당 봉사자의 상세 정보 조회 권한이 없습니다."), CANNOT_CANCEL_DELETED_INTEREST_CENTER("이미 삭제된 관심 기관은 취소할 수 없습니다."), - DUPLICATE_INTEREST_CENTER("이미 관심 표시한 기관입니다.") + DUPLICATE_INTEREST_CENTER("이미 관심 표시한 기관입니다."), + NOT_EXISTS_VOLUNTEER_APPLY("존재하지 않는 봉사 활동 지원입니다."), + REVIEW_ALREADY_EXISTS("이미 작성한 리뷰가 존재합니다."), + REVIEW_RESTRICTED_TO_ATTENDED("리뷰는 참석한 봉사에 한해서만 작성할 수 있습니다.") ; private final String message; diff --git a/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java b/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java index d734b3d3d..6201d687d 100644 --- a/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java @@ -19,8 +19,8 @@ ProblemDetail handleBadRequestException(final BadRequestException e) { //status와 에러에 대한 자세한 설명 ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage()); - //아래와 같이 필드 확장 가능 - problemDetail.setTitle("무슨 에러입니다"); + // 아래와 같이 필드 확장 가능 + problemDetail.setTitle("잘못된 요청입니다"); return problemDetail; } diff --git a/src/main/java/com/somemore/recruitboard/controller/RecruitBoardCommandApiController.java b/src/main/java/com/somemore/recruitboard/controller/RecruitBoardCommandApiController.java index 6d55d5ff2..d8274b6a0 100644 --- a/src/main/java/com/somemore/recruitboard/controller/RecruitBoardCommandApiController.java +++ b/src/main/java/com/somemore/recruitboard/controller/RecruitBoardCommandApiController.java @@ -68,7 +68,7 @@ public ApiResponse updateRecruitBoard( @AuthenticationPrincipal String userId, @PathVariable Long id, @Valid @RequestPart("data") RecruitBoardUpdateRequestDto requestDto, - @RequestPart("img_file") MultipartFile image + @RequestPart(value = "img_file", required = false) MultipartFile image ) { String imgUrl = imageUploadUseCase.uploadImage(new ImageUploadRequestDto(image)); updateRecruitBoardUseCase.updateRecruitBoard(requestDto, id, getCenterId(userId), imgUrl); diff --git a/src/main/java/com/somemore/review/controller/ReviewCommandApiController.java b/src/main/java/com/somemore/review/controller/ReviewCommandApiController.java new file mode 100644 index 000000000..f316b73d2 --- /dev/null +++ b/src/main/java/com/somemore/review/controller/ReviewCommandApiController.java @@ -0,0 +1,52 @@ +package com.somemore.review.controller; + +import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; + +import com.somemore.global.common.response.ApiResponse; +import com.somemore.imageupload.dto.ImageUploadRequestDto; +import com.somemore.imageupload.usecase.ImageUploadUseCase; +import com.somemore.review.dto.request.ReviewCreateRequestDto; +import com.somemore.review.usecase.CreateReviewUseCase; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@Tag(name = "Review Command API", description = "리뷰 생성 수정 삭제 API") +@RequiredArgsConstructor +@RequestMapping("/api") +@RestController +public class ReviewCommandApiController { + + private final CreateReviewUseCase createReviewUseCase; + private final ImageUploadUseCase imageUploadUseCase; + + @Secured("ROLE_VOLUNTEER") + @Operation(summary = "리뷰 등록", description = "리뷰를 등록합니다.") + @PostMapping(value = "/review", consumes = MULTIPART_FORM_DATA_VALUE) + public ApiResponse createReview( + @AuthenticationPrincipal String userId, + @Valid @RequestPart("data") ReviewCreateRequestDto requestDto, + @RequestPart(value = "img_file", required = false) MultipartFile image) { + + String imgUrl = imageUploadUseCase.uploadImage(new ImageUploadRequestDto(image)); + return ApiResponse.ok( + 201, + createReviewUseCase.createReview(requestDto, getId(userId), imgUrl), + "리뷰 등록 성공" + ); + } + + private static UUID getId(String id) { + return UUID.fromString(id); + } + +} diff --git a/src/main/java/com/somemore/review/domain/Review.java b/src/main/java/com/somemore/review/domain/Review.java new file mode 100644 index 000000000..b4967407b --- /dev/null +++ b/src/main/java/com/somemore/review/domain/Review.java @@ -0,0 +1,53 @@ +package com.somemore.review.domain; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import com.somemore.global.common.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Lob; +import jakarta.persistence.Table; +import java.util.UUID; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +@Table(name = "review") +public class Review extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @Column(name = "volunteer_apply_id", nullable = false) + private Long volunteerApplyId; + + @Column(name = "volunteer_id", nullable = false, length = 16) + private UUID volunteerId; + + @Column(name = "title", nullable = false) + private String title; + + @Lob + @Column(name = "content", nullable = false) + private String content; + + @Column(name = "img_url", nullable = false) + private String imgUrl; + + @Builder + public Review(Long volunteerApplyId, UUID volunteerId, String title, + String content, String imgUrl) { + this.volunteerApplyId = volunteerApplyId; + this.volunteerId = volunteerId; + this.title = title; + this.content = content; + this.imgUrl = imgUrl; + } +} diff --git a/src/main/java/com/somemore/review/dto/request/ReviewCreateRequestDto.java b/src/main/java/com/somemore/review/dto/request/ReviewCreateRequestDto.java new file mode 100644 index 000000000..026929be8 --- /dev/null +++ b/src/main/java/com/somemore/review/dto/request/ReviewCreateRequestDto.java @@ -0,0 +1,36 @@ +package com.somemore.review.dto.request; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.somemore.review.domain.Review; +import com.somemore.volunteerapply.domain.VolunteerApply; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.UUID; +import lombok.Builder; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@Builder +public record ReviewCreateRequestDto( + @Schema(description = "봉사 모집글 아이디", example = "1") + @NotNull(message = "봉사 모집글 아이디는 필수 값입니다.") + Long recruitBoardId, + @Schema(description = "리뷰 제목", example = "내 인생 최고의 봉사 활동") + @NotBlank(message = "리뷰 제목은 필수 값입니다.") + String title, + @Schema(description = "리뷰 내용", example = "담당자님도 정말 친절하였고 정말 보람찬 봉사였어요
") + @NotBlank(message = "리뷰 내용은 필수 값입니다.") + String content +) { + + public Review toEntity(VolunteerApply apply, UUID volunteerId, String imgUrl) { + return Review.builder() + .volunteerApplyId(apply.getId()) + .volunteerId(volunteerId) + .title(title) + .content(content) + .imgUrl(imgUrl) + .build(); + } +} diff --git a/src/main/java/com/somemore/review/repository/ReviewJpaRepository.java b/src/main/java/com/somemore/review/repository/ReviewJpaRepository.java new file mode 100644 index 000000000..694055295 --- /dev/null +++ b/src/main/java/com/somemore/review/repository/ReviewJpaRepository.java @@ -0,0 +1,15 @@ +package com.somemore.review.repository; + +import com.somemore.review.domain.Review; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface ReviewJpaRepository extends JpaRepository { + + Optional findByIdAndDeletedFalse(Long id); + + @Query("SELECT COUNT(r) > 0 FROM Review r WHERE r.volunteerApplyId = :volunteerId AND r.deleted = false") + boolean existsByVolunteerApplyId(Long volunteerId); + +} diff --git a/src/main/java/com/somemore/review/repository/ReviewRepository.java b/src/main/java/com/somemore/review/repository/ReviewRepository.java new file mode 100644 index 000000000..08b817fd8 --- /dev/null +++ b/src/main/java/com/somemore/review/repository/ReviewRepository.java @@ -0,0 +1,13 @@ +package com.somemore.review.repository; + +import com.somemore.review.domain.Review; +import java.util.Optional; + +public interface ReviewRepository { + + Review save(Review review); + + Optional findById(Long id); + + boolean existsByVolunteerApplyId(Long volunteerApplyId); +} diff --git a/src/main/java/com/somemore/review/repository/ReviewRepositoryImpl.java b/src/main/java/com/somemore/review/repository/ReviewRepositoryImpl.java new file mode 100644 index 000000000..856efe188 --- /dev/null +++ b/src/main/java/com/somemore/review/repository/ReviewRepositoryImpl.java @@ -0,0 +1,30 @@ +package com.somemore.review.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.somemore.review.domain.Review; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class ReviewRepositoryImpl implements ReviewRepository { + + private final ReviewJpaRepository reviewJpaRepository; + private final JPAQueryFactory queryFactory; + + @Override + public Review save(Review review) { + return reviewJpaRepository.save(review); + } + + @Override + public Optional findById(Long id) { + return reviewJpaRepository.findByIdAndDeletedFalse(id); + } + + @Override + public boolean existsByVolunteerApplyId(Long volunteerApplyId) { + return reviewJpaRepository.existsByVolunteerApplyId(volunteerApplyId); + } +} diff --git a/src/main/java/com/somemore/review/service/CreateReviewService.java b/src/main/java/com/somemore/review/service/CreateReviewService.java new file mode 100644 index 000000000..fa53b6255 --- /dev/null +++ b/src/main/java/com/somemore/review/service/CreateReviewService.java @@ -0,0 +1,52 @@ +package com.somemore.review.service; + +import static com.somemore.global.exception.ExceptionMessage.REVIEW_ALREADY_EXISTS; +import static com.somemore.global.exception.ExceptionMessage.REVIEW_RESTRICTED_TO_ATTENDED; + +import com.somemore.global.exception.BadRequestException; +import com.somemore.review.domain.Review; +import com.somemore.review.dto.request.ReviewCreateRequestDto; +import com.somemore.review.repository.ReviewRepository; +import com.somemore.review.usecase.CreateReviewUseCase; +import com.somemore.volunteerapply.domain.VolunteerApply; +import com.somemore.volunteerapply.usecase.VolunteerApplyQueryUseCase; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional +@Service +public class CreateReviewService implements CreateReviewUseCase { + + private final ReviewRepository reviewRepository; + private final VolunteerApplyQueryUseCase volunteerApplyQueryUseCase; + + @Override + public Long createReview(ReviewCreateRequestDto requestDto, UUID volunteerId, String imgUrl) { + VolunteerApply apply = getVolunteerApply(requestDto.recruitBoardId(), volunteerId); + validateReviewNotExist(apply); + validateActivityCompletion(apply); + + Review review = requestDto.toEntity(apply, volunteerId, imgUrl); + return reviewRepository.save(review).getId(); + } + + private VolunteerApply getVolunteerApply(Long recruitBoardId, UUID volunteerId) { + return volunteerApplyQueryUseCase.getByRecruitIdAndVolunteerId(recruitBoardId, volunteerId); + } + + private void validateReviewNotExist(VolunteerApply apply) { + if (reviewRepository.existsByVolunteerApplyId(apply.getId())) { + throw new BadRequestException(REVIEW_ALREADY_EXISTS.getMessage()); + } + } + + private void validateActivityCompletion(VolunteerApply apply) { + if (apply.isVolunteerActivityCompleted()) { + return; + } + throw new BadRequestException(REVIEW_RESTRICTED_TO_ATTENDED.getMessage()); + } +} diff --git a/src/main/java/com/somemore/review/usecase/CreateReviewUseCase.java b/src/main/java/com/somemore/review/usecase/CreateReviewUseCase.java new file mode 100644 index 000000000..ec2fb7f40 --- /dev/null +++ b/src/main/java/com/somemore/review/usecase/CreateReviewUseCase.java @@ -0,0 +1,11 @@ +package com.somemore.review.usecase; + +import com.somemore.review.dto.request.ReviewCreateRequestDto; +import java.util.UUID; + +public interface CreateReviewUseCase { + + Long createReview(ReviewCreateRequestDto requestDto, UUID volunteerId, String imgUrl); + + +} diff --git a/src/main/java/com/somemore/volunteerapply/domain/VolunteerApply.java b/src/main/java/com/somemore/volunteerapply/domain/VolunteerApply.java index 64ceaf9c1..2f0a54615 100644 --- a/src/main/java/com/somemore/volunteerapply/domain/VolunteerApply.java +++ b/src/main/java/com/somemore/volunteerapply/domain/VolunteerApply.java @@ -1,14 +1,22 @@ package com.somemore.volunteerapply.domain; +import static com.somemore.volunteerapply.domain.ApplyStatus.APPROVED; + import com.somemore.global.common.BaseEntity; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.UUID; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.UUID; - @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity @@ -33,12 +41,29 @@ public class VolunteerApply extends BaseEntity { private Boolean attended = false; @Builder - public VolunteerApply(UUID volunteerId, Long recruitBoardId, ApplyStatus status, Boolean attended) { + public VolunteerApply(UUID volunteerId, Long recruitBoardId, ApplyStatus status, + Boolean attended) { this.volunteerId = volunteerId; this.recruitBoardId = recruitBoardId; this.status = status; this.attended = attended; } -} -// TODO 상태 업데이트 메서드들을 만들고 빌더에서 status를 변경 불가하도록 + public void changeStatus(ApplyStatus status) { + if (isVolunteerActivityCompleted()) { + throw new IllegalStateException("이미 완료된 봉사활동에 대해서는 변경이 불가능합니다."); + } + this.status = status; + } + + public void changeAttended(Boolean attended) { + if (this.status != APPROVED) { + throw new IllegalStateException("승인되지 않은 봉사 지원은 참석 여부를 변경할 수 없습니다."); + } + this.attended = attended; + } + + public boolean isVolunteerActivityCompleted() { + return this.attended && this.status == APPROVED; + } +} diff --git a/src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepository.java b/src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepository.java index 5f68b0c95..cd160a1fd 100644 --- a/src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepository.java +++ b/src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepository.java @@ -1,12 +1,11 @@ package com.somemore.volunteerapply.repository; import com.somemore.volunteerapply.domain.VolunteerApply; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - import java.util.List; import java.util.Optional; import java.util.UUID; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; public interface VolunteerApplyRepository { @@ -14,4 +13,5 @@ public interface VolunteerApplyRepository { Optional findById(Long id); List findVolunteerIdsByRecruitIds(List recruitIds); Page findAllByRecruitId(Long recruitId, Pageable pageable); + Optional findByRecruitIdAndVolunteerId(Long recruitId, UUID volunteerId); } diff --git a/src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImpl.java b/src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImpl.java index 61112ce1b..3c0031234 100644 --- a/src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImpl.java +++ b/src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImpl.java @@ -5,6 +5,9 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import com.somemore.volunteerapply.domain.QVolunteerApply; import com.somemore.volunteerapply.domain.VolunteerApply; +import java.util.List; +import java.util.Optional; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -12,10 +15,6 @@ import org.springframework.data.domain.Sort; import org.springframework.stereotype.Repository; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - @RequiredArgsConstructor @Repository public class VolunteerApplyRepositoryImpl implements VolunteerApplyRepository { @@ -71,6 +70,14 @@ public Page findAllByRecruitId(Long recruitId, Pageable pageable ); } + @Override + public Optional findByRecruitIdAndVolunteerId(Long recruitId, + UUID volunteerId) { + BooleanExpression exp = volunteerApply.recruitBoardId.eq(recruitId) + .and(volunteerApply.volunteerId.eq(volunteerId)); + return findOne(exp); + } + private Long getCount(BooleanExpression exp) { return queryFactory .select(volunteerApply.count()) diff --git a/src/main/java/com/somemore/volunteerapply/service/VolunteerApplyQueryService.java b/src/main/java/com/somemore/volunteerapply/service/VolunteerApplyQueryService.java index 100765e23..6a832e5eb 100644 --- a/src/main/java/com/somemore/volunteerapply/service/VolunteerApplyQueryService.java +++ b/src/main/java/com/somemore/volunteerapply/service/VolunteerApplyQueryService.java @@ -1,15 +1,18 @@ package com.somemore.volunteerapply.service; +import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_VOLUNTEER_APPLY; + +import com.somemore.global.exception.BadRequestException; +import com.somemore.volunteerapply.domain.VolunteerApply; import com.somemore.volunteerapply.repository.VolunteerApplyRepository; import com.somemore.volunteerapply.usecase.VolunteerApplyQueryUseCase; +import java.util.List; +import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.UUID; - @Slf4j @Service @RequiredArgsConstructor @@ -17,9 +20,20 @@ public class VolunteerApplyQueryService implements VolunteerApplyQueryUseCase { private final VolunteerApplyRepository volunteerApplyRepository; + @Override public List getVolunteerIdsByRecruitIds(List recruitIds) { - return volunteerApplyRepository.findVolunteerIdsByRecruitIds(recruitIds); } + + @Override + public VolunteerApply getByRecruitIdAndVolunteerId(Long recruitId, UUID volunteerId) { + return getVolunteerApplyBy(recruitId, volunteerId); + } + + private VolunteerApply getVolunteerApplyBy(Long recruitBoardId, UUID volunteerId) { + return volunteerApplyRepository.findByRecruitIdAndVolunteerId(recruitBoardId, + volunteerId).orElseThrow( + () -> new BadRequestException(NOT_EXISTS_VOLUNTEER_APPLY.getMessage())); + } } diff --git a/src/main/java/com/somemore/volunteerapply/usecase/VolunteerApplyQueryUseCase.java b/src/main/java/com/somemore/volunteerapply/usecase/VolunteerApplyQueryUseCase.java index e7ab746b2..80916166c 100644 --- a/src/main/java/com/somemore/volunteerapply/usecase/VolunteerApplyQueryUseCase.java +++ b/src/main/java/com/somemore/volunteerapply/usecase/VolunteerApplyQueryUseCase.java @@ -1,9 +1,10 @@ package com.somemore.volunteerapply.usecase; +import com.somemore.volunteerapply.domain.VolunteerApply; import java.util.List; import java.util.UUID; public interface VolunteerApplyQueryUseCase { - List getVolunteerIdsByRecruitIds(List recruitIds); + VolunteerApply getByRecruitIdAndVolunteerId(Long recruitId, UUID volunteerId); } diff --git a/src/test/java/com/somemore/review/controller/ReviewCommandApiControllerTest.java b/src/test/java/com/somemore/review/controller/ReviewCommandApiControllerTest.java new file mode 100644 index 000000000..2e7b1487c --- /dev/null +++ b/src/test/java/com/somemore/review/controller/ReviewCommandApiControllerTest.java @@ -0,0 +1,84 @@ +package com.somemore.review.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.springframework.http.MediaType.MULTIPART_FORM_DATA; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.somemore.ControllerTestSupport; +import com.somemore.WithMockCustomUser; +import com.somemore.imageupload.usecase.ImageUploadUseCase; +import com.somemore.review.dto.request.ReviewCreateRequestDto; +import com.somemore.review.usecase.CreateReviewUseCase; +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; + +class ReviewCommandApiControllerTest extends ControllerTestSupport { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private ImageUploadUseCase imageUploadUseCase; + + @MockBean + private CreateReviewUseCase createReviewUseCase; + + @DisplayName("리뷰 생성") + @Test + @WithMockCustomUser() + void createReview() throws Exception { + // given + ReviewCreateRequestDto requestDto = ReviewCreateRequestDto.builder() + .recruitBoardId(1L) + .title("리뷰 제목") + .content("리뷰 내용") + .build(); + + MockMultipartFile imageFile = new MockMultipartFile( + "img_file", + "test-image.jpg", + MediaType.IMAGE_JPEG_VALUE, + "test image content".getBytes() + ); + + MockMultipartFile requestData = new MockMultipartFile( + "data", + "", + MediaType.APPLICATION_JSON_VALUE, + objectMapper.writeValueAsBytes(requestDto) + ); + + String imgUrl = "https://example.com/image/test-image.jpg"; + Long reviewId = 1L; + + given(imageUploadUseCase.uploadImage(any())).willReturn(imgUrl); + given(createReviewUseCase.createReview(any(), any(UUID.class), + anyString())).willReturn(reviewId); + + // when + mockMvc.perform(multipart("/api/review") + .file(requestData) + .file(imageFile) + .contentType(MULTIPART_FORM_DATA) + .header("Authorization", "Bearer access-token")) + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(201)) + .andExpect(jsonPath("$.data").value(reviewId)) + .andExpect(jsonPath("$.message").value("리뷰 등록 성공")); + } +} diff --git a/src/test/java/com/somemore/review/repository/ReviewRepositoryImplTest.java b/src/test/java/com/somemore/review/repository/ReviewRepositoryImplTest.java new file mode 100644 index 000000000..5a7697c8a --- /dev/null +++ b/src/test/java/com/somemore/review/repository/ReviewRepositoryImplTest.java @@ -0,0 +1,62 @@ +package com.somemore.review.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.somemore.IntegrationTestSupport; +import com.somemore.review.domain.Review; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +class ReviewRepositoryImplTest extends IntegrationTestSupport { + + @Autowired + private ReviewRepositoryImpl reviewRepository; + + @DisplayName("리뷰 생성 및 조회") + @Test + void saveAndFind() { + // given + Review review = Review.builder() + .volunteerApplyId(1L) + .volunteerId(UUID.randomUUID()) + .title("리뷰 제목") + .content("리뷰 내용") + .imgUrl("") + .build(); + reviewRepository.save(review); + + // when + Optional findReview = reviewRepository.findById(review.getId()); + + // then + assertThat(findReview).isPresent(); + assertThat(findReview.get().getId()).isEqualTo(review.getId()); + } + + @DisplayName("봉사 지원 아이디로 리뷰 존재 유무를 확인할 수 있다") + @Test + void existsByVolunteerApplyId() { + // given + Long volunteerApplyId = 1L; + Review review = Review.builder() + .volunteerApplyId(volunteerApplyId) + .volunteerId(UUID.randomUUID()) + .title("리뷰 제목") + .content("리뷰 내용") + .imgUrl("") + .build(); + reviewRepository.save(review); + + // when + boolean result = reviewRepository.existsByVolunteerApplyId(volunteerApplyId); + + // then + assertThat(result).isTrue(); + } + +} diff --git a/src/test/java/com/somemore/review/service/CreateReviewServiceTest.java b/src/test/java/com/somemore/review/service/CreateReviewServiceTest.java new file mode 100644 index 000000000..3b7ec75d6 --- /dev/null +++ b/src/test/java/com/somemore/review/service/CreateReviewServiceTest.java @@ -0,0 +1,130 @@ +package com.somemore.review.service; + +import static com.somemore.global.exception.ExceptionMessage.REVIEW_ALREADY_EXISTS; +import static com.somemore.global.exception.ExceptionMessage.REVIEW_RESTRICTED_TO_ATTENDED; +import static com.somemore.volunteerapply.domain.ApplyStatus.APPROVED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.somemore.IntegrationTestSupport; +import com.somemore.global.exception.BadRequestException; +import com.somemore.review.domain.Review; +import com.somemore.review.dto.request.ReviewCreateRequestDto; +import com.somemore.review.repository.ReviewRepository; +import com.somemore.volunteerapply.domain.VolunteerApply; +import com.somemore.volunteerapply.repository.VolunteerApplyRepository; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +class CreateReviewServiceTest extends IntegrationTestSupport { + + @Autowired + private CreateReviewService createReviewService; + + @Autowired + private ReviewRepository reviewRepository; + + @Autowired + private VolunteerApplyRepository volunteerApplyRepository; + + @DisplayName("리뷰 생성") + @Test + void createReview() { + // given + UUID volunteerId = UUID.randomUUID(); + Long recruitId = 200L; + VolunteerApply apply = VolunteerApply.builder() + .volunteerId(volunteerId) + .recruitBoardId(200L) + .status(APPROVED) + .attended(true) + .build(); + volunteerApplyRepository.save(apply); + + ReviewCreateRequestDto requestDto = ReviewCreateRequestDto.builder() + .recruitBoardId(recruitId) + .title("리뷰 제목") + .content("리뷰 내용") + .build(); + + // when + Long reviewId = createReviewService.createReview(requestDto, volunteerId, ""); + + // then + Optional findReview = reviewRepository.findById(reviewId); + assertThat(findReview).isPresent(); + assertThat(findReview.get().getId()).isEqualTo(reviewId); + } + + @DisplayName("참석하지 않은 봉사 활동에 대해 리뷰를 생성하면 에러가 발생한다") + @Test + void createReviewWhenNotCompleted() { + // given + UUID volunteerId = UUID.randomUUID(); + Long recruitId = 200L; + VolunteerApply apply = VolunteerApply.builder() + .volunteerId(volunteerId) + .recruitBoardId(200L) + .status(APPROVED) + .attended(false) + .build(); + volunteerApplyRepository.save(apply); + + ReviewCreateRequestDto requestDto = ReviewCreateRequestDto.builder() + .recruitBoardId(recruitId) + .title("리뷰 제목") + .content("리뷰 내용") + .build(); + + // when + // then + assertThatThrownBy( + () -> createReviewService.createReview(requestDto, volunteerId, "") + ).isInstanceOf(BadRequestException.class) + .hasMessage(REVIEW_RESTRICTED_TO_ATTENDED.getMessage()); + } + + @DisplayName("이미 작성한 봉사 활동에 대해 리뷰를 생성하면 에러가 발생한다") + @Test + void createReviewWhenExistsReview() { + // given + UUID volunteerId = UUID.randomUUID(); + Long recruitId = 200L; + VolunteerApply apply = VolunteerApply.builder() + .volunteerId(volunteerId) + .recruitBoardId(200L) + .status(APPROVED) + .attended(false) + .build(); + volunteerApplyRepository.save(apply); + + Review review = Review.builder() + .volunteerApplyId(apply.getId()) + .volunteerId(volunteerId) + .title("리뷰 제목") + .content("리뷰 내용") + .imgUrl("") + .build(); + + reviewRepository.save(review); + + ReviewCreateRequestDto requestDto = ReviewCreateRequestDto.builder() + .recruitBoardId(recruitId) + .title("리뷰 제목") + .content("리뷰 내용") + .build(); + + // when + // then + assertThatThrownBy( + () -> createReviewService.createReview(requestDto, volunteerId, "") + ).isInstanceOf(BadRequestException.class) + .hasMessage(REVIEW_ALREADY_EXISTS.getMessage()); + } + +} diff --git a/src/test/java/com/somemore/volunteerapply/domain/VolunteerApplyTest.java b/src/test/java/com/somemore/volunteerapply/domain/VolunteerApplyTest.java new file mode 100644 index 000000000..187068966 --- /dev/null +++ b/src/test/java/com/somemore/volunteerapply/domain/VolunteerApplyTest.java @@ -0,0 +1,104 @@ +package com.somemore.volunteerapply.domain; + +import static com.somemore.volunteerapply.domain.ApplyStatus.APPROVED; +import static com.somemore.volunteerapply.domain.ApplyStatus.REJECTED; +import static com.somemore.volunteerapply.domain.ApplyStatus.WAITING; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class VolunteerApplyTest { + + @DisplayName("지원 상태를 변경할 수 있다") + @Test + void changeStatus() { + // given + VolunteerApply apply = VolunteerApply.builder() + .volunteerId(UUID.randomUUID()) + .recruitBoardId(1L) + .status(WAITING) + .attended(false) + .build(); + + // when + apply.changeStatus(APPROVED); + + // then + assertThat(apply.getStatus()).isEqualTo(APPROVED); + } + + @Test + @DisplayName("지원 상태 변경 실패 - 이미 완료된 봉사활동") + void changeStatusWhenCompletedActivity() { + // given + VolunteerApply apply = VolunteerApply.builder() + .volunteerId(UUID.randomUUID()) + .recruitBoardId(1L) + .status(APPROVED) + .attended(true) + .build(); + + // when + // then + assertThatThrownBy( + () -> apply.changeStatus(REJECTED) + ).isInstanceOf(IllegalStateException.class); + } + + @DisplayName("봉사 지원 상태를 변경") + @Test + void markAsAttended() { + // given + VolunteerApply volunteerApply = VolunteerApply.builder() + .volunteerId(UUID.randomUUID()) + .recruitBoardId(1L) + .status(APPROVED) + .attended(false) + .build(); + + // when + volunteerApply.changeAttended(true); + + // then + assertThat(volunteerApply.getAttended()).isTrue(); + } + + @Test + @DisplayName("참석 처리 실패 - 승인되지 않은 상태") + void changeAttendedWhenNotApproved() { + // given + VolunteerApply apply = VolunteerApply.builder() + .volunteerId(UUID.randomUUID()) + .recruitBoardId(1L) + .status(WAITING) + .attended(false) + .build(); + + // when & then + assertThatThrownBy( + () -> apply.changeAttended(true) + ).isInstanceOf(IllegalStateException.class); + } + + @DisplayName("봉사 참석 여부 확인") + @Test + void isCompleted() { + // given + VolunteerApply apply = VolunteerApply.builder() + .volunteerId(UUID.randomUUID()) + .recruitBoardId(1L) + .status(APPROVED) + .attended(true) + .build(); + + // when + boolean res = apply.isVolunteerActivityCompleted(); + + // then + assertThat(res).isTrue(); + } +} + diff --git a/src/test/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImplTest.java b/src/test/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImplTest.java index 7792a3967..5ee125c8f 100644 --- a/src/test/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImplTest.java +++ b/src/test/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImplTest.java @@ -1,8 +1,13 @@ package com.somemore.volunteerapply.repository; +import static org.assertj.core.api.Assertions.assertThat; + import com.somemore.IntegrationTestSupport; -import com.somemore.volunteerapply.domain.VolunteerApply; import com.somemore.volunteerapply.domain.ApplyStatus; +import com.somemore.volunteerapply.domain.VolunteerApply; +import java.util.List; +import java.util.Optional; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -12,12 +17,6 @@ import org.springframework.data.domain.Sort; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - @Transactional class VolunteerApplyRepositoryImplTest extends IntegrationTestSupport { @@ -52,12 +51,7 @@ void setUp() { @Test void saveAndFindById() { // Given - VolunteerApply newApply = VolunteerApply.builder() - .volunteerId(UUID.randomUUID()) - .recruitBoardId(1L) - .status(ApplyStatus.APPROVED) - .attended(false) - .build(); + VolunteerApply newApply = createApply(UUID.randomUUID(), 1L); VolunteerApply savedApply = volunteerApplyRepository.save(newApply); // When @@ -73,7 +67,8 @@ void saveAndFindById() { @Test void findVolunteerIdsByRecruitIds() { // When - List volunteerIds = volunteerApplyRepository.findVolunteerIdsByRecruitIds(List.of(1L, 2L)); + List volunteerIds = volunteerApplyRepository.findVolunteerIdsByRecruitIds( + List.of(1L, 2L)); // Then assertThat(volunteerIds).hasSize(20); @@ -87,8 +82,10 @@ void findAllByRecruitId() { PageRequest secondPage = PageRequest.of(1, 10, Sort.by(Sort.Order.asc("created_at"))); // When - Page firstPageResult = volunteerApplyRepository.findAllByRecruitId(1L, firstPage); - Page secondPageResult = volunteerApplyRepository.findAllByRecruitId(1L, secondPage); + Page firstPageResult = volunteerApplyRepository.findAllByRecruitId(1L, + firstPage); + Page secondPageResult = volunteerApplyRepository.findAllByRecruitId(1L, + secondPage); // Then assertThat(firstPageResult.getContent()).hasSize(10); @@ -101,4 +98,31 @@ void findAllByRecruitId() { assertThat(secondPageResult.hasNext()).isFalse(); assertThat(secondPageResult.hasPrevious()).isTrue(); } -} \ No newline at end of file + + @DisplayName("모집글 아이디와 봉사자 아이디로 봉사 지원을 조회할 수 있다.") + @Test + void findByRecruitIdAndVolunteerId() { + // given + Long recruitId = 1234L; + UUID volunteerId = UUID.randomUUID(); + + VolunteerApply newApply = createApply(volunteerId, recruitId); + volunteerApplyRepository.save(newApply); + + // when + Optional findApply = volunteerApplyRepository.findByRecruitIdAndVolunteerId( + recruitId, volunteerId); + + // then + assertThat(findApply).isPresent(); + } + + private static VolunteerApply createApply(UUID volunteerId, Long recruitId) { + return VolunteerApply.builder() + .volunteerId(volunteerId) + .recruitBoardId(recruitId) + .status(ApplyStatus.APPROVED) + .attended(false) + .build(); + } +} diff --git a/src/test/java/com/somemore/volunteerapply/service/VolunteerApplyQueryServiceTest.java b/src/test/java/com/somemore/volunteerapply/service/VolunteerApplyQueryServiceTest.java index 96168a3d1..f5285a143 100644 --- a/src/test/java/com/somemore/volunteerapply/service/VolunteerApplyQueryServiceTest.java +++ b/src/test/java/com/somemore/volunteerapply/service/VolunteerApplyQueryServiceTest.java @@ -1,19 +1,18 @@ package com.somemore.volunteerapply.service; +import static org.assertj.core.api.Assertions.assertThat; + import com.somemore.IntegrationTestSupport; import com.somemore.volunteerapply.domain.ApplyStatus; import com.somemore.volunteerapply.domain.VolunteerApply; import com.somemore.volunteerapply.repository.VolunteerApplyRepository; +import java.util.List; +import java.util.UUID; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - @Transactional class VolunteerApplyQueryServiceTest extends IntegrationTestSupport { @@ -39,7 +38,8 @@ void getVolunteerIdsByRecruitIds() { volunteerApplyRepository.save(apply2); // When - List volunteerIds = volunteerApplyQueryService.getVolunteerIdsByRecruitIds(List.of(recruitId1, recruitId2)); + List volunteerIds = volunteerApplyQueryService.getVolunteerIdsByRecruitIds( + List.of(recruitId1, recruitId2)); // Then assertThat(volunteerIds) @@ -55,4 +55,32 @@ private VolunteerApply createVolunteerApply(Long recruitId, UUID volunteerId) { .attended(false) .build(); } -} \ No newline at end of file + + @DisplayName("모집글 아이디와 봉사자 아이디로 조회할 수 있다") + @Test + void getByRecruitIdAndVolunteerId() { + // given + Long recruitId = 1234L; + UUID volunteerId = UUID.randomUUID(); + + VolunteerApply newApply = createApply(volunteerId, recruitId); + volunteerApplyRepository.save(newApply); + + // when + VolunteerApply apply = volunteerApplyQueryService.getByRecruitIdAndVolunteerId( + recruitId, volunteerId); + + // then + assertThat(apply.getRecruitBoardId()).isEqualTo(recruitId); + assertThat(apply.getVolunteerId()).isEqualTo(volunteerId); + } + + private static VolunteerApply createApply(UUID volunteerId, Long recruitId) { + return VolunteerApply.builder() + .volunteerId(volunteerId) + .recruitBoardId(recruitId) + .status(ApplyStatus.APPROVED) + .attended(false) + .build(); + } +}