105DAYS) [Main-Project] reservation 기본 구조 생성(1차 구현), reviewImage Multipart 이용하여 이미지 업로드

nacSeo (낙서)·2023년 3월 20일
0

주말 간 그 동안 미뤄왔던 reservation 파트를 구현 시작하였다.
Controller, Entity, Dto, Service, CompositeService, mapper, repository 등 기본 구조를 생성하면서 1차 구현을 한 뒤, reservation을 함께 구현하기로 한 BE 팀원에게 넘겨줬다.

오늘부터는 review 파트에서 처음 시도해보는 이미지 업로드 로직을 작성해야 한다. 배운 적도 없고 처음 접하는 기술이기에 여러 가지 의문점이 들었다.

처음 드는 의문이
그동안 Param이나 JSON같은 걸로만 받아왔는데 이미지 File은 뭘로 받아야할까? 였다. 그래서 찾아본 게, MultipartFile Interface다.
큰 File을 부분으로 쪼개서 효율적으로 File을 Upload할 수 있는 인터페이슨데 이를 통해, 필요한 이미지 File을 업로드하는 데에 적합할 것이라 판단하여 사용하게 됐다.

두 번째 의문은 하나의 API 요청에 JSON도 받아야 하고 MultipartFile도 받아야 한다는 것이었다. 그래서 찾아낸 게 @RequestPart 어노테이션이다.
Image File을 Multipart/form-data로 받아오기 위해 필요한 어노테이션이기도 했고 우리 프로젝트에서는 Image 파일 뿐만 아니라, Long이나 String이 포함된 JSON 타입으로도 Request를 받아야 했다. 따라서 RequestPart 어노테이션을 통해 form-data로 요청을 받아오는 방식을 선택하게 됐다.

마지막으로 AWS S3 Bucket 활용이었다. 한 번도 직접 AWS 계정을 파서 사용해본 적이 없어 막막하고 과금 이야기도 많이 들어본 적이 있어 약간의 두려움이 있었지만, 업로드한 이미지들을 저장할 수 있는 버킷 활용이 가장 일반적이고 좋은 방법이라 생각하여 경험해보고자 했다.

여러 블로그 중 우리 프로젝트에 적합하다 싶은 블로그 내용을 찾았고,
참고 블로그
이를 우리 코드에 맞게 적용시켜 보았다.

reviewCompositeService

@RequiredArgsConstructor
@Service
public class ReviewCompositeService {

    private final ReviewService reviewService;
    private final MemberService memberService;

    public Review createReview(Review creatingReview, MultipartFile reviewImage, String email) {
        Member member = memberService.findLoginMemberByEmail(email);

        creatingReview.setMember(member);

        Review createdReview = reviewService.createReview(creatingReview, reviewImage);

        return createdReview;
    }

    public Review updateReview(Review updatingReview, String email) {
        Member member = memberService.findLoginMemberByEmail(email);

        updatingReview.setMember(member);

        Review updatedReview = reviewService.updateReview(updatingReview, member.getMemberId());

        return updatedReview;
    }

    public Review getReview(Long reviewId) {
        Review foundReview = reviewService.findReview(reviewId);

        return foundReview;
    }

    public Page<Review> getHairShopReviews(long hairShopId, int page, int size) {
        return reviewService.findHairShopReviews(hairShopId,page-1, size);
    }

    public void deleteReview(Long reviewId, String email) {
        Member member = memberService.findLoginMemberByEmail(email);

        reviewService.deleteReview(reviewId, member.getMemberId());
    }
}

reviewController

@RestController
@RequestMapping("/reviews")
@RequiredArgsConstructor
@CrossOrigin
public class ReviewController {

    private final ReviewCompositeService compositeService;
    private final ReviewMapper mapper;
    @PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
    public ResponseEntity postReview(@RequestPart ReviewDto.Post postDto,
                                     @RequestPart MultipartFile reviewImage,
                                     Principal principal) {
        Review creatingReview = mapper.reviewPostDtoToReview(postDto);
        Review createdReview = compositeService.createReview(creatingReview, reviewImage, principal.getName());

        return ResponseEntity.status(HttpStatus.CREATED).location(URI.create("/reviews")).body(mapper.reviewToReviewResponseDto(createdReview));
    }

    @PatchMapping("/{review-id}")
    public ResponseEntity patchReview(@PathVariable("review-id") @Positive Long reviewId,
                                      @RequestBody ReviewDto.Patch patchDto, Principal principal) {
        patchDto.setReviewId(reviewId);

        Review updatingReview = mapper.reviewPatchDtoToReview(patchDto);
        Review updatedReview = compositeService.updateReview(updatingReview, principal.getName());
        ReviewDto.Response response = mapper.reviewToReviewResponseDto(updatedReview);

        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @GetMapping("/{review-id}")
    public ResponseEntity getReview(@PathVariable("review-id") @Positive Long reviewId) {
        Review review = compositeService.getReview(reviewId);
        ReviewDto.Response response = mapper.reviewToReviewResponseDto(review);

        return new ResponseEntity<>(response, HttpStatus.OK);
    }

    @GetMapping
    public ResponseEntity getHairShopReviews(@Positive @RequestParam Long hairShopId,
                                             @Positive @RequestParam int page,
                                             @Positive @RequestParam int size) {
        Page<Review> pageReviews = compositeService.getHairShopReviews(hairShopId, page, size);
        List<Review> reviews = pageReviews.getContent();

        MultiResponseDto response = new MultiResponseDto(mapper.reviewsToReviewResponseDto(reviews), pageReviews);

        return ResponseEntity.status(HttpStatus.OK).body(response);
    }

    @DeleteMapping("/{review-id}")
    public ResponseEntity deleteReview(@PathVariable("review-id") @Positive Long reviewId, Principal principal) {
        compositeService.deleteReview(reviewId, principal.getName());

        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }
}

reviewDto

public class ReviewDto {
    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Post {
        @NotBlank
        private long hairShopId;
//        @NotBlank
//        private MultipartFile reviewImage;
        @NotBlank
        private String reviewText;
        private Member member;
    }

    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Patch {
        private Long reviewId;
//        @Nullable
//        private String reviewImage;
        @Nullable
        private String reviewText;
        private Member member;
    }

    @Getter
    @Setter
    @AllArgsConstructor
    public static class Response {
        private Long reviewId;
//        private String reviewImage;
        private String reviewText;
        private LocalDateTime createdAt;
        private LocalDateTime modifiedAt;
    }

    @Getter
    @Setter
    @AllArgsConstructor
    public static class listResponse {
        private Long reviewId;
//        private String reviewImage;
        private String reviewText;
        private LocalDateTime createdAt;
    }
}

review (Entity)

@NoArgsConstructor
@Getter
@Setter
@Entity
public class Review {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long reviewId;

    @Column(nullable = false)
    private String reviewText;

//    @Transient
//    private MultipartFile reviewImage;

    @Column(nullable = false)
    private LocalDateTime createdAt;

    @Column(name = "LAST_MODIFIED_AT")
    private LocalDateTime modifiedAt;

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;

    @OneToMany(mappedBy = "review", cascade = CascadeType.ALL)
    private List<StyleLike> styleLikes = new ArrayList<>();

    @ManyToOne
    @JoinColumn(name = "HAIR_SHOP_ID")
    private HairShop hairShop;

    @Transient
    private int likeCount;

    @Transient
    private long myStyleLikeId;
}

reviewService

@Service
@RequiredArgsConstructor
public class ReviewService {
    private final ReviewRepository reviewRepository;

    public Review createReview(Review review, MultipartFile reviewImage) {
        review.setCreatedAt(LocalDateTime.now());
        review.setReviewImage(reviewImage);

        return reviewRepository.save(review);
    }

    public Review updateReview(Review review, Long memberId) {
        // 존재하는 리뷰인지 확인
        Review findReview = findVerifiedReview(review.getReviewId());
        // 멤버id와 로그인 멤버id를 비교하는 로직
        compareIdAndLoginId(findReview.getMember().getMemberId(), memberId);

//        Optional.ofNullable(review.getReviewImage())
//                .ifPresent(review_pic -> findReview.setReviewImage(review_pic));
        Optional.ofNullable(review.getReviewText())
                .ifPresent(review_text -> findReview.setReviewText(review_text));

        findReview.setModifiedAt(LocalDateTime.now());

        return reviewRepository.save(findReview);
    }

    public Review findReview(Long reviewId) {
        Review review = findVerifiedReview(reviewId);

        return review;
    }

    public Page<Review> findHairShopReviews(long hairShopId, int page, int size) {
        return reviewRepository.findAllByHairShopHairShopId(hairShopId, PageRequest.of(page, size, Sort.by("reviewId").descending()));
    }

    public void deleteReview(Long reviewId, Long memberId) {
        Review findReview = findVerifiedReview(reviewId);
        compareIdAndLoginId(findReview.getMember().getMemberId(), memberId);

        reviewRepository.delete(findReview);
    }

    // 존재하는 리뷰인지 확인
    private Review findVerifiedReview(Long reviewId) {
        Optional<Review> optionalReview = reviewRepository.findById(reviewId);

        Review findReview =
                optionalReview.orElseThrow(() -> null);

        return findReview;
    }
    // principal로 받아온 memberId랑 DB에 저장된 Review의 memberId 같은지 검증하는 로직
    // 틀리면 -> 요청 오류
    private void compareIdAndLoginId(Long id, Long memberId) {
        if(!id.equals(memberId))
            throw null;
    }
}

reviewService의 createReview에 MultipartFile reviewImage를 매개변수로 추가해주고 review의 이미지를 설정하는 로직을 추가해줬다.

reviewCompositeService에도 createReview 메서드에 MultipartFile 매개변수를 추가해주고, reviewService의 createReview를 가져올 때 reviewImage를 매개변수로 추가해주었다.

reviewController에는 @RequestPart 어노테이션으로 MultipartFile 데이터를 받아오고 작성해둔 service 로직을 적용시켰다.

나머지는 아직 완성을 못하고 주석 처리를 해두었다.

profile
백엔드 개발자 김창하입니다 🙇‍♂️

0개의 댓글