Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6d2cefb
feat(volunteer-apply): 기본 엔티티 레포지토리 추가
leebs0521 Dec 1, 2024
e6f0414
feat(volunteer-apply): 기본 엔티티 레포지토리 테스트
leebs0521 Dec 1, 2024
7654061
feat(volunteer-apply): 리뷰 생성에 필요한 조회 기능 추가
leebs0521 Dec 1, 2024
d8106ff
test(volunteer-apply): 리뷰 생성에 필요한 조회 기능 테스트
leebs0521 Dec 1, 2024
62ffd5c
feat(review): 리뷰 엔티티 레포지토리 생성
leebs0521 Dec 1, 2024
3ffa4b9
test(review): 리뷰 엔티티 레포지토리 테스트
leebs0521 Dec 1, 2024
282d5f2
feat(review): 리뷰 생성 기능 추가
leebs0521 Dec 1, 2024
e64afd8
test(review): 리뷰 생성 기능 테스트
leebs0521 Dec 1, 2024
5aa9c90
fix: conflict remove
leebs0521 Dec 1, 2024
9846ca4
refactor: 불필요한 import 제거
leebs0521 Dec 1, 2024
47ac38e
chore(review): import 추가
leebs0521 Dec 1, 2024
d89ae97
feat(review): 리뷰 등록 컨트롤러
leebs0521 Dec 1, 2024
53fd38e
test(review): 리뷰 등록 컨트롤러 테스트
leebs0521 Dec 1, 2024
db50577
feat(exception-handler): 400 에러 title 문구 변경
leebs0521 Dec 1, 2024
d2bfe3c
test(review): 리뷰 사항 반영
leebs0521 Dec 1, 2024
fbb7ac6
refactor(review): @AuthenticationPrincipal 을 사용하여 userId 가져오도록 변경
leebs0521 Dec 1, 2024
b73407e
hotfix(recruit-board): recruitBoard 수정 시 이미지 파일 optional 변경
leebs0521 Dec 1, 2024
b2996f3
refactor(review): 생성 RequestDto 예제 문구 수정
leebs0521 Dec 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 0 additions & 27 deletions src/main/java/com/somemore/domains/Review.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ ProblemDetail handleBadRequestException(final BadRequestException e) {
//status와 에러에 대한 자세한 설명
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage());

//아래와 같이 필드 확장 가능
problemDetail.setTitle("무슨 에러입니다");
// 아래와 같이 필드 확장 가능
problemDetail.setTitle("잘못된 요청입니다");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋아요 ㅎㅎ

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

감사합니다


return problemDetail;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public ApiResponse<String> 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Long> 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);
}

}
53 changes: 53 additions & 0 deletions src/main/java/com/somemore/review/domain/Review.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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 = "담당자님도 정말 친절하였고 정말 보람찬 봉사였어요 <br>")
@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();
}
}
Original file line number Diff line number Diff line change
@@ -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<Review, Long> {

Optional<Review> findByIdAndDeletedFalse(Long id);

@Query("SELECT COUNT(r) > 0 FROM Review r WHERE r.volunteerApplyId = :volunteerId AND r.deleted = false")
boolean existsByVolunteerApplyId(Long volunteerId);

}
13 changes: 13 additions & 0 deletions src/main/java/com/somemore/review/repository/ReviewRepository.java
Original file line number Diff line number Diff line change
@@ -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<Review> findById(Long id);

boolean existsByVolunteerApplyId(Long volunteerApplyId);
}
Original file line number Diff line number Diff line change
@@ -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<Review> findById(Long id) {
return reviewJpaRepository.findByIdAndDeletedFalse(id);
}

@Override
public boolean existsByVolunteerApplyId(Long volunteerApplyId) {
return reviewJpaRepository.existsByVolunteerApplyId(volunteerApplyId);
}
}
52 changes: 52 additions & 0 deletions src/main/java/com/somemore/review/service/CreateReviewService.java
Original file line number Diff line number Diff line change
@@ -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());
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/somemore/review/usecase/CreateReviewUseCase.java
Original file line number Diff line number Diff line change
@@ -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);


}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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;
}
}
Loading