Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion src/main/java/kr/mayb/controller/OrderController.java
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public ResponseEntity<ApiResponse<PageResponse<OrderInfo, List<ProductSimple>>>>
@PatchMapping("orders/{orderId}/members/{memberId}/payment-status")
public ResponseEntity<ApiResponse<OrderInfo>> updatePaymentStatus(@PathVariable long orderId,
@PathVariable long memberId,
@RequestBody @Valid OrderController.PaymentStatusUpdateRequest request) {
@RequestBody @Valid PaymentStatusUpdateRequest request) {
OrderInfo response = orderFacade.updatePaymentStatus(orderId, memberId, request.status());
return Responses.ok(response);

Expand Down
102 changes: 102 additions & 0 deletions src/main/java/kr/mayb/controller/ReviewController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package kr.mayb.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import kr.mayb.dto.ImageDto;
import kr.mayb.dto.ReviewDto;
import kr.mayb.dto.ReviewRequest;
import kr.mayb.error.BadRequestException;
import kr.mayb.facade.ReviewFacade;
import kr.mayb.security.PermitAll;
import kr.mayb.security.PermitAuthenticated;
import kr.mayb.util.request.PageRequest;
import kr.mayb.util.response.ApiResponse;
import kr.mayb.util.response.PageResponse;
import kr.mayb.util.response.Responses;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@Tag(name = "Review", description = "리뷰 API")
@RestController
@RequiredArgsConstructor
public class ReviewController {

private final ReviewFacade reviewFacade;

@Operation(summary = "상품 리뷰 작성")
@PermitAuthenticated
@PostMapping("/reviews")
public ResponseEntity<ApiResponse<ReviewDto>> writeReview(@RequestPart("review") ReviewRequest request,
@RequestPart(value = "images", required = false) List<MultipartFile> images) {
if (images.size() > 5) {
throw new BadRequestException("Review images must be less than 5");
}

ReviewDto response = reviewFacade.writeReview(request, images);
return Responses.ok(response);
}

@Operation(summary = "상품 리뷰 상세 조회")
@PermitAll
@GetMapping("/reviews/{reviewId}")
public ResponseEntity<ApiResponse<ReviewDto>> getReview(@PathVariable long reviewId) {
ReviewDto response = reviewFacade.getReview(reviewId);
return Responses.ok(response);
}

@Operation(summary = "상품 리뷰 조회")
@PermitAll
@GetMapping("/reviews")
public ResponseEntity<ApiResponse<PageResponse<ReviewDto, Void>>> getReviews(@RequestParam(value = "pid") long productId,
PageRequest pageRequest) {
PageResponse<ReviewDto, Void> response = reviewFacade.getReviews(productId, pageRequest);
return Responses.ok(response);
}

@Operation(summary = "상품 리뷰 수정")
@PermitAuthenticated
@PutMapping("/reviews/{reviewId}")
public ResponseEntity<ApiResponse<ReviewDto>> updateReview(@PathVariable long reviewId, @RequestBody @Valid ReviewUpdateRequest request) {
ReviewDto response = reviewFacade.updateReview(reviewId, request.content(), request.starRating());
return Responses.ok(response);
}

@Operation(summary = "상품 리뷰 이미지 추가 (리뷰 수정 시)")
@PermitAuthenticated
@PostMapping("/reviews/{reviewId}/images")
public ResponseEntity<ApiResponse<ImageDto>> addReviewImage(@PathVariable long reviewId, @RequestParam("image") MultipartFile image) {
ImageDto response = reviewFacade.addReviewImage(reviewId, image);
return Responses.ok(response);
}

@Operation(summary = "상품 리뷰 이미지 삭제 (리뷰 수정 시)")
@PermitAuthenticated
@DeleteMapping("/reviews/{reviewId}/images/{imageId}")
public ResponseEntity<Void> removeReviewImage(@PathVariable long reviewId, @PathVariable long imageId) {
reviewFacade.removeReviewImage(reviewId, imageId);
return Responses.noContent();
}

@Operation(summary = "상품 리뷰 삭제")
@PermitAuthenticated
@DeleteMapping("/reviews/{reviewId}")
public ResponseEntity<Void> removeReview(@PathVariable long reviewId) {
reviewFacade.removeReview(reviewId);
return Responses.noContent();
}

private record ReviewUpdateRequest(
@NotBlank
String content,
@NotNull
int starRating
) {
}
}
12 changes: 12 additions & 0 deletions src/main/java/kr/mayb/data/model/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import kr.mayb.enums.Gender;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.annotations.BatchSize;

import java.time.LocalDate;
Expand Down Expand Up @@ -65,4 +66,15 @@ public class Member extends BaseEntity{
@Column
@Enumerated(EnumType.STRING)
private AccountStatus status;

public String getMaskedName() {
if (StringUtils.isBlank(this.name) || this.name.length() <= 1) {
return this.name;
}

String firstChar = this.name.substring(0, 1);
String masked = this.name.substring(1).replaceAll("\\.", "*");

return firstChar + masked;
}
}
51 changes: 51 additions & 0 deletions src/main/java/kr/mayb/data/model/Review.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package kr.mayb.data.model;

import jakarta.persistence.*;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.BatchSize;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Getter
@Setter
@Table(schema = "mayb")
@Entity
public class Review extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;

@Column(nullable = false)
private String content;

@Min(1)
@Max(5)
@Column(nullable = false)
private int starRating;

@Column
private String gender;

@Column
private LocalDateTime scheduledAt;

@Column
private long productId;

@Column
private long orderId;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;

@BatchSize(size = 10)
@OneToMany(mappedBy = "review", cascade = CascadeType.ALL)
private List<ReviewImage> reviewImages = new ArrayList<>();
}
23 changes: 23 additions & 0 deletions src/main/java/kr/mayb/data/model/ReviewImage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package kr.mayb.data.model;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Table(schema = "mayb")
@Entity
public class ReviewImage {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;

@Column
private String imageUrl;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "review_id", nullable = false)
private Review review;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kr.mayb.data.repository;

import kr.mayb.data.model.ReviewImage;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ReviewImageRepository extends JpaRepository<ReviewImage, Long> {
}
12 changes: 12 additions & 0 deletions src/main/java/kr/mayb/data/repository/ReviewRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package kr.mayb.data.repository;

import kr.mayb.data.model.Review;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ReviewRepository extends JpaRepository<Review, Long> {
@EntityGraph(attributePaths = {"reviewImages", "member"})
Page<Review> findAllByProductId(long productId, Pageable pageable);
}
12 changes: 12 additions & 0 deletions src/main/java/kr/mayb/dto/ImageDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package kr.mayb.dto;

import kr.mayb.data.model.ReviewImage;

public record ImageDto(
long reviewImageId,
String imageUrl
) {
public static ImageDto of(ReviewImage reviewImage) {
return new ImageDto(reviewImage.getId(), reviewImage.getImageUrl());
}
}
53 changes: 53 additions & 0 deletions src/main/java/kr/mayb/dto/ReviewDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package kr.mayb.dto;

import kr.mayb.data.model.Member;
import kr.mayb.data.model.Review;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.List;

public record ReviewDto(
long reviewId,
String content,
String author,
int starRating,
String gender,
LocalDateTime scheduledAt,
long memberId,
List<ImageDto> images,
OffsetDateTime createdAt,
boolean isMyReview
) {
private ReviewDto(Review review, List<ImageDto> images, Member author, boolean isMyReview) {
this(
review.getId(),
review.getContent(),
author.getMaskedName(),
review.getStarRating(),
review.getGender(),
review.getScheduledAt(),
author.getId(),
images,
review.getCreatedAt(),
isMyReview
);
}

public static ReviewDto of(Review review) {
Member author = review.getMember();
List<ImageDto> images = review.getReviewImages().stream().map(ImageDto::of).toList();
return new ReviewDto(review, images, author, false);
}

public static ReviewDto of(Review review, long currentMemberId) {
Member author = review.getMember();
List<ImageDto> images = review.getReviewImages().stream().map(ImageDto::of).toList();
boolean isMyReview = isMyReview(author.getId(), currentMemberId);
return new ReviewDto(review, images, author, isMyReview);
}

private static boolean isMyReview(long authorId, long currentMemberId) {
return authorId == currentMemberId;
}
}
17 changes: 17 additions & 0 deletions src/main/java/kr/mayb/dto/ReviewRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package kr.mayb.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record ReviewRequest(
long productId,

long orderId,

@Size(min = 1, max = 1000)
@NotBlank
String content,

int starRating
) {
}
2 changes: 2 additions & 0 deletions src/main/java/kr/mayb/enums/GcsBucketPath.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public enum GcsBucketPath {
PROFILE("profile/"),
PRODUCT_PROFILE("product_profile/"),
PRODUCT_DETAIL("product_detail/"),
REVIEW("review/")
;

private final String value;
Expand All @@ -17,6 +18,7 @@ public static String getPath(GcsBucketPath pathType) {
case PROFILE -> PROFILE.value;
case PRODUCT_PROFILE -> PRODUCT_PROFILE.value;
case PRODUCT_DETAIL -> PRODUCT_DETAIL.value;
case REVIEW -> REVIEW.value;
default -> throw new BadRequestException("Invalid GcsPathType" + pathType);
};
}
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/kr/mayb/enums/ReviewSort.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package kr.mayb.enums;

import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Sort;

import java.util.Arrays;
import java.util.Optional;

@RequiredArgsConstructor
public enum ReviewSort {
NEWEST_FIRST("createdAt"),
RATING_DESC("starRating"),
RATING_ASC("starRating"),
;

private final String value;

public static Optional<ReviewSort> find(String name) {
return Arrays.stream(ReviewSort.values())
.filter(v -> StringUtils.equalsIgnoreCase(v.name(), name))
.findFirst();
}

public Sort toSortOption() {
return switch (this) {
case NEWEST_FIRST -> Sort.by(Sort.Direction.DESC, NEWEST_FIRST.value);
case RATING_DESC -> Sort.by(Sort.Direction.DESC, RATING_DESC.value);
case RATING_ASC -> Sort.by(Sort.Direction.ASC, RATING_ASC.value);
};
}
}
Loading
Loading