Skip to content

Commit 1c5f3b8

Browse files
authored
feat: 리뷰 등록 (#102)
* feat(volunteer-apply): 기본 엔티티 레포지토리 추가 * feat(volunteer-apply): 기본 엔티티 레포지토리 테스트 * feat(volunteer-apply): 리뷰 생성에 필요한 조회 기능 추가 * test(volunteer-apply): 리뷰 생성에 필요한 조회 기능 테스트 * feat(review): 리뷰 엔티티 레포지토리 생성 * test(review): 리뷰 엔티티 레포지토리 테스트 * feat(review): 리뷰 생성 기능 추가 * test(review): 리뷰 생성 기능 테스트 * fix: conflict remove * refactor: 불필요한 import 제거 * chore(review): import 추가 * feat(review): 리뷰 등록 컨트롤러 * test(review): 리뷰 등록 컨트롤러 테스트 * feat(exception-handler): 400 에러 title 문구 변경 * test(review): 리뷰 사항 반영 * refactor(review): @AuthenticationPrincipal 을 사용하여 userId 가져오도록 변경 * hotfix(recruit-board): recruitBoard 수정 시 이미지 파일 optional 변경 * refactor(review): 생성 RequestDto 예제 문구 수정
1 parent f14f2f1 commit 1c5f3b8

23 files changed

+790
-73
lines changed

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

Lines changed: 0 additions & 27 deletions
This file was deleted.

src/main/java/com/somemore/global/exception/ExceptionMessage.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ public enum ExceptionMessage {
2424
NOT_EXISTS_VOLUNTEER("존재하지 않는 봉사자입니다."),
2525
UNAUTHORIZED_VOLUNTEER_DETAIL("해당 봉사자의 상세 정보 조회 권한이 없습니다."),
2626
CANNOT_CANCEL_DELETED_INTEREST_CENTER("이미 삭제된 관심 기관은 취소할 수 없습니다."),
27-
DUPLICATE_INTEREST_CENTER("이미 관심 표시한 기관입니다.")
27+
DUPLICATE_INTEREST_CENTER("이미 관심 표시한 기관입니다."),
28+
NOT_EXISTS_VOLUNTEER_APPLY("존재하지 않는 봉사 활동 지원입니다."),
29+
REVIEW_ALREADY_EXISTS("이미 작성한 리뷰가 존재합니다."),
30+
REVIEW_RESTRICTED_TO_ATTENDED("리뷰는 참석한 봉사에 한해서만 작성할 수 있습니다.")
2831
;
2932

3033
private final String message;

src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ ProblemDetail handleBadRequestException(final BadRequestException e) {
1919
//status와 에러에 대한 자세한 설명
2020
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage());
2121

22-
//아래와 같이 필드 확장 가능
23-
problemDetail.setTitle("무슨 에러입니다");
22+
// 아래와 같이 필드 확장 가능
23+
problemDetail.setTitle("잘못된 요청입니다");
2424

2525
return problemDetail;
2626
}

src/main/java/com/somemore/recruitboard/controller/RecruitBoardCommandApiController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public ApiResponse<String> updateRecruitBoard(
6868
@AuthenticationPrincipal String userId,
6969
@PathVariable Long id,
7070
@Valid @RequestPart("data") RecruitBoardUpdateRequestDto requestDto,
71-
@RequestPart("img_file") MultipartFile image
71+
@RequestPart(value = "img_file", required = false) MultipartFile image
7272
) {
7373
String imgUrl = imageUploadUseCase.uploadImage(new ImageUploadRequestDto(image));
7474
updateRecruitBoardUseCase.updateRecruitBoard(requestDto, id, getCenterId(userId), imgUrl);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.somemore.review.controller;
2+
3+
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
4+
5+
import com.somemore.global.common.response.ApiResponse;
6+
import com.somemore.imageupload.dto.ImageUploadRequestDto;
7+
import com.somemore.imageupload.usecase.ImageUploadUseCase;
8+
import com.somemore.review.dto.request.ReviewCreateRequestDto;
9+
import com.somemore.review.usecase.CreateReviewUseCase;
10+
import io.swagger.v3.oas.annotations.Operation;
11+
import io.swagger.v3.oas.annotations.tags.Tag;
12+
import jakarta.validation.Valid;
13+
import java.util.UUID;
14+
import lombok.RequiredArgsConstructor;
15+
import org.springframework.security.access.annotation.Secured;
16+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
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;
21+
import org.springframework.web.multipart.MultipartFile;
22+
23+
@Tag(name = "Review Command API", description = "리뷰 생성 수정 삭제 API")
24+
@RequiredArgsConstructor
25+
@RequestMapping("/api")
26+
@RestController
27+
public class ReviewCommandApiController {
28+
29+
private final CreateReviewUseCase createReviewUseCase;
30+
private final ImageUploadUseCase imageUploadUseCase;
31+
32+
@Secured("ROLE_VOLUNTEER")
33+
@Operation(summary = "리뷰 등록", description = "리뷰를 등록합니다.")
34+
@PostMapping(value = "/review", consumes = MULTIPART_FORM_DATA_VALUE)
35+
public ApiResponse<Long> createReview(
36+
@AuthenticationPrincipal String userId,
37+
@Valid @RequestPart("data") ReviewCreateRequestDto requestDto,
38+
@RequestPart(value = "img_file", required = false) MultipartFile image) {
39+
40+
String imgUrl = imageUploadUseCase.uploadImage(new ImageUploadRequestDto(image));
41+
return ApiResponse.ok(
42+
201,
43+
createReviewUseCase.createReview(requestDto, getId(userId), imgUrl),
44+
"리뷰 등록 성공"
45+
);
46+
}
47+
48+
private static UUID getId(String id) {
49+
return UUID.fromString(id);
50+
}
51+
52+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.somemore.review.domain;
2+
3+
import static jakarta.persistence.GenerationType.IDENTITY;
4+
import static lombok.AccessLevel.PROTECTED;
5+
6+
import com.somemore.global.common.BaseEntity;
7+
import jakarta.persistence.Column;
8+
import jakarta.persistence.Entity;
9+
import jakarta.persistence.GeneratedValue;
10+
import jakarta.persistence.Id;
11+
import jakarta.persistence.Lob;
12+
import jakarta.persistence.Table;
13+
import java.util.UUID;
14+
import lombok.Builder;
15+
import lombok.Getter;
16+
import lombok.NoArgsConstructor;
17+
18+
@Getter
19+
@NoArgsConstructor(access = PROTECTED)
20+
@Entity
21+
@Table(name = "review")
22+
public class Review extends BaseEntity {
23+
24+
@Id
25+
@GeneratedValue(strategy = IDENTITY)
26+
private Long id;
27+
28+
@Column(name = "volunteer_apply_id", nullable = false)
29+
private Long volunteerApplyId;
30+
31+
@Column(name = "volunteer_id", nullable = false, length = 16)
32+
private UUID volunteerId;
33+
34+
@Column(name = "title", nullable = false)
35+
private String title;
36+
37+
@Lob
38+
@Column(name = "content", nullable = false)
39+
private String content;
40+
41+
@Column(name = "img_url", nullable = false)
42+
private String imgUrl;
43+
44+
@Builder
45+
public Review(Long volunteerApplyId, UUID volunteerId, String title,
46+
String content, String imgUrl) {
47+
this.volunteerApplyId = volunteerApplyId;
48+
this.volunteerId = volunteerId;
49+
this.title = title;
50+
this.content = content;
51+
this.imgUrl = imgUrl;
52+
}
53+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.somemore.review.dto.request;
2+
3+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
4+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
5+
import com.somemore.review.domain.Review;
6+
import com.somemore.volunteerapply.domain.VolunteerApply;
7+
import io.swagger.v3.oas.annotations.media.Schema;
8+
import jakarta.validation.constraints.NotBlank;
9+
import jakarta.validation.constraints.NotNull;
10+
import java.util.UUID;
11+
import lombok.Builder;
12+
13+
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
14+
@Builder
15+
public record ReviewCreateRequestDto(
16+
@Schema(description = "봉사 모집글 아이디", example = "1")
17+
@NotNull(message = "봉사 모집글 아이디는 필수 값입니다.")
18+
Long recruitBoardId,
19+
@Schema(description = "리뷰 제목", example = "내 인생 최고의 봉사 활동")
20+
@NotBlank(message = "리뷰 제목은 필수 값입니다.")
21+
String title,
22+
@Schema(description = "리뷰 내용", example = "담당자님도 정말 친절하였고 정말 보람찬 봉사였어요 <br>")
23+
@NotBlank(message = "리뷰 내용은 필수 값입니다.")
24+
String content
25+
) {
26+
27+
public Review toEntity(VolunteerApply apply, UUID volunteerId, String imgUrl) {
28+
return Review.builder()
29+
.volunteerApplyId(apply.getId())
30+
.volunteerId(volunteerId)
31+
.title(title)
32+
.content(content)
33+
.imgUrl(imgUrl)
34+
.build();
35+
}
36+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.somemore.review.repository;
2+
3+
import com.somemore.review.domain.Review;
4+
import java.util.Optional;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Query;
7+
8+
public interface ReviewJpaRepository extends JpaRepository<Review, Long> {
9+
10+
Optional<Review> findByIdAndDeletedFalse(Long id);
11+
12+
@Query("SELECT COUNT(r) > 0 FROM Review r WHERE r.volunteerApplyId = :volunteerId AND r.deleted = false")
13+
boolean existsByVolunteerApplyId(Long volunteerId);
14+
15+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.somemore.review.repository;
2+
3+
import com.somemore.review.domain.Review;
4+
import java.util.Optional;
5+
6+
public interface ReviewRepository {
7+
8+
Review save(Review review);
9+
10+
Optional<Review> findById(Long id);
11+
12+
boolean existsByVolunteerApplyId(Long volunteerApplyId);
13+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.somemore.review.repository;
2+
3+
import com.querydsl.jpa.impl.JPAQueryFactory;
4+
import com.somemore.review.domain.Review;
5+
import java.util.Optional;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.stereotype.Repository;
8+
9+
@RequiredArgsConstructor
10+
@Repository
11+
public class ReviewRepositoryImpl implements ReviewRepository {
12+
13+
private final ReviewJpaRepository reviewJpaRepository;
14+
private final JPAQueryFactory queryFactory;
15+
16+
@Override
17+
public Review save(Review review) {
18+
return reviewJpaRepository.save(review);
19+
}
20+
21+
@Override
22+
public Optional<Review> findById(Long id) {
23+
return reviewJpaRepository.findByIdAndDeletedFalse(id);
24+
}
25+
26+
@Override
27+
public boolean existsByVolunteerApplyId(Long volunteerApplyId) {
28+
return reviewJpaRepository.existsByVolunteerApplyId(volunteerApplyId);
29+
}
30+
}

0 commit comments

Comments
 (0)