diff --git a/module-admin/src/main/java/com/kernel360/product/dto/ProductDto.java b/module-admin/src/main/java/com/kernel360/product/dto/ProductDto.java index 31ad43d7..ea8564bb 100644 --- a/module-admin/src/main/java/com/kernel360/product/dto/ProductDto.java +++ b/module-admin/src/main/java/com/kernel360/product/dto/ProductDto.java @@ -73,11 +73,4 @@ public static ProductDto from(Product entity) { entity.getModifiedBy() ); } - - public Product toEntity() { - return Product.of( - productNo, - productName - ); - } } diff --git a/module-admin/src/main/resources/application.yml b/module-admin/src/main/resources/application.yml index c31422aa..c74f5dee 100644 --- a/module-admin/src/main/resources/application.yml +++ b/module-admin/src/main/resources/application.yml @@ -13,3 +13,5 @@ spring: server: port: 8082 +module: + name: admin diff --git a/module-api/src/main/java/com/kernel360/file/repository/FileRepository.java b/module-api/src/main/java/com/kernel360/file/repository/FileRepository.java new file mode 100644 index 00000000..42dc1afb --- /dev/null +++ b/module-api/src/main/java/com/kernel360/file/repository/FileRepository.java @@ -0,0 +1,4 @@ +package com.kernel360.file.repository; + +public interface FileRepository extends FileRepositoryJpa { +} diff --git a/module-api/src/main/java/com/kernel360/member/dto/MemberDto.java b/module-api/src/main/java/com/kernel360/member/dto/MemberDto.java index 6c5b9af9..df3cc873 100644 --- a/module-api/src/main/java/com/kernel360/member/dto/MemberDto.java +++ b/module-api/src/main/java/com/kernel360/member/dto/MemberDto.java @@ -153,4 +153,26 @@ public static MemberDto of( null ); } + + /** find review **/ + public static MemberDto of( + Long memberNo, + String id, + int age, + int gender + ){ + return new MemberDto( + memberNo, + id, + null, + null, + Gender.ordinalToName(gender), + Age.ordinalToValue(age), + null, + null, + null, + null, + null + ); + } } \ No newline at end of file diff --git a/module-api/src/main/java/com/kernel360/product/dto/ProductDetailDto.java b/module-api/src/main/java/com/kernel360/product/dto/ProductDetailDto.java index d7f48dca..dd20c33a 100644 --- a/module-api/src/main/java/com/kernel360/product/dto/ProductDetailDto.java +++ b/module-api/src/main/java/com/kernel360/product/dto/ProductDetailDto.java @@ -1,10 +1,6 @@ package com.kernel360.product.dto; import com.kernel360.product.entity.Product; -import jakarta.persistence.Column; -import org.springframework.data.annotation.CreatedBy; -import org.springframework.data.annotation.LastModifiedBy; -import org.springframework.data.annotation.LastModifiedDate; import java.time.LocalDate; @@ -117,6 +113,51 @@ public static ProductDetailDto of( ); } + /** find review **/ + public static ProductDetailDto of( + Long productNo, + String productName, + String imageSource, + String companyName, + String upperItem, + String item + ) { + return new ProductDetailDto( + productNo, + productName, + null, + imageSource, + null, + null, + null, + companyName, + null, + null, + null, + upperItem, + item, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + } + public static ProductDetailDto from(Product entity) { return ProductDetailDto.of( entity.getProductNo(), diff --git a/module-api/src/main/java/com/kernel360/product/dto/ProductDto.java b/module-api/src/main/java/com/kernel360/product/dto/ProductDto.java index 31ad43d7..ea8564bb 100644 --- a/module-api/src/main/java/com/kernel360/product/dto/ProductDto.java +++ b/module-api/src/main/java/com/kernel360/product/dto/ProductDto.java @@ -73,11 +73,4 @@ public static ProductDto from(Product entity) { entity.getModifiedBy() ); } - - public Product toEntity() { - return Product.of( - productNo, - productName - ); - } } diff --git a/module-api/src/main/java/com/kernel360/review/code/ReviewErrorCode.java b/module-api/src/main/java/com/kernel360/review/code/ReviewErrorCode.java index 7ff710ec..e0004316 100644 --- a/module-api/src/main/java/com/kernel360/review/code/ReviewErrorCode.java +++ b/module-api/src/main/java/com/kernel360/review/code/ReviewErrorCode.java @@ -7,7 +7,8 @@ @RequiredArgsConstructor public enum ReviewErrorCode implements ErrorCode { INVALID_STAR_RATING_VALUE(HttpStatus.BAD_REQUEST.value(), "ERV001", "유효하지 않은 별점입니다."), - INVALID_REVIEW_WRITE_REQUEST(HttpStatus.BAD_REQUEST.value(), "ERV002", "리뷰가 중복되거나 유효하지 않습니다."); + INVALID_REVIEW_WRITE_REQUEST(HttpStatus.BAD_REQUEST.value(), "ERV002", "리뷰가 중복되거나 유효하지 않습니다."), + NOT_FOUND_REVIEW(HttpStatus.BAD_REQUEST.value(), "ERV003", "리뷰가 존재하지 않습니다."); private final int status; private final String code; diff --git a/module-api/src/main/java/com/kernel360/review/controller/ReviewController.java b/module-api/src/main/java/com/kernel360/review/controller/ReviewController.java index 0eaddc7c..df6a5f57 100644 --- a/module-api/src/main/java/com/kernel360/review/controller/ReviewController.java +++ b/module-api/src/main/java/com/kernel360/review/controller/ReviewController.java @@ -2,13 +2,17 @@ import com.kernel360.response.ApiResponse; import com.kernel360.review.code.ReviewBusinessCode; -import com.kernel360.review.dto.ReviewDto; +import com.kernel360.review.dto.ReviewResponseDto; +import com.kernel360.review.dto.ReviewRequestDto; import com.kernel360.review.service.ReviewService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; @RestController @RequiredArgsConstructor @@ -17,31 +21,44 @@ public class ReviewController { private final ReviewService reviewService; - @GetMapping("") - public ResponseEntity>> getReviewsByProduct( - @RequestParam(name = "productNo") Long productNo, + @GetMapping("/product/{productNo}") + public ResponseEntity>> getReviewsByProduct( + @PathVariable Long productNo, @RequestParam(name = "sortBy", defaultValue = "reviewNo", required = false) String sortBy, Pageable pageable) { return ApiResponse.toResponseEntity(ReviewBusinessCode.SUCCESS_GET_REVIEWS, reviewService.getReviewsByProduct(productNo, sortBy, pageable)); } + @GetMapping("/member/{memberNo}") + public ResponseEntity>> getReviewsByMember( + @PathVariable Long memberNo, + @RequestParam(name = "sortBy", defaultValue = "reviewNo", required = false) String sortBy, + Pageable pageable) { + + return ApiResponse.toResponseEntity(ReviewBusinessCode.SUCCESS_GET_REVIEWS, reviewService.getReviewsByMember(memberNo, sortBy, pageable)); + } + @GetMapping("/{reviewNo}") - public ResponseEntity> getReview(@PathVariable Long reviewNo) { + public ResponseEntity> getReview(@PathVariable Long reviewNo) { return ApiResponse.toResponseEntity(ReviewBusinessCode.SUCCESS_GET_REVIEW, reviewService.getReview(reviewNo)); } @PostMapping("") - public ResponseEntity> createReview(@RequestBody ReviewDto reviewDto) { - reviewService.createReview(reviewDto); + public ResponseEntity> createReview( + @RequestPart ReviewRequestDto review, + @RequestPart(required = false) List files) { + reviewService.createReview(review, files); return ApiResponse.toResponseEntity(ReviewBusinessCode.SUCCESS_CREATE_REVIEW); } @PatchMapping("") - public ResponseEntity> updateReview(@RequestBody ReviewDto reviewDto) { - reviewService.updateReview(reviewDto); + public ResponseEntity> updateReview( + @RequestPart ReviewRequestDto review, + @RequestPart(required = false) List files) { + reviewService.updateReview(review, files); return ApiResponse.toResponseEntity(ReviewBusinessCode.SUCCESS_UPDATE_REVIEW); } diff --git a/module-api/src/main/java/com/kernel360/review/dto/ReviewDto.java b/module-api/src/main/java/com/kernel360/review/dto/ReviewDto.java deleted file mode 100644 index 06579d25..00000000 --- a/module-api/src/main/java/com/kernel360/review/dto/ReviewDto.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.kernel360.review.dto; - -import com.kernel360.member.dto.MemberDto; -import com.kernel360.product.dto.ProductDto; -import com.kernel360.review.entity.Review; - -import java.math.BigDecimal; -import java.time.LocalDate; - -/** - * DTO for {@link com.kernel360.review.dto.ReviewDto} - */ -public record ReviewDto(Long reviewNo, - ProductDto productDto, - MemberDto memberDto, - BigDecimal starRating, - String title, - String contents, - LocalDate createdAt, - String createdBy, - LocalDate modifiedAt, - String modifiedBy) { - - public static ReviewDto of( - Long reviewNo, - ProductDto productDto, - MemberDto memberDto, - BigDecimal starRating, - String title, - String contents, - LocalDate createdAt, - String createdBy, - LocalDate modifiedAt, - String modifiedBy - ) { - return new ReviewDto( - reviewNo, - productDto, - memberDto, - starRating, - title, - contents, - createdAt, - createdBy, - modifiedAt, - modifiedBy - ); - } - - public static ReviewDto from(Review entity) { - return ReviewDto.of( - entity.getReviewNo(), - ProductDto.from(entity.getProduct()), - MemberDto.from(entity.getMember()), - entity.getStarRating(), - entity.getTitle(), - entity.getContents(), - entity.getCreatedAt(), - entity.getCreatedBy(), - entity.getModifiedAt(), - entity.getModifiedBy() - ); - } - - public Review toEntity() { - return Review.of( - reviewNo, - productDto.toEntity(), - memberDto.toEntity(), - starRating, - title, - contents - ); - } -} \ No newline at end of file diff --git a/module-api/src/main/java/com/kernel360/review/dto/ReviewRequestDto.java b/module-api/src/main/java/com/kernel360/review/dto/ReviewRequestDto.java new file mode 100644 index 00000000..b2394b98 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/review/dto/ReviewRequestDto.java @@ -0,0 +1,75 @@ +package com.kernel360.review.dto; + +import com.kernel360.member.entity.Member; +import com.kernel360.product.entity.Product; +import com.kernel360.review.entity.Review; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +/** + * DTO for {@link ReviewRequestDto} + */ +public record ReviewRequestDto(Long reviewNo, + Long productNo, + Long memberNo, + BigDecimal starRating, + String title, + String contents, + LocalDate createdAt, + String createdBy, + LocalDate modifiedAt, + String modifiedBy, + List files) { + + public static ReviewRequestDto of( + Long reviewNo, + Long productNo, + Long memberNo, + BigDecimal starRating, + String title, + String contents, + LocalDate createdAt, + String createdBy, + LocalDate modifiedAt, + String modifiedBy, + List files + ) { + return new ReviewRequestDto( + reviewNo, + productNo, + memberNo, + starRating, + title, + contents, + createdAt, + createdBy, + modifiedAt, + modifiedBy, + files + ); + } + + public Review toEntity() { + return Review.of( + reviewNo, + Product.of(productNo), + Member.of(memberNo), + starRating, + title, + contents, + true + ); + } + + public Review toEntityForUpdate() { + return Review.of( + reviewNo, + starRating, + title, + contents, + true + ); + } +} \ No newline at end of file diff --git a/module-api/src/main/java/com/kernel360/review/dto/ReviewResponseDto.java b/module-api/src/main/java/com/kernel360/review/dto/ReviewResponseDto.java new file mode 100644 index 00000000..a197f7df --- /dev/null +++ b/module-api/src/main/java/com/kernel360/review/dto/ReviewResponseDto.java @@ -0,0 +1,52 @@ +package com.kernel360.review.dto; + +import com.kernel360.member.dto.MemberDto; +import com.kernel360.product.dto.ProductDetailDto; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +/** + * DTO for {@link com.kernel360.review.dto.ReviewResponseDto} + */ +public record ReviewResponseDto(Long reviewNo, + ProductDetailDto product, + MemberDto member, + BigDecimal starRating, + String title, + String contents, + LocalDate createdAt, + String createdBy, + LocalDate modifiedAt, + String modifiedBy, + List files) { + + public static ReviewResponseDto of( + Long reviewNo, + ProductDetailDto productDto, + MemberDto memberDto, + BigDecimal starRating, + String title, + String contents, + LocalDate createdAt, + String createdBy, + LocalDate modifiedAt, + String modifiedBy, + List files + ) { + return new ReviewResponseDto( + reviewNo, + productDto, + memberDto, + starRating, + title, + contents, + createdAt, + createdBy, + modifiedAt, + modifiedBy, + files + ); + } +} \ No newline at end of file diff --git a/module-api/src/main/java/com/kernel360/review/dto/ReviewSearchDto.java b/module-api/src/main/java/com/kernel360/review/dto/ReviewSearchDto.java index 4b7b52cc..b6ed0107 100644 --- a/module-api/src/main/java/com/kernel360/review/dto/ReviewSearchDto.java +++ b/module-api/src/main/java/com/kernel360/review/dto/ReviewSearchDto.java @@ -13,7 +13,6 @@ public static ReviewSearchDto byProductNo(Long productNo, String sortBy) { return ReviewSearchDto.of(productNo, null, sortBy); } - // TODO: 추후 mypage 리뷰 관리에서 사용 예정 public static ReviewSearchDto byMemberNo(Long memberNo, String sortBy) { return ReviewSearchDto.of(null, memberNo, sortBy); } diff --git a/module-api/src/main/java/com/kernel360/review/dto/ReviewSearchResult.java b/module-api/src/main/java/com/kernel360/review/dto/ReviewSearchResult.java new file mode 100644 index 00000000..f8fcc773 --- /dev/null +++ b/module-api/src/main/java/com/kernel360/review/dto/ReviewSearchResult.java @@ -0,0 +1,81 @@ +package com.kernel360.review.dto; + +import com.kernel360.member.dto.MemberDto; +import com.kernel360.product.dto.ProductDetailDto; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * DTO for {@link ReviewSearchResult} + */ +@Getter +@NoArgsConstructor +public class ReviewSearchResult { + // review + Long reviewNo; + BigDecimal starRating; + String title; + String contents; + LocalDate createdAt; + String createdBy; + LocalDate modifiedAt; + String modifiedBy; + + // member + Long memberNo; + String id; + int age; + int gender; + + // product + Long productNo; + String productName; + String companyName; + String imageSource; + String upperItem; + String item; + + // file + String fileUrls; + + public static ReviewResponseDto toDto(ReviewSearchResult response) { + List fileUrls = new ArrayList<>(); + + if (Objects.nonNull(response.getFileUrls())) { + fileUrls = Arrays.stream(response.getFileUrls().split("\\|")).toList(); + } + + return ReviewResponseDto.of( + response.getReviewNo(), + ProductDetailDto.of( + response.getProductNo(), + response.getProductName(), + response.getImageSource(), + response.getCompanyName(), + response.getUpperItem(), + response.getItem() + ), + MemberDto.of( + response.getMemberNo(), + response.getId(), + response.getAge(), + response.getGender() + ), + response.getStarRating(), + response.getTitle(), + response.getContents(), + response.getCreatedAt(), + response.getCreatedBy(), + response.getModifiedAt(), + response.getModifiedBy(), + fileUrls + ); + } +} \ No newline at end of file diff --git a/module-api/src/main/java/com/kernel360/review/repository/ReviewRepository.java b/module-api/src/main/java/com/kernel360/review/repository/ReviewRepository.java index 70be6d3d..ed4f23a3 100644 --- a/module-api/src/main/java/com/kernel360/review/repository/ReviewRepository.java +++ b/module-api/src/main/java/com/kernel360/review/repository/ReviewRepository.java @@ -1,4 +1,9 @@ package com.kernel360.review.repository; +import com.kernel360.review.entity.Review; + +import java.util.Optional; + public interface ReviewRepository extends ReviewRepositoryJpa, ReviewRepositoryDsl { + Optional findByReviewNoAndIsVisibleTrue(Long reviewNo); } diff --git a/module-api/src/main/java/com/kernel360/review/repository/ReviewRepositoryDsl.java b/module-api/src/main/java/com/kernel360/review/repository/ReviewRepositoryDsl.java index 45cbc0dd..e98fc962 100644 --- a/module-api/src/main/java/com/kernel360/review/repository/ReviewRepositoryDsl.java +++ b/module-api/src/main/java/com/kernel360/review/repository/ReviewRepositoryDsl.java @@ -1,10 +1,12 @@ package com.kernel360.review.repository; import com.kernel360.review.dto.ReviewSearchDto; -import com.kernel360.review.entity.Review; +import com.kernel360.review.dto.ReviewSearchResult; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; public interface ReviewRepositoryDsl { - Page findAllByCondition(ReviewSearchDto condition, Pageable pageable); + Page findAllByCondition(ReviewSearchDto condition, Pageable pageable); + + ReviewSearchResult findByReviewNo(Long reviewNo); } diff --git a/module-api/src/main/java/com/kernel360/review/repository/ReviewRepositoryImpl.java b/module-api/src/main/java/com/kernel360/review/repository/ReviewRepositoryImpl.java index 1794fc7f..de3eb5d7 100644 --- a/module-api/src/main/java/com/kernel360/review/repository/ReviewRepositoryImpl.java +++ b/module-api/src/main/java/com/kernel360/review/repository/ReviewRepositoryImpl.java @@ -1,18 +1,25 @@ package com.kernel360.review.repository; +import com.kernel360.file.entity.FileReferType; import com.kernel360.review.dto.ReviewSearchDto; -import com.kernel360.review.entity.Review; +import com.kernel360.review.dto.ReviewSearchResult; import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; import java.util.List; +import static com.kernel360.file.entity.QFile.file; +import static com.kernel360.member.entity.QMember.member; +import static com.kernel360.product.entity.QProduct.product; import static com.kernel360.review.entity.QReview.review; +import static com.querydsl.core.types.dsl.Expressions.stringTemplate; @RequiredArgsConstructor public class ReviewRepositoryImpl implements ReviewRepositoryDsl { @@ -20,27 +27,75 @@ public class ReviewRepositoryImpl implements ReviewRepositoryDsl { private final JPAQueryFactory queryFactory; @Override - public Page findAllByCondition(ReviewSearchDto condition, Pageable pageable) { - List reviews = queryFactory - .select(review) - .from(review) - .where( - productNoEq(condition.productNo()), - memberNoEq(condition.memberNo())) - .offset(pageable.getOffset()) - .limit(pageable.getPageSize()) - .orderBy(sort(condition.sortBy())) - .fetch(); + public Page findAllByCondition(ReviewSearchDto condition, Pageable pageable) { + List reviews = + getJoinedResults() + .where( + productNoEq(condition.productNo()), + memberNoEq(condition.memberNo()) + ) + .groupBy(review.reviewNo, member.memberNo, member.id, member.age, member.gender, product.productNo) + .orderBy(sort(condition.sortBy())) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); - Long totalCount = queryFactory + JPAQuery totalCountQuery = queryFactory .select(review.count()) .from(review) .where( + review.isVisible.eq(true), productNoEq(condition.productNo()), - memberNoEq(condition.memberNo())) + memberNoEq(condition.memberNo()) + ); + + return PageableExecutionUtils.getPage(reviews, pageable, totalCountQuery::fetchOne); + } + + @Override + public ReviewSearchResult findByReviewNo(Long reviewNo) { + return getJoinedResults() + .where( + review.isVisible.eq(true), + review.reviewNo.eq(reviewNo) + ) + .groupBy(review.reviewNo, member.memberNo, member.id, member.age, member.gender, product.productNo) .fetchOne(); + } - return new PageImpl<>(reviews, pageable, totalCount); + private JPAQuery getJoinedResults() { + return queryFactory + .select(Projections.fields(ReviewSearchResult.class, + review.reviewNo, + review.starRating, + review.title, + review.contents, + review.createdAt, + review.createdBy, + review.modifiedAt, + review.modifiedBy, + member.memberNo, + stringTemplate("SUBSTRING({0}, 1, 2) || REPEAT('*', LENGTH({0}) - 2)", member.id).as("id"), + member.age, + member.gender, + product.productNo, + product.productName, + product.companyName, + product.imageSource, + product.upperItem, + product.item, + stringTemplate("STRING_AGG({0}, '|')", file.fileUrl).as("fileUrls") + )) + .from(review) + .leftJoin(file) + .on( + file.referenceType.eq(FileReferType.REVIEW.getCode()), + file.referenceNo.eq(review.reviewNo) + ) + .join(member) + .on(review.member.memberNo.eq(member.memberNo)) + .join(product) + .on(review.product.productNo.eq(product.productNo)); } private BooleanExpression productNoEq(Long productNo) { diff --git a/module-api/src/main/java/com/kernel360/review/service/ReviewService.java b/module-api/src/main/java/com/kernel360/review/service/ReviewService.java index 2be8b398..e78d9c92 100644 --- a/module-api/src/main/java/com/kernel360/review/service/ReviewService.java +++ b/module-api/src/main/java/com/kernel360/review/service/ReviewService.java @@ -1,20 +1,31 @@ package com.kernel360.review.service; import com.kernel360.exception.BusinessException; +import com.kernel360.file.entity.File; +import com.kernel360.file.entity.FileReferType; +import com.kernel360.file.repository.FileRepository; import com.kernel360.review.code.ReviewErrorCode; -import com.kernel360.review.dto.ReviewDto; +import com.kernel360.review.dto.ReviewRequestDto; +import com.kernel360.review.dto.ReviewResponseDto; import com.kernel360.review.dto.ReviewSearchDto; +import com.kernel360.review.dto.ReviewSearchResult; import com.kernel360.review.entity.Review; import com.kernel360.review.repository.ReviewRepository; +import com.kernel360.utils.file.FileUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; import java.math.BigDecimal; +import java.util.List; +import java.util.Objects; +import java.util.Optional; @Slf4j @Service @@ -22,62 +33,131 @@ public class ReviewService { private final ReviewRepository reviewRepository; + private final FileRepository fileRepository; + private final FileUtils fileUtils; + + @Value("${aws.s3.bucket.url}") + private String bucketUrl; private static final double MAX_STAR_RATING = 5.0; + private static final String REVIEW_DOMAIN = FileReferType.REVIEW.getDomain(); + private static final String REVIEW_CODE = FileReferType.REVIEW.getCode(); @Transactional(readOnly = true) - public Page getReviewsByProduct(Long productNo, String sortBy, Pageable pageable) { + public Page getReviewsByProduct(Long productNo, String sortBy, Pageable pageable) { log.info("제품 리뷰 목록 조회 -> product_no {}", productNo); - // TODO: 유효하지 않은 productNo 인 경우, custom error 보내기 return reviewRepository.findAllByCondition(ReviewSearchDto.byProductNo(productNo, sortBy), pageable) - .map(ReviewDto::from); + .map(ReviewSearchResult::toDto); + } + + @Transactional(readOnly = true) + public Page getReviewsByMember(Long memberNo, String sortBy, Pageable pageable) { + log.info("멤버 리뷰 목록 조회 -> memberNo {}", memberNo); + + return reviewRepository.findAllByCondition(ReviewSearchDto.byMemberNo(memberNo, sortBy), pageable) + .map(ReviewSearchResult::toDto); } @Transactional(readOnly = true) - public ReviewDto getReview(Long reviewNo) { + public ReviewResponseDto getReview(Long reviewNo) { log.info("리뷰 단건 조회 -> review_no {}", reviewNo); + ReviewSearchResult review = reviewRepository.findByReviewNo(reviewNo); - return ReviewDto.from(reviewRepository.findByReviewNo(reviewNo)); + if (Objects.isNull(review)) { + throw new BusinessException(ReviewErrorCode.NOT_FOUND_REVIEW); + } + + return ReviewSearchResult.toDto(review); } @Transactional - public Review createReview(ReviewDto reviewDto) { - isValidStarRating(reviewDto.starRating()); + public Review createReview(ReviewRequestDto reviewRequestDto, List files) { + isValidStarRating(reviewRequestDto.starRating()); Review review; try { - review = reviewRepository.saveAndFlush(reviewDto.toEntity()); + review = reviewRepository.saveAndFlush(reviewRequestDto.toEntity()); + log.info("리뷰 등록 -> review_no {}", review.getReviewNo()); + + if (Objects.nonNull(files)) { + uploadFiles(files, reviewRequestDto.productNo(), review.getReviewNo()); + } } catch (DataIntegrityViolationException e) { throw new BusinessException(ReviewErrorCode.INVALID_REVIEW_WRITE_REQUEST); } - log.info("리뷰 등록 -> review_no {}", review.getReviewNo()); return review; } + private void uploadFiles(List files, Long productNo, Long reviewNo) { + files.stream().forEach(file -> { + String path = String.join("/", REVIEW_DOMAIN, productNo.toString()); + String fileKey = fileUtils.upload(path, file); + String fileUrl = String.join("/", bucketUrl, fileKey); + + File fileInfo = fileRepository.save(File.of(null, file.getOriginalFilename(), fileKey, fileUrl, REVIEW_CODE, reviewNo)); + log.info("리뷰 파일 등록 -> file_no {}", fileInfo.getFileNo()); + }); + } @Transactional - public void updateReview(ReviewDto reviewDto) { - isValidStarRating(reviewDto.starRating()); + public void updateReview(ReviewRequestDto reviewRequestDto, List files) { + Review review = isVisibleReview(reviewRequestDto.reviewNo()); + long productNo = review.getProduct().getProductNo(); + + isValidStarRating(reviewRequestDto.starRating()); try { - reviewRepository.saveAndFlush(reviewDto.toEntity()); + reviewRepository.saveAndFlush(reviewRequestDto.toEntityForUpdate()); + log.info("리뷰 수정 -> review_no {}", reviewRequestDto.reviewNo()); + + fileRepository.findByReferenceNo(reviewRequestDto.reviewNo()) + .stream() + .forEach(file -> { + if (!reviewRequestDto.files().contains(file.getFileUrl())) { + fileUtils.delete(file.getFileKey()); + fileRepository.deleteById(file.getFileNo()); + log.info("리뷰 파일 삭제 -> file_no {}", file.getFileNo()); + } + }); + + if (Objects.nonNull(files)) { + uploadFiles(files, productNo, reviewRequestDto.reviewNo()); + } } catch (DataIntegrityViolationException e) { throw new BusinessException(ReviewErrorCode.INVALID_REVIEW_WRITE_REQUEST); } - - log.info("리뷰 수정 -> review_no {}", reviewDto.reviewNo()); } @Transactional public void deleteReview(Long reviewNo) { + isVisibleReview(reviewNo); + reviewRepository.deleteById(reviewNo); log.info("리뷰 삭제 -> review_no {}", reviewNo); + + fileRepository.findByReferenceNo(reviewNo) + .stream() + .forEach(file -> { + fileUtils.delete(file.getFileKey()); + log.info("리뷰 파일 삭제 -> file_no {}", file.getFileNo()); + }); + fileRepository.deleteByReferenceNo(reviewNo); + } + + private Review isVisibleReview(Long reviewNo) { + Optional review = reviewRepository.findByReviewNoAndIsVisibleTrue(reviewNo); + + if (review.isEmpty()) { + throw new BusinessException(ReviewErrorCode.NOT_FOUND_REVIEW); + } + + return review.get(); } - private static void isValidStarRating(BigDecimal starRating) { + private void isValidStarRating(BigDecimal starRating) { if (BigDecimal.ZERO.compareTo(starRating) > 0) { throw new BusinessException(ReviewErrorCode.INVALID_STAR_RATING_VALUE); } diff --git a/module-api/src/main/resources/application-local.yml b/module-api/src/main/resources/application-local.yml index 38d6f693..ab46e9da 100644 --- a/module-api/src/main/resources/application-local.yml +++ b/module-api/src/main/resources/application-local.yml @@ -85,4 +85,4 @@ aws: s3: bucket: name: ENC(JQIi11b8LB+99FnX02wCGwdXTOEax3VkuzgNqAVshK4=) - url: ENC(9P2gRaZoGkR4SCgoTS/6sEQP0kVWwVWFaDckr1/FUoRV1MPnGXQL6OJKsGlHegk8h1d69uFDKTuZpLntfyn3nVMXLz18t8ls) \ No newline at end of file + url: ENC(vhIgYYu6Nz9zFBDC3Rd3IoRGZBoT2zFnYDODVl/3f9MN/rDj9/9ArlT3B1shs2Y+A67BebgtMkp9v8jP3EN5owVEbXPu0vYY) \ No newline at end of file diff --git a/module-api/src/main/resources/application.yml b/module-api/src/main/resources/application.yml index cc7635e9..3af0882f 100644 --- a/module-api/src/main/resources/application.yml +++ b/module-api/src/main/resources/application.yml @@ -4,4 +4,7 @@ spring: servlet: multipart: max-file-size: 50MB - max-request-size: 500MB \ No newline at end of file + max-request-size: 500MB + +module: + name: api \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.11__create_file_table.sql b/module-api/src/main/resources/db/migration/V1.0.11__create_file_table.sql new file mode 100644 index 00000000..66d0d722 --- /dev/null +++ b/module-api/src/main/resources/db/migration/V1.0.11__create_file_table.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS file +( + file_no BIGSERIAL PRIMARY KEY, + file_name VARCHAR(255) NOT NULL, + file_key VARCHAR(255) NOT NULL UNIQUE, + file_url VARCHAR(500) NOT NULL UNIQUE, + reference_type VARCHAR(50), + reference_no BIGINT, + created_at DATE NOT NULL, + created_by VARCHAR NOT NULL, + modified_at DATE, + modified_by VARCHAR +); + +alter sequence file_file_no_seq increment by 50; \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.12__modify_review_table.sql b/module-api/src/main/resources/db/migration/V1.0.12__modify_review_table.sql new file mode 100644 index 00000000..e50868cc --- /dev/null +++ b/module-api/src/main/resources/db/migration/V1.0.12__modify_review_table.sql @@ -0,0 +1,8 @@ +alter table review + alter column title type varchar(255) using title::varchar(255); + +alter table review + alter column contents type varchar(4000) using contents::varchar(4000); + +alter table review + add is_visible bool default true not null; \ No newline at end of file diff --git a/module-common/src/main/java/com/kernel360/utils/file/FileUtils.java b/module-common/src/main/java/com/kernel360/utils/file/FileUtils.java index bd7ddbca..b54a26b0 100644 --- a/module-common/src/main/java/com/kernel360/utils/file/FileUtils.java +++ b/module-common/src/main/java/com/kernel360/utils/file/FileUtils.java @@ -25,14 +25,14 @@ public class FileUtils { @Value("${aws.s3.bucket.name}") private String bucketName; - @Value("${aws.s3.bucket.url}") - private String bucketUrl; - @Value("${spring.profiles.active}") private String profile; - public String upload(S3BucketPath s3BucketPath, MultipartFile multipartFile) { - String filePath = makeFilePath(s3BucketPath); + @Value("${module.name}") + private String moduleName; + + public String upload(String path, MultipartFile multipartFile) { + String filePath = makeFilePath(path); String filename = makeFileName(); String fileExtension = getFileExtension(multipartFile.getOriginalFilename()); String fileKey = String.join("", filePath, filename, fileExtension); @@ -54,17 +54,15 @@ public String upload(S3BucketPath s3BucketPath, MultipartFile multipartFile) { throw new BusinessException(CommonErrorCode.FAIL_FILE_UPLOAD); } - return amazonS3.getUrl(bucketName, fileKey).toString(); + return fileKey; } - private String makeFilePath(S3BucketPath s3BucketPath) { + private String makeFilePath(String path) { + if (!path.endsWith("/")) { + path += "/"; + } - return String.join( - "/", - profile, - s3BucketPath.getModulePath(), - s3BucketPath.getDomainPath(), - s3BucketPath.getCustomPath()); + return String.join("/", profile, moduleName, path); } private String makeFileName() { @@ -76,6 +74,7 @@ private String makeFileName() { } private String getFileExtension(String originalFilename) { + // TODO: 확장자에 대한 검사 로직 추가할 수 있을지 체크 try { return originalFilename.substring(originalFilename.lastIndexOf(".")); } catch (StringIndexOutOfBoundsException e) { @@ -83,7 +82,7 @@ private String getFileExtension(String originalFilename) { } } - public void delete(String fileUrl) { - amazonS3.deleteObject(bucketName, fileUrl.split(bucketUrl)[1]); + public void delete(String fileKey) { + amazonS3.deleteObject(bucketName, fileKey); } } diff --git a/module-common/src/main/java/com/kernel360/utils/file/S3BucketPath.java b/module-common/src/main/java/com/kernel360/utils/file/S3BucketPath.java deleted file mode 100644 index 3a7d0f27..00000000 --- a/module-common/src/main/java/com/kernel360/utils/file/S3BucketPath.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.kernel360.utils.file; - -import lombok.Builder; -import lombok.Getter; - -@Getter -@Builder -public class S3BucketPath { - private final String modulePath; - private final String domainPath; - private final String customPath; -} diff --git a/module-domain/src/main/java/com/kernel360/file/entity/File.java b/module-domain/src/main/java/com/kernel360/file/entity/File.java new file mode 100644 index 00000000..66bf81ef --- /dev/null +++ b/module-domain/src/main/java/com/kernel360/file/entity/File.java @@ -0,0 +1,51 @@ +package com.kernel360.file.entity; + +import com.kernel360.base.BaseEntity; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "file") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class File extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "file_id_gen") + @SequenceGenerator(name = "file_id_gen", sequenceName = "file_file_no_seq") + @Column(name = "file_no", nullable = false) + private Long fileNo; + + @Column(name = "file_name", nullable = false) + private String fileName; + + @Column(name = "file_key", nullable = false) + private String fileKey; + + @Column(name = "file_url", nullable = false, length = 500) + private String fileUrl; + + @Column(name = "reference_type", nullable = false, length = 50) + private String referenceType; + + @Column(name = "reference_no", nullable = false) + private Long referenceNo; + + public File(Long fileNo, String fileName, String fileKey, String fileUrl, String referenceType, Long referenceNo) { + this.fileNo = fileNo; + this.fileName = fileName; + this.fileKey = fileKey; + this.fileUrl = fileUrl; + this.referenceType = referenceType; + this.referenceNo = referenceNo; + } + + public static File of(Long fileNo, String fileName, String fileKey, String fileUrl, String referenceType, Long referenceNo) { + return new File(fileNo, fileName, fileKey, fileUrl, referenceType, referenceNo); + } + + public static File of(Long fileNo, String fileName, String fileKey, String fileUrl) { + return new File(fileNo, fileName, fileKey, fileUrl, null, null); + } +} \ No newline at end of file diff --git a/module-domain/src/main/java/com/kernel360/file/entity/FileReferType.java b/module-domain/src/main/java/com/kernel360/file/entity/FileReferType.java new file mode 100644 index 00000000..d1016507 --- /dev/null +++ b/module-domain/src/main/java/com/kernel360/file/entity/FileReferType.java @@ -0,0 +1,19 @@ +package com.kernel360.file.entity; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum FileReferType { + REVIEW("review", "RV"); + + private final String domain; + private final String code; + + public String getDomain() { + return domain; + } + + public String getCode() { + return code; + } +} diff --git a/module-domain/src/main/java/com/kernel360/file/repository/FileRepositoryJpa.java b/module-domain/src/main/java/com/kernel360/file/repository/FileRepositoryJpa.java new file mode 100644 index 00000000..2a041eba --- /dev/null +++ b/module-domain/src/main/java/com/kernel360/file/repository/FileRepositoryJpa.java @@ -0,0 +1,12 @@ +package com.kernel360.file.repository; + +import com.kernel360.file.entity.File; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface FileRepositoryJpa extends JpaRepository { + List findByReferenceNo(Long referenceNo); + + void deleteByReferenceNo(Long referenceNo); +} diff --git a/module-domain/src/main/java/com/kernel360/member/entity/Member.java b/module-domain/src/main/java/com/kernel360/member/entity/Member.java index 27e154e7..18d88d51 100644 --- a/module-domain/src/main/java/com/kernel360/member/entity/Member.java +++ b/module-domain/src/main/java/com/kernel360/member/entity/Member.java @@ -122,4 +122,12 @@ public void updateFromInfo(int gender, int age) { this.gender = gender; this.age = age; } + + private Member (Long memberNo) { + this.memberNo = memberNo; + } + + public static Member of(Long memberNo) { + return new Member(memberNo); + } } \ No newline at end of file diff --git a/module-domain/src/main/java/com/kernel360/product/entity/Product.java b/module-domain/src/main/java/com/kernel360/product/entity/Product.java index 50986ba9..75674e18 100644 --- a/module-domain/src/main/java/com/kernel360/product/entity/Product.java +++ b/module-domain/src/main/java/com/kernel360/product/entity/Product.java @@ -171,12 +171,8 @@ private Product( this.violationInfo = violationInfo; } - private Product( - Long productNo, - String productName - ) { + private Product(Long productNo) { this.productNo = productNo; - this.productName = productName; } public static Product of(String productName, @@ -214,8 +210,8 @@ public static Product of(String productName, fluorescentWhitening, manufactureType, manufactureMethod, manufactureNation, violation_info); } - public static Product of(Long productNo, String productName) { - return new Product(productNo, productName); + public static Product of(Long productNo) { + return new Product(productNo); } public void updateDetail( diff --git a/module-domain/src/main/java/com/kernel360/review/entity/Review.java b/module-domain/src/main/java/com/kernel360/review/entity/Review.java index 641348a9..eb01cd30 100644 --- a/module-domain/src/main/java/com/kernel360/review/entity/Review.java +++ b/module-domain/src/main/java/com/kernel360/review/entity/Review.java @@ -32,22 +32,38 @@ public class Review extends BaseEntity { @Column(name = "star_rating", nullable = false, precision = 3, scale = 1) private BigDecimal starRating; - @Column(name = "title", nullable = false, length = Integer.MAX_VALUE) + @Column(name = "title", nullable = false) private String title; - @Column(name = "contents", nullable = false, length = Integer.MAX_VALUE) + @Column(name = "contents", nullable = false, length = 4000) private String contents; - private Review(Long reviewNo, Product product, Member member, BigDecimal starRating, String title, String contents) { + @Column(name = "is_visible", nullable = false) + private Boolean isVisible; + + private Review(Long reviewNo, Product product, Member member, BigDecimal starRating, String title, String contents, Boolean isVisible) { this.reviewNo = reviewNo; this.product = product; this.member = member; this.starRating = starRating; this.title = title; this.contents = contents; + this.isVisible = isVisible; + } + + public Review(Long reviewNo, BigDecimal starRating, String title, String contents, Boolean isVisible) { + this.reviewNo = reviewNo; + this.starRating = starRating; + this.title = title; + this.contents = contents; + this.isVisible = isVisible; + } + + public static Review of(Long reviewNo, Product product, Member member, BigDecimal starRating, String title, String contents, Boolean isVisible) { + return new Review(reviewNo, product, member, starRating, title, contents, isVisible); } - public static Review of(Long reviewNo, Product product, Member member, BigDecimal starRating, String title, String contents) { - return new Review(reviewNo, product, member, starRating, title, contents); + public static Review of(Long reviewNo, BigDecimal starRating, String title, String contents, Boolean isVisible) { + return new Review(reviewNo, starRating, title, contents, isVisible); } } \ No newline at end of file diff --git a/module-domain/src/main/java/com/kernel360/review/repository/ReviewRepositoryJpa.java b/module-domain/src/main/java/com/kernel360/review/repository/ReviewRepositoryJpa.java index 23628bcd..f3d6c05e 100644 --- a/module-domain/src/main/java/com/kernel360/review/repository/ReviewRepositoryJpa.java +++ b/module-domain/src/main/java/com/kernel360/review/repository/ReviewRepositoryJpa.java @@ -4,5 +4,4 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface ReviewRepositoryJpa extends JpaRepository { - Review findByReviewNo(Long reviewNo); }