Skip to content

Commit e74ad11

Browse files
authored
feat: 리뷰 수정 및 삭제 (#263)
* feat(review): 리뷰 삭제 * test(review): 리뷰 삭제 테스트 * feat(review): 리뷰 수정 * test(review): 리뷰 수정 테스트 * refactor(review): 리뷰 조회 리팩토링 - getById, getDetailById * test(review): 리뷰 조회 리팩토링 테스트 - getById, getDetailById * refactor(volunteer-apply): 봉사 지원 조회 리팩토링 - getById * test(volunteer-apply): 봉사 지원 조회 리팩토링 테스트 - getById * refactor(review): 봉사 지원 생성 리팩토링 - ReviewQueryUseCase 의존하도록 변경 * test(review): 봉사 지원 생성 리팩토링에 따른 테스트 - ReviewQueryUseCase 의존하도록 변경 * refactor(review): 봉사 지원 수정 및 삭제 리팩토링 - ReviewQueryUseCase 의존하도록 변경 * chore: 불필요한 import 제거 * chore: sonarqube 이슈 해결 * refactor: QueryUseCase에서 이미 작성된 테스트 삭제
1 parent 35fa1c3 commit e74ad11

26 files changed

+899
-223
lines changed

src/main/java/com/somemore/domains/review/controller/ReviewCommandApiController.java

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,35 @@
11
package com.somemore.domains.review.controller;
22

3-
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
4-
3+
import com.somemore.domains.review.dto.request.ReviewCreateRequestDto;
4+
import com.somemore.domains.review.dto.request.ReviewUpdateRequestDto;
55
import com.somemore.domains.review.usecase.CreateReviewUseCase;
6+
import com.somemore.domains.review.usecase.DeleteReviewUseCase;
7+
import com.somemore.domains.review.usecase.UpdateReviewUseCase;
68
import com.somemore.global.auth.annotation.CurrentUser;
79
import com.somemore.global.common.response.ApiResponse;
810
import com.somemore.global.imageupload.dto.ImageUploadRequestDto;
911
import com.somemore.global.imageupload.usecase.ImageUploadUseCase;
10-
import com.somemore.domains.review.dto.request.ReviewCreateRequestDto;
1112
import io.swagger.v3.oas.annotations.Operation;
1213
import io.swagger.v3.oas.annotations.tags.Tag;
1314
import jakarta.validation.Valid;
14-
import java.util.UUID;
1515
import lombok.RequiredArgsConstructor;
1616
import org.springframework.security.access.annotation.Secured;
17-
import org.springframework.web.bind.annotation.PostMapping;
18-
import org.springframework.web.bind.annotation.RequestMapping;
19-
import org.springframework.web.bind.annotation.RequestPart;
20-
import org.springframework.web.bind.annotation.RestController;
17+
import org.springframework.web.bind.annotation.*;
2118
import org.springframework.web.multipart.MultipartFile;
2219

20+
import java.util.UUID;
21+
22+
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
23+
2324
@Tag(name = "Review Command API", description = "리뷰 생성 수정 삭제 API")
2425
@RequiredArgsConstructor
2526
@RequestMapping("/api")
2627
@RestController
2728
public class ReviewCommandApiController {
2829

2930
private final CreateReviewUseCase createReviewUseCase;
31+
private final UpdateReviewUseCase updateReviewUseCase;
32+
private final DeleteReviewUseCase deleteReviewUseCase;
3033
private final ImageUploadUseCase imageUploadUseCase;
3134

3235
@Secured("ROLE_VOLUNTEER")
@@ -45,4 +48,47 @@ public ApiResponse<Long> createReview(
4548
);
4649
}
4750

51+
@Secured("ROLE_VOLUNTEER")
52+
@Operation(summary = "리뷰 수정", description = "리뷰를 수정합니다.")
53+
@PutMapping(value = "/review/{id}")
54+
public ApiResponse<String> updateReview(
55+
@CurrentUser UUID userId,
56+
@PathVariable Long id,
57+
@Valid @RequestBody ReviewUpdateRequestDto requestDto) {
58+
59+
updateReviewUseCase.updateReview(id, userId, requestDto);
60+
61+
return ApiResponse.ok(
62+
200,
63+
"리뷰 수정 성공"
64+
);
65+
}
66+
67+
@Secured("ROLE_VOLUNTEER")
68+
@Operation(summary = "리뷰 이미지 수정", description = "리뷰 이미지를 수정합니다.")
69+
@PutMapping(value = "/review/{id}", consumes = MULTIPART_FORM_DATA_VALUE)
70+
public ApiResponse<String> updateReviewImage(
71+
@CurrentUser UUID userId,
72+
@PathVariable Long id,
73+
@RequestPart(value = "img_file", required = false) MultipartFile image) {
74+
75+
String newImgUrl = imageUploadUseCase.uploadImage(new ImageUploadRequestDto(image));
76+
updateReviewUseCase.updateReviewImageUrl(id, userId, newImgUrl);
77+
return ApiResponse.ok(
78+
200,
79+
"리뷰 이미지 수정 성공"
80+
);
81+
}
82+
83+
@Secured("ROLE_VOLUNTEER")
84+
@Operation(summary = "리뷰 삭제", description = "리뷰를 삭제합니다.")
85+
@DeleteMapping(value = "/review/{id}")
86+
public ApiResponse<String> createReview(
87+
@CurrentUser UUID userId,
88+
@PathVariable Long id
89+
) {
90+
deleteReviewUseCase.deleteReview(userId, id);
91+
return ApiResponse.ok("리뷰 삭제 성공");
92+
}
93+
4894
}

src/main/java/com/somemore/domains/review/controller/ReviewQueryApiController.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
import com.somemore.domains.recruitboard.domain.VolunteerCategory;
66
import com.somemore.global.common.response.ApiResponse;
77
import com.somemore.domains.review.dto.condition.ReviewSearchCondition;
8-
import com.somemore.domains.review.dto.response.ReviewResponseDto;
9-
import com.somemore.domains.review.dto.response.ReviewWithNicknameResponseDto;
8+
import com.somemore.domains.review.dto.response.ReviewDetailResponseDto;
9+
import com.somemore.domains.review.dto.response.ReviewDetailWithNicknameResponseDto;
1010
import com.somemore.domains.review.usecase.ReviewQueryUseCase;
1111
import io.swagger.v3.oas.annotations.Operation;
1212
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -31,18 +31,18 @@ public class ReviewQueryApiController {
3131

3232
@Operation(summary = "리뷰 단건 조회", description = "리뷰 ID를 사용하여 단건 리뷰 조회")
3333
@GetMapping("/review/{id}")
34-
public ApiResponse<ReviewResponseDto> getById(@PathVariable Long id) {
34+
public ApiResponse<ReviewDetailResponseDto> getById(@PathVariable Long id) {
3535

3636
return ApiResponse.ok(
3737
200,
38-
reviewQueryUseCase.getReviewById(id),
38+
reviewQueryUseCase.getDetailById(id),
3939
"리뷰 단건 조회 성공"
4040
);
4141
}
4242

4343
@Operation(summary = "기관별 리뷰 조회", description = "기관 ID를 사용하여 리뷰 조회")
4444
@GetMapping("/reviews/center/{centerId}")
45-
public ApiResponse<Page<ReviewWithNicknameResponseDto>> getReviewsByCenterId(
45+
public ApiResponse<Page<ReviewDetailWithNicknameResponseDto>> getReviewsByCenterId(
4646
@PathVariable UUID centerId,
4747
@PageableDefault(sort = "created_at", direction = DESC) Pageable pageable,
4848
@RequestParam(required = false) VolunteerCategory category
@@ -54,14 +54,14 @@ public ApiResponse<Page<ReviewWithNicknameResponseDto>> getReviewsByCenterId(
5454

5555
return ApiResponse.ok(
5656
200,
57-
reviewQueryUseCase.getReviewsByCenterId(centerId, condition),
57+
reviewQueryUseCase.getDetailsWithNicknameByCenterId(centerId, condition),
5858
"기관 리뷰 리스트 조회 성공"
5959
);
6060
}
6161

6262
@Operation(summary = "봉사자 리뷰 조회", description = "봉사자 ID를 사용하여 리뷰 조회")
6363
@GetMapping("/reviews/volunteer/{volunteerId}")
64-
public ApiResponse<Page<ReviewWithNicknameResponseDto>> getReviewsByVolunteerId(
64+
public ApiResponse<Page<ReviewDetailWithNicknameResponseDto>> getReviewsByVolunteerId(
6565
@PathVariable UUID volunteerId,
6666
@PageableDefault(sort = "created_at", direction = DESC) Pageable pageable,
6767
@RequestParam(required = false) VolunteerCategory category
@@ -73,7 +73,7 @@ public ApiResponse<Page<ReviewWithNicknameResponseDto>> getReviewsByVolunteerId(
7373

7474
return ApiResponse.ok(
7575
200,
76-
reviewQueryUseCase.getReviewsByVolunteerId(volunteerId, condition),
76+
reviewQueryUseCase.getDetailsWithNicknameByVolunteerId(volunteerId, condition),
7777
"유저 리뷰 리스트 조회 성공"
7878
);
7979
}

src/main/java/com/somemore/domains/review/domain/Review.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static jakarta.persistence.GenerationType.IDENTITY;
44
import static lombok.AccessLevel.PROTECTED;
55

6+
import com.somemore.domains.review.dto.request.ReviewUpdateRequestDto;
67
import com.somemore.global.common.entity.BaseEntity;
78
import jakarta.persistence.Column;
89
import jakarta.persistence.Entity;
@@ -50,4 +51,17 @@ public Review(Long volunteerApplyId, UUID volunteerId, String title,
5051
this.content = content;
5152
this.imgUrl = imgUrl;
5253
}
54+
55+
public boolean isAuthor(UUID volunteerId) {
56+
return this.volunteerId.equals(volunteerId);
57+
}
58+
59+
public void updateWith(ReviewUpdateRequestDto dto) {
60+
this.title = dto.title();
61+
this.content = dto.content();
62+
}
63+
64+
public void updateWith(String imgUrl) {
65+
this.imgUrl = imgUrl;
66+
}
5367
}

src/main/java/com/somemore/domains/review/dto/request/ReviewCreateRequestDto.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
package com.somemore.domains.review.dto.request;
22

3-
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
3+
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
44
import com.fasterxml.jackson.databind.annotation.JsonNaming;
5-
import com.somemore.domains.volunteerapply.domain.VolunteerApply;
65
import com.somemore.domains.review.domain.Review;
76
import io.swagger.v3.oas.annotations.media.Schema;
87
import jakarta.validation.constraints.NotBlank;
98
import jakarta.validation.constraints.NotNull;
10-
import java.util.UUID;
119
import lombok.Builder;
1210

13-
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
11+
import java.util.UUID;
12+
13+
@JsonNaming(SnakeCaseStrategy.class)
1414
@Builder
1515
public record ReviewCreateRequestDto(
16-
@Schema(description = "봉사 모집글 아이디", example = "1")
17-
@NotNull(message = "봉사 모집글 아이디는 필수 값입니다.")
18-
Long recruitBoardId,
16+
@Schema(description = "봉사 지원 아이디", example = "1")
17+
@NotNull(message = "봉사 지원 아이디는 필수 값입니다.")
18+
Long volunteerApplyId,
1919
@Schema(description = "리뷰 제목", example = "내 인생 최고의 봉사 활동")
2020
@NotBlank(message = "리뷰 제목은 필수 값입니다.")
2121
String title,
@@ -24,9 +24,9 @@ public record ReviewCreateRequestDto(
2424
String content
2525
) {
2626

27-
public Review toEntity(VolunteerApply apply, UUID volunteerId, String imgUrl) {
27+
public Review toEntity(UUID volunteerId, String imgUrl) {
2828
return Review.builder()
29-
.volunteerApplyId(apply.getId())
29+
.volunteerApplyId(volunteerApplyId)
3030
.volunteerId(volunteerId)
3131
.title(title)
3232
.content(content)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.somemore.domains.review.dto.request;
2+
3+
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
4+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import jakarta.validation.constraints.NotBlank;
7+
import lombok.Builder;
8+
9+
@JsonNaming(SnakeCaseStrategy.class)
10+
@Builder
11+
public record ReviewUpdateRequestDto(
12+
@Schema(description = "리뷰 제목", example = "내 인생 최고의 봉사 활동")
13+
@NotBlank(message = "리뷰 제목은 필수 값입니다.")
14+
String title,
15+
@Schema(description = "리뷰 내용", example = "담당자님도 정말 친절하였고 정말 보람찬 봉사였어요 <br>")
16+
@NotBlank(message = "리뷰 내용은 필수 값입니다.")
17+
String content
18+
) {
19+
}

src/main/java/com/somemore/domains/review/dto/response/ReviewResponseDto.java renamed to src/main/java/com/somemore/domains/review/dto/response/ReviewDetailResponseDto.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
@Builder
1212
@JsonNaming(SnakeCaseStrategy.class)
13-
@Schema(description = "리뷰 응답 DTO")
14-
public record ReviewResponseDto(
13+
@Schema(description = "리뷰 상세 조회 응답 DTO")
14+
public record ReviewDetailResponseDto(
1515
@Schema(description = "리뷰 ID", example = "123")
1616
Long id,
1717
@Schema(description = "봉사자(작성자) ID", example = "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d")
@@ -28,8 +28,8 @@ public record ReviewResponseDto(
2828
LocalDateTime updatedAt
2929
) {
3030

31-
public static ReviewResponseDto from(Review review) {
32-
return ReviewResponseDto.builder()
31+
public static ReviewDetailResponseDto from(Review review) {
32+
return ReviewDetailResponseDto.builder()
3333
.id(review.getId())
3434
.volunteerId(review.getVolunteerId())
3535
.title(review.getTitle())

src/main/java/com/somemore/domains/review/dto/response/ReviewWithNicknameResponseDto.java renamed to src/main/java/com/somemore/domains/review/dto/response/ReviewDetailWithNicknameResponseDto.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
@Builder
1212
@JsonNaming(SnakeCaseStrategy.class)
1313
@Schema(description = "작성자 닉네임이 포함된 리뷰 응답 DTO")
14-
public record ReviewWithNicknameResponseDto(
14+
public record ReviewDetailWithNicknameResponseDto(
1515
@Schema(description = "리뷰 ID", example = "123")
1616
Long id,
1717
@Schema(description = "봉사자(작성자) ID", example = "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d")
@@ -30,8 +30,8 @@ public record ReviewWithNicknameResponseDto(
3030
LocalDateTime updateAt
3131
) {
3232

33-
public static ReviewWithNicknameResponseDto from(Review review, String volunteerNickname) {
34-
return ReviewWithNicknameResponseDto.builder()
33+
public static ReviewDetailWithNicknameResponseDto from(Review review, String volunteerNickname) {
34+
return ReviewDetailWithNicknameResponseDto.builder()
3535
.id(review.getId())
3636
.volunteerId(review.getVolunteerId())
3737
.volunteerNickname(volunteerNickname)

src/main/java/com/somemore/domains/review/service/CreateReviewService.java

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static com.somemore.global.exception.ExceptionMessage.REVIEW_RESTRICTED_TO_ATTENDED;
55

66
import com.somemore.domains.review.repository.ReviewRepository;
7+
import com.somemore.domains.review.usecase.ReviewQueryUseCase;
78
import com.somemore.domains.volunteerapply.domain.VolunteerApply;
89
import com.somemore.domains.volunteerapply.usecase.VolunteerApplyQueryUseCase;
910
import com.somemore.global.exception.BadRequestException;
@@ -23,24 +24,24 @@
2324
public class CreateReviewService implements CreateReviewUseCase {
2425

2526
private final ReviewRepository reviewRepository;
27+
private final ReviewQueryUseCase reviewQueryUseCase;
2628
private final VolunteerApplyQueryUseCase volunteerApplyQueryUseCase;
2729

2830
@Override
2931
public Long createReview(ReviewCreateRequestDto requestDto, UUID volunteerId, String imgUrl) {
30-
VolunteerApply apply = getVolunteerApply(requestDto.recruitBoardId(), volunteerId);
31-
validateDuplicateReview(apply);
32+
validateDuplicateReview(requestDto.volunteerApplyId());
33+
34+
VolunteerApply apply = volunteerApplyQueryUseCase.getById(requestDto.volunteerApplyId());
3235
validateActivityCompletion(apply);
3336

34-
Review review = requestDto.toEntity(apply, volunteerId, imgUrl);
35-
return reviewRepository.save(review).getId();
36-
}
37+
Review review = requestDto.toEntity(volunteerId, imgUrl);
38+
reviewRepository.save(review);
3739

38-
private VolunteerApply getVolunteerApply(Long recruitBoardId, UUID volunteerId) {
39-
return volunteerApplyQueryUseCase.getByRecruitIdAndVolunteerId(recruitBoardId, volunteerId);
40+
return review.getId();
4041
}
4142

42-
private void validateDuplicateReview(VolunteerApply apply) {
43-
if (reviewRepository.existsByVolunteerApplyId(apply.getId())) {
43+
private void validateDuplicateReview(Long volunteerApplyId) {
44+
if (reviewQueryUseCase.existsByVolunteerApplyId(volunteerApplyId)) {
4445
throw new DuplicateException(REVIEW_ALREADY_EXISTS);
4546
}
4647
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.somemore.domains.review.service;
2+
3+
import com.somemore.domains.review.domain.Review;
4+
import com.somemore.domains.review.service.validator.ReviewValidator;
5+
import com.somemore.domains.review.usecase.DeleteReviewUseCase;
6+
import com.somemore.domains.review.usecase.ReviewQueryUseCase;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.stereotype.Service;
9+
import org.springframework.transaction.annotation.Transactional;
10+
11+
import java.util.UUID;
12+
13+
@RequiredArgsConstructor
14+
@Transactional
15+
@Service
16+
public class DeleteReviewService implements DeleteReviewUseCase {
17+
18+
private final ReviewQueryUseCase reviewQueryUseCase;
19+
private final ReviewValidator reviewValidator;
20+
21+
@Override
22+
public void deleteReview(UUID volunteerId, Long id) {
23+
Review review = reviewQueryUseCase.getById(id);
24+
25+
reviewValidator.validateAuthor(review, volunteerId);
26+
review.markAsDeleted();
27+
}
28+
29+
}

0 commit comments

Comments
 (0)