diff --git a/README.md b/README.md index ffd189097..a3819192f 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,107 @@ -# 2023-fun-eat +
-## ํŒ€์› ๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ +
-| Frontend | Frontend | Frontend | -|:-------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------:| -| ํƒ€๋ฏธ | ํ•ด์˜จ | ํ™ฉํŽญ | -| [ํƒ€๋ฏธ](https://github.com/xodms0309) | [ํ•ด์˜จ](https://github.com/hae-on) | [ํ™ฉํŽญ](https://github.com/Leejin-Yang) | + -| Backend | Backend | Backend | Backend | -|:--------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------:|:--------------------------------------------------------------------------------------:|:-------------------------------------------------------------------------------------:| -| ๋กœ๊ฑด | ๋ง๊ณ  | ์˜ค์ž‰ | ์šฐ๊ฐ€ | -| [๋กœ๊ฑด](https://github.com/70825) | [๋ง๊ณ ](https://github.com/Go-Jaecheol) | [์˜ค์ž‰](https://github.com/hanueleee) | [์šฐ๊ฐ€](https://github.com/wugawuga) | +
+
-


+๊ถ๊ธˆํ•ด? ๋ง›์žˆ์„๊ฑธ? ๋จน์–ด๋ด!
+๐Ÿ™ ํŽธ์˜์  ์Œ์‹ ๋ฆฌ๋ทฐ & ๊ฟ€์กฐํ•ฉ ๊ณต์œ  ์„œ๋น„์Šค ๐Ÿ™
+ +
+ +[![Application](http://img.shields.io/badge/funeat.site-D8EAFF?style=for-the-badge&logo=aHR0cHM6Ly9naXRodWIuY29tL3dvb3dhY291cnNlLXRlYW1zLzIwMjMtZnVuLWVhdC9hc3NldHMvODA0NjQ5NjEvOWI1OWY3NzktY2M5MS00MTJhLWE3NDUtZGQ3M2IzY2UxZGNk&logoColor=black&link=https://funeat.site/)](https://funeat.site/) +[![WIKI](http://img.shields.io/badge/-GitHub%20WiKi-FFEC99?style=for-the-badge&logoColor=black&link=https://github.com/woowacourse-teams/2023-fun-eat/wiki)](https://github.com/woowacourse-teams/2023-fun-eat/wiki) +[![Release](https://img.shields.io/github/v/release/woowacourse-teams/2023-fun-eat?style=for-the-badge&color=FFCFCF)](https://github.com/woowacourse-teams/2023-fun-eat/releases/tag/v1.3.0) + +
+ +
+ +# ๐Ÿฅ„ ์„œ๋น„์Šค ์†Œ๊ฐœ + +![1_๋ฉ”์ธํŽ˜์ด์ง€](https://github.com/woowacourse-teams/2023-fun-eat/assets/55427367/9663f7b5-cd38-4f06-86fb-c6636fc364c6) + +
+ +## 1. ํŽธ์˜์ ๋งˆ๋‹ค ํŠน์ƒ‰์žˆ๋Š” ์Œ์‹ ๊ถ๊ธˆํ•ด? + +![5_์ƒํ’ˆ๋ชฉ๋ก](https://github.com/woowacourse-teams/2023-fun-eat/assets/55427367/03fb9955-61fa-4228-a270-ce9dffc710c6) +![6_์ƒํ’ˆ์ƒ์„ธ](https://github.com/woowacourse-teams/2023-fun-eat/assets/55427367/694bc8db-74bd-4fa1-b499-900cd27f5028) +![4_๊ฒ€์ƒ‰](https://github.com/woowacourse-teams/2023-fun-eat/assets/55427367/6a157e08-79d8-450b-9511-ffa461000a22) + +
+
+ +## 2. ์†”์งํ•œ ๋ฆฌ๋ทฐ๋ฅผ ๋ณด๋ฉด ๋” ๋ง›์žˆ์„๊ฑธ? + +![2_๋ฆฌ๋ทฐ](https://github.com/woowacourse-teams/2023-fun-eat/assets/55427367/4bf5ecd7-df08-45d0-b592-8629f3a4e3e6) + +
+
+ +## 3. ์ƒ๊ฐ์ง€ ๋ชปํ–ˆ๋˜ ๊ฟ€์กฐํ•ฉ, ๋จน์–ด๋ด! + +![3_๊ฟ€์กฐํ•ฉ](https://github.com/woowacourse-teams/2023-fun-eat/assets/55427367/8e560b40-d039-47ce-ad29-5e244cba4bf2) + +
+
+ +# ๐Ÿ› ๏ธ ๊ธฐ์ˆ  ์Šคํƒ + +### ๋ฐฑ์—”๋“œ + +
+ BE_๊ธฐ์ˆ ์Šคํƒ +
+ +
+ +### ํ”„๋ก ํŠธ์—”๋“œ + +
+ FE_๊ธฐ์ˆ ์Šคํƒ +
+ +
+ +### ์ธํ”„๋ผ + +
+ ์ธํ”„๋ผ_๊ธฐ์ˆ ์Šคํƒ +
+ +
+
+ +# ์ธํ”„๋ผ ๊ตฌ์กฐ + +### CI/CD + +
+ cicd +
+ +### ๊ตฌ์กฐ + +
+ ์ธํ”„๋ผ ๊ตฌ์กฐ +
+ +
+
+ +# ๐Ÿ‘จโ€๐Ÿ‘จโ€๐Ÿ‘งโ€๐Ÿ‘ง๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ ํŒ€์› + +| Frontend | Frontend | Frontend | Backend | Backend | Backend | Backend | +| :-------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------: | +| ํƒ€๋ฏธ | ํ•ด์˜จ | ํ™ฉํŽญ | ๋กœ๊ฑด | ๋ง๊ณ  | ์˜ค์ž‰ | ์šฐ๊ฐ€ | +| [๐Ÿฐ ํƒ€๋ฏธ](https://github.com/xodms0309) | [๐ŸŒž ํ•ด์˜จ](https://github.com/hae-on) | [๐Ÿง ํ™ฉํŽญ](https://github.com/Leejin-Yang) | [๐Ÿ˜บ ๋กœ๊ฑด](https://github.com/70825) | [๐Ÿฅญ ๋ง๊ณ ](https://github.com/Go-Jaecheol) | [๐Ÿ‘ป ์˜ค์ž‰](https://github.com/hanueleee) | [๐Ÿ– ์šฐ๊ฐ€](https://github.com/wugawuga) | + +
+ +
+ ํŒ€์†Œ๊ฐœ +
diff --git a/backend/src/main/java/com/funeat/FuneatApplication.java b/backend/src/main/java/com/funeat/FuneatApplication.java index 53bd185c0..34909202c 100644 --- a/backend/src/main/java/com/funeat/FuneatApplication.java +++ b/backend/src/main/java/com/funeat/FuneatApplication.java @@ -3,8 +3,10 @@ import com.funeat.common.repository.BaseRepositoryImpl; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +@EnableAsync @SpringBootApplication @EnableJpaRepositories(repositoryBaseClass = BaseRepositoryImpl.class) public class FuneatApplication { @@ -12,5 +14,4 @@ public class FuneatApplication { public static void main(String[] args) { SpringApplication.run(FuneatApplication.class, args); } - } diff --git a/backend/src/main/java/com/funeat/admin/specification/AdminProductSpecification.java b/backend/src/main/java/com/funeat/admin/specification/AdminProductSpecification.java index a8e63b748..c48ea0305 100644 --- a/backend/src/main/java/com/funeat/admin/specification/AdminProductSpecification.java +++ b/backend/src/main/java/com/funeat/admin/specification/AdminProductSpecification.java @@ -13,6 +13,9 @@ public class AdminProductSpecification { private static final List> COUNT_RESULT_TYPES = List.of(Long.class, long.class); + private AdminProductSpecification() { + } + public static Specification searchBy(final ProductSearchCondition condition) { return (root, query, criteriaBuilder) -> { if (!COUNT_RESULT_TYPES.contains(query.getResultType())) { diff --git a/backend/src/main/java/com/funeat/admin/specification/AdminReviewSpecification.java b/backend/src/main/java/com/funeat/admin/specification/AdminReviewSpecification.java index b7c345f14..045147de5 100644 --- a/backend/src/main/java/com/funeat/admin/specification/AdminReviewSpecification.java +++ b/backend/src/main/java/com/funeat/admin/specification/AdminReviewSpecification.java @@ -11,6 +11,9 @@ public class AdminReviewSpecification { + private AdminReviewSpecification() { + } + public static Specification searchBy(final ReviewSearchCondition condition) { return (root, query, criteriaBuilder) -> { if (query.getResultType() != Long.class && query.getResultType() != long.class) { diff --git a/backend/src/main/java/com/funeat/banner/application/BannerService.java b/backend/src/main/java/com/funeat/banner/application/BannerService.java new file mode 100644 index 000000000..48864670a --- /dev/null +++ b/backend/src/main/java/com/funeat/banner/application/BannerService.java @@ -0,0 +1,28 @@ +package com.funeat.banner.application; + +import com.funeat.banner.domain.Banner; +import com.funeat.banner.dto.BannerResponse; +import com.funeat.banner.persistence.BannerRepository; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class BannerService { + + private final BannerRepository bannerRepository; + + public BannerService(final BannerRepository bannerRepository) { + this.bannerRepository = bannerRepository; + } + + public List getAllBanners() { + final List findBanners = bannerRepository.findAllByOrderByIdDesc(); + + return findBanners.stream() + .map(BannerResponse::toResponse) + .collect(Collectors.toList()); + } +} diff --git a/backend/src/main/java/com/funeat/banner/domain/Banner.java b/backend/src/main/java/com/funeat/banner/domain/Banner.java new file mode 100644 index 000000000..9ea8eabd2 --- /dev/null +++ b/backend/src/main/java/com/funeat/banner/domain/Banner.java @@ -0,0 +1,38 @@ +package com.funeat.banner.domain; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +public class Banner { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String link; + + private String image; + + protected Banner() { + } + + public Banner(final String link, final String image) { + this.link = link; + this.image = image; + } + + public Long getId() { + return id; + } + + public String getLink() { + return link; + } + + public String getImage() { + return image; + } +} diff --git a/backend/src/main/java/com/funeat/banner/dto/BannerResponse.java b/backend/src/main/java/com/funeat/banner/dto/BannerResponse.java new file mode 100644 index 000000000..fcff62c2f --- /dev/null +++ b/backend/src/main/java/com/funeat/banner/dto/BannerResponse.java @@ -0,0 +1,32 @@ +package com.funeat.banner.dto; + +import com.funeat.banner.domain.Banner; + +public class BannerResponse { + + private final Long id; + private final String link; + private final String image; + + private BannerResponse(final Long id, final String link, final String image) { + this.id = id; + this.link = link; + this.image = image; + } + + public static BannerResponse toResponse(final Banner banner) { + return new BannerResponse(banner.getId(), banner.getLink(), banner.getImage()); + } + + public Long getId() { + return id; + } + + public String getLink() { + return link; + } + + public String getImage() { + return image; + } +} diff --git a/backend/src/main/java/com/funeat/banner/persistence/BannerRepository.java b/backend/src/main/java/com/funeat/banner/persistence/BannerRepository.java new file mode 100644 index 000000000..30f598755 --- /dev/null +++ b/backend/src/main/java/com/funeat/banner/persistence/BannerRepository.java @@ -0,0 +1,10 @@ +package com.funeat.banner.persistence; + +import com.funeat.banner.domain.Banner; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BannerRepository extends JpaRepository { + + List findAllByOrderByIdDesc(); +} diff --git a/backend/src/main/java/com/funeat/banner/presentation/BannerApiController.java b/backend/src/main/java/com/funeat/banner/presentation/BannerApiController.java new file mode 100644 index 000000000..7465189ab --- /dev/null +++ b/backend/src/main/java/com/funeat/banner/presentation/BannerApiController.java @@ -0,0 +1,21 @@ +package com.funeat.banner.presentation; + +import com.funeat.banner.dto.BannerResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; + +@Tag(name = "08.Banner", description = "๋ฐฐ๋„ˆ ๊ด€๋ จ API ์ž…๋‹ˆ๋‹ค.") +public interface BannerApiController { + + @Operation(summary = "๋ฐฐ๋„ˆ ์ „์ฒด ์กฐํšŒ", description = "๋ฐฐ๋„ˆ ์ „์ฒด๋ฅผ ์กฐํšŒํ•œ๋‹ค.") + @ApiResponse( + responseCode = "200", + description = "๋ฐฐ๋„ˆ ์ „์ฒด ์กฐํšŒ ์„ฑ๊ณต." + ) + @GetMapping + ResponseEntity> getBanners(); +} diff --git a/backend/src/main/java/com/funeat/banner/presentation/BannerController.java b/backend/src/main/java/com/funeat/banner/presentation/BannerController.java new file mode 100644 index 000000000..164df97cd --- /dev/null +++ b/backend/src/main/java/com/funeat/banner/presentation/BannerController.java @@ -0,0 +1,25 @@ +package com.funeat.banner.presentation; + +import com.funeat.banner.application.BannerService; +import com.funeat.banner.dto.BannerResponse; +import java.util.List; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class BannerController implements BannerApiController { + + private final BannerService bannerService; + + public BannerController(final BannerService bannerService) { + this.bannerService = bannerService; + } + + @GetMapping("/api/banners") + public ResponseEntity> getBanners() { + final List responses = bannerService.getAllBanners(); + + return ResponseEntity.ok(responses); + } +} diff --git a/backend/src/main/java/com/funeat/comment/domain/Comment.java b/backend/src/main/java/com/funeat/comment/domain/Comment.java new file mode 100644 index 000000000..4e6798b9d --- /dev/null +++ b/backend/src/main/java/com/funeat/comment/domain/Comment.java @@ -0,0 +1,59 @@ +package com.funeat.comment.domain; + +import com.funeat.member.domain.Member; +import com.funeat.recipe.domain.Recipe; +import java.time.LocalDateTime; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; + +@Entity +public class Comment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String comment; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "recipe_id") + private Recipe recipe; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @Column(nullable = false) + private LocalDateTime createdAt = LocalDateTime.now(); + + protected Comment() { + } + + public Comment(final Recipe recipe, final Member member, final String comment) { + this.recipe = recipe; + this.member = member; + this.comment = comment; + } + + public Long getId() { + return id; + } + + public String getComment() { + return comment; + } + + public Member getMember() { + return member; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } +} diff --git a/backend/src/main/java/com/funeat/comment/persistence/CommentRepository.java b/backend/src/main/java/com/funeat/comment/persistence/CommentRepository.java new file mode 100644 index 000000000..e40a47f67 --- /dev/null +++ b/backend/src/main/java/com/funeat/comment/persistence/CommentRepository.java @@ -0,0 +1,8 @@ +package com.funeat.comment.persistence; + +import com.funeat.comment.domain.Comment; +import com.funeat.common.repository.BaseRepository; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CommentRepository extends JpaRepository, BaseRepository { +} diff --git a/backend/src/main/java/com/funeat/comment/specification/CommentSpecification.java b/backend/src/main/java/com/funeat/comment/specification/CommentSpecification.java new file mode 100644 index 000000000..db6c734bb --- /dev/null +++ b/backend/src/main/java/com/funeat/comment/specification/CommentSpecification.java @@ -0,0 +1,56 @@ +package com.funeat.comment.specification; + +import com.funeat.comment.domain.Comment; +import com.funeat.recipe.domain.Recipe; +import java.util.List; +import java.util.Objects; +import javax.persistence.criteria.JoinType; +import javax.persistence.criteria.Path; +import org.springframework.data.jpa.domain.Specification; + +public class CommentSpecification { + + private CommentSpecification() { + } + + private static final List> COUNT_RESULT_TYPES = List.of(Long.class, long.class); + + public static Specification findAllByRecipe(final Recipe recipe, final Long lastCommentId) { + return (root, query, criteriaBuilder) -> { + if (!COUNT_RESULT_TYPES.contains(query.getResultType())) { + root.fetch("member", JoinType.LEFT); + } + + criteriaBuilder.desc(root.get("id")); + + return Specification + .where(lessThan(lastCommentId)) + .and(equalToRecipe(recipe)) + .toPredicate(root, query, criteriaBuilder); + }; + } + + private static Specification lessThan(final Long commentId) { + return (root, query, criteriaBuilder) -> { + if (Objects.isNull(commentId)) { + return null; + } + + final Path commentIdPath = root.get("id"); + + return criteriaBuilder.lessThan(commentIdPath, commentId); + }; + } + + private static Specification equalToRecipe(final Recipe recipe) { + return (root, query, criteriaBuilder) -> { + if (Objects.isNull(recipe)) { + return null; + } + + final Path recipePath = root.get("recipe"); + + return criteriaBuilder.equal(recipePath, recipe); + }; + } +} diff --git a/backend/src/main/java/com/funeat/common/ImageUploader.java b/backend/src/main/java/com/funeat/common/ImageUploader.java index 754b1affd..afd4b5c10 100644 --- a/backend/src/main/java/com/funeat/common/ImageUploader.java +++ b/backend/src/main/java/com/funeat/common/ImageUploader.java @@ -5,4 +5,6 @@ public interface ImageUploader { String upload(final MultipartFile image); + + void delete(final String fileName); } diff --git a/backend/src/main/java/com/funeat/common/OpenApiConfig.java b/backend/src/main/java/com/funeat/common/OpenApiConfig.java index 47c4df2bb..51c42429d 100644 --- a/backend/src/main/java/com/funeat/common/OpenApiConfig.java +++ b/backend/src/main/java/com/funeat/common/OpenApiConfig.java @@ -19,6 +19,7 @@ @Tag(name = "05.Member", description = "์‚ฌ์šฉ์ž ๊ธฐ๋Šฅ"), @Tag(name = "06.Login", description = "๋กœ๊ทธ์ธ ๊ธฐ๋Šฅ"), @Tag(name = "07.Recipe", description = "๊ฟ€์กฐํ•ฉ ๊ธฐ๋Šฅ"), + @Tag(name = "08.Banner", description = "๋ฐฐ๋„ˆ ๊ธฐ๋Šฅ"), } ) @Configuration diff --git a/backend/src/main/java/com/funeat/common/exception/CommonException.java b/backend/src/main/java/com/funeat/common/exception/CommonException.java index e2e822c68..55be12d5d 100644 --- a/backend/src/main/java/com/funeat/common/exception/CommonException.java +++ b/backend/src/main/java/com/funeat/common/exception/CommonException.java @@ -22,4 +22,10 @@ public S3UploadFailException(final CommonErrorCode errorCode) { super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); } } + + public static class S3DeleteFailException extends CommonException { + public S3DeleteFailException(final CommonErrorCode errorCode) { + super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); + } + } } diff --git a/backend/src/main/java/com/funeat/common/repository/BaseRepository.java b/backend/src/main/java/com/funeat/common/repository/BaseRepository.java index 9c7197243..448db7766 100644 --- a/backend/src/main/java/com/funeat/common/repository/BaseRepository.java +++ b/backend/src/main/java/com/funeat/common/repository/BaseRepository.java @@ -1,6 +1,7 @@ package com.funeat.common.repository; import java.io.Serializable; +import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; @@ -11,4 +12,6 @@ public interface BaseRepository extends JpaRepository { Page findAllForPagination(final Specification spec, final Pageable pageable, final Long totalElements); + + List findAllWithSpecification(final Specification spec, final int pageSize); } diff --git a/backend/src/main/java/com/funeat/common/repository/BaseRepositoryImpl.java b/backend/src/main/java/com/funeat/common/repository/BaseRepositoryImpl.java index 773b95269..64cd508f6 100644 --- a/backend/src/main/java/com/funeat/common/repository/BaseRepositoryImpl.java +++ b/backend/src/main/java/com/funeat/common/repository/BaseRepositoryImpl.java @@ -1,12 +1,15 @@ package com.funeat.common.repository; import java.io.Serializable; +import java.util.List; import javax.persistence.EntityManager; import javax.persistence.TypedQuery; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; + import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.support.JpaEntityInformation; import org.springframework.data.jpa.repository.support.SimpleJpaRepository; @@ -37,4 +40,12 @@ public Page findAllForPagination(final Specification spec, final Pageable return new PageImpl<>(query.getResultList(), PageRequest.of(0, pageSize), totalElements); } + + @Override + public List findAllWithSpecification(final Specification spec, final int pageSize) { + final TypedQuery query = getQuery(spec, Sort.unsorted()); + query.setMaxResults(pageSize); + + return query.getResultList(); + } } diff --git a/backend/src/main/java/com/funeat/common/s3/S3Uploader.java b/backend/src/main/java/com/funeat/common/s3/S3Uploader.java index 3f9c86caa..97e6241b7 100644 --- a/backend/src/main/java/com/funeat/common/s3/S3Uploader.java +++ b/backend/src/main/java/com/funeat/common/s3/S3Uploader.java @@ -3,15 +3,19 @@ import static com.funeat.exception.CommonErrorCode.IMAGE_EXTENSION_ERROR_CODE; import static com.funeat.exception.CommonErrorCode.UNKNOWN_SERVER_ERROR_CODE; +import com.amazonaws.AmazonServiceException; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import com.funeat.common.ImageUploader; import com.funeat.common.exception.CommonException.NotAllowedFileExtensionException; +import com.funeat.common.exception.CommonException.S3DeleteFailException; import com.funeat.common.exception.CommonException.S3UploadFailException; import java.io.IOException; import java.util.List; import java.util.UUID; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; @@ -21,8 +25,11 @@ @Profile("!test") public class S3Uploader implements ImageUploader { + private static final int BEGIN_FILE_NAME_INDEX_WITHOUT_CLOUDFRONT_PATH = 31; private static final List INCLUDE_EXTENSIONS = List.of("image/jpeg", "image/png", "image/webp"); + private final Logger log = LoggerFactory.getLogger(this.getClass()); + @Value("${cloud.aws.s3.bucket}") private String bucket; @@ -53,6 +60,18 @@ public String upload(final MultipartFile image) { } } + @Override + public void delete(final String image) { + final String imageName = image.substring(BEGIN_FILE_NAME_INDEX_WITHOUT_CLOUDFRONT_PATH); + try { + final String key = folder + imageName; + amazonS3.deleteObject(bucket, key); + } catch (final AmazonServiceException e) { + log.error("S3 ์ด๋ฏธ์ง€ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ : {}", image); + throw new S3DeleteFailException(UNKNOWN_SERVER_ERROR_CODE); + } + } + private void validateExtension(final MultipartFile image) { final String contentType = image.getContentType(); if (!INCLUDE_EXTENSIONS.contains(contentType)) { diff --git a/backend/src/main/java/com/funeat/member/domain/bookmark/ProductBookmark.java b/backend/src/main/java/com/funeat/member/domain/bookmark/ProductBookmark.java deleted file mode 100644 index c18c84b59..000000000 --- a/backend/src/main/java/com/funeat/member/domain/bookmark/ProductBookmark.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.funeat.member.domain.bookmark; - -import com.funeat.member.domain.Member; -import com.funeat.product.domain.Product; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; - -@Entity -public class ProductBookmark { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "member_id") - private Member member; - - @ManyToOne - @JoinColumn(name = "product_id") - private Product product; - - private Boolean checked; -} diff --git a/backend/src/main/java/com/funeat/member/domain/bookmark/RecipeBookmark.java b/backend/src/main/java/com/funeat/member/domain/bookmark/RecipeBookmark.java deleted file mode 100644 index 9dc0b75ad..000000000 --- a/backend/src/main/java/com/funeat/member/domain/bookmark/RecipeBookmark.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.funeat.member.domain.bookmark; - -import com.funeat.member.domain.Member; -import com.funeat.recipe.domain.Recipe; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; - -@Entity -public class RecipeBookmark { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne - @JoinColumn(name = "member_id") - private Member member; - - @ManyToOne - @JoinColumn(name = "recipe_id") - private Recipe recipe; - - private Boolean checked; -} diff --git a/backend/src/main/java/com/funeat/member/persistence/ProductBookmarkRepository.java b/backend/src/main/java/com/funeat/member/persistence/ProductBookmarkRepository.java deleted file mode 100644 index c7651b592..000000000 --- a/backend/src/main/java/com/funeat/member/persistence/ProductBookmarkRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.funeat.member.persistence; - -import com.funeat.member.domain.bookmark.ProductBookmark; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface ProductBookmarkRepository extends JpaRepository { -} diff --git a/backend/src/main/java/com/funeat/member/persistence/RecipeBookMarkRepository.java b/backend/src/main/java/com/funeat/member/persistence/RecipeBookMarkRepository.java deleted file mode 100644 index 4ed5cce46..000000000 --- a/backend/src/main/java/com/funeat/member/persistence/RecipeBookMarkRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.funeat.member.persistence; - -import com.funeat.member.domain.bookmark.RecipeBookmark; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface RecipeBookMarkRepository extends JpaRepository { -} diff --git a/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java b/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java index 2e96e623a..f1ae40e5d 100644 --- a/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java +++ b/backend/src/main/java/com/funeat/member/persistence/ReviewFavoriteRepository.java @@ -3,10 +3,15 @@ import com.funeat.member.domain.Member; import com.funeat.member.domain.favorite.ReviewFavorite; import com.funeat.review.domain.Review; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface ReviewFavoriteRepository extends JpaRepository { Optional findByMemberAndReview(final Member member, final Review review); + + void deleteByReview(final Review review); + + List findByReview(final Review review); } diff --git a/backend/src/main/java/com/funeat/member/presentation/MemberApiController.java b/backend/src/main/java/com/funeat/member/presentation/MemberApiController.java index 6ee963d54..af00932f7 100644 --- a/backend/src/main/java/com/funeat/member/presentation/MemberApiController.java +++ b/backend/src/main/java/com/funeat/member/presentation/MemberApiController.java @@ -15,7 +15,9 @@ import org.springframework.data.web.PageableDefault; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; @@ -69,4 +71,13 @@ public ResponseEntity getMemberRecipe(@AuthenticationPrin return ResponseEntity.ok().body(response); } + + @Logging + @DeleteMapping("/reviews/{reviewId}") + public ResponseEntity deleteReview(@PathVariable final Long reviewId, + @AuthenticationPrincipal final LoginInfo loginInfo) { + reviewService.deleteReview(reviewId, loginInfo.getId()); + + return ResponseEntity.noContent().build(); + } } diff --git a/backend/src/main/java/com/funeat/member/presentation/MemberController.java b/backend/src/main/java/com/funeat/member/presentation/MemberController.java index 5d5748fd7..9c5e60763 100644 --- a/backend/src/main/java/com/funeat/member/presentation/MemberController.java +++ b/backend/src/main/java/com/funeat/member/presentation/MemberController.java @@ -12,7 +12,9 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.multipart.MultipartFile; @@ -55,4 +57,13 @@ ResponseEntity getMemberReview(@AuthenticationPrincipal f @GetMapping ResponseEntity getMemberRecipe(@AuthenticationPrincipal final LoginInfo loginInfo, @PageableDefault final Pageable pageable); + + @Operation(summary = "๋ฆฌ๋ทฐ ์‚ญ์ œ", description = "์ž์‹ ์ด ์ž‘์„ฑํ•œ ๋ฆฌ๋ทฐ๋ฅผ ์‚ญ์ œํ•œ๋‹ค.") + @ApiResponse( + responseCode = "204", + description = "๋ฆฌ๋ทฐ ์‚ญ์ œ ์„ฑ๊ณต." + ) + @DeleteMapping + ResponseEntity deleteReview(@PathVariable final Long reviewId, + @AuthenticationPrincipal final LoginInfo loginInfo); } diff --git a/backend/src/main/java/com/funeat/product/application/ProductService.java b/backend/src/main/java/com/funeat/product/application/ProductService.java index 5aa2b9986..e6e0eed3d 100644 --- a/backend/src/main/java/com/funeat/product/application/ProductService.java +++ b/backend/src/main/java/com/funeat/product/application/ProductService.java @@ -9,6 +9,7 @@ import com.funeat.product.dto.ProductInCategoryDto; import com.funeat.product.dto.ProductResponse; import com.funeat.product.dto.ProductReviewCountDto; +import com.funeat.product.dto.ProductSortCondition; import com.funeat.product.dto.ProductsInCategoryResponse; import com.funeat.product.dto.RankingProductDto; import com.funeat.product.dto.RankingProductsResponse; @@ -21,6 +22,7 @@ import com.funeat.product.persistence.CategoryRepository; import com.funeat.product.persistence.ProductRecipeRepository; import com.funeat.product.persistence.ProductRepository; +import com.funeat.product.persistence.ProductSpecification; import com.funeat.recipe.domain.Recipe; import com.funeat.recipe.domain.RecipeImage; import com.funeat.recipe.dto.RecipeDto; @@ -30,13 +32,14 @@ import com.funeat.review.persistence.ReviewRepository; import com.funeat.review.persistence.ReviewTagRepository; import com.funeat.tag.domain.Tag; +import java.time.LocalDateTime; import java.util.Comparator; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -46,9 +49,10 @@ public class ProductService { private static final int THREE = 3; private static final int TOP = 0; - public static final String REVIEW_COUNT = "reviewCount"; private static final int RANKING_SIZE = 3; private static final int PAGE_SIZE = 10; + private static final int DEFAULT_PAGE_SIZE = 10; + private static final int DEFAULT_CURSOR_PAGINATION_SIZE = 11; private final CategoryRepository categoryRepository; private final ProductRepository productRepository; @@ -61,7 +65,8 @@ public class ProductService { public ProductService(final CategoryRepository categoryRepository, final ProductRepository productRepository, final ReviewTagRepository reviewTagRepository, final ReviewRepository reviewRepository, final ProductRecipeRepository productRecipeRepository, - final RecipeImageRepository recipeImageRepository, final RecipeRepository recipeRepository) { + final RecipeImageRepository recipeImageRepository, + final RecipeRepository recipeRepository) { this.categoryRepository = categoryRepository; this.productRepository = productRepository; this.reviewTagRepository = reviewTagRepository; @@ -71,38 +76,53 @@ public ProductService(final CategoryRepository categoryRepository, final Product this.recipeRepository = recipeRepository; } - public ProductsInCategoryResponse getAllProductsInCategory(final Long categoryId, - final Pageable pageable) { + public ProductsInCategoryResponse getAllProductsInCategory(final Long categoryId, final Long lastProductId, + final ProductSortCondition sortCondition) { final Category category = categoryRepository.findById(categoryId) .orElseThrow(() -> new CategoryNotFoundException(CATEGORY_NOT_FOUND, categoryId)); + final Product lastProduct = productRepository.findById(lastProductId).orElse(null); - final Page pages = getAllProductsInCategory(pageable, category); + final Specification specification = ProductSpecification.searchBy(category, lastProduct, sortCondition); + final List findResults = productRepository.findAllWithSpecification(specification, DEFAULT_CURSOR_PAGINATION_SIZE); - final PageDto pageDto = PageDto.toDto(pages); - final List productDtos = pages.getContent(); + final List productDtos = getProductInCategoryDtos(findResults); + final boolean hasNext = hasNextPage(findResults); - return ProductsInCategoryResponse.toResponse(pageDto, productDtos); + return ProductsInCategoryResponse.toResponse(hasNext, productDtos); } - private Page getAllProductsInCategory(final Pageable pageable, final Category category) { - if (Objects.nonNull(pageable.getSort().getOrderFor(REVIEW_COUNT))) { - final PageRequest pageRequest = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize()); - return productRepository.findAllByCategoryOrderByReviewCountDesc(category, pageRequest); + private List getProductInCategoryDtos(final List findProducts) { + final int resultSize = getResultSize(findProducts); + final List products = findProducts.subList(0, resultSize); + + return products.stream() + .map(ProductInCategoryDto::toDto) + .collect(Collectors.toList()); + } + + private int getResultSize(final List findProducts) { + if (findProducts.size() < DEFAULT_CURSOR_PAGINATION_SIZE) { + return findProducts.size(); } - return productRepository.findAllByCategory(category, pageable); + return DEFAULT_PAGE_SIZE; + } + + private boolean hasNextPage(final List findProducts) { + return findProducts.size() > DEFAULT_PAGE_SIZE; } public ProductResponse findProductDetail(final Long productId) { final Product product = productRepository.findById(productId) .orElseThrow(() -> new ProductNotFoundException(PRODUCT_NOT_FOUND, productId)); - final Long reviewCount = reviewRepository.countByProduct(product); final List tags = reviewTagRepository.findTop3TagsByReviewIn(productId, PageRequest.of(TOP, THREE)); - return ProductResponse.toResponse(product, reviewCount, tags); + return ProductResponse.toResponse(product, tags); } public RankingProductsResponse getTop3Products() { - final List productsAndReviewCounts = productRepository.findAllByAverageRatingGreaterThan3(); + final LocalDateTime endDateTime = LocalDateTime.now(); + final LocalDateTime startDateTime = endDateTime.minusWeeks(2L); + final List productsAndReviewCounts = productRepository.findAllByAverageRatingGreaterThan3(startDateTime, endDateTime); final Comparator rankingScoreComparator = Comparator.comparing( (ProductReviewCountDto it) -> it.getProduct().calculateRankingScore(it.getReviewCount()) ).reversed(); diff --git a/backend/src/main/java/com/funeat/product/domain/Category.java b/backend/src/main/java/com/funeat/product/domain/Category.java index 5d6c62a08..7504aad1d 100644 --- a/backend/src/main/java/com/funeat/product/domain/Category.java +++ b/backend/src/main/java/com/funeat/product/domain/Category.java @@ -1,6 +1,11 @@ package com.funeat.product.domain; -import javax.persistence.*; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; @Entity public class Category { @@ -25,6 +30,10 @@ public Category(final String name, final CategoryType type, final String image) this.image = image; } + public boolean isFood() { + return type == CategoryType.FOOD; + } + public Long getId() { return id; } diff --git a/backend/src/main/java/com/funeat/product/domain/Product.java b/backend/src/main/java/com/funeat/product/domain/Product.java index a485eaf55..a95a7b91e 100644 --- a/backend/src/main/java/com/funeat/product/domain/Product.java +++ b/backend/src/main/java/com/funeat/product/domain/Product.java @@ -1,6 +1,5 @@ package com.funeat.product.domain; -import com.funeat.member.domain.bookmark.ProductBookmark; import com.funeat.review.domain.Review; import java.util.List; import javax.persistence.Entity; @@ -15,6 +14,8 @@ @Entity public class Product { + public static final String BASIC_IMAGE = null; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -29,6 +30,8 @@ public class Product { private Double averageRating = 0.0; + private Long reviewCount = 0L; + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "category_id") private Category category; @@ -39,11 +42,6 @@ public class Product { @OneToMany(mappedBy = "product") private List productRecipes; - @OneToMany(mappedBy = "product") - private List productBookmarks; - - private Long reviewCount = 0L; - protected Product() { } @@ -66,22 +64,56 @@ public Product(final String name, final Long price, final String image, final St this.category = category; } + public Product(final String name, final Long price, final String image, final String content, + final Category category, final Long reviewCount) { + this.name = name; + this.price = price; + this.image = image; + this.content = content; + this.category = category; + this.reviewCount = reviewCount; + } + + public Product(final String name, final Long price, final String image, final String content, + final Double averageRating, final Category category, final Long reviewCount) { + this.name = name; + this.price = price; + this.image = image; + this.content = content; + this.averageRating = averageRating; + this.category = category; + this.reviewCount = reviewCount; + } + public static Product create(final String name, final Long price, final String content, final Category category) { return new Product(name, price, null, content, category); } - public void updateAverageRating(final Long rating, final Long count) { + public void updateAverageRatingForInsert(final Long count, final Long rating) { final double calculatedRating = ((count - 1) * averageRating + rating) / count; this.averageRating = Math.round(calculatedRating * 10.0) / 10.0; } + public void updateAverageRatingForDelete(final Long deletedRating) { + if (reviewCount == 1) { + this.averageRating = 0.0; + return; + } + final double calculatedRating = (reviewCount * averageRating - deletedRating) / (reviewCount - 1); + this.averageRating = Math.round(calculatedRating * 10.0) / 10.0; + } + public Double calculateRankingScore(final Long reviewCount) { final double exponent = -Math.log10(reviewCount + 1); final double factor = Math.pow(2, exponent); return averageRating - (averageRating - 3.0) * factor; } - public void updateImage(final String topFavoriteImage) { + public void updateBasicImage() { + this.image = BASIC_IMAGE; + } + + public void updateFavoriteImage(final String topFavoriteImage) { this.image = topFavoriteImage; } @@ -127,4 +159,8 @@ public Long getReviewCount() { public void addReviewCount() { reviewCount++; } + + public void minusReviewCount() { + reviewCount--; + } } diff --git a/backend/src/main/java/com/funeat/product/dto/ProductInCategoryDto.java b/backend/src/main/java/com/funeat/product/dto/ProductInCategoryDto.java index 7ab4bf467..e4c73b606 100644 --- a/backend/src/main/java/com/funeat/product/dto/ProductInCategoryDto.java +++ b/backend/src/main/java/com/funeat/product/dto/ProductInCategoryDto.java @@ -21,6 +21,11 @@ public ProductInCategoryDto(final Long id, final String name, final Long price, this.reviewCount = reviewCount; } + public static ProductInCategoryDto toDto(final Product product) { + return new ProductInCategoryDto(product.getId(), product.getName(), product.getPrice(), product.getImage(), + product.getAverageRating(), product.getReviewCount()); + } + public static ProductInCategoryDto toDto(final Product product, final Long reviewCount) { return new ProductInCategoryDto(product.getId(), product.getName(), product.getPrice(), product.getImage(), product.getAverageRating(), reviewCount); diff --git a/backend/src/main/java/com/funeat/product/dto/ProductResponse.java b/backend/src/main/java/com/funeat/product/dto/ProductResponse.java index 49c5bba53..d3c0ed264 100644 --- a/backend/src/main/java/com/funeat/product/dto/ProductResponse.java +++ b/backend/src/main/java/com/funeat/product/dto/ProductResponse.java @@ -29,13 +29,13 @@ public ProductResponse(final Long id, final String name, final Long price, final this.tags = tags; } - public static ProductResponse toResponse(final Product product, final Long reviewCount, final List tags) { + public static ProductResponse toResponse(final Product product, final List tags) { List tagDtos = new ArrayList<>(); for (Tag tag : tags) { tagDtos.add(TagDto.toDto(tag)); } return new ProductResponse(product.getId(), product.getName(), product.getPrice(), product.getImage(), - product.getContent(), product.getAverageRating(), reviewCount, tagDtos); + product.getContent(), product.getAverageRating(), product.getReviewCount(), tagDtos); } public Long getId() { diff --git a/backend/src/main/java/com/funeat/product/dto/ProductSortCondition.java b/backend/src/main/java/com/funeat/product/dto/ProductSortCondition.java new file mode 100644 index 000000000..8a929f99c --- /dev/null +++ b/backend/src/main/java/com/funeat/product/dto/ProductSortCondition.java @@ -0,0 +1,25 @@ +package com.funeat.product.dto; + +public class ProductSortCondition { + + private final String by; + private final String order; + + private ProductSortCondition(final String by, final String order) { + this.by = by; + this.order = order; + } + + public static ProductSortCondition toDto(final String sort) { + final String[] split = sort.split(","); + return new ProductSortCondition(split[0], split[1]); + } + + public String getBy() { + return by; + } + + public String getOrder() { + return order; + } +} diff --git a/backend/src/main/java/com/funeat/product/dto/ProductsInCategoryResponse.java b/backend/src/main/java/com/funeat/product/dto/ProductsInCategoryResponse.java index 4712e90fb..39c685268 100644 --- a/backend/src/main/java/com/funeat/product/dto/ProductsInCategoryResponse.java +++ b/backend/src/main/java/com/funeat/product/dto/ProductsInCategoryResponse.java @@ -1,24 +1,24 @@ package com.funeat.product.dto; -import com.funeat.common.dto.PageDto; import java.util.List; public class ProductsInCategoryResponse { - private final PageDto page; + private final boolean hasNext; private final List products; - public ProductsInCategoryResponse(final PageDto page, final List products) { - this.page = page; + public ProductsInCategoryResponse(final boolean hasNext, final List products) { + this.hasNext = hasNext; this.products = products; } - public static ProductsInCategoryResponse toResponse(final PageDto page, final List products) { - return new ProductsInCategoryResponse(page, products); + public static ProductsInCategoryResponse toResponse(final boolean hasNext, + final List products) { + return new ProductsInCategoryResponse(hasNext, products); } - public PageDto getPage() { - return page; + public boolean isHasNext() { + return hasNext; } public List getProducts() { diff --git a/backend/src/main/java/com/funeat/product/exception/ProductErrorCode.java b/backend/src/main/java/com/funeat/product/exception/ProductErrorCode.java index 933f91098..e3b4d3ccc 100644 --- a/backend/src/main/java/com/funeat/product/exception/ProductErrorCode.java +++ b/backend/src/main/java/com/funeat/product/exception/ProductErrorCode.java @@ -5,6 +5,7 @@ public enum ProductErrorCode { PRODUCT_NOT_FOUND(HttpStatus.NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ’ˆ์ž…๋‹ˆ๋‹ค. ์ƒํ’ˆ id๋ฅผ ํ™•์ธํ•˜์„ธ์š”.", "1001"), + NOT_SUPPORTED_PRODUCT_SORTING_CONDITION(HttpStatus.BAD_REQUEST, "์ •๋ ฌ ์กฐ๊ฑด์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ •๋ ฌ ์กฐ๊ฑด์„ ํ™•์ธํ•˜์„ธ์š”", "1002"); ; private final HttpStatus status; diff --git a/backend/src/main/java/com/funeat/product/exception/ProductException.java b/backend/src/main/java/com/funeat/product/exception/ProductException.java index c9b1f1720..bdbb4782b 100644 --- a/backend/src/main/java/com/funeat/product/exception/ProductException.java +++ b/backend/src/main/java/com/funeat/product/exception/ProductException.java @@ -15,4 +15,10 @@ public ProductNotFoundException(final ProductErrorCode errorCode, final Long pro super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), productId)); } } + + public static class NotSupportedProductSortingConditionException extends ProductException { + public NotSupportedProductSortingConditionException(final ProductErrorCode errorCode, final String sortBy) { + super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), sortBy)); + } + } } diff --git a/backend/src/main/java/com/funeat/product/persistence/ProductRepository.java b/backend/src/main/java/com/funeat/product/persistence/ProductRepository.java index 83a53f483..8d07bc496 100644 --- a/backend/src/main/java/com/funeat/product/persistence/ProductRepository.java +++ b/backend/src/main/java/com/funeat/product/persistence/ProductRepository.java @@ -1,43 +1,25 @@ package com.funeat.product.persistence; -import com.funeat.product.domain.Category; +import com.funeat.common.repository.BaseRepository; import com.funeat.product.domain.Product; -import com.funeat.product.dto.ProductInCategoryDto; import com.funeat.product.dto.ProductReviewCountDto; +import java.time.LocalDateTime; import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -public interface ProductRepository extends JpaRepository, JpaSpecificationExecutor { - - @Query(value = "SELECT new com.funeat.product.dto.ProductInCategoryDto(p.id, p.name, p.price, p.image, p.averageRating, COUNT(r)) " - + "FROM Product p " - + "LEFT JOIN p.reviews r " - + "WHERE p.category = :category " - + "GROUP BY p ", - countQuery = "SELECT COUNT(p) FROM Product p WHERE p.category = :category") - Page findAllByCategory(@Param("category") final Category category, final Pageable pageable); - - @Query(value = "SELECT new com.funeat.product.dto.ProductInCategoryDto(p.id, p.name, p.price, p.image, p.averageRating, COUNT(r)) " - + "FROM Product p " - + "LEFT JOIN p.reviews r " - + "WHERE p.category = :category " - + "GROUP BY p " - + "ORDER BY COUNT(r) DESC, p.id DESC ", - countQuery = "SELECT COUNT(p) FROM Product p WHERE p.category = :category") - Page findAllByCategoryOrderByReviewCountDesc(@Param("category") final Category category, - final Pageable pageable); +public interface ProductRepository extends BaseRepository { @Query("SELECT new com.funeat.product.dto.ProductReviewCountDto(p, COUNT(r.id)) " + "FROM Product p " + "LEFT JOIN Review r ON r.product.id = p.id " + "WHERE p.averageRating > 3.0 " + + "AND r.createdAt BETWEEN :startDateTime AND :endDateTime " + "GROUP BY p.id") - List findAllByAverageRatingGreaterThan3(); + List findAllByAverageRatingGreaterThan3(final LocalDateTime startDateTime, + final LocalDateTime endDateTime); @Query("SELECT p FROM Product p " + "WHERE p.name LIKE CONCAT('%', :name, '%') " diff --git a/backend/src/main/java/com/funeat/product/persistence/ProductSpecification.java b/backend/src/main/java/com/funeat/product/persistence/ProductSpecification.java new file mode 100644 index 000000000..f2dbd2ff2 --- /dev/null +++ b/backend/src/main/java/com/funeat/product/persistence/ProductSpecification.java @@ -0,0 +1,108 @@ +package com.funeat.product.persistence; + +import static com.funeat.product.exception.ProductErrorCode.NOT_SUPPORTED_PRODUCT_SORTING_CONDITION; + +import com.funeat.product.domain.Category; +import com.funeat.product.domain.Product; +import com.funeat.product.dto.ProductSortCondition; +import com.funeat.product.exception.ProductException.NotSupportedProductSortingConditionException; +import java.util.Objects; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Path; +import javax.persistence.criteria.Root; +import org.springframework.data.jpa.domain.Specification; + +public class ProductSpecification { + + private ProductSpecification() { + } + + private static final String DESC = "desc"; + private static final String CATEGORY = "category"; + private static final String ID = "id"; + private static final String REVIEW_COUNT = "reviewCount"; + private static final String AVERAGE_RATING = "averageRating"; + private static final String PRICE = "price"; + + public static Specification searchBy(final Category category, final Product lastProduct, + final ProductSortCondition sortCondition) { + return (root, query, builder) -> { + setOrderBy(sortCondition, root, query, builder); + + return Specification + .where(sameCategory(category)) + .and(nextCursor(lastProduct, sortCondition)) + .toPredicate(root, query, builder); + }; + } + + private static void setOrderBy(final ProductSortCondition sortCondition, final Root root, + final CriteriaQuery query, final CriteriaBuilder builder) { + final String sortBy = sortCondition.getBy(); + final String sortOrder = sortCondition.getOrder(); + + if (DESC.equals(sortOrder)) { + query.orderBy(builder.desc(root.get(sortBy)), builder.desc(root.get(ID))); + } else { + query.orderBy(builder.asc(root.get(sortBy)), builder.desc(root.get(ID))); + } + } + + private static Specification sameCategory(final Category category) { + return (root, query, builder) -> { + final Path categoryPath = root.get(CATEGORY); + + return builder.equal(categoryPath, category); + }; + } + + private static Specification nextCursor(final Product lastProduct, final ProductSortCondition sortCondition) { + final String sortBy = sortCondition.getBy(); + final String sortOrder = sortCondition.getOrder(); + + return (root, query, builder) -> { + if (Objects.isNull(lastProduct)) { + return null; + } + + final Comparable comparisonValue = (Comparable) getComparisonValue(lastProduct, sortBy); + + return builder.or( + sameValueAndSmallerId(sortBy, lastProduct.getId(), comparisonValue).toPredicate(root, query, builder), + nextValue(sortBy, sortOrder, comparisonValue).toPredicate(root, query, builder) + ); + }; + } + + private static Object getComparisonValue(final Product lastProduct, final String sortBy) { + if (PRICE.equals(sortBy)) { + return lastProduct.getPrice(); + } + if (AVERAGE_RATING.equals(sortBy)) { + return lastProduct.getAverageRating(); + } + if (REVIEW_COUNT.equals(sortBy)) { + return lastProduct.getReviewCount(); + } + throw new NotSupportedProductSortingConditionException(NOT_SUPPORTED_PRODUCT_SORTING_CONDITION, sortBy); + } + + private static Specification sameValueAndSmallerId(final String sortBy, final Long lastProductId, + final Comparable comparisonValue) { + return (root, query, builder) -> builder.and( + builder.equal(root.get(sortBy), comparisonValue), + builder.lessThan(root.get(ID), lastProductId)); + } + + private static Specification nextValue(final String sortBy, final String sortOrder, + final Comparable comparisonValue) { + return (root, query, builder) -> { + if (DESC.equals(sortOrder)) { + return builder.lessThan(root.get(sortBy), comparisonValue); + } else { + return builder.greaterThan(root.get(sortBy), comparisonValue); + } + }; + } +} diff --git a/backend/src/main/java/com/funeat/product/presentation/ProductApiController.java b/backend/src/main/java/com/funeat/product/presentation/ProductApiController.java index 4fe3bdfe9..30f0701cc 100644 --- a/backend/src/main/java/com/funeat/product/presentation/ProductApiController.java +++ b/backend/src/main/java/com/funeat/product/presentation/ProductApiController.java @@ -2,6 +2,7 @@ import com.funeat.product.application.ProductService; import com.funeat.product.dto.ProductResponse; +import com.funeat.product.dto.ProductSortCondition; import com.funeat.product.dto.ProductsInCategoryResponse; import com.funeat.product.dto.RankingProductsResponse; import com.funeat.product.dto.SearchProductResultsResponse; @@ -29,8 +30,10 @@ public ProductApiController(final ProductService productService) { @GetMapping("/categories/{categoryId}/products") public ResponseEntity getAllProductsInCategory(@PathVariable final Long categoryId, - @PageableDefault final Pageable pageable) { - final ProductsInCategoryResponse response = productService.getAllProductsInCategory(categoryId, pageable); + @RequestParam final Long lastProductId, + @RequestParam final String sort) { + final ProductSortCondition sortCondition = ProductSortCondition.toDto(sort); + final ProductsInCategoryResponse response = productService.getAllProductsInCategory(categoryId, lastProductId, sortCondition); return ResponseEntity.ok(response); } diff --git a/backend/src/main/java/com/funeat/product/presentation/ProductController.java b/backend/src/main/java/com/funeat/product/presentation/ProductController.java index bbec1c93d..2adff0880 100644 --- a/backend/src/main/java/com/funeat/product/presentation/ProductController.java +++ b/backend/src/main/java/com/funeat/product/presentation/ProductController.java @@ -26,7 +26,9 @@ public interface ProductController { ) @GetMapping ResponseEntity getAllProductsInCategory( - @PathVariable(name = "category_id") final Long categoryId, @PageableDefault final Pageable pageable + @PathVariable final Long categoryId, + @RequestParam final Long lastProductId, + @RequestParam final String sort ); @Operation(summary = "ํ•ด๋‹น ์ƒํ’ˆ ์ƒ์„ธ ์กฐํšŒ", description = "ํ•ด๋‹น ์ƒํ’ˆ ์ƒ์„ธ์ •๋ณด๋ฅผ ์กฐํšŒํ•œ๋‹ค.") diff --git a/backend/src/main/java/com/funeat/recipe/application/RecipeService.java b/backend/src/main/java/com/funeat/recipe/application/RecipeService.java index d67fd7b79..19545b20b 100644 --- a/backend/src/main/java/com/funeat/recipe/application/RecipeService.java +++ b/backend/src/main/java/com/funeat/recipe/application/RecipeService.java @@ -5,6 +5,9 @@ import static com.funeat.product.exception.ProductErrorCode.PRODUCT_NOT_FOUND; import static com.funeat.recipe.exception.RecipeErrorCode.RECIPE_NOT_FOUND; +import com.funeat.comment.domain.Comment; +import com.funeat.comment.persistence.CommentRepository; +import com.funeat.comment.specification.CommentSpecification; import com.funeat.common.ImageUploader; import com.funeat.common.dto.PageDto; import com.funeat.member.domain.Member; @@ -26,6 +29,10 @@ import com.funeat.recipe.dto.RankingRecipeDto; import com.funeat.recipe.dto.RankingRecipesResponse; import com.funeat.recipe.dto.RecipeAuthorDto; +import com.funeat.recipe.dto.RecipeCommentCondition; +import com.funeat.recipe.dto.RecipeCommentCreateRequest; +import com.funeat.recipe.dto.RecipeCommentResponse; +import com.funeat.recipe.dto.RecipeCommentsResponse; import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeDetailResponse; import com.funeat.recipe.dto.RecipeDto; @@ -36,6 +43,8 @@ import com.funeat.recipe.exception.RecipeException.RecipeNotFoundException; import com.funeat.recipe.persistence.RecipeImageRepository; import com.funeat.recipe.persistence.RecipeRepository; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @@ -43,6 +52,8 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -51,8 +62,10 @@ @Transactional(readOnly = true) public class RecipeService { - private static final int THREE = 3; - private static final int TOP = 0; + private static final long RANKING_MINIMUM_FAVORITE_COUNT = 1L; + private static final int RANKING_SIZE = 3; + private static final int RECIPE_COMMENT_PAGE_SIZE = 10; + private static final int DEFAULT_CURSOR_PAGINATION_SIZE = 11; private final MemberRepository memberRepository; private final ProductRepository productRepository; @@ -60,18 +73,21 @@ public class RecipeService { private final RecipeRepository recipeRepository; private final RecipeImageRepository recipeImageRepository; private final RecipeFavoriteRepository recipeFavoriteRepository; + private final CommentRepository commentRepository; private final ImageUploader imageUploader; public RecipeService(final MemberRepository memberRepository, final ProductRepository productRepository, final ProductRecipeRepository productRecipeRepository, final RecipeRepository recipeRepository, final RecipeImageRepository recipeImageRepository, - final RecipeFavoriteRepository recipeFavoriteRepository, final ImageUploader imageUploader) { + final RecipeFavoriteRepository recipeFavoriteRepository, + final CommentRepository commentRepository, final ImageUploader imageUploader) { this.memberRepository = memberRepository; this.productRepository = productRepository; this.productRecipeRepository = productRecipeRepository; this.recipeRepository = recipeRepository; this.recipeImageRepository = recipeImageRepository; this.recipeFavoriteRepository = recipeFavoriteRepository; + this.commentRepository = commentRepository; this.imageUploader = imageUploader; } @@ -166,7 +182,8 @@ public void likeRecipe(final Long memberId, final Long recipeId, final RecipeFav recipeFavorite.updateFavorite(request.getFavorite()); } - private RecipeFavorite createAndSaveRecipeFavorite(final Member member, final Recipe recipe, final Boolean favorite) { + private RecipeFavorite createAndSaveRecipeFavorite(final Member member, final Recipe recipe, + final Boolean favorite) { try { final RecipeFavorite recipeFavorite = RecipeFavorite.create(member, recipe, favorite); return recipeFavoriteRepository.save(recipeFavorite); @@ -190,9 +207,11 @@ public SearchRecipeResultsResponse getSearchResults(final String query, final Pa } public RankingRecipesResponse getTop3Recipes() { - final List recipes = recipeRepository.findRecipesByOrderByFavoriteCountDesc(PageRequest.of(TOP, THREE)); + final List recipes = recipeRepository.findRecipesByFavoriteCountGreaterThanEqual(RANKING_MINIMUM_FAVORITE_COUNT); final List dtos = recipes.stream() + .sorted(Comparator.comparing(Recipe::calculateRankingScore).reversed()) + .limit(RANKING_SIZE) .map(recipe -> { final List findRecipeImages = recipeImageRepository.findByRecipe(recipe); final RecipeAuthorDto author = RecipeAuthorDto.toDto(recipe.getMember()); @@ -201,4 +220,63 @@ public RankingRecipesResponse getTop3Recipes() { .collect(Collectors.toList()); return RankingRecipesResponse.toResponse(dtos); } + + @Transactional + public Long writeCommentOfRecipe(final Long memberId, final Long recipeId, + final RecipeCommentCreateRequest request) { + final Member findMember = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND, memberId)); + + final Recipe findRecipe = recipeRepository.findById(recipeId) + .orElseThrow(() -> new RecipeNotFoundException(RECIPE_NOT_FOUND, recipeId)); + + final Comment comment = new Comment(findRecipe, findMember, request.getComment()); + + final Comment savedComment = commentRepository.save(comment); + return savedComment.getId(); + } + + public RecipeCommentsResponse getCommentsOfRecipe(final Long recipeId, final RecipeCommentCondition condition) { + final Recipe findRecipe = recipeRepository.findById(recipeId) + .orElseThrow(() -> new RecipeNotFoundException(RECIPE_NOT_FOUND, recipeId)); + + final Specification specification = CommentSpecification.findAllByRecipe(findRecipe, + condition.getLastId()); + + final PageRequest pageable = PageRequest.of(0, DEFAULT_CURSOR_PAGINATION_SIZE, Sort.by("id").descending()); + + final Page commentPaginationResult = commentRepository.findAllForPagination(specification, pageable, + condition.getTotalElements()); + + final List recipeCommentResponses = getRecipeCommentResponses( + commentPaginationResult.getContent()); + + final Boolean hasNext = hasNextPage(commentPaginationResult); + + return RecipeCommentsResponse.toResponse(recipeCommentResponses, hasNext, + commentPaginationResult.getTotalElements()); + } + + private List getRecipeCommentResponses(final List findComments) { + final List recipeCommentResponses = new ArrayList<>(); + final int resultSize = getResultSize(findComments); + final List comments = findComments.subList(0, resultSize); + + for (final Comment comment : comments) { + final RecipeCommentResponse recipeCommentResponse = RecipeCommentResponse.toResponse(comment); + recipeCommentResponses.add(recipeCommentResponse); + } + return recipeCommentResponses; + } + + private int getResultSize(final List findComments) { + if (findComments.size() < DEFAULT_CURSOR_PAGINATION_SIZE) { + return findComments.size(); + } + return RECIPE_COMMENT_PAGE_SIZE; + } + + private Boolean hasNextPage(final Page findComments) { + return findComments.getContent().size() > RECIPE_COMMENT_PAGE_SIZE; + } } diff --git a/backend/src/main/java/com/funeat/recipe/domain/Recipe.java b/backend/src/main/java/com/funeat/recipe/domain/Recipe.java index dcb607148..5ffb0438b 100644 --- a/backend/src/main/java/com/funeat/recipe/domain/Recipe.java +++ b/backend/src/main/java/com/funeat/recipe/domain/Recipe.java @@ -2,6 +2,7 @@ import com.funeat.member.domain.Member; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; @@ -14,6 +15,8 @@ @Entity public class Recipe { + private static final double RANKING_GRAVITY = 0.1; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -48,6 +51,21 @@ public Recipe(final String title, final String content, final Member member, this.favoriteCount = favoriteCount; } + public Recipe(final String title, final String content, final Member member, final Long favoriteCount, + final LocalDateTime createdAt) { + this.title = title; + this.content = content; + this.member = member; + this.favoriteCount = favoriteCount; + this.createdAt = createdAt; + } + + public Double calculateRankingScore() { + final long age = ChronoUnit.DAYS.between(createdAt, LocalDateTime.now()); + final double denominator = Math.pow(age + 1.0, RANKING_GRAVITY); + return favoriteCount / denominator; + } + public void addFavoriteCount() { this.favoriteCount++; } diff --git a/backend/src/main/java/com/funeat/recipe/dto/RankingRecipeDto.java b/backend/src/main/java/com/funeat/recipe/dto/RankingRecipeDto.java index c6fdcfc7c..3f26a69ca 100644 --- a/backend/src/main/java/com/funeat/recipe/dto/RankingRecipeDto.java +++ b/backend/src/main/java/com/funeat/recipe/dto/RankingRecipeDto.java @@ -2,6 +2,7 @@ import com.funeat.recipe.domain.Recipe; import com.funeat.recipe.domain.RecipeImage; +import java.time.LocalDateTime; import java.util.List; public class RankingRecipeDto { @@ -11,23 +12,26 @@ public class RankingRecipeDto { private final String title; private final RecipeAuthorDto author; private final Long favoriteCount; + private final LocalDateTime createdAt; public RankingRecipeDto(final Long id, final String image, final String title, final RecipeAuthorDto author, - final Long favoriteCount) { + final Long favoriteCount, final LocalDateTime createdAt) { this.id = id; this.image = image; this.title = title; this.author = author; this.favoriteCount = favoriteCount; + this.createdAt = createdAt; } public static RankingRecipeDto toDto(final Recipe recipe, final List images, final RecipeAuthorDto author) { if (images.isEmpty()) { - return new RankingRecipeDto(recipe.getId(), null, recipe.getTitle(), author, recipe.getFavoriteCount()); + return new RankingRecipeDto(recipe.getId(), null, recipe.getTitle(), author, + recipe.getFavoriteCount(), recipe.getCreatedAt()); } return new RankingRecipeDto(recipe.getId(), images.get(0).getImage(), recipe.getTitle(), author, - recipe.getFavoriteCount()); + recipe.getFavoriteCount(), recipe.getCreatedAt()); } public Long getId() { @@ -49,4 +53,8 @@ public RecipeAuthorDto getAuthor() { public Long getFavoriteCount() { return favoriteCount; } + + public LocalDateTime getCreatedAt() { + return createdAt; + } } diff --git a/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentCondition.java b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentCondition.java new file mode 100644 index 000000000..dcb3cf2d1 --- /dev/null +++ b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentCondition.java @@ -0,0 +1,20 @@ +package com.funeat.recipe.dto; + +public class RecipeCommentCondition { + + private final Long lastId; + private final Long totalElements; + + public RecipeCommentCondition(final Long lastId, final Long totalElements) { + this.lastId = lastId; + this.totalElements = totalElements; + } + + public Long getLastId() { + return lastId; + } + + public Long getTotalElements() { + return totalElements; + } +} diff --git a/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentCreateRequest.java b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentCreateRequest.java new file mode 100644 index 000000000..2b24e9207 --- /dev/null +++ b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentCreateRequest.java @@ -0,0 +1,20 @@ +package com.funeat.recipe.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +public class RecipeCommentCreateRequest { + + @NotBlank(message = "๊ฟ€์กฐํ•ฉ ๋Œ“๊ธ€์„ ํ™•์ธํ•ด ์ฃผ์„ธ์š”") + @Size(max = 200, message = "๊ฟ€์กฐํ•ฉ ๋Œ“๊ธ€์€ ์ตœ๋Œ€ 200์ž๊นŒ์ง€ ์ž…๋ ฅ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค") + private final String comment; + + public RecipeCommentCreateRequest(@JsonProperty("comment") final String comment) { + this.comment = comment; + } + + public String getComment() { + return comment; + } +} diff --git a/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentMemberResponse.java b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentMemberResponse.java new file mode 100644 index 000000000..ad66d7811 --- /dev/null +++ b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentMemberResponse.java @@ -0,0 +1,26 @@ +package com.funeat.recipe.dto; + +import com.funeat.member.domain.Member; + +public class RecipeCommentMemberResponse { + + private final String nickname; + private final String profileImage; + + private RecipeCommentMemberResponse(final String nickname, final String profileImage) { + this.nickname = nickname; + this.profileImage = profileImage; + } + + public static RecipeCommentMemberResponse toResponse(final Member member) { + return new RecipeCommentMemberResponse(member.getNickname(), member.getProfileImage()); + } + + public String getNickname() { + return nickname; + } + + public String getProfileImage() { + return profileImage; + } +} diff --git a/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentResponse.java b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentResponse.java new file mode 100644 index 000000000..989e52bd5 --- /dev/null +++ b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentResponse.java @@ -0,0 +1,42 @@ +package com.funeat.recipe.dto; + +import com.funeat.comment.domain.Comment; +import java.time.LocalDateTime; + +public class RecipeCommentResponse { + + private final Long id; + private final String comment; + private final LocalDateTime createdAt; + private final RecipeCommentMemberResponse author; + + private RecipeCommentResponse(final Long id, final String comment, final LocalDateTime createdAt, + final RecipeCommentMemberResponse author) { + this.id = id; + this.comment = comment; + this.createdAt = createdAt; + this.author = author; + } + + public static RecipeCommentResponse toResponse(final Comment comment) { + final RecipeCommentMemberResponse author = RecipeCommentMemberResponse.toResponse(comment.getMember()); + + return new RecipeCommentResponse(comment.getId(), comment.getComment(), comment.getCreatedAt(), author); + } + + public Long getId() { + return id; + } + + public String getComment() { + return comment; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public RecipeCommentMemberResponse getAuthor() { + return author; + } +} diff --git a/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentsResponse.java b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentsResponse.java new file mode 100644 index 000000000..7e7d6dc19 --- /dev/null +++ b/backend/src/main/java/com/funeat/recipe/dto/RecipeCommentsResponse.java @@ -0,0 +1,34 @@ +package com.funeat.recipe.dto; + +import java.util.List; + +public class RecipeCommentsResponse { + + private final List comments; + private final boolean hasNext; + private final Long totalElements; + + private RecipeCommentsResponse(final List comments, final boolean hasNext, + final Long totalElements) { + this.comments = comments; + this.hasNext = hasNext; + this.totalElements = totalElements; + } + + public static RecipeCommentsResponse toResponse(final List comments, final boolean hasNext, + final Long totalElements) { + return new RecipeCommentsResponse(comments, hasNext, totalElements); + } + + public List getComments() { + return comments; + } + + public boolean getHasNext() { + return hasNext; + } + + public Long getTotalElements() { + return totalElements; + } +} diff --git a/backend/src/main/java/com/funeat/recipe/persistence/RecipeRepository.java b/backend/src/main/java/com/funeat/recipe/persistence/RecipeRepository.java index 4d1a3a306..ce5ef3c31 100644 --- a/backend/src/main/java/com/funeat/recipe/persistence/RecipeRepository.java +++ b/backend/src/main/java/com/funeat/recipe/persistence/RecipeRepository.java @@ -32,9 +32,9 @@ public interface RecipeRepository extends JpaRepository { @Query("SELECT r FROM Recipe r LEFT JOIN ProductRecipe pr ON pr.product = :product WHERE pr.recipe.id = r.id") Page findRecipesByProduct(final Product product, final Pageable pageable); - List findRecipesByOrderByFavoriteCountDesc(final Pageable pageable); - @Lock(PESSIMISTIC_WRITE) @Query("SELECT r FROM Recipe r WHERE r.id=:id") Optional findByIdForUpdate(final Long id); + + List findRecipesByFavoriteCountGreaterThanEqual(final Long favoriteCount); } diff --git a/backend/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java b/backend/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java index 8406c1645..17eb1f1d6 100644 --- a/backend/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java +++ b/backend/src/main/java/com/funeat/recipe/presentation/RecipeApiController.java @@ -5,6 +5,9 @@ import com.funeat.common.logging.Logging; import com.funeat.recipe.application.RecipeService; import com.funeat.recipe.dto.RankingRecipesResponse; +import com.funeat.recipe.dto.RecipeCommentCondition; +import com.funeat.recipe.dto.RecipeCommentCreateRequest; +import com.funeat.recipe.dto.RecipeCommentsResponse; import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeDetailResponse; import com.funeat.recipe.dto.RecipeFavoriteRequest; @@ -19,6 +22,7 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -88,4 +92,22 @@ public ResponseEntity getSearchResults(@RequestPara return ResponseEntity.ok(response); } + + @PostMapping("/api/recipes/{recipeId}/comments") + public ResponseEntity writeComment(@AuthenticationPrincipal final LoginInfo loginInfo, + @PathVariable final Long recipeId, + @RequestBody @Valid final RecipeCommentCreateRequest request) { + final Long savedCommentId = recipeService.writeCommentOfRecipe(loginInfo.getId(), recipeId, request); + + return ResponseEntity.created(URI.create("/api/recipes/" + recipeId + "/" + savedCommentId)).build(); + } + + @GetMapping("/api/recipes/{recipeId}/comments") + public ResponseEntity getCommentsOfRecipe( + @AuthenticationPrincipal final LoginInfo loginInfo, @PathVariable final Long recipeId, + @ModelAttribute final RecipeCommentCondition condition) { + final RecipeCommentsResponse response = recipeService.getCommentsOfRecipe(recipeId, condition); + + return ResponseEntity.ok(response); + } } diff --git a/backend/src/main/java/com/funeat/recipe/presentation/RecipeController.java b/backend/src/main/java/com/funeat/recipe/presentation/RecipeController.java index 013c559cd..05602cc7f 100644 --- a/backend/src/main/java/com/funeat/recipe/presentation/RecipeController.java +++ b/backend/src/main/java/com/funeat/recipe/presentation/RecipeController.java @@ -3,6 +3,9 @@ import com.funeat.auth.dto.LoginInfo; import com.funeat.auth.util.AuthenticationPrincipal; import com.funeat.recipe.dto.RankingRecipesResponse; +import com.funeat.recipe.dto.RecipeCommentCondition; +import com.funeat.recipe.dto.RecipeCommentCreateRequest; +import com.funeat.recipe.dto.RecipeCommentsResponse; import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeDetailResponse; import com.funeat.recipe.dto.RecipeFavoriteRequest; @@ -16,6 +19,7 @@ import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -80,4 +84,24 @@ ResponseEntity likeRecipe(@AuthenticationPrincipal final LoginInfo loginIn @GetMapping ResponseEntity getSearchResults(@RequestParam final String query, @PageableDefault final Pageable pageable); + + @Operation(summary = "๊ฟ€์กฐํ•ฉ ๋Œ“๊ธ€ ์ž‘์„ฑ", description = "๊ฟ€์กฐํ•ฉ ์ƒ์„ธ์—์„œ ๋Œ“๊ธ€์„ ์ž‘์„ฑํ•œ๋‹ค.") + @ApiResponse( + responseCode = "201", + description = "๊ฟ€์กฐํ•ฉ ๋Œ“๊ธ€ ์ž‘์„ฑ ์„ฑ๊ณต." + ) + @PostMapping("/api/recipes/{recipeId}/comments") + ResponseEntity writeComment(@AuthenticationPrincipal final LoginInfo loginInfo, + @PathVariable final Long recipeId, + @RequestBody final RecipeCommentCreateRequest request); + + @Operation(summary = "๊ฟ€์กฐํ•ฉ ๋Œ“๊ธ€ ์กฐํšŒ", description = "๊ฟ€์กฐํ•ฉ ์ƒ์„ธ์—์„œ ๋Œ“๊ธ€์„ ์กฐํšŒํ•œ๋‹ค.") + @ApiResponse( + responseCode = "200", + description = "๊ฟ€์กฐํ•ฉ ๋Œ“๊ธ€ ์กฐํšŒ ์„ฑ๊ณต." + ) + @GetMapping("/api/recipes/{recipeId}/comments") + ResponseEntity getCommentsOfRecipe(@AuthenticationPrincipal final LoginInfo loginInfo, + @PathVariable final Long recipeId, + @ModelAttribute final RecipeCommentCondition condition); } diff --git a/backend/src/main/java/com/funeat/review/application/ReviewDeleteEvent.java b/backend/src/main/java/com/funeat/review/application/ReviewDeleteEvent.java new file mode 100644 index 000000000..7c69eee3c --- /dev/null +++ b/backend/src/main/java/com/funeat/review/application/ReviewDeleteEvent.java @@ -0,0 +1,14 @@ +package com.funeat.review.application; + +public class ReviewDeleteEvent { + + private final String image; + + public ReviewDeleteEvent(final String image) { + this.image = image; + } + + public String getImage() { + return image; + } +} diff --git a/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java b/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java new file mode 100644 index 000000000..a92c4f943 --- /dev/null +++ b/backend/src/main/java/com/funeat/review/application/ReviewDeleteEventListener.java @@ -0,0 +1,27 @@ +package com.funeat.review.application; + +import com.funeat.common.ImageUploader; +import io.micrometer.core.instrument.util.StringUtils; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +public class ReviewDeleteEventListener { + + private final ImageUploader imageUploader; + + public ReviewDeleteEventListener(final ImageUploader imageUploader) { + this.imageUploader = imageUploader; + } + + @Async + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void deleteReviewImageInS3(final ReviewDeleteEvent event) { + final String image = event.getImage(); + if (!StringUtils.isBlank(image)) { + imageUploader.delete(image); + } + } +} diff --git a/backend/src/main/java/com/funeat/review/application/ReviewService.java b/backend/src/main/java/com/funeat/review/application/ReviewService.java index ee482a8c5..a65922c82 100644 --- a/backend/src/main/java/com/funeat/review/application/ReviewService.java +++ b/backend/src/main/java/com/funeat/review/application/ReviewService.java @@ -3,6 +3,7 @@ import static com.funeat.member.exception.MemberErrorCode.MEMBER_DUPLICATE_FAVORITE; import static com.funeat.member.exception.MemberErrorCode.MEMBER_NOT_FOUND; import static com.funeat.product.exception.ProductErrorCode.PRODUCT_NOT_FOUND; +import static com.funeat.review.exception.ReviewErrorCode.NOT_AUTHOR_OF_REVIEW; import static com.funeat.review.exception.ReviewErrorCode.REVIEW_NOT_FOUND; import com.funeat.common.ImageUploader; @@ -24,21 +25,29 @@ import com.funeat.review.dto.RankingReviewDto; import com.funeat.review.dto.RankingReviewsResponse; import com.funeat.review.dto.ReviewCreateRequest; +import com.funeat.review.dto.ReviewDetailResponse; import com.funeat.review.dto.ReviewFavoriteRequest; import com.funeat.review.dto.SortingReviewDto; +import com.funeat.review.dto.SortingReviewDtoWithoutTag; +import com.funeat.review.dto.SortingReviewRequest; import com.funeat.review.dto.SortingReviewsResponse; +import com.funeat.review.exception.ReviewException.NotAuthorOfReviewException; import com.funeat.review.exception.ReviewException.ReviewNotFoundException; import com.funeat.review.persistence.ReviewRepository; import com.funeat.review.persistence.ReviewTagRepository; +import com.funeat.review.specification.SortingReviewSpecification; import com.funeat.tag.domain.Tag; import com.funeat.tag.persistence.TagRepository; +import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -47,9 +56,13 @@ @Transactional(readOnly = true) public class ReviewService { - private static final int TOP = 0; + private static final int FIRST_PAGE = 0; + private static final int START_INDEX = 0; private static final int ONE = 1; private static final String EMPTY_URL = ""; + private static final int RANKING_SIZE = 3; + private static final long RANKING_MINIMUM_FAVORITE_COUNT = 1L; + private static final int REVIEW_PAGE_SIZE = 10; private final ReviewRepository reviewRepository; private final TagRepository tagRepository; @@ -58,11 +71,13 @@ public class ReviewService { private final ProductRepository productRepository; private final ReviewFavoriteRepository reviewFavoriteRepository; private final ImageUploader imageUploader; + private final ApplicationEventPublisher eventPublisher; public ReviewService(final ReviewRepository reviewRepository, final TagRepository tagRepository, final ReviewTagRepository reviewTagRepository, final MemberRepository memberRepository, final ProductRepository productRepository, - final ReviewFavoriteRepository reviewFavoriteRepository, final ImageUploader imageUploader) { + final ReviewFavoriteRepository reviewFavoriteRepository, + final ImageUploader imageUploader, final ApplicationEventPublisher eventPublisher) { this.reviewRepository = reviewRepository; this.tagRepository = tagRepository; this.reviewTagRepository = reviewTagRepository; @@ -70,6 +85,7 @@ public ReviewService(final ReviewRepository reviewRepository, final TagRepositor this.productRepository = productRepository; this.reviewFavoriteRepository = reviewFavoriteRepository; this.imageUploader = imageUploader; + this.eventPublisher = eventPublisher; } @Transactional @@ -95,7 +111,7 @@ public void create(final Long productId, final Long memberId, final MultipartFil final Long countByProduct = reviewRepository.countByProduct(findProduct); - findProduct.updateAverageRating(savedReview.getRating(), countByProduct); + findProduct.updateAverageRatingForInsert(countByProduct, savedReview.getRating()); findProduct.addReviewCount(); reviewTagRepository.saveAll(reviewTags); } @@ -115,8 +131,7 @@ public void likeReview(final Long reviewId, final Long memberId, final ReviewFav private ReviewFavorite saveReviewFavorite(final Member member, final Review review, final Boolean favorite) { try { - final ReviewFavorite reviewFavorite = ReviewFavorite.create(member, review, - favorite); + final ReviewFavorite reviewFavorite = ReviewFavorite.create(member, review, favorite); return reviewFavoriteRepository.save(reviewFavorite); } catch (final DataIntegrityViolationException e) { throw new MemberDuplicateFavoriteException(MEMBER_DUPLICATE_FAVORITE, member.getId()); @@ -124,42 +139,85 @@ private ReviewFavorite saveReviewFavorite(final Member member, final Review revi } @Transactional - public void updateProductImage(final Long reviewId) { - final Review review = reviewRepository.findById(reviewId) - .orElseThrow(() -> new ReviewNotFoundException(REVIEW_NOT_FOUND, reviewId)); + public void updateProductImage(final Long productId) { + final Product product = productRepository.findById(productId) + .orElseThrow(() -> new ProductNotFoundException(PRODUCT_NOT_FOUND, productId)); - final Product product = review.getProduct(); - final Long productId = product.getId(); - final PageRequest pageRequest = PageRequest.of(TOP, ONE); + final PageRequest pageRequest = PageRequest.of(FIRST_PAGE, ONE); final List topFavoriteReview = reviewRepository.findPopularReviewWithImage(productId, pageRequest); - if (!topFavoriteReview.isEmpty()) { - final String topFavoriteReviewImage = topFavoriteReview.get(TOP).getImage(); - product.updateImage(topFavoriteReviewImage); + if (topFavoriteReview.isEmpty()) { + product.updateBasicImage(); + return; } + final String topFavoriteReviewImage = topFavoriteReview.get(START_INDEX).getImage(); + product.updateFavoriteImage(topFavoriteReviewImage); } - public SortingReviewsResponse sortingReviews(final Long productId, final Pageable pageable, final Long memberId) { - final Member member = memberRepository.findById(memberId) + public SortingReviewsResponse sortingReviews(final Long productId, final Long memberId, + final SortingReviewRequest request) { + final Member findMember = memberRepository.findById(memberId) .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND, memberId)); - - final Product product = productRepository.findById(productId) + final Product findProduct = productRepository.findById(productId) .orElseThrow(() -> new ProductNotFoundException(PRODUCT_NOT_FOUND, productId)); - final Page reviewPage = reviewRepository.findReviewsByProduct(pageable, product); + final List sortingReviews = getSortingReviews(findMember, findProduct, request); + final int resultSize = getResultSize(sortingReviews); + + final List resizeSortingReviews = sortingReviews.subList(START_INDEX, resultSize); + final Boolean hasNext = hasNextPage(sortingReviews); + + return SortingReviewsResponse.toResponse(resizeSortingReviews, hasNext); + } + + private List getSortingReviews(final Member member, final Product product, + final SortingReviewRequest request) { + final Long lastReviewId = request.getLastReviewId(); + final String sortOption = request.getSort(); + + final Specification specification = getSortingSpecification(product, sortOption, lastReviewId); + final List sortingReviewDtoWithoutTags = reviewRepository.getSortingReview(member, + specification, sortOption); + + return addTagsToSortingReviews(sortingReviewDtoWithoutTags); + } - final PageDto pageDto = PageDto.toDto(reviewPage); - final List reviewDtos = reviewPage.stream() - .map(review -> SortingReviewDto.toDto(review, member)) + private List addTagsToSortingReviews( + final List sortingReviewDtoWithoutTags) { + return sortingReviewDtoWithoutTags.stream() + .map(reviewDto -> SortingReviewDto.toDto(reviewDto, + tagRepository.findTagsByReviewId(reviewDto.getId()))) .collect(Collectors.toList()); + } + + private Specification getSortingSpecification(final Product product, final String sortOption, + final Long lastReviewId) { + if (lastReviewId == FIRST_PAGE) { + return SortingReviewSpecification.sortingFirstPageBy(product); + } - return SortingReviewsResponse.toResponse(pageDto, reviewDtos); + final Review lastReview = reviewRepository.findById(lastReviewId) + .orElseThrow(() -> new ReviewNotFoundException(REVIEW_NOT_FOUND, lastReviewId)); + + return SortingReviewSpecification.sortingBy(product, sortOption, lastReview); } - public RankingReviewsResponse getTopReviews() { - final List rankingReviews = reviewRepository.findTop3ByOrderByFavoriteCountDesc(); + private int getResultSize(final List sortingReviews) { + if (sortingReviews.size() <= REVIEW_PAGE_SIZE) { + return sortingReviews.size(); + } + return REVIEW_PAGE_SIZE; + } + + private Boolean hasNextPage(final List sortingReviews) { + return sortingReviews.size() > REVIEW_PAGE_SIZE; + } - final List dtos = rankingReviews.stream() + public RankingReviewsResponse getTopReviews() { + final List reviews = reviewRepository.findReviewsByFavoriteCountGreaterThanEqual(RANKING_MINIMUM_FAVORITE_COUNT); + final List dtos = reviews.stream() + .sorted(Comparator.comparing(Review::calculateRankingScore).reversed()) + .limit(RANKING_SIZE) .map(RankingReviewDto::toDto) .collect(Collectors.toList()); @@ -180,6 +238,52 @@ public MemberReviewsResponse findReviewByMember(final Long memberId, final Pagea return MemberReviewsResponse.toResponse(pageDto, dtos); } + @Transactional + public void deleteReview(final Long reviewId, final Long memberId) { + final Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberNotFoundException(MEMBER_NOT_FOUND, memberId)); + final Review review = reviewRepository.findById(reviewId) + .orElseThrow(() -> new ReviewNotFoundException(REVIEW_NOT_FOUND, reviewId)); + final Product product = review.getProduct(); + final String image = review.getImage(); + + if (review.checkAuthor(member)) { + eventPublisher.publishEvent(new ReviewDeleteEvent(image)); + deleteThingsRelatedToReview(review); + updateProduct(product, review.getRating()); + return; + } + throw new NotAuthorOfReviewException(NOT_AUTHOR_OF_REVIEW, memberId); + } + + private void deleteThingsRelatedToReview(final Review review) { + deleteReviewTags(review); + deleteReviewFavorites(review); + reviewRepository.delete(review); + } + + private void deleteReviewTags(final Review review) { + final List reviewTags = reviewTagRepository.findByReview(review); + final List ids = reviewTags.stream() + .map(ReviewTag::getId) + .collect(Collectors.toList()); + reviewTagRepository.deleteAllByIdInBatch(ids); + } + + private void deleteReviewFavorites(final Review review) { + final List reviewFavorites = reviewFavoriteRepository.findByReview(review); + final List ids = reviewFavorites.stream() + .map(ReviewFavorite::getId) + .collect(Collectors.toList()); + reviewFavoriteRepository.deleteAllByIdInBatch(ids); + } + + private void updateProduct(final Product product, final Long deletedRating) { + product.updateAverageRatingForDelete(deletedRating); + product.minusReviewCount(); + updateProductImage(product.getId()); + } + public Optional getMostFavoriteReview(final Long productId) { final Product findProduct = productRepository.findById(productId) .orElseThrow(() -> new ProductNotFoundException(PRODUCT_NOT_FOUND, productId)); @@ -188,4 +292,10 @@ public Optional getMostFavoriteReview(final Long pro return MostFavoriteReviewResponse.toResponse(review); } + + public ReviewDetailResponse getReviewDetail(final Long reviewId) { + final Review review = reviewRepository.findById(reviewId) + .orElseThrow(() -> new ReviewNotFoundException(REVIEW_NOT_FOUND, reviewId)); + return ReviewDetailResponse.toResponse(review); + } } diff --git a/backend/src/main/java/com/funeat/review/domain/Review.java b/backend/src/main/java/com/funeat/review/domain/Review.java index 3545371e3..9b1a458d3 100644 --- a/backend/src/main/java/com/funeat/review/domain/Review.java +++ b/backend/src/main/java/com/funeat/review/domain/Review.java @@ -4,6 +4,7 @@ import com.funeat.member.domain.favorite.ReviewFavorite; import com.funeat.product.domain.Product; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -20,6 +21,8 @@ @Entity public class Review { + private static final double RANKING_GRAVITY = 0.5; + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -80,6 +83,18 @@ public Review(final Member member, final Product findProduct, final String image this.favoriteCount = favoriteCount; } + public Review(final Member member, final Product findProduct, final String image, final Long rating, + final String content, final Boolean reBuy, final Long favoriteCount, final LocalDateTime createdAt) { + this.member = member; + this.product = findProduct; + this.image = image; + this.rating = rating; + this.content = content; + this.reBuy = reBuy; + this.favoriteCount = favoriteCount; + this.createdAt = createdAt; + } + public void addFavoriteCount() { this.favoriteCount++; } @@ -88,6 +103,16 @@ public void minusFavoriteCount() { this.favoriteCount--; } + public Double calculateRankingScore() { + final long age = ChronoUnit.DAYS.between(createdAt, LocalDateTime.now()); + final double denominator = Math.pow(age + 1.0, RANKING_GRAVITY); + return favoriteCount / denominator; + } + + public boolean checkAuthor(final Member member) { + return Objects.equals(this.member, member); + } + public Long getId() { return id; } diff --git a/backend/src/main/java/com/funeat/review/dto/RankingReviewDto.java b/backend/src/main/java/com/funeat/review/dto/RankingReviewDto.java index cecbe2cec..5ac70bb59 100644 --- a/backend/src/main/java/com/funeat/review/dto/RankingReviewDto.java +++ b/backend/src/main/java/com/funeat/review/dto/RankingReviewDto.java @@ -1,6 +1,7 @@ package com.funeat.review.dto; import com.funeat.review.domain.Review; +import java.time.LocalDateTime; public class RankingReviewDto { @@ -11,10 +12,11 @@ public class RankingReviewDto { private final String content; private final Long rating; private final Long favoriteCount; + private final LocalDateTime createdAt; private RankingReviewDto(final Long reviewId, final Long productId, final String categoryType, final String productName, final String content, - final Long rating, final Long favoriteCount) { + final Long rating, final Long favoriteCount, final LocalDateTime createdAt) { this.reviewId = reviewId; this.productId = productId; this.categoryType = categoryType; @@ -22,6 +24,7 @@ private RankingReviewDto(final Long reviewId, final Long productId, final String this.content = content; this.rating = rating; this.favoriteCount = favoriteCount; + this.createdAt = createdAt; } public static RankingReviewDto toDto(final Review review) { @@ -32,8 +35,8 @@ public static RankingReviewDto toDto(final Review review) { review.getProduct().getName(), review.getContent(), review.getRating(), - review.getFavoriteCount() - ); + review.getFavoriteCount(), + review.getCreatedAt()); } public Long getReviewId() { @@ -63,4 +66,8 @@ public Long getFavoriteCount() { public String getCategoryType() { return categoryType; } + + public LocalDateTime getCreatedAt() { + return createdAt; + } } diff --git a/backend/src/main/java/com/funeat/review/dto/ReviewDetailResponse.java b/backend/src/main/java/com/funeat/review/dto/ReviewDetailResponse.java new file mode 100644 index 000000000..7e0c9858b --- /dev/null +++ b/backend/src/main/java/com/funeat/review/dto/ReviewDetailResponse.java @@ -0,0 +1,121 @@ +package com.funeat.review.dto; + +import com.funeat.review.domain.Review; +import com.funeat.review.domain.ReviewTag; +import com.funeat.tag.dto.TagDto; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +public class ReviewDetailResponse { + + private final Long id; + private final String userName; + private final String profileImage; + private final String image; + private final Long rating; + private final List tags; + private final String content; + private final boolean rebuy; + private final Long favoriteCount; + private final LocalDateTime createdAt; + private final String categoryType; + private final Long productId; + private final String productName; + + public ReviewDetailResponse(final Long id, final String userName, final String profileImage, final String image, + final Long rating, final List tags, final String content, final boolean rebuy, + final Long favoriteCount, final LocalDateTime createdAt, final String categoryType, + final Long productId, final String productName) { + this.id = id; + this.userName = userName; + this.profileImage = profileImage; + this.image = image; + this.rating = rating; + this.tags = tags; + this.content = content; + this.rebuy = rebuy; + this.favoriteCount = favoriteCount; + this.createdAt = createdAt; + this.categoryType = categoryType; + this.productId = productId; + this.productName = productName; + } + + public static ReviewDetailResponse toResponse(final Review review) { + return new ReviewDetailResponse( + review.getId(), + review.getMember().getNickname(), + review.getMember().getProfileImage(), + review.getImage(), + review.getRating(), + findTagDtos(review), + review.getContent(), + review.getReBuy(), + review.getFavoriteCount(), + review.getCreatedAt(), + review.getProduct().getCategory().getType().getName(), + review.getProduct().getId(), + review.getProduct().getName() + ); + } + + private static List findTagDtos(final Review review) { + return review.getReviewTags().stream() + .map(ReviewTag::getTag) + .map(TagDto::toDto) + .collect(Collectors.toList()); + } + + public Long getId() { + return id; + } + + public String getUserName() { + return userName; + } + + public String getProfileImage() { + return profileImage; + } + + public String getImage() { + return image; + } + + public Long getRating() { + return rating; + } + + public List getTags() { + return tags; + } + + public String getContent() { + return content; + } + + public boolean isRebuy() { + return rebuy; + } + + public Long getFavoriteCount() { + return favoriteCount; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public String getCategoryType() { + return categoryType; + } + + public Long getProductId() { + return productId; + } + + public String getProductName() { + return productName; + } +} diff --git a/backend/src/main/java/com/funeat/review/dto/SortingReviewDto.java b/backend/src/main/java/com/funeat/review/dto/SortingReviewDto.java index 7254dd6c1..1231d0058 100644 --- a/backend/src/main/java/com/funeat/review/dto/SortingReviewDto.java +++ b/backend/src/main/java/com/funeat/review/dto/SortingReviewDto.java @@ -1,12 +1,16 @@ package com.funeat.review.dto; +import com.fasterxml.jackson.annotation.JsonCreator; import com.funeat.member.domain.Member; import com.funeat.member.domain.favorite.ReviewFavorite; import com.funeat.review.domain.Review; import com.funeat.review.domain.ReviewTag; +import com.funeat.tag.domain.Tag; import com.funeat.tag.dto.TagDto; import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; public class SortingReviewDto { @@ -23,6 +27,7 @@ public class SortingReviewDto { private final boolean favorite; private final LocalDateTime createdAt; + @JsonCreator public SortingReviewDto(final Long id, final String userName, final String profileImage, final String image, final Long rating, final List tags, final String content, final boolean rebuy, final Long favoriteCount, final boolean favorite, @@ -40,37 +45,23 @@ public SortingReviewDto(final Long id, final String userName, final String profi this.createdAt = createdAt; } - public static SortingReviewDto toDto(final Review review, final Member member) { - return new SortingReviewDto( - review.getId(), - review.getMember().getNickname(), - review.getMember().getProfileImage(), - review.getImage(), - review.getRating(), - findTagDtos(review), - review.getContent(), - review.getReBuy(), - review.getFavoriteCount(), - findReviewFavoriteChecked(review, member), - review.getCreatedAt() - ); - } - - private static List findTagDtos(final Review review) { - return review.getReviewTags().stream() - .map(ReviewTag::getTag) + public static SortingReviewDto toDto(final SortingReviewDtoWithoutTag sortingReviewDto, final List tags) { + final List tagDtos = tags.stream() .map(TagDto::toDto) .collect(Collectors.toList()); - } - private static boolean findReviewFavoriteChecked(final Review review, final Member member) { - return review.getReviewFavorites() - .stream() - .filter(reviewFavorite -> reviewFavorite.getReview().equals(review)) - .filter(reviewFavorite -> reviewFavorite.getMember().equals(member)) - .findFirst() - .map(ReviewFavorite::getFavorite) - .orElse(false); + return new SortingReviewDto( + sortingReviewDto.getId(), + sortingReviewDto.getUserName(), + sortingReviewDto.getProfileImage(), + sortingReviewDto.getImage(), + sortingReviewDto.getRating(), + tagDtos, + sortingReviewDto.getContent(), + sortingReviewDto.getRebuy(), + sortingReviewDto.getFavoriteCount(), + sortingReviewDto.getFavorite(), + sortingReviewDto.getCreatedAt()); } public Long getId() { @@ -101,7 +92,7 @@ public String getContent() { return content; } - public boolean isRebuy() { + public boolean getRebuy() { return rebuy; } @@ -109,7 +100,7 @@ public Long getFavoriteCount() { return favoriteCount; } - public boolean isFavorite() { + public boolean getFavorite() { return favorite; } diff --git a/backend/src/main/java/com/funeat/review/dto/SortingReviewDtoWithoutTag.java b/backend/src/main/java/com/funeat/review/dto/SortingReviewDtoWithoutTag.java new file mode 100644 index 000000000..b58ac7165 --- /dev/null +++ b/backend/src/main/java/com/funeat/review/dto/SortingReviewDtoWithoutTag.java @@ -0,0 +1,84 @@ +package com.funeat.review.dto; + +import java.time.LocalDateTime; +import java.util.Objects; + +public class SortingReviewDtoWithoutTag { + + private final Long id; + private final String userName; + private final String profileImage; + private final String image; + private final Long rating; + private final String content; + private final boolean rebuy; + private final Long favoriteCount; + private final boolean favorite; + private final LocalDateTime createdAt; + + public SortingReviewDtoWithoutTag(final Long id, final String userName, final String profileImage, + final String image, final Long rating, + final String content, final boolean rebuy, final Long favoriteCount, + final Boolean favorite, + final LocalDateTime createdAt) { + final Boolean isFavorite = checkingFavorite(favorite); + + this.id = id; + this.userName = userName; + this.profileImage = profileImage; + this.image = image; + this.rating = rating; + this.content = content; + this.rebuy = rebuy; + this.favoriteCount = favoriteCount; + this.favorite = isFavorite; + this.createdAt = createdAt; + } + + private static Boolean checkingFavorite(final Boolean favorite) { + if (Objects.isNull(favorite)) { + return Boolean.FALSE; + } + return favorite; + } + + public Long getId() { + return id; + } + + public String getUserName() { + return userName; + } + + public String getProfileImage() { + return profileImage; + } + + public String getImage() { + return image; + } + + public Long getRating() { + return rating; + } + + public String getContent() { + return content; + } + + public boolean getRebuy() { + return rebuy; + } + + public Long getFavoriteCount() { + return favoriteCount; + } + + public boolean getFavorite() { + return favorite; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } +} diff --git a/backend/src/main/java/com/funeat/review/dto/SortingReviewRequest.java b/backend/src/main/java/com/funeat/review/dto/SortingReviewRequest.java new file mode 100644 index 000000000..b6bdeb1eb --- /dev/null +++ b/backend/src/main/java/com/funeat/review/dto/SortingReviewRequest.java @@ -0,0 +1,27 @@ +package com.funeat.review.dto; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.PositiveOrZero; + +public class SortingReviewRequest { + + @NotNull(message = "์ •๋ ฌ ์กฐ๊ฑด์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”") + private String sort; + + @NotNull(message = "๋งˆ์ง€๋ง‰์œผ๋กœ ์กฐํšŒํ•œ ๋ฆฌ๋ทฐ ID๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”") + @PositiveOrZero(message = "๋งˆ์ง€๋ง‰์œผ๋กœ ์กฐํšŒํ•œ ID๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. (์ฒ˜์Œ ์กฐํšŒํ•˜๋ฉด 0)") + private Long lastReviewId; + + public SortingReviewRequest(final String sort, final Long lastReviewId) { + this.sort = sort; + this.lastReviewId = lastReviewId; + } + + public String getSort() { + return sort; + } + + public Long getLastReviewId() { + return lastReviewId; + } +} diff --git a/backend/src/main/java/com/funeat/review/dto/SortingReviewsResponse.java b/backend/src/main/java/com/funeat/review/dto/SortingReviewsResponse.java index caf1ea155..1dc082fe0 100644 --- a/backend/src/main/java/com/funeat/review/dto/SortingReviewsResponse.java +++ b/backend/src/main/java/com/funeat/review/dto/SortingReviewsResponse.java @@ -1,27 +1,26 @@ package com.funeat.review.dto; -import com.funeat.common.dto.PageDto; import java.util.List; public class SortingReviewsResponse { - private final PageDto page; private final List reviews; + private final Boolean hasNext; - public SortingReviewsResponse(final PageDto page, final List reviews) { - this.page = page; + public SortingReviewsResponse(final List reviews, final Boolean hasNext) { this.reviews = reviews; + this.hasNext = hasNext; } - public static SortingReviewsResponse toResponse(final PageDto page, final List reviews) { - return new SortingReviewsResponse(page, reviews); - } - - public PageDto getPage() { - return page; + public static SortingReviewsResponse toResponse(final List reviews, final Boolean hasNextReview) { + return new SortingReviewsResponse(reviews, hasNextReview); } public List getReviews() { return reviews; } + + public Boolean getHasNext() { + return hasNext; + } } diff --git a/backend/src/main/java/com/funeat/review/exception/ReviewErrorCode.java b/backend/src/main/java/com/funeat/review/exception/ReviewErrorCode.java index d91c0c8c3..2f5fb5c64 100644 --- a/backend/src/main/java/com/funeat/review/exception/ReviewErrorCode.java +++ b/backend/src/main/java/com/funeat/review/exception/ReviewErrorCode.java @@ -5,6 +5,8 @@ public enum ReviewErrorCode { REVIEW_NOT_FOUND(HttpStatus.NOT_FOUND, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ฆฌ๋ทฐ์ž…๋‹ˆ๋‹ค. ๋ฆฌ๋ทฐ id๋ฅผ ํ™•์ธํ•˜์„ธ์š”.", "3001"), + NOT_SUPPORTED_REVIEW_SORTING_CONDITION(HttpStatus.BAD_REQUEST, "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ •๋ ฌ ์˜ต์…˜์ž…๋‹ˆ๋‹ค. ์ •๋ ฌ ์˜ต์…˜์„ ํ™•์ธํ•˜์„ธ์š”.", "3002"), + NOT_AUTHOR_OF_REVIEW(HttpStatus.BAD_REQUEST, "ํ•ด๋‹น ๋ฆฌ๋ทฐ๋ฅผ ์ž‘์„ฑํ•œ ํšŒ์›์ด ์•„๋‹™๋‹ˆ๋‹ค", "3003") ; private final HttpStatus status; diff --git a/backend/src/main/java/com/funeat/review/exception/ReviewException.java b/backend/src/main/java/com/funeat/review/exception/ReviewException.java index 4699f3af6..85fd3f666 100644 --- a/backend/src/main/java/com/funeat/review/exception/ReviewException.java +++ b/backend/src/main/java/com/funeat/review/exception/ReviewException.java @@ -15,4 +15,16 @@ public ReviewNotFoundException(final ReviewErrorCode errorCode, final Long revie super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), reviewId)); } } + + public static class NotSupportedReviewSortingConditionException extends ReviewException { + public NotSupportedReviewSortingConditionException(final ReviewErrorCode errorCode, final String sortFieldName) { + super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), sortFieldName)); + } + } + + public static class NotAuthorOfReviewException extends ReviewException { + public NotAuthorOfReviewException(final ReviewErrorCode errorCode, final Long memberId) { + super(errorCode.getStatus(), new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), memberId)); + } + } } diff --git a/backend/src/main/java/com/funeat/review/persistence/ReviewCustomRepository.java b/backend/src/main/java/com/funeat/review/persistence/ReviewCustomRepository.java new file mode 100644 index 000000000..e2dd79992 --- /dev/null +++ b/backend/src/main/java/com/funeat/review/persistence/ReviewCustomRepository.java @@ -0,0 +1,14 @@ +package com.funeat.review.persistence; + +import com.funeat.member.domain.Member; +import com.funeat.review.domain.Review; +import com.funeat.review.dto.SortingReviewDtoWithoutTag; +import java.util.List; +import org.springframework.data.jpa.domain.Specification; + +public interface ReviewCustomRepository { + + List getSortingReview(final Member loginMember, + final Specification specification, + final String sortField); +} diff --git a/backend/src/main/java/com/funeat/review/persistence/ReviewRepository.java b/backend/src/main/java/com/funeat/review/persistence/ReviewRepository.java index f5ed0058f..69d35018d 100644 --- a/backend/src/main/java/com/funeat/review/persistence/ReviewRepository.java +++ b/backend/src/main/java/com/funeat/review/persistence/ReviewRepository.java @@ -14,11 +14,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -public interface ReviewRepository extends JpaRepository { - - Page findReviewsByProduct(final Pageable pageable, final Product product); - - List findTop3ByOrderByFavoriteCountDesc(); +public interface ReviewRepository extends JpaRepository, ReviewCustomRepository { Long countByProduct(final Product product); @@ -36,4 +32,6 @@ public interface ReviewRepository extends JpaRepository { List findPopularReviewWithImage(@Param("id") final Long productId, final Pageable pageable); Optional findTopByProductOrderByFavoriteCountDescIdDesc(final Product product); + + List findReviewsByFavoriteCountGreaterThanEqual(final Long favoriteCount); } diff --git a/backend/src/main/java/com/funeat/review/persistence/ReviewRepositoryImpl.java b/backend/src/main/java/com/funeat/review/persistence/ReviewRepositoryImpl.java new file mode 100644 index 000000000..ae47f7127 --- /dev/null +++ b/backend/src/main/java/com/funeat/review/persistence/ReviewRepositoryImpl.java @@ -0,0 +1,91 @@ +package com.funeat.review.persistence; + +import com.funeat.member.domain.Member; +import com.funeat.member.domain.favorite.ReviewFavorite; +import com.funeat.review.domain.Review; +import com.funeat.review.dto.SortingReviewDtoWithoutTag; +import java.util.List; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.persistence.TypedQuery; +import javax.persistence.criteria.CompoundSelection; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.JoinType; +import javax.persistence.criteria.Order; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Repository; + +@Repository +public class ReviewRepositoryImpl implements ReviewCustomRepository { + + @PersistenceContext + private EntityManager em; + + @Override + public List getSortingReview(final Member loginMember, + final Specification specification, + final String sortOption) { + final CriteriaBuilder cb = em.getCriteriaBuilder(); + final CriteriaQuery cq = cb.createQuery(SortingReviewDtoWithoutTag.class); + final Root root = cq.from(Review.class); + + // sortField, sortOrder + final String[] sortOptionSplit = sortOption.split(","); + final String sortField = sortOptionSplit[0]; + final String sortOrder = sortOptionSplit[1]; + + // join + final Join joinMember = root.join("member", JoinType.INNER); + + // left join + final Join leftJoinReviewFavorite = root.join("reviewFavorites", JoinType.LEFT); + final Predicate condition = cb.equal(leftJoinReviewFavorite.get("member"), loginMember); + leftJoinReviewFavorite.on(condition); + + // select - from - where - order by + cq.select(getConstruct(root, cb, joinMember, leftJoinReviewFavorite)) + .where(specification.toPredicate(root, cq, cb)) + .orderBy(getOrderBy(root, cb, sortField, sortOrder)); + + // limit + final TypedQuery query = em.createQuery(cq); + query.setMaxResults(11); + + // result + return query.getResultList(); + } + + private CompoundSelection getConstruct(final Root root, + final CriteriaBuilder cb, + final Join joinMember, + final Join leftJoinReviewFavorite) { + + return cb.construct(SortingReviewDtoWithoutTag.class, + root.get("id"), + joinMember.get("nickname"), + joinMember.get("profileImage"), + root.get("image"), + root.get("rating"), + root.get("content"), + root.get("reBuy"), + root.get("favoriteCount"), + leftJoinReviewFavorite.get("favorite"), + root.get("createdAt")); + } + + private List getOrderBy(final Root root, + final CriteriaBuilder cb, + final String fieldName, + final String sortOption) { + if ("ASC".equalsIgnoreCase(sortOption)) { + final Order order = cb.asc(root.get(fieldName)); + return List.of(order, cb.desc(root.get("id"))); + } + final Order order = cb.desc(root.get(fieldName)); + return List.of(order, cb.desc(root.get("id"))); + } +} diff --git a/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java b/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java index 7129a711c..cbdf3c3bf 100644 --- a/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java +++ b/backend/src/main/java/com/funeat/review/persistence/ReviewTagRepository.java @@ -1,5 +1,6 @@ package com.funeat.review.persistence; +import com.funeat.review.domain.Review; import com.funeat.review.domain.ReviewTag; import com.funeat.tag.domain.Tag; import java.util.List; @@ -16,4 +17,8 @@ public interface ReviewTagRepository extends JpaRepository { + "GROUP BY rt.tag " + "ORDER BY cnt DESC") List findTop3TagsByReviewIn(final Long productId, final Pageable pageable); + + void deleteByReview(final Review review); + + List findByReview(final Review review); } diff --git a/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java b/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java index 57bf20359..ff82ff499 100644 --- a/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java +++ b/backend/src/main/java/com/funeat/review/presentation/ReviewApiController.java @@ -7,17 +7,17 @@ import com.funeat.review.dto.MostFavoriteReviewResponse; import com.funeat.review.dto.RankingReviewsResponse; import com.funeat.review.dto.ReviewCreateRequest; +import com.funeat.review.dto.ReviewDetailResponse; import com.funeat.review.dto.ReviewFavoriteRequest; +import com.funeat.review.dto.SortingReviewRequest; import com.funeat.review.dto.SortingReviewsResponse; import java.net.URI; -import java.util.Objects; import java.util.Optional; import javax.validation.Valid; -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -49,11 +49,12 @@ public ResponseEntity writeReview(@PathVariable final Long productId, @Logging @PatchMapping("/api/products/{productId}/reviews/{reviewId}") - public ResponseEntity toggleLikeReview(@PathVariable final Long reviewId, + public ResponseEntity toggleLikeReview(@PathVariable final Long productId, + @PathVariable final Long reviewId, @AuthenticationPrincipal final LoginInfo loginInfo, @RequestBody @Valid final ReviewFavoriteRequest request) { reviewService.likeReview(reviewId, loginInfo.getId(), request); - reviewService.updateProductImage(reviewId); + reviewService.updateProductImage(productId); return ResponseEntity.noContent().build(); } @@ -61,8 +62,8 @@ public ResponseEntity toggleLikeReview(@PathVariable final Long reviewId, @GetMapping("/api/products/{productId}/reviews") public ResponseEntity getSortingReviews(@AuthenticationPrincipal final LoginInfo loginInfo, @PathVariable final Long productId, - @PageableDefault final Pageable pageable) { - final SortingReviewsResponse response = reviewService.sortingReviews(productId, pageable, loginInfo.getId()); + @ModelAttribute final SortingReviewRequest request) { + final SortingReviewsResponse response = reviewService.sortingReviews(productId, loginInfo.getId(), request); return ResponseEntity.ok(response); } @@ -83,4 +84,11 @@ public ResponseEntity> getMostFavoriteRevie } return ResponseEntity.ok(response); } + + @GetMapping("/api/reviews/{reviewId}") + public ResponseEntity getReviewDetail(@PathVariable final Long reviewId) { + final ReviewDetailResponse response = reviewService.getReviewDetail(reviewId); + + return ResponseEntity.ok(response); + } } diff --git a/backend/src/main/java/com/funeat/review/presentation/ReviewController.java b/backend/src/main/java/com/funeat/review/presentation/ReviewController.java index 886ee5a15..13fb85e94 100644 --- a/backend/src/main/java/com/funeat/review/presentation/ReviewController.java +++ b/backend/src/main/java/com/funeat/review/presentation/ReviewController.java @@ -5,16 +5,17 @@ import com.funeat.review.dto.MostFavoriteReviewResponse; import com.funeat.review.dto.RankingReviewsResponse; import com.funeat.review.dto.ReviewCreateRequest; +import com.funeat.review.dto.ReviewDetailResponse; import com.funeat.review.dto.ReviewFavoriteRequest; +import com.funeat.review.dto.SortingReviewRequest; import com.funeat.review.dto.SortingReviewsResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.Optional; -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -42,7 +43,8 @@ ResponseEntity writeReview(@PathVariable final Long productId, description = "๋ฆฌ๋ทฐ ์ข‹์•„์š”(์ทจ์†Œ) ์„ฑ๊ณต." ) @PatchMapping - ResponseEntity toggleLikeReview(@PathVariable final Long reviewId, + ResponseEntity toggleLikeReview(@PathVariable final Long productId, + @PathVariable final Long reviewId, @AuthenticationPrincipal final LoginInfo loginInfo, @RequestBody final ReviewFavoriteRequest request); @@ -54,7 +56,7 @@ ResponseEntity toggleLikeReview(@PathVariable final Long reviewId, @GetMapping ResponseEntity getSortingReviews(@AuthenticationPrincipal final LoginInfo loginInfo, @PathVariable final Long productId, - @PageableDefault final Pageable pageable); + @ModelAttribute final SortingReviewRequest request); @Operation(summary = "๋ฆฌ๋ทฐ ๋žญํ‚น Top3 ์กฐํšŒ", description = "๋ฆฌ๋ทฐ ๋žญํ‚น Top3 ์กฐํšŒํ•œ๋‹ค.") @ApiResponse( @@ -71,4 +73,12 @@ ResponseEntity getSortingReviews(@AuthenticationPrincipa ) @GetMapping ResponseEntity> getMostFavoriteReview(@PathVariable final Long productId); + + @Operation(summary = "๋ฆฌ๋ทฐ ์ƒ์„ธ ์กฐํšŒ", description = "๋ฆฌ๋ทฐ์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•œ๋‹ค.") + @ApiResponse( + responseCode = "200", + description = "๋ฆฌ๋ทฐ ์ƒ์„ธ ์กฐํšŒ ์„ฑ๊ณต." + ) + @GetMapping + ResponseEntity getReviewDetail(@PathVariable final Long reviewId); } diff --git a/backend/src/main/java/com/funeat/review/specification/LongTypeReviewSortSpec.java b/backend/src/main/java/com/funeat/review/specification/LongTypeReviewSortSpec.java new file mode 100644 index 000000000..23914e003 --- /dev/null +++ b/backend/src/main/java/com/funeat/review/specification/LongTypeReviewSortSpec.java @@ -0,0 +1,27 @@ +package com.funeat.review.specification; + +import com.funeat.review.domain.Review; +import java.util.Arrays; +import java.util.function.Function; + +public enum LongTypeReviewSortSpec { + + FAVORITE_COUNT("favoriteCount", Review::getFavoriteCount), + RATING("rating", Review::getRating); + + private final String fieldName; + private final Function function; + + LongTypeReviewSortSpec(final String fieldName, final Function function) { + this.fieldName = fieldName; + this.function = function; + } + + public static Long find(final String fieldName, final Review lastReview) { + return Arrays.stream(LongTypeReviewSortSpec.values()) + .filter(reviewSortSpec -> reviewSortSpec.fieldName.equals(fieldName)) + .findFirst() + .orElseThrow(IllegalArgumentException::new) + .function.apply(lastReview); + } +} diff --git a/backend/src/main/java/com/funeat/review/specification/SortingReviewSpecification.java b/backend/src/main/java/com/funeat/review/specification/SortingReviewSpecification.java new file mode 100644 index 000000000..781032017 --- /dev/null +++ b/backend/src/main/java/com/funeat/review/specification/SortingReviewSpecification.java @@ -0,0 +1,161 @@ +package com.funeat.review.specification; + +import static com.funeat.review.exception.ReviewErrorCode.NOT_SUPPORTED_REVIEW_SORTING_CONDITION; + +import com.funeat.product.domain.Product; +import com.funeat.review.domain.Review; +import com.funeat.review.exception.ReviewException.NotSupportedReviewSortingConditionException; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Path; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import org.springframework.data.jpa.domain.Specification; + +public final class SortingReviewSpecification { + + private static final List LOCALDATETIME_TYPE_INCLUDE = List.of("createdAt"); + private static final List LONG_TYPE_INCLUDE = List.of("favoriteCount", "rating"); + private static final String DELIMITER = ","; + private static final String PRODUCT = "product"; + private static final String ID = "id"; + private static final String ASC = "ASC"; + + private SortingReviewSpecification() { + } + + public static Specification sortingFirstPageBy(final Product product) { + return (root, query, criteriaBuilder) -> Specification + .where(equalsProduct(product)) + .toPredicate(root, query, criteriaBuilder); + } + + public static Specification sortingBy(final Product product, final String sortOption, + final Review lastReview) { + return (root, query, criteriaBuilder) -> { + final String[] sortFieldSplit = sortOption.split(DELIMITER); + final String field = sortFieldSplit[0]; + final String sort = sortFieldSplit[1]; + + return Specification + .where((equalsProduct(product).and(equals(field, lastReview)).and(lessThanLastReviewId(lastReview))) + .or(equalsProduct(product).and(lessOrGreaterThan(field, sort, lastReview)))) + .toPredicate(root, query, criteriaBuilder); + }; + } + + private static Specification equalsProduct(final Product product) { + return (root, query, criteriaBuilder) -> { + if (Objects.isNull(product)) { + return null; + } + + final Path productPath = root.get(PRODUCT); + + return criteriaBuilder.equal(productPath, product); + }; + } + + private static Specification lessThanLastReviewId(final Review lastReview) { + return (root, query, criteriaBuilder) -> { + if (Objects.isNull(lastReview)) { + return null; + } + + final Path reviewPath = root.get(ID); + + return criteriaBuilder.lessThan(reviewPath, lastReview.getId()); + }; + } + + private static Specification equals(final String fieldName, final Review lastReview) { + return (root, query, criteriaBuilder) -> { + if (validateNull(fieldName, lastReview)) { + return null; + } + + return checkEquals(fieldName, lastReview, root, criteriaBuilder); + }; + } + + private static Predicate checkEquals(final String fieldName, + final Review lastReview, + final Root root, + final CriteriaBuilder criteriaBuilder) { + if (LOCALDATETIME_TYPE_INCLUDE.contains(fieldName)) { + final Path createdAtPath = root.get(fieldName); + final LocalDateTime lastReviewCreatedAt = lastReview.getCreatedAt(); + return criteriaBuilder.equal(createdAtPath, lastReviewCreatedAt); + } + if (LONG_TYPE_INCLUDE.contains(fieldName)) { + final Path reviewPath = root.get(fieldName); + final Long lastReviewField = LongTypeReviewSortSpec.find(fieldName, lastReview); + return criteriaBuilder.equal(reviewPath, lastReviewField); + } + throw new NotSupportedReviewSortingConditionException(NOT_SUPPORTED_REVIEW_SORTING_CONDITION, fieldName); + } + + private static Specification lessOrGreaterThan(final String field, final String sort, + final Review lastReview) { + if (ASC.equalsIgnoreCase(sort)) { + return greaterThan(field, lastReview); + } + return lessThan(field, lastReview); + } + + private static Specification greaterThan(final String fieldName, final Review lastReview) { + return (root, query, criteriaBuilder) -> { + if (validateNull(fieldName, lastReview)) { + return null; + } + + return checkGreaterThan(fieldName, lastReview, root, criteriaBuilder); + }; + } + + private static Predicate checkGreaterThan(final String fieldName, final Review lastReview, final Root root, + final CriteriaBuilder criteriaBuilder) { + if (LOCALDATETIME_TYPE_INCLUDE.contains(fieldName)) { + final Path createdAtPath = root.get(fieldName); + final LocalDateTime lastReviewCreatedAt = lastReview.getCreatedAt(); + return criteriaBuilder.greaterThan(createdAtPath, lastReviewCreatedAt); + } + if (LONG_TYPE_INCLUDE.contains(fieldName)) { + final Path reviewPath = root.get(fieldName); + final Long lastReviewField = LongTypeReviewSortSpec.find(fieldName, lastReview); + return criteriaBuilder.greaterThan(reviewPath, lastReviewField); + } + throw new NotSupportedReviewSortingConditionException(NOT_SUPPORTED_REVIEW_SORTING_CONDITION, fieldName); + } + + private static Specification lessThan(final String fieldName, final Review lastReview) { + return (root, query, criteriaBuilder) -> { + if (validateNull(fieldName, lastReview)) { + return null; + } + + return checkLessThan(fieldName, lastReview, root, criteriaBuilder); + }; + } + + private static boolean validateNull(final String fieldName, final Review lastReview) { + return Objects.isNull(fieldName) || Objects.isNull(lastReview); + } + + private static Predicate checkLessThan(final String fieldName, final Review lastReview, final Root root, + final CriteriaBuilder criteriaBuilder) { + if (LOCALDATETIME_TYPE_INCLUDE.contains(fieldName)) { + final Path createdAtPath = root.get(fieldName); + final LocalDateTime lastReviewCreatedAt = lastReview.getCreatedAt(); + return criteriaBuilder.lessThan(createdAtPath, lastReviewCreatedAt); + } + if (LONG_TYPE_INCLUDE.contains(fieldName)) { + final Path reviewPath = root.get(fieldName); + final Long lastReviewField = LongTypeReviewSortSpec.find(fieldName, lastReview); + return criteriaBuilder.lessThan(reviewPath, lastReviewField); + } + throw new NotSupportedReviewSortingConditionException(NOT_SUPPORTED_REVIEW_SORTING_CONDITION, fieldName); + } +} diff --git a/backend/src/main/java/com/funeat/tag/persistence/TagRepository.java b/backend/src/main/java/com/funeat/tag/persistence/TagRepository.java index b74e0197c..9ad319f7a 100644 --- a/backend/src/main/java/com/funeat/tag/persistence/TagRepository.java +++ b/backend/src/main/java/com/funeat/tag/persistence/TagRepository.java @@ -4,10 +4,18 @@ import com.funeat.tag.domain.TagType; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface TagRepository extends JpaRepository { List findTagsByIdIn(final List tagIds); List findTagsByTagType(final TagType tagType); + + @Query("SELECT t " + + "FROM ReviewTag rt " + + "JOIN rt.tag t " + + "WHERE rt.review.id = :reviewId") + List findTagsByReviewId(@Param("reviewId") final Long reviewId); } diff --git a/backend/src/main/resources/application-dev.yml b/backend/src/main/resources/application-dev.yml index dfa2fd258..0c1a72220 100644 --- a/backend/src/main/resources/application-dev.yml +++ b/backend/src/main/resources/application-dev.yml @@ -35,8 +35,6 @@ management: enabled: true prometheus: enabled: true - server: - port: { SERVER_PORT } cloud: aws: diff --git a/backend/src/main/resources/application-local.yml b/backend/src/main/resources/application-local.yml index dd4ef8afb..27ba9bccb 100644 --- a/backend/src/main/resources/application-local.yml +++ b/backend/src/main/resources/application-local.yml @@ -27,5 +27,5 @@ cloud: static: { S3_REGION } s3: bucket: { S3_BUCKET } - folder: { S3_DEV_FOLDER } - cloudfrontPath: { S3_DEV_CLOUDFRONT_PATH } + folder: { S3_LOCAL_FOLDER } + cloudfrontPath: { S3_LOCAL_CLOUDFRONT_PATH } diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index 5dbe5844c..1943e2bf8 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -34,8 +34,6 @@ management: enabled: true prometheus: enabled: true - server: - port: { SERVER_PORT } cloud: aws: diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 8235adae4..941adb9cf 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -6,10 +6,19 @@ spring: enabled: true maxFileSize: 10MB maxRequestSize: 15MB + task: + execution: + pool: + core-size: { THREAD_CORE_SIZE } + max-size: { THREAD_MAX_SIZE } session: store-type: jdbc jdbc: initialize-schema: never + datasource: + hikari: + connection-timeout: { CONNECTION_TIMEOUT } + maximum-pool-size: { MAXIMUM_POOL_SIZE } springdoc: swagger-ui: diff --git a/backend/src/test/java/com/funeat/acceptance/auth/LoginSteps.java b/backend/src/test/java/com/funeat/acceptance/auth/LoginSteps.java index 21dd1bf77..92d2f48c7 100644 --- a/backend/src/test/java/com/funeat/acceptance/auth/LoginSteps.java +++ b/backend/src/test/java/com/funeat/acceptance/auth/LoginSteps.java @@ -29,7 +29,7 @@ public class LoginSteps { public static ExtractableResponse ๋กœ๊ทธ์•„์›ƒ_์š”์ฒญ(final String loginCookie) { return given() - .cookie("JSESSIONID", loginCookie) + .cookie("SESSION", loginCookie) .when() .post("/api/logout") .then() @@ -44,6 +44,6 @@ public class LoginSteps { .then() .extract() .response() - .getCookie("JSESSIONID"); + .getCookie("SESSION"); } } diff --git a/backend/src/test/java/com/funeat/acceptance/banner/BannerAcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/banner/BannerAcceptanceTest.java new file mode 100644 index 000000000..e5b3a39fd --- /dev/null +++ b/backend/src/test/java/com/funeat/acceptance/banner/BannerAcceptanceTest.java @@ -0,0 +1,62 @@ +package com.funeat.acceptance.banner; + +import static com.funeat.acceptance.banner.BannerSteps.๋ฐฐ๋„ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ; +import static com.funeat.acceptance.common.CommonSteps.STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค; +import static com.funeat.acceptance.common.CommonSteps.์ •์ƒ_์ฒ˜๋ฆฌ; +import static com.funeat.fixture.BannerFixture.๋ฐฐ๋„ˆ1_์ƒ์„ฑ; +import static com.funeat.fixture.BannerFixture.๋ฐฐ๋„ˆ2_์ƒ์„ฑ; +import static com.funeat.fixture.BannerFixture.๋ฐฐ๋„ˆ3_์ƒ์„ฑ; +import static com.funeat.fixture.BannerFixture.๋ฐฐ๋„ˆ4_์ƒ์„ฑ; +import static com.funeat.fixture.BannerFixture.๋ฐฐ๋„ˆ5_์ƒ์„ฑ; +import static org.assertj.core.api.Assertions.assertThat; + +import com.funeat.acceptance.common.AcceptanceTest; +import com.funeat.banner.domain.Banner; +import com.funeat.banner.dto.BannerResponse; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +public class BannerAcceptanceTest extends AcceptanceTest { + + @Nested + class getBanners_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ๋ฐฐ๋„ˆ๋ฅผ_์•„์ด๋””_๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ_์ „์ฒด_์กฐํšŒํ•œ๋‹ค() { + // given + final var ๋ฐฐ๋„ˆ1 = ๋ฐฐ๋„ˆ1_์ƒ์„ฑ(); + final var ๋ฐฐ๋„ˆ2 = ๋ฐฐ๋„ˆ2_์ƒ์„ฑ(); + final var ๋ฐฐ๋„ˆ3 = ๋ฐฐ๋„ˆ3_์ƒ์„ฑ(); + final var ๋ฐฐ๋„ˆ4 = ๋ฐฐ๋„ˆ4_์ƒ์„ฑ(); + final var ๋ฐฐ๋„ˆ5 = ๋ฐฐ๋„ˆ5_์ƒ์„ฑ(); + final var ์ƒ์„ฑํ• _๋ฐฐ๋„ˆ_๋ฆฌ์ŠคํŠธ = Arrays.asList(๋ฐฐ๋„ˆ1, ๋ฐฐ๋„ˆ2, ๋ฐฐ๋„ˆ3, ๋ฐฐ๋„ˆ4, ๋ฐฐ๋„ˆ5); + bannerRepository.saveAll(์ƒ์„ฑํ• _๋ฐฐ๋„ˆ_๋ฆฌ์ŠคํŠธ); + + // when + final var ์‘๋‹ต = ๋ฐฐ๋„ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(); + + // then + STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); + ๋ฐฐ๋„ˆ_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ƒ์„ฑํ• _๋ฐฐ๋„ˆ_๋ฆฌ์ŠคํŠธ); + } + } + + private void ๋ฐฐ๋„ˆ_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(final ExtractableResponse response, + final List expected) { + List expectedResponse = new ArrayList<>(); + for (int i = expected.size() - 1; i >= 0; i--) { + expectedResponse.add(BannerResponse.toResponse(expected.get(i))); + } + + final List result = response.jsonPath().getList("$", BannerResponse.class); + assertThat(result).usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(expectedResponse); + } +} diff --git a/backend/src/test/java/com/funeat/acceptance/banner/BannerSteps.java b/backend/src/test/java/com/funeat/acceptance/banner/BannerSteps.java new file mode 100644 index 000000000..6fbc155f9 --- /dev/null +++ b/backend/src/test/java/com/funeat/acceptance/banner/BannerSteps.java @@ -0,0 +1,17 @@ +package com.funeat.acceptance.banner; + +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; + +@SuppressWarnings("NonAsciiCharacters") +public class BannerSteps { + + public static ExtractableResponse ๋ฐฐ๋„ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ() { + return RestAssured.given() + .when() + .get("/api/banners") + .then() + .extract(); + } +} diff --git a/backend/src/test/java/com/funeat/acceptance/common/AcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/common/AcceptanceTest.java index 17c6d725b..2e5be6b6d 100644 --- a/backend/src/test/java/com/funeat/acceptance/common/AcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/common/AcceptanceTest.java @@ -1,5 +1,7 @@ package com.funeat.acceptance.common; +import com.funeat.banner.persistence.BannerRepository; +import com.funeat.comment.persistence.CommentRepository; import com.funeat.common.DataClearExtension; import com.funeat.member.domain.Member; import com.funeat.member.persistence.MemberRepository; @@ -57,16 +59,22 @@ public abstract class AcceptanceTest { protected ReviewFavoriteRepository reviewFavoriteRepository; @Autowired - public RecipeRepository recipeRepository; + protected RecipeRepository recipeRepository; @Autowired - public RecipeImageRepository recipeImageRepository; + protected RecipeImageRepository recipeImageRepository; @Autowired protected ProductRecipeRepository productRecipeRepository; @Autowired - public RecipeFavoriteRepository recipeFavoriteRepository; + protected RecipeFavoriteRepository recipeFavoriteRepository; + + @Autowired + protected BannerRepository bannerRepository; + + @Autowired + protected CommentRepository commentRepository; @BeforeEach void setUp() { diff --git a/backend/src/test/java/com/funeat/acceptance/common/CommonSteps.java b/backend/src/test/java/com/funeat/acceptance/common/CommonSteps.java index 32dcb85e2..af33aa14f 100644 --- a/backend/src/test/java/com/funeat/acceptance/common/CommonSteps.java +++ b/backend/src/test/java/com/funeat/acceptance/common/CommonSteps.java @@ -74,4 +74,10 @@ public class CommonSteps { assertThat(actual).usingRecursiveComparison() .isEqualTo(expected); } + + public static void ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(final ExtractableResponse response, final boolean expected) { + final var actual = response.jsonPath().getBoolean("hasNext"); + + assertThat(actual).isEqualTo(expected); + } } diff --git a/backend/src/test/java/com/funeat/acceptance/member/MemberAcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/member/MemberAcceptanceTest.java index cb3b8e629..b2a94b2c0 100644 --- a/backend/src/test/java/com/funeat/acceptance/member/MemberAcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/member/MemberAcceptanceTest.java @@ -8,7 +8,9 @@ import static com.funeat.acceptance.common.CommonSteps.์ž˜๋ชป๋œ_์š”์ฒญ; import static com.funeat.acceptance.common.CommonSteps.์ •์ƒ_์ฒ˜๋ฆฌ; import static com.funeat.acceptance.common.CommonSteps.์ •์ƒ_์ฒ˜๋ฆฌ_NO_CONTENT; +import static com.funeat.acceptance.common.CommonSteps.์ฐพ์„์ˆ˜_์—†์Œ; import static com.funeat.acceptance.common.CommonSteps.ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค; +import static com.funeat.acceptance.member.MemberSteps.๋ฆฌ๋ทฐ_์‚ญ์ œ_์š”์ฒญ; import static com.funeat.acceptance.member.MemberSteps.์‚ฌ์šฉ์ž_๊ฟ€์กฐํ•ฉ_์กฐํšŒ_์š”์ฒญ; import static com.funeat.acceptance.member.MemberSteps.์‚ฌ์šฉ์ž_๋ฆฌ๋ทฐ_์กฐํšŒ_์š”์ฒญ; import static com.funeat.acceptance.member.MemberSteps.์‚ฌ์šฉ์ž_์ •๋ณด_์ˆ˜์ •_์š”์ฒญ; @@ -33,17 +35,22 @@ import static com.funeat.fixture.PageFixture.์ด_๋ฐ์ดํ„ฐ_๊ฐœ์ˆ˜; import static com.funeat.fixture.PageFixture.์ด_ํŽ˜์ด์ง€; import static com.funeat.fixture.PageFixture.์ตœ์‹ ์ˆœ; +import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 5์ _์ƒ์„ฑ; import static com.funeat.fixture.RecipeFixture.๋ ˆ์‹œํ”ผ; import static com.funeat.fixture.RecipeFixture.๋ ˆ์‹œํ”ผ1; import static com.funeat.fixture.RecipeFixture.๋ ˆ์‹œํ”ผ2; import static com.funeat.fixture.RecipeFixture.๋ ˆ์‹œํ”ผ์ถ”๊ฐ€์š”์ฒญ_์ƒ์„ฑ; +import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ1; +import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ2; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคX_์ƒ์„ฑ; import static com.funeat.fixture.ScoreFixture.์ ์ˆ˜_1์ ; import static com.funeat.fixture.ScoreFixture.์ ์ˆ˜_2์ ; import static com.funeat.fixture.ScoreFixture.์ ์ˆ˜_3์ ; +import static com.funeat.fixture.ScoreFixture.์ ์ˆ˜_4์ ; import static com.funeat.fixture.TagFixture.ํƒœ๊ทธ_๋ง›์žˆ์–ด์š”_TASTE_์ƒ์„ฑ; +import static com.funeat.review.exception.ReviewErrorCode.REVIEW_NOT_FOUND; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; @@ -300,6 +307,81 @@ class getMemberRecipes_์‹คํŒจ_ํ…Œ์ŠคํŠธ { } } + @Nested + class deleteReview_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ์ž์‹ ์ด_์ž‘์„ฑํ•œ_๋ฆฌ๋ทฐ๋ฅผ_์‚ญ์ œํ• _์ˆ˜_์žˆ๋‹ค() { + // given + final var ์นดํ…Œ๊ณ ๋ฆฌ = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(์นดํ…Œ๊ณ ๋ฆฌ); + final var ์ƒํ’ˆ = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); + final var ํƒœ๊ทธ = ๋‹จ์ผ_ํƒœ๊ทธ_์ €์žฅ(ํƒœ๊ทธ_๋ง›์žˆ์–ด์š”_TASTE_์ƒ์„ฑ()); + ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€1), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(์ ์ˆ˜_4์ , List.of(ํƒœ๊ทธ))); + + // when + final var ์‘๋‹ต = ๋ฆฌ๋ทฐ_์‚ญ์ œ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ๋ฆฌ๋ทฐ1); + + // then + STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ_NO_CONTENT); + } + } + + @Nested + class deleteReview_์‹คํŒจ_ํ…Œ์ŠคํŠธ { + + @ParameterizedTest + @NullAndEmptySource + void ๋กœ๊ทธ์ธํ•˜์ง€_์•Š๋Š”_์‚ฌ์šฉ์ž๊ฐ€_๋ฆฌ๋ทฐ_์‚ญ์ œ์‹œ_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค(final String cookie) { + // given + final var ์นดํ…Œ๊ณ ๋ฆฌ = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(์นดํ…Œ๊ณ ๋ฆฌ); + final var ์ƒํ’ˆ = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); + final var ํƒœ๊ทธ = ๋‹จ์ผ_ํƒœ๊ทธ_์ €์žฅ(ํƒœ๊ทธ_๋ง›์žˆ์–ด์š”_TASTE_์ƒ์„ฑ()); + ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€1), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(์ ์ˆ˜_4์ , List.of(ํƒœ๊ทธ))); + + // when + final var ์‘๋‹ต = ๋ฆฌ๋ทฐ_์‚ญ์ œ_์š”์ฒญ(cookie, ๋ฆฌ๋ทฐ1); + + // then + STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ธ์ฆ๋˜์ง€_์•Š์Œ); + RESPONSE_CODE์™€_MESSAGE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, LOGIN_MEMBER_NOT_FOUND.getCode(), LOGIN_MEMBER_NOT_FOUND.getMessage()); + } + + @Test + void ์กด์žฌํ•˜์ง€_์•Š๋Š”_๋ฆฌ๋ทฐ๋ฅผ_์‚ญ์ œํ• _์ˆ˜_์—†๋‹ค() { + // given + final var ์นดํ…Œ๊ณ ๋ฆฌ = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(์นดํ…Œ๊ณ ๋ฆฌ); + final var ์ƒํ’ˆ = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); + final var ํƒœ๊ทธ = ๋‹จ์ผ_ํƒœ๊ทธ_์ €์žฅ(ํƒœ๊ทธ_๋ง›์žˆ์–ด์š”_TASTE_์ƒ์„ฑ()); + ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€1), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(์ ์ˆ˜_4์ , List.of(ํƒœ๊ทธ))); + + // when + final var ์‘๋‹ต = ๋ฆฌ๋ทฐ_์‚ญ์ œ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ๋ฆฌ๋ทฐ2); + + // then + STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ฐพ์„์ˆ˜_์—†์Œ); + RESPONSE_CODE์™€_MESSAGE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, REVIEW_NOT_FOUND.getCode(), REVIEW_NOT_FOUND.getMessage()); + } + + @Test + void ์ž์‹ ์ด_์ž‘์„ฑํ•˜์ง€_์•Š์€_๋ฆฌ๋ทฐ๋Š”_์‚ญ์ œํ• _์ˆ˜_์—†๋‹ค() { + // given + final var ์นดํ…Œ๊ณ ๋ฆฌ = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(์นดํ…Œ๊ณ ๋ฆฌ); + final var ์ƒํ’ˆ = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); + final var ํƒœ๊ทธ = ๋‹จ์ผ_ํƒœ๊ทธ_์ €์žฅ(ํƒœ๊ทธ_๋ง›์žˆ์–ด์š”_TASTE_์ƒ์„ฑ()); + ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€1), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(์ ์ˆ˜_4์ , List.of(ํƒœ๊ทธ))); + + // when + final var ์‘๋‹ต = ๋ฆฌ๋ทฐ_์‚ญ์ œ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„2), ๋ฆฌ๋ทฐ1); + + // then + STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ž˜๋ชป๋œ_์š”์ฒญ); + } + } + private void ์‚ฌ์šฉ์ž_๋ฆฌ๋ทฐ_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(final ExtractableResponse response, final int expectedReviewSize) { final var actual = response.jsonPath().getList("reviews", MemberReviewDto.class); diff --git a/backend/src/test/java/com/funeat/acceptance/member/MemberSteps.java b/backend/src/test/java/com/funeat/acceptance/member/MemberSteps.java index ca5600fdc..98cf4d6fa 100644 --- a/backend/src/test/java/com/funeat/acceptance/member/MemberSteps.java +++ b/backend/src/test/java/com/funeat/acceptance/member/MemberSteps.java @@ -13,7 +13,7 @@ public class MemberSteps { public static ExtractableResponse ์‚ฌ์šฉ์ž_์ •๋ณด_์กฐํšŒ_์š”์ฒญ(final String loginCookie) { return given() - .cookie("JSESSIONID", loginCookie) + .cookie("SESSION", loginCookie) .when() .get("/api/members") .then() @@ -24,7 +24,7 @@ public class MemberSteps { final MultiPartSpecification image, final MemberRequest request) { final var requestSpec = given() - .cookie("JSESSIONID", loginCookie); + .cookie("SESSION", loginCookie); if (Objects.nonNull(image)) { requestSpec.multiPart(image); @@ -43,7 +43,7 @@ public class MemberSteps { final Long page) { return given() .when() - .cookie("JSESSIONID", loginCookie) + .cookie("SESSION", loginCookie) .queryParam("sort", sort) .queryParam("page", page) .get("/api/members/reviews") @@ -55,11 +55,20 @@ public class MemberSteps { final Long page) { return given() .when() - .cookie("JSESSIONID", loginCookie) + .cookie("SESSION", loginCookie) .queryParam("sort", sort) .queryParam("page", page) .get("/api/members/recipes") .then() .extract(); } + + public static ExtractableResponse ๋ฆฌ๋ทฐ_์‚ญ์ œ_์š”์ฒญ(final String loginCookie, final Long reviewId) { + return given() + .cookie("SESSION", loginCookie) + .when() + .delete("/api/members/reviews/{reviewId}", reviewId) + .then() + .extract(); + } } diff --git a/backend/src/test/java/com/funeat/acceptance/product/ProductAcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/product/ProductAcceptanceTest.java index c2c43b812..46fc15208 100644 --- a/backend/src/test/java/com/funeat/acceptance/product/ProductAcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/product/ProductAcceptanceTest.java @@ -2,6 +2,7 @@ import static com.funeat.acceptance.auth.LoginSteps.๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“; import static com.funeat.acceptance.common.CommonSteps.STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค; +import static com.funeat.acceptance.common.CommonSteps.๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค; import static com.funeat.acceptance.common.CommonSteps.์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ; import static com.funeat.acceptance.common.CommonSteps.์—ฌ๋Ÿฌ๊ฐœ_์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ; import static com.funeat.acceptance.common.CommonSteps.์ •์ƒ_์ฒ˜๋ฆฌ; @@ -125,14 +126,12 @@ class ๊ฐ€๊ฒฉ_๊ธฐ์ค€_๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ_์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ { final var ์ƒํ’ˆ2 = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 3์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); final var ์ƒํ’ˆ3 = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ4000์›_ํ‰์ 4์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(์ด_๋ฐ์ดํ„ฐ_๊ฐœ์ˆ˜(3L), ์ด_ํŽ˜์ด์ง€(1L), ์ฒซํŽ˜์ด์ง€O, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€O, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(1L, ๊ฐ€๊ฒฉ_๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(1L, ๊ฐ€๊ฒฉ_๋‚ด๋ฆผ์ฐจ์ˆœ, 0L); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€X); ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(์ƒํ’ˆ3, ์ƒํ’ˆ1, ์ƒํ’ˆ2)); } @@ -145,14 +144,12 @@ class ๊ฐ€๊ฒฉ_๊ธฐ์ค€_๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ_์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ { final var ์ƒํ’ˆ2 = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); final var ์ƒํ’ˆ3 = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 1์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(์ด_๋ฐ์ดํ„ฐ_๊ฐœ์ˆ˜(3L), ์ด_ํŽ˜์ด์ง€(1L), ์ฒซํŽ˜์ด์ง€O, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€O, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(์นดํ…Œ๊ณ ๋ฆฌ_์•„์ด๋””, ๊ฐ€๊ฒฉ_๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(์นดํ…Œ๊ณ ๋ฆฌ_์•„์ด๋””, ๊ฐ€๊ฒฉ_๋‚ด๋ฆผ์ฐจ์ˆœ, 0L); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€X); ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(์ƒํ’ˆ3, ์ƒํ’ˆ2, ์ƒํ’ˆ1)); } } @@ -169,14 +166,12 @@ class ๊ฐ€๊ฒฉ_๊ธฐ์ค€_์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ_์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ { final var ์ƒํ’ˆ2 = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ4000์›_ํ‰์ 4์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); final var ์ƒํ’ˆ3 = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 3์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(์ด_๋ฐ์ดํ„ฐ_๊ฐœ์ˆ˜(3L), ์ด_ํŽ˜์ด์ง€(1L), ์ฒซํŽ˜์ด์ง€O, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€O, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(์นดํ…Œ๊ณ ๋ฆฌ_์•„์ด๋””, ๊ฐ€๊ฒฉ_์˜ค๋ฆ„์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(์นดํ…Œ๊ณ ๋ฆฌ_์•„์ด๋””, ๊ฐ€๊ฒฉ_์˜ค๋ฆ„์ฐจ์ˆœ, 0L); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€X); ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(์ƒํ’ˆ1, ์ƒํ’ˆ3, ์ƒํ’ˆ2)); } @@ -189,14 +184,12 @@ class ๊ฐ€๊ฒฉ_๊ธฐ์ค€_์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ_์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ { ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 1์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(3L, 1L, true, true, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(1L, ๊ฐ€๊ฒฉ_์˜ค๋ฆ„์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(1L, ๊ฐ€๊ฒฉ_์˜ค๋ฆ„์ฐจ์ˆœ, 0L); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€X); ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(3L, 2L, 1L)); } } @@ -213,14 +206,12 @@ class ํ‰์ _๊ธฐ์ค€_๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ_์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ { ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 5์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 1์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(3L, 1L, true, true, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(1L, ํ‰๊ท _ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(1L, ํ‰๊ท _ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ, 0L); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€X); ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(2L, 1L, 3L)); } @@ -233,14 +224,12 @@ class ํ‰์ _๊ธฐ์ค€_๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ_์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ { ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 1์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 1์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(3L, 1L, true, true, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(1L, ํ‰๊ท _ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(1L, ํ‰๊ท _ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ, 0L); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€X); ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(3L, 2L, 1L)); } } @@ -257,14 +246,12 @@ class ํ‰์ _๊ธฐ์ค€_์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ_์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ { ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 5์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 3์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(3L, 1L, true, true, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(1L, ํ‰๊ท _ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(1L, ํ‰๊ท _ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ, 0L); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€X); ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(1L, 3L, 2L)); } @@ -277,14 +264,12 @@ class ํ‰์ _๊ธฐ์ค€_์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ_์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ { ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 1์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 1์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(3L, 1L, true, true, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(1L, ํ‰๊ท _ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(1L, ํ‰๊ท _ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ, 0L); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€X); ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(3L, 2L, 1L)); } } @@ -306,14 +291,12 @@ class ๋ฆฌ๋ทฐ์ˆ˜_๊ธฐ์ค€_๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ_์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ { ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ2, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€2), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(์ ์ˆ˜_3์ , List.of(ํƒœ๊ทธ))); ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„2), ์ƒํ’ˆ2, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€3), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(์ ์ˆ˜_2์ , List.of(ํƒœ๊ทธ))); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(์ด_๋ฐ์ดํ„ฐ_๊ฐœ์ˆ˜(3L), ์ด_ํŽ˜์ด์ง€(1L), ์ฒซํŽ˜์ด์ง€O, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€O, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(์นดํ…Œ๊ณ ๋ฆฌ_์•„์ด๋””, ๋ฆฌ๋ทฐ์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(์นดํ…Œ๊ณ ๋ฆฌ_์•„์ด๋””, ๋ฆฌ๋ทฐ์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ, 0L); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€X); ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(์ƒํ’ˆ2, ์ƒํ’ˆ1, ์ƒํ’ˆ3)); } @@ -326,14 +309,12 @@ class ๋ฆฌ๋ทฐ์ˆ˜_๊ธฐ์ค€_๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ_์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ { final var ์ƒํ’ˆ2 = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ5000์›_ํ‰์ 3์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); final var ์ƒํ’ˆ3 = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ3000์›_ํ‰์ 1์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(์ด_๋ฐ์ดํ„ฐ_๊ฐœ์ˆ˜(3L), ์ด_ํŽ˜์ด์ง€(1L), ์ฒซํŽ˜์ด์ง€O, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€O, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(์นดํ…Œ๊ณ ๋ฆฌ_์•„์ด๋””, ๋ฆฌ๋ทฐ์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(์นดํ…Œ๊ณ ๋ฆฌ_์•„์ด๋””, ๋ฆฌ๋ทฐ์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ, 0L); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€X); ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(์ƒํ’ˆ3, ์ƒํ’ˆ2, ์ƒํ’ˆ1)); } } diff --git a/backend/src/test/java/com/funeat/acceptance/product/ProductSteps.java b/backend/src/test/java/com/funeat/acceptance/product/ProductSteps.java index a24440dc1..7de0073c4 100644 --- a/backend/src/test/java/com/funeat/acceptance/product/ProductSteps.java +++ b/backend/src/test/java/com/funeat/acceptance/product/ProductSteps.java @@ -9,10 +9,10 @@ public class ProductSteps { public static ExtractableResponse ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(final Long categoryId, final String sort, - final Long page) { + final Long lastProductId) { return given() .queryParam("sort", sort) - .queryParam("page", page) + .queryParam("lastProductId", lastProductId) .when() .get("/api/categories/{category_id}/products", categoryId) .then() diff --git a/backend/src/test/java/com/funeat/acceptance/recipe/RecipeAcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/recipe/RecipeAcceptanceTest.java index af0b74801..bb388ea78 100644 --- a/backend/src/test/java/com/funeat/acceptance/recipe/RecipeAcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/recipe/RecipeAcceptanceTest.java @@ -11,6 +11,8 @@ import static com.funeat.acceptance.common.CommonSteps.์ฐพ์„์ˆ˜_์—†์Œ; import static com.funeat.acceptance.common.CommonSteps.ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค; import static com.funeat.acceptance.recipe.RecipeSteps.๋ ˆ์‹œํ”ผ_๊ฒ€์ƒ‰_๊ฒฐ๊ณผ_์กฐํšŒ_์š”์ฒญ; +import static com.funeat.acceptance.recipe.RecipeSteps.๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์ž‘์„ฑ_์š”์ฒญ; +import static com.funeat.acceptance.recipe.RecipeSteps.๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์กฐํšŒ_์š”์ฒญ; import static com.funeat.acceptance.recipe.RecipeSteps.๋ ˆ์‹œํ”ผ_๋žญํ‚น_์กฐํšŒ_์š”์ฒญ; import static com.funeat.acceptance.recipe.RecipeSteps.๋ ˆ์‹œํ”ผ_๋ชฉ๋ก_์š”์ฒญ; import static com.funeat.acceptance.recipe.RecipeSteps.๋ ˆ์‹œํ”ผ_์ƒ์„ธ_์ •๋ณด_์š”์ฒญ; @@ -59,12 +61,14 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import com.funeat.acceptance.common.AcceptanceTest; -import com.funeat.common.dto.PageDto; import com.funeat.member.domain.Member; import com.funeat.recipe.domain.Recipe; import com.funeat.recipe.dto.ProductRecipeDto; import com.funeat.recipe.dto.RankingRecipeDto; import com.funeat.recipe.dto.RecipeAuthorDto; +import com.funeat.recipe.dto.RecipeCommentCondition; +import com.funeat.recipe.dto.RecipeCommentCreateRequest; +import com.funeat.recipe.dto.RecipeCommentResponse; import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeDetailResponse; import com.funeat.recipe.dto.RecipeDto; @@ -532,6 +536,189 @@ class getRankingRecipes_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { } } + @Nested + class writeRecipeComment_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ๊ฟ€์กฐํ•ฉ์—_๋Œ“๊ธ€์„_์ž‘์„ฑํ• _์ˆ˜_์žˆ๋‹ค() { + // given + final var ์นดํ…Œ๊ณ ๋ฆฌ = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(์นดํ…Œ๊ณ ๋ฆฌ); + final var ์ƒํ’ˆ = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 5์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); + final var ๊ฟ€์กฐํ•ฉ_์ž‘์„ฑ_์‘๋‹ต = ๋ ˆ์‹œํ”ผ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์—ฌ๋Ÿฌ๊ฐœ_์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€1), ๋ ˆ์‹œํ”ผ์ถ”๊ฐ€์š”์ฒญ_์ƒ์„ฑ(์ƒํ’ˆ)); + + // when + final var ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋”” = ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””_์ถ”์ถœ(๊ฟ€์กฐํ•ฉ_์ž‘์„ฑ_์‘๋‹ต); + final var ๋Œ“๊ธ€์ž‘์„ฑ์ž_๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“ = ๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„2); + final var ๊ฟ€์กฐํ•ฉ_๋Œ“๊ธ€ = new RecipeCommentCreateRequest("ํ…Œ์ŠคํŠธ ์ฝ”๋ฉ˜ํŠธ 1"); + + final var ์‘๋‹ต = ๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์ž‘์„ฑ_์š”์ฒญ(๋Œ“๊ธ€์ž‘์„ฑ์ž_๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“, ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””, ๊ฟ€์กฐํ•ฉ_๋Œ“๊ธ€); + + // then + STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ƒ์„ฑ); + ๊ฟ€์กฐํ•ฉ_๋Œ“๊ธ€_์ž‘์„ฑ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋ฉค๋ฒ„2, ๊ฟ€์กฐํ•ฉ_๋Œ“๊ธ€); + } + } + + @Nested + class writeRecipeComment_์‹คํŒจ_ํ…Œ์ŠคํŠธ { + + @ParameterizedTest + @NullAndEmptySource + void ๊ฟ€์กฐํ•ฉ์—_๋Œ“๊ธ€์„_์ž‘์„ฑํ• ๋•Œ_๋Œ“๊ธ€์ด_๋น„์–ด์žˆ์„์‹œ_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค(final String comment) { + // given + final var ์นดํ…Œ๊ณ ๋ฆฌ = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(์นดํ…Œ๊ณ ๋ฆฌ); + final var ์ƒํ’ˆ = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 5์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); + final var ๊ฟ€์กฐํ•ฉ_์ž‘์„ฑ_์‘๋‹ต = ๋ ˆ์‹œํ”ผ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์—ฌ๋Ÿฌ๊ฐœ_์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€1), ๋ ˆ์‹œํ”ผ์ถ”๊ฐ€์š”์ฒญ_์ƒ์„ฑ(์ƒํ’ˆ)); + + // when + final var ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋”” = ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””_์ถ”์ถœ(๊ฟ€์กฐํ•ฉ_์ž‘์„ฑ_์‘๋‹ต); + final var ๋Œ“๊ธ€์ž‘์„ฑ์ž_๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“ = ๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„2); + final var ๊ฟ€์กฐํ•ฉ_๋Œ“๊ธ€ = new RecipeCommentCreateRequest(comment); + + final var ๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์ž‘์„ฑ_์š”์ฒญ = ๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์ž‘์„ฑ_์š”์ฒญ(๋Œ“๊ธ€์ž‘์„ฑ์ž_๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“, ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””, ๊ฟ€์กฐํ•ฉ_๋Œ“๊ธ€); + + // then + STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์ž‘์„ฑ_์š”์ฒญ, ์ž˜๋ชป๋œ_์š”์ฒญ); + RESPONSE_CODE์™€_MESSAGE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์ž‘์„ฑ_์š”์ฒญ, REQUEST_VALID_ERROR_CODE.getCode(), + "๊ฟ€์กฐํ•ฉ ๋Œ“๊ธ€์„ ํ™•์ธํ•ด ์ฃผ์„ธ์š”. " + REQUEST_VALID_ERROR_CODE.getMessage()); + } + + @Test + void ๊ฟ€์กฐํ•ฉ์—_๋Œ“๊ธ€์„_์ž‘์„ฑํ• ๋•Œ_๋Œ“๊ธ€์ด_200์ž_์ดˆ๊ณผ์‹œ_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + // given + final var ์นดํ…Œ๊ณ ๋ฆฌ = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(์นดํ…Œ๊ณ ๋ฆฌ); + final var ์ƒํ’ˆ = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 5์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); + final var ๊ฟ€์กฐํ•ฉ_์ž‘์„ฑ_์‘๋‹ต = ๋ ˆ์‹œํ”ผ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์—ฌ๋Ÿฌ๊ฐœ_์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€1), ๋ ˆ์‹œํ”ผ์ถ”๊ฐ€์š”์ฒญ_์ƒ์„ฑ(์ƒํ’ˆ)); + + // when + final var ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋”” = ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””_์ถ”์ถœ(๊ฟ€์กฐํ•ฉ_์ž‘์„ฑ_์‘๋‹ต); + final var ๋Œ“๊ธ€์ž‘์„ฑ์ž_๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“ = ๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„2); + final var ๊ฟ€์กฐํ•ฉ_๋Œ“๊ธ€ = new RecipeCommentCreateRequest("1" + "๋Œ“๊ธ€์ž…๋‹ˆ๋‹ค".repeat(40)); + + final var ์‘๋‹ต = ๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์ž‘์„ฑ_์š”์ฒญ(๋Œ“๊ธ€์ž‘์„ฑ์ž_๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“, ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””, ๊ฟ€์กฐํ•ฉ_๋Œ“๊ธ€); + + // then + STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ž˜๋ชป๋œ_์š”์ฒญ); + RESPONSE_CODE์™€_MESSAGE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, REQUEST_VALID_ERROR_CODE.getCode(), + "๊ฟ€์กฐํ•ฉ ๋Œ“๊ธ€์€ ์ตœ๋Œ€ 200์ž๊นŒ์ง€ ์ž…๋ ฅ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. " + REQUEST_VALID_ERROR_CODE.getMessage()); + } + + @ParameterizedTest + @NullAndEmptySource + void ๋กœ๊ทธ์ธ_ํ•˜์ง€์•Š์€_์‚ฌ์šฉ์ž๊ฐ€_๊ฟ€์กฐํ•ฉ_๋Œ“๊ธ€_์ž‘์„ฑ์‹œ_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค(final String cookie) { + // given + final var ์นดํ…Œ๊ณ ๋ฆฌ = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(์นดํ…Œ๊ณ ๋ฆฌ); + final var ์ƒํ’ˆ = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 5์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); + final var ๊ฟ€์กฐํ•ฉ_์ž‘์„ฑ_์‘๋‹ต = ๋ ˆ์‹œํ”ผ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์—ฌ๋Ÿฌ๊ฐœ_์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€1), ๋ ˆ์‹œํ”ผ์ถ”๊ฐ€์š”์ฒญ_์ƒ์„ฑ(์ƒํ’ˆ)); + + // when + final var ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋”” = ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””_์ถ”์ถœ(๊ฟ€์กฐํ•ฉ_์ž‘์„ฑ_์‘๋‹ต); + final var ๊ฟ€์กฐํ•ฉ_๋Œ“๊ธ€ = new RecipeCommentCreateRequest("ํ…Œ์ŠคํŠธ ์ฝ”๋ฉ˜ํŠธ 1"); + + final var ์‘๋‹ต = ๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์ž‘์„ฑ_์š”์ฒญ(cookie, ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””, ๊ฟ€์กฐํ•ฉ_๋Œ“๊ธ€); + + // then + STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ธ์ฆ๋˜์ง€_์•Š์Œ); + RESPONSE_CODE์™€_MESSAGE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, LOGIN_MEMBER_NOT_FOUND.getCode(), + LOGIN_MEMBER_NOT_FOUND.getMessage()); + } + } + + @Nested + class getRecipeComment_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ๊ฟ€์กฐํ•ฉ์—_๋Œ“๊ธ€์„_์กฐํšŒํ• _์ˆ˜_์žˆ๋‹ค() { + // given + final var ์นดํ…Œ๊ณ ๋ฆฌ = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(์นดํ…Œ๊ณ ๋ฆฌ); + final var ์ƒํ’ˆ = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 5์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); + final var ๊ฟ€์กฐํ•ฉ_์ž‘์„ฑ_์‘๋‹ต = ๋ ˆ์‹œํ”ผ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์—ฌ๋Ÿฌ๊ฐœ_์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€1), ๋ ˆ์‹œํ”ผ์ถ”๊ฐ€์š”์ฒญ_์ƒ์„ฑ(์ƒํ’ˆ)); + + final var ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋”” = ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””_์ถ”์ถœ(๊ฟ€์กฐํ•ฉ_์ž‘์„ฑ_์‘๋‹ต); + final var ๋Œ“๊ธ€์ž‘์„ฑ์ž_๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“ = ๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„2); + + for (int i = 1; i <= 15; i++) { + ๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์ž‘์„ฑ_์š”์ฒญ(๋Œ“๊ธ€์ž‘์„ฑ์ž_๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“, ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””, + new RecipeCommentCreateRequest("ํ…Œ์ŠคํŠธ ์ฝ”๋ฉ˜ํŠธ" + i)); + } + + // when + final var ์‘๋‹ต = ๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””, + new RecipeCommentCondition(null, null)); + + // then + final var expectedSize = 10; + final var expectedHasNext = true; + + STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); + ๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, expectedSize, expectedHasNext); + } + + @Test + void ๊ฟ€์กฐํ•ฉ์—_๋Œ“๊ธ€์„_๋งˆ์ง€๋ง‰_ํŽ˜์ด์ง€๋ฅผ_์กฐํšŒํ• _์ˆ˜_์žˆ๋‹ค() { + // given + final var ์นดํ…Œ๊ณ ๋ฆฌ = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(์นดํ…Œ๊ณ ๋ฆฌ); + final var ์ƒํ’ˆ = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 5์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); + final var ๊ฟ€์กฐํ•ฉ_์ž‘์„ฑ_์‘๋‹ต = ๋ ˆ์‹œํ”ผ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์—ฌ๋Ÿฌ๊ฐœ_์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€1), ๋ ˆ์‹œํ”ผ์ถ”๊ฐ€์š”์ฒญ_์ƒ์„ฑ(์ƒํ’ˆ)); + + final var ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋”” = ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””_์ถ”์ถœ(๊ฟ€์กฐํ•ฉ_์ž‘์„ฑ_์‘๋‹ต); + final var ๋Œ“๊ธ€์ž‘์„ฑ์ž_๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“ = ๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„2); + + final var totalElements = 15L; + final var lastId = 6L; + + for (int i = 1; i <= totalElements; i++) { + ๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์ž‘์„ฑ_์š”์ฒญ(๋Œ“๊ธ€์ž‘์„ฑ์ž_๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“, ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””, + new RecipeCommentCreateRequest("ํ…Œ์ŠคํŠธ ์ฝ”๋ฉ˜ํŠธ" + i)); + } + + // when + final var ์‘๋‹ต = ๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””, new RecipeCommentCondition(lastId, totalElements)); + + // then + final var expectedSize = 5; + final var expectedHasNext = false; + + STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); + ๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, expectedSize, expectedHasNext); + } + } + + @Nested + class getRecipeComment_์‹คํŒจ_ํ…Œ์ŠคํŠธ { + + @ParameterizedTest + @NullAndEmptySource + void ๋กœ๊ทธ์ธ_ํ•˜์ง€์•Š์€_์‚ฌ์šฉ์ž๊ฐ€_๊ฟ€์กฐํ•ฉ_๋Œ“๊ธ€_์กฐํšŒ์‹œ_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค(final String cookie) { + // given + final var ์นดํ…Œ๊ณ ๋ฆฌ = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(์นดํ…Œ๊ณ ๋ฆฌ); + final var ์ƒํ’ˆ = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 5์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); + final var ๊ฟ€์กฐํ•ฉ_์ž‘์„ฑ_์‘๋‹ต = ๋ ˆ์‹œํ”ผ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์—ฌ๋Ÿฌ๊ฐœ_์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€1), ๋ ˆ์‹œํ”ผ์ถ”๊ฐ€์š”์ฒญ_์ƒ์„ฑ(์ƒํ’ˆ)); + + final var ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋”” = ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””_์ถ”์ถœ(๊ฟ€์กฐํ•ฉ_์ž‘์„ฑ_์‘๋‹ต); + final var ๋Œ“๊ธ€์ž‘์„ฑ์ž_๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“ = ๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„2); + final var ๊ฟ€์กฐํ•ฉ_๋Œ“๊ธ€ = new RecipeCommentCreateRequest("ํ…Œ์ŠคํŠธ ์ฝ”๋ฉ˜ํŠธ"); + + ๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์ž‘์„ฑ_์š”์ฒญ(๋Œ“๊ธ€์ž‘์„ฑ์ž_๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“, ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””, ๊ฟ€์กฐํ•ฉ_๋Œ“๊ธ€); + + // when + final var ์‘๋‹ต = ๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์กฐํšŒ_์š”์ฒญ(cookie, ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””, + new RecipeCommentCondition(6L, 15L)); + + // then + STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ธ์ฆ๋˜์ง€_์•Š์Œ); + RESPONSE_CODE์™€_MESSAGE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, LOGIN_MEMBER_NOT_FOUND.getCode(), + LOGIN_MEMBER_NOT_FOUND.getMessage()); + } + } + private void ๋ ˆ์‹œํ”ผ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(final ExtractableResponse response, final List recipeIds) { final var actual = response.jsonPath().getList("recipes", RecipeDto.class); @@ -590,4 +777,30 @@ class getRankingRecipes_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { assertThat(actual).extracting(SearchRecipeResultDto::getId) .containsExactlyElementsOf(recipeIds); } + + private Long ์ž‘์„ฑ๋œ_๊ฟ€์กฐํ•ฉ_์•„์ด๋””_์ถ”์ถœ(final ExtractableResponse response) { + return Long.parseLong(response.header("Location").split("/")[3]); + } + + private void ๊ฟ€์กฐํ•ฉ_๋Œ“๊ธ€_์ž‘์„ฑ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(final ExtractableResponse response, final Long memberId, + final RecipeCommentCreateRequest request) { + final var savedCommentId = Long.parseLong(response.header("Location").split("/")[4]); + + final var findComments = commentRepository.findAll(); + + assertSoftly(soft -> { + soft.assertThat(savedCommentId).isEqualTo(findComments.get(0).getId()); + soft.assertThat(memberId).isEqualTo(findComments.get(0).getMember().getId()); + soft.assertThat(request.getComment()).isEqualTo(findComments.get(0).getComment()); + }); + } + + private void ๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(final ExtractableResponse response, final int expectedSize, + final boolean expectedHasNext) { + final var actualComments = response.jsonPath().getList("comments", RecipeCommentResponse.class); + final var actualHasNext = response.jsonPath().getBoolean("hasNext"); + + assertThat(actualComments).hasSize(expectedSize); + assertThat(actualHasNext).isEqualTo(expectedHasNext); + } } diff --git a/backend/src/test/java/com/funeat/acceptance/recipe/RecipeSteps.java b/backend/src/test/java/com/funeat/acceptance/recipe/RecipeSteps.java index 4ff18dc2e..8d68fe7e9 100644 --- a/backend/src/test/java/com/funeat/acceptance/recipe/RecipeSteps.java +++ b/backend/src/test/java/com/funeat/acceptance/recipe/RecipeSteps.java @@ -4,6 +4,8 @@ import static com.funeat.fixture.RecipeFixture.๋ ˆ์‹œํ”ผ์ข‹์•„์š”์š”์ฒญ_์ƒ์„ฑ; import static io.restassured.RestAssured.given; +import com.funeat.recipe.dto.RecipeCommentCondition; +import com.funeat.recipe.dto.RecipeCommentCreateRequest; import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeFavoriteRequest; import io.restassured.response.ExtractableResponse; @@ -19,7 +21,7 @@ public class RecipeSteps { final List images, final RecipeCreateRequest recipeRequest) { final var requestSpec = given() - .cookie("JSESSIONID", loginCookie); + .cookie("SESSION", loginCookie); if (Objects.nonNull(images) && !images.isEmpty()) { images.forEach(requestSpec::multiPart); @@ -35,7 +37,7 @@ public class RecipeSteps { public static ExtractableResponse ๋ ˆ์‹œํ”ผ_์ƒ์„ธ_์ •๋ณด_์š”์ฒญ(final String loginCookie, final Long recipeId) { return given() - .cookie("JSESSIONID", loginCookie) + .cookie("SESSION", loginCookie) .when() .get("/api/recipes/{recipeId}", recipeId) .then() @@ -55,7 +57,7 @@ public class RecipeSteps { public static ExtractableResponse ๋ ˆ์‹œํ”ผ_์ข‹์•„์š”_์š”์ฒญ(final String loginCookie, final Long recipeId, final RecipeFavoriteRequest request) { return given() - .cookie("JSESSIONID", loginCookie) + .cookie("SESSION", loginCookie) .contentType("application/json") .body(request) .when() @@ -90,4 +92,30 @@ public class RecipeSteps { .then() .extract(); } + + public static ExtractableResponse ๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์ž‘์„ฑ_์š”์ฒญ(final String loginCookie, + final Long recipeId, + final RecipeCommentCreateRequest request) { + return given() + .cookie("SESSION", loginCookie) + .contentType("application/json") + .body(request) + .when() + .post("/api/recipes/" + recipeId + "/comments") + .then() + .extract(); + } + + public static ExtractableResponse ๋ ˆ์‹œํ”ผ_๋Œ“๊ธ€_์กฐํšŒ_์š”์ฒญ(final String loginCookie, final Long recipeId, + final RecipeCommentCondition condition) { + return given() + .cookie("SESSION", loginCookie) + .contentType("application/json") + .param("lastId", condition.getLastId()) + .param("totalElements", condition.getTotalElements()) + .when() + .get("/api/recipes/" + recipeId + "/comments") + .then() + .extract(); + } } diff --git a/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java b/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java index b1c7218db..233cc4ca5 100644 --- a/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java +++ b/backend/src/test/java/com/funeat/acceptance/review/ReviewAcceptanceTest.java @@ -2,6 +2,7 @@ import static com.funeat.acceptance.auth.LoginSteps.๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“; import static com.funeat.acceptance.common.CommonSteps.STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค; +import static com.funeat.acceptance.common.CommonSteps.๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค; import static com.funeat.acceptance.common.CommonSteps.์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ; import static com.funeat.acceptance.common.CommonSteps.์ธ์ฆ๋˜์ง€_์•Š์Œ; import static com.funeat.acceptance.common.CommonSteps.์ž˜๋ชป๋œ_์š”์ฒญ; @@ -9,9 +10,9 @@ import static com.funeat.acceptance.common.CommonSteps.์ •์ƒ_์ฒ˜๋ฆฌ; import static com.funeat.acceptance.common.CommonSteps.์ •์ƒ_์ฒ˜๋ฆฌ_NO_CONTENT; import static com.funeat.acceptance.common.CommonSteps.์ฐพ์„์ˆ˜_์—†์Œ; -import static com.funeat.acceptance.common.CommonSteps.ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค; import static com.funeat.acceptance.product.ProductSteps.์ƒํ’ˆ_์ƒ์„ธ_์กฐํšŒ_์š”์ฒญ; import static com.funeat.acceptance.review.ReviewSteps.๋ฆฌ๋ทฐ_๋žญํ‚น_์กฐํšŒ_์š”์ฒญ; +import static com.funeat.acceptance.review.ReviewSteps.๋ฆฌ๋ทฐ_์ƒ์„ธ_์กฐํšŒ_์š”์ฒญ; import static com.funeat.acceptance.review.ReviewSteps.๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ; import static com.funeat.acceptance.review.ReviewSteps.๋ฆฌ๋ทฐ_์ข‹์•„์š”_์š”์ฒญ; import static com.funeat.acceptance.review.ReviewSteps.์—ฌ๋Ÿฌ๋ช…์ด_๋ฆฌ๋ทฐ_์ข‹์•„์š”_์š”์ฒญ; @@ -29,13 +30,7 @@ import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„2; import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„3; import static com.funeat.fixture.PageFixture.FIRST_PAGE; -import static com.funeat.fixture.PageFixture.PAGE_SIZE; -import static com.funeat.fixture.PageFixture.๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€O; -import static com.funeat.fixture.PageFixture.์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ; import static com.funeat.fixture.PageFixture.์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ; -import static com.funeat.fixture.PageFixture.์ฒซํŽ˜์ด์ง€O; -import static com.funeat.fixture.PageFixture.์ด_๋ฐ์ดํ„ฐ_๊ฐœ์ˆ˜; -import static com.funeat.fixture.PageFixture.์ด_ํŽ˜์ด์ง€; import static com.funeat.fixture.PageFixture.์ตœ์‹ ์ˆœ; import static com.funeat.fixture.PageFixture.ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ; import static com.funeat.fixture.PageFixture.ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ; @@ -43,6 +38,7 @@ import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 3์ _์ƒ์„ฑ; import static com.funeat.fixture.ProductFixture.์กด์žฌํ•˜์ง€_์•Š๋Š”_์ƒํ’ˆ; +import static com.funeat.fixture.ReviewFixture.๋‹ค์Œ_๋ฐ์ดํ„ฐ_์กด์žฌX; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ1; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ2; @@ -56,6 +52,7 @@ import static com.funeat.fixture.ReviewFixture.์กด์žฌํ•˜์ง€_์•Š๋Š”_๋ฆฌ๋ทฐ; import static com.funeat.fixture.ReviewFixture.์ข‹์•„์š”O; import static com.funeat.fixture.ReviewFixture.์ข‹์•„์š”X; +import static com.funeat.fixture.ReviewFixture.์ฒซ_๋ชฉ๋ก์„_๊ฐ€์ ธ์˜ด; import static com.funeat.fixture.ScoreFixture.์ ์ˆ˜_1์ ; import static com.funeat.fixture.ScoreFixture.์ ์ˆ˜_2์ ; import static com.funeat.fixture.ScoreFixture.์ ์ˆ˜_3์ ; @@ -70,7 +67,9 @@ import com.funeat.acceptance.common.AcceptanceTest; import com.funeat.review.dto.RankingReviewDto; import com.funeat.review.dto.ReviewCreateRequest; +import com.funeat.review.dto.ReviewDetailResponse; import com.funeat.review.dto.SortingReviewDto; +import com.funeat.tag.dto.TagDto; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; import java.util.Collections; @@ -386,14 +385,12 @@ class ์ข‹์•„์š”_๊ธฐ์ค€_๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ { ์—ฌ๋Ÿฌ๋ช…์ด_๋ฆฌ๋ทฐ_์ข‹์•„์š”_์š”์ฒญ(List.of(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ๋ฆฌ๋ทฐ3, ์ข‹์•„์š”O); ์—ฌ๋Ÿฌ๋ช…์ด_๋ฆฌ๋ทฐ_์ข‹์•„์š”_์š”์ฒญ(List.of(๋ฉค๋ฒ„2, ๋ฉค๋ฒ„3), ์ƒํ’ˆ, ๋ฆฌ๋ทฐ2, ์ข‹์•„์š”O); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(์ด_๋ฐ์ดํ„ฐ_๊ฐœ์ˆ˜(3L), ์ด_ํŽ˜์ด์ง€(1L), ์ฒซํŽ˜์ด์ง€O, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€O, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ์ฒซ_๋ชฉ๋ก์„_๊ฐ€์ ธ์˜ด, ์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋‹ค์Œ_๋ฐ์ดํ„ฐ_์กด์žฌX); ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(๋ฆฌ๋ทฐ2, ๋ฆฌ๋ทฐ3, ๋ฆฌ๋ทฐ1)); } @@ -409,14 +406,12 @@ class ์ข‹์•„์š”_๊ธฐ์ค€_๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ { ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„2), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€2), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(์ ์ˆ˜_4์ , List.of(ํƒœ๊ทธ))); ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„3), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€3), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(์ ์ˆ˜_3์ , List.of(ํƒœ๊ทธ))); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(์ด_๋ฐ์ดํ„ฐ_๊ฐœ์ˆ˜(3L), ์ด_ํŽ˜์ด์ง€(1L), ์ฒซํŽ˜์ด์ง€O, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€O, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ์ฒซ_๋ชฉ๋ก์„_๊ฐ€์ ธ์˜ด, ์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋‹ค์Œ_๋ฐ์ดํ„ฐ_์กด์žฌX); ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(๋ฆฌ๋ทฐ3, ๋ฆฌ๋ทฐ2, ๋ฆฌ๋ทฐ1)); } } @@ -436,14 +431,12 @@ class ํ‰์ _๊ธฐ์ค€_์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ_๋ฆฌ๋ทฐ_๋ชฉ๋ก์„_์กฐํšŒ { ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„2), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€2), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(์ ์ˆ˜_4์ , List.of(ํƒœ๊ทธ))); ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„3), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€3), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(์ ์ˆ˜_3์ , List.of(ํƒœ๊ทธ))); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(์ด_๋ฐ์ดํ„ฐ_๊ฐœ์ˆ˜(3L), ์ด_ํŽ˜์ด์ง€(1L), ์ฒซํŽ˜์ด์ง€O, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€O, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ์ฒซ_๋ชฉ๋ก์„_๊ฐ€์ ธ์˜ด, ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ, FIRST_PAGE); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋‹ค์Œ_๋ฐ์ดํ„ฐ_์กด์žฌX); ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(๋ฆฌ๋ทฐ1, ๋ฆฌ๋ทฐ3, ๋ฆฌ๋ทฐ2)); } @@ -459,14 +452,12 @@ class ํ‰์ _๊ธฐ์ค€_์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ_๋ฆฌ๋ทฐ_๋ชฉ๋ก์„_์กฐํšŒ { ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„2), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€2), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(์ ์ˆ˜_3์ , List.of(ํƒœ๊ทธ))); ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„3), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€3), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(์ ์ˆ˜_3์ , List.of(ํƒœ๊ทธ))); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(์ด_๋ฐ์ดํ„ฐ_๊ฐœ์ˆ˜(3L), ์ด_ํŽ˜์ด์ง€(1L), ์ฒซํŽ˜์ด์ง€O, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€O, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ์ฒซ_๋ชฉ๋ก์„_๊ฐ€์ ธ์˜ด, ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ, FIRST_PAGE); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋‹ค์Œ_๋ฐ์ดํ„ฐ_์กด์žฌX); ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(๋ฆฌ๋ทฐ3, ๋ฆฌ๋ทฐ2, ๋ฆฌ๋ทฐ1)); } } @@ -486,14 +477,12 @@ class ํ‰์ _๊ธฐ์ค€_๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ { ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„2), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€2), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(์ ์ˆ˜_4์ , List.of(ํƒœ๊ทธ))); ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„3), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€3), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(์ ์ˆ˜_3์ , List.of(ํƒœ๊ทธ))); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(์ด_๋ฐ์ดํ„ฐ_๊ฐœ์ˆ˜(3L), ์ด_ํŽ˜์ด์ง€(1L), ์ฒซํŽ˜์ด์ง€O, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€O, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ์ฒซ_๋ชฉ๋ก์„_๊ฐ€์ ธ์˜ด, ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋‹ค์Œ_๋ฐ์ดํ„ฐ_์กด์žฌX); ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(๋ฆฌ๋ทฐ2, ๋ฆฌ๋ทฐ3, ๋ฆฌ๋ทฐ1)); } @@ -509,14 +498,12 @@ class ํ‰์ _๊ธฐ์ค€_๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ { ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„2), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€2), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(์ ์ˆ˜_3์ , List.of(ํƒœ๊ทธ))); ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„3), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€3), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(์ ์ˆ˜_3์ , List.of(ํƒœ๊ทธ))); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(์ด_๋ฐ์ดํ„ฐ_๊ฐœ์ˆ˜(3L), ์ด_ํŽ˜์ด์ง€(1L), ์ฒซํŽ˜์ด์ง€O, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€O, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ์ฒซ_๋ชฉ๋ก์„_๊ฐ€์ ธ์˜ด, ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋‹ค์Œ_๋ฐ์ดํ„ฐ_์กด์žฌX); ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(๋ฆฌ๋ทฐ3, ๋ฆฌ๋ทฐ2, ๋ฆฌ๋ทฐ1)); } } @@ -536,14 +523,12 @@ class ์ตœ์‹ ์ˆœ์œผ๋กœ_๋ฆฌ๋ทฐ_๋ชฉ๋ก์„_์กฐํšŒ { ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„2), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€2), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(์ ์ˆ˜_4์ , List.of(ํƒœ๊ทธ))); ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„3), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€3), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(์ ์ˆ˜_3์ , List.of(ํƒœ๊ทธ))); - final var ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€ = ์‘๋‹ต_ํŽ˜์ด์ง€_์ƒ์„ฑ(์ด_๋ฐ์ดํ„ฐ_๊ฐœ์ˆ˜(3L), ์ด_ํŽ˜์ด์ง€(1L), ์ฒซํŽ˜์ด์ง€O, ๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€O, FIRST_PAGE, PAGE_SIZE); - // when - final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ์ตœ์‹ ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ์ฒซ_๋ชฉ๋ก์„_๊ฐ€์ ธ์˜ด, ์ตœ์‹ ์ˆœ, FIRST_PAGE); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); - ํŽ˜์ด์ง€๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์˜ˆ์ƒ_์‘๋‹ต_ํŽ˜์ด์ง€); + ๋‹ค์Œ_๋ฐ์ดํ„ฐ๊ฐ€_์žˆ๋Š”์ง€_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ๋‹ค์Œ_๋ฐ์ดํ„ฐ_์กด์žฌX); ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, List.of(๋ฆฌ๋ทฐ3, ๋ฆฌ๋ทฐ2, ๋ฆฌ๋ทฐ1)); } } @@ -564,7 +549,7 @@ class getSortingReviews_์‹คํŒจ_ํ…Œ์ŠคํŠธ { ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€1), ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(์ ์ˆ˜_2์ , List.of(ํƒœ๊ทธ))); // when - final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(cookie, ์ƒํ’ˆ, ์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); + final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(cookie, ์ƒํ’ˆ, ์ฒซ_๋ชฉ๋ก์„_๊ฐ€์ ธ์˜ด, ์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ธ์ฆ๋˜์ง€_์•Š์Œ); @@ -574,8 +559,8 @@ class getSortingReviews_์‹คํŒจ_ํ…Œ์ŠคํŠธ { @Test void ์กด์žฌํ•˜์ง€_์•Š๋Š”_์ƒํ’ˆ์˜_๋ฆฌ๋ทฐ_๋ชฉ๋ก์„_์กฐํšŒ์‹œ_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { - // given & when - final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์กด์žฌํ•˜์ง€_์•Š๋Š”_์ƒํ’ˆ, ์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); + // given && when + final var ์‘๋‹ต = ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์กด์žฌํ•˜์ง€_์•Š๋Š”_์ƒํ’ˆ, ์ฒซ_๋ชฉ๋ก์„_๊ฐ€์ ธ์˜ด, ์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ, FIRST_PAGE); // then STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ฐพ์„์ˆ˜_์—†์Œ); @@ -666,6 +651,43 @@ class getMostFavoriteReview_์‹คํŒจ_ํ…Œ์ŠคํŠธ { } } + @Nested + class getReviewDetail_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ๋ฆฌ๋ทฐ_์ƒ์„ธ_์ •๋ณด๋ฅผ_์กฐํšŒํ•œ๋‹ค() { + // given + final var ์นดํ…Œ๊ณ ๋ฆฌ = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(์นดํ…Œ๊ณ ๋ฆฌ); + final var ์ƒํ’ˆ = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(์นดํ…Œ๊ณ ๋ฆฌ)); + final var ํƒœ๊ทธ = ๋‹จ์ผ_ํƒœ๊ทธ_์ €์žฅ(ํƒœ๊ทธ_๋ง›์žˆ์–ด์š”_TASTE_์ƒ์„ฑ()); + + final var ์š”์ฒญ = ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(์ ์ˆ˜_3์ , List.of(ํƒœ๊ทธ)); + ๋ฆฌ๋ทฐ_์ž‘์„ฑ_์š”์ฒญ(๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“(๋ฉค๋ฒ„1), ์ƒํ’ˆ, ์‚ฌ์ง„_๋ช…์„ธ_์š”์ฒญ(์ด๋ฏธ์ง€1), ์š”์ฒญ); + + // when + final var ์‘๋‹ต = ๋ฆฌ๋ทฐ_์ƒ์„ธ_์กฐํšŒ_์š”์ฒญ(๋ฆฌ๋ทฐ); + + // then + STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ •์ƒ_์ฒ˜๋ฆฌ); + ๋ฆฌ๋ทฐ_์ƒ์„ธ_์ •๋ณด_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์š”์ฒญ); + } + } + + @Nested + class getReviewDetail_์‹คํŒจ_ํ…Œ์ŠคํŠธ { + + @Test + void ์กด์žฌํ•˜์ง€_์•Š๋Š”_๋ฆฌ๋ทฐ_์กฐํšŒ์‹œ_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + // given & when + final var ์‘๋‹ต = ๋ฆฌ๋ทฐ_์ƒ์„ธ_์กฐํšŒ_์š”์ฒญ(์กด์žฌํ•˜์ง€_์•Š๋Š”_๋ฆฌ๋ทฐ); + + // then + STATUS_CODE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, ์ฐพ์„์ˆ˜_์—†์Œ); + RESPONSE_CODE์™€_MESSAGE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(์‘๋‹ต, REVIEW_NOT_FOUND.getCode(), REVIEW_NOT_FOUND.getMessage()); + } + } + private void RESPONSE_CODE์™€_MESSAGE๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(final ExtractableResponse response, final String expectedCode, final String expectedMessage) { assertSoftly(soft -> { @@ -711,4 +733,20 @@ class getMostFavoriteReview_์‹คํŒจ_ํ…Œ์ŠคํŠธ { assertThat(actual).isEqualTo(expected); } + + private void ๋ฆฌ๋ทฐ_์ƒ์„ธ_์ •๋ณด_์กฐํšŒ_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(final ExtractableResponse response, final ReviewCreateRequest request) { + final var actual = response.as(ReviewDetailResponse.class); + final var actualTags = response.jsonPath().getList("tags", TagDto.class); + + assertSoftly(soft -> { + soft.assertThat(actual.getId()).isEqualTo(๋ฆฌ๋ทฐ); + soft.assertThat(actual.getImage()).isEqualTo("1.png"); + soft.assertThat(actual.getRating()).isEqualTo(request.getRating()); + soft.assertThat(actual.getContent()).isEqualTo(request.getContent()); + soft.assertThat(actual.isRebuy()).isEqualTo(request.getRebuy()); + soft.assertThat(actual.getFavoriteCount()).isEqualTo(0L); + soft.assertThat(actualTags).extracting(TagDto::getId) + .containsExactlyElementsOf(request.getTagIds()); + }); + } } diff --git a/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java b/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java index 0d8ce8fd7..d1b556d7d 100644 --- a/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java +++ b/backend/src/test/java/com/funeat/acceptance/review/ReviewSteps.java @@ -1,7 +1,6 @@ package com.funeat.acceptance.review; import static com.funeat.acceptance.auth.LoginSteps.๋กœ๊ทธ์ธ_์ฟ ํ‚ค_ํš๋“; -import static com.funeat.acceptance.common.CommonSteps.LOCATION_ํ—ค๋”์—์„œ_ID_์ถ”์ถœ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ์ข‹์•„์š”์š”์ฒญ_์ƒ์„ฑ; import static io.restassured.RestAssured.given; @@ -20,7 +19,7 @@ public class ReviewSteps { final MultiPartSpecification image, final ReviewCreateRequest request) { final var requestSpec = given() - .cookie("JSESSIONID", loginCookie); + .cookie("SESSION", loginCookie); if (Objects.nonNull(image)) { requestSpec.multiPart(image); @@ -37,7 +36,7 @@ public class ReviewSteps { public static ExtractableResponse ๋ฆฌ๋ทฐ_์ข‹์•„์š”_์š”์ฒญ(final String loginCookie, final Long productId, final Long reviewId, final ReviewFavoriteRequest request) { return given() - .cookie("JSESSIONID", loginCookie) + .cookie("SESSION", loginCookie) .contentType("application/json") .body(request) .when() @@ -56,14 +55,16 @@ public class ReviewSteps { } public static ExtractableResponse ์ •๋ ฌ๋œ_๋ฆฌ๋ทฐ_๋ชฉ๋ก_์กฐํšŒ_์š”์ฒญ(final String loginCookie, final Long productId, + final Long lastReviewId, final String sort, final Long page) { return given() - .cookie("JSESSIONID", loginCookie) + .cookie("SESSION", loginCookie) .queryParam("sort", sort) .queryParam("page", page) + .queryParam("lastReviewId", lastReviewId).log().all() .when() .get("/api/products/{product_id}/reviews", productId) - .then() + .then().log().all() .extract(); } @@ -82,4 +83,12 @@ public class ReviewSteps { .then().log().all() .extract(); } + + public static ExtractableResponse ๋ฆฌ๋ทฐ_์ƒ์„ธ_์กฐํšŒ_์š”์ฒญ(final Long reviewId) { + return given() + .when() + .get("/api/reviews/{reviewId}", reviewId) + .then() + .extract(); + } } diff --git a/backend/src/test/java/com/funeat/banner/application/BannerServiceTest.java b/backend/src/test/java/com/funeat/banner/application/BannerServiceTest.java new file mode 100644 index 000000000..dad7292f4 --- /dev/null +++ b/backend/src/test/java/com/funeat/banner/application/BannerServiceTest.java @@ -0,0 +1,47 @@ +package com.funeat.banner.application; + +import static com.funeat.fixture.BannerFixture.๋ฐฐ๋„ˆ1_์ƒ์„ฑ; +import static com.funeat.fixture.BannerFixture.๋ฐฐ๋„ˆ2_์ƒ์„ฑ; +import static com.funeat.fixture.BannerFixture.๋ฐฐ๋„ˆ3_์ƒ์„ฑ; +import static com.funeat.fixture.BannerFixture.๋ฐฐ๋„ˆ4_์ƒ์„ฑ; +import static com.funeat.fixture.BannerFixture.๋ฐฐ๋„ˆ5_์ƒ์„ฑ; +import static org.assertj.core.api.Assertions.assertThat; + +import com.funeat.banner.dto.BannerResponse; +import com.funeat.common.ServiceTest; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +public class BannerServiceTest extends ServiceTest { + + @Nested + class getBanners_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ๋ฐฐ๋„ˆ๋ฅผ_์•„์ด๋””_๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ_์ „์ฒด_์กฐํšŒํ•œ๋‹ค() { + // given + final var ๋ฐฐ๋„ˆ1 = ๋ฐฐ๋„ˆ1_์ƒ์„ฑ(); + final var ๋ฐฐ๋„ˆ2 = ๋ฐฐ๋„ˆ2_์ƒ์„ฑ(); + final var ๋ฐฐ๋„ˆ3 = ๋ฐฐ๋„ˆ3_์ƒ์„ฑ(); + final var ๋ฐฐ๋„ˆ4 = ๋ฐฐ๋„ˆ4_์ƒ์„ฑ(); + final var ๋ฐฐ๋„ˆ5 = ๋ฐฐ๋„ˆ5_์ƒ์„ฑ(); + ๋ณต์ˆ˜_๋ฐฐ๋„ˆ_์ €์žฅ(๋ฐฐ๋„ˆ1, ๋ฐฐ๋„ˆ2, ๋ฐฐ๋„ˆ3, ๋ฐฐ๋„ˆ4, ๋ฐฐ๋„ˆ5); + + // when + final var result = bannerService.getAllBanners(); + + // then + final var ๋ฐฐ๋„ˆ๋“ค = List.of(๋ฐฐ๋„ˆ5, ๋ฐฐ๋„ˆ4, ๋ฐฐ๋„ˆ3, ๋ฐฐ๋„ˆ2, ๋ฐฐ๋„ˆ1); + final var bannerResponses = ๋ฐฐ๋„ˆ๋“ค.stream() + .map(BannerResponse::toResponse) + .collect(Collectors.toList()); + + assertThat(result).usingRecursiveComparison() + .ignoringFields("id") + .isEqualTo(bannerResponses); + } + } +} diff --git a/backend/src/test/java/com/funeat/banner/persistence/BannerRepositoryTest.java b/backend/src/test/java/com/funeat/banner/persistence/BannerRepositoryTest.java new file mode 100644 index 000000000..34ef6e6b9 --- /dev/null +++ b/backend/src/test/java/com/funeat/banner/persistence/BannerRepositoryTest.java @@ -0,0 +1,39 @@ +package com.funeat.banner.persistence; + +import static com.funeat.fixture.BannerFixture.๋ฐฐ๋„ˆ1_์ƒ์„ฑ; +import static com.funeat.fixture.BannerFixture.๋ฐฐ๋„ˆ2_์ƒ์„ฑ; +import static com.funeat.fixture.BannerFixture.๋ฐฐ๋„ˆ3_์ƒ์„ฑ; +import static com.funeat.fixture.BannerFixture.๋ฐฐ๋„ˆ4_์ƒ์„ฑ; +import static com.funeat.fixture.BannerFixture.๋ฐฐ๋„ˆ5_์ƒ์„ฑ; +import static org.assertj.core.api.Assertions.assertThat; + +import com.funeat.common.RepositoryTest; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +public class BannerRepositoryTest extends RepositoryTest { + + @Nested + class findAllByOrderByIdDesc_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ๋ฐฐ๋„ˆ๋Š”_์•„์ด๋””๊ฐ€_๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ_์กฐํšŒ๋œ๋‹ค() { + // given + final var ๋ฐฐ๋„ˆ1 = ๋ฐฐ๋„ˆ1_์ƒ์„ฑ(); + final var ๋ฐฐ๋„ˆ2 = ๋ฐฐ๋„ˆ2_์ƒ์„ฑ(); + final var ๋ฐฐ๋„ˆ3 = ๋ฐฐ๋„ˆ3_์ƒ์„ฑ(); + final var ๋ฐฐ๋„ˆ4 = ๋ฐฐ๋„ˆ4_์ƒ์„ฑ(); + final var ๋ฐฐ๋„ˆ5 = ๋ฐฐ๋„ˆ5_์ƒ์„ฑ(); + ๋ณต์ˆ˜_๋ฐฐ๋„ˆ_์ €์žฅ(๋ฐฐ๋„ˆ1, ๋ฐฐ๋„ˆ2, ๋ฐฐ๋„ˆ3, ๋ฐฐ๋„ˆ4, ๋ฐฐ๋„ˆ5); + + // when + final var bannersOrderByIdDesc = bannerRepository.findAllByOrderByIdDesc(); + + // then + assertThat(bannersOrderByIdDesc).usingRecursiveComparison() + .isEqualTo(List.of(๋ฐฐ๋„ˆ5, ๋ฐฐ๋„ˆ4, ๋ฐฐ๋„ˆ3, ๋ฐฐ๋„ˆ2, ๋ฐฐ๋„ˆ1)); + } + } +} diff --git a/backend/src/test/java/com/funeat/common/EventTest.java b/backend/src/test/java/com/funeat/common/EventTest.java new file mode 100644 index 000000000..aeb72d418 --- /dev/null +++ b/backend/src/test/java/com/funeat/common/EventTest.java @@ -0,0 +1,66 @@ +package com.funeat.common; + +import com.funeat.member.domain.Member; +import com.funeat.member.persistence.MemberRepository; +import com.funeat.product.domain.Category; +import com.funeat.product.domain.Product; +import com.funeat.product.persistence.CategoryRepository; +import com.funeat.product.persistence.ProductRepository; +import com.funeat.review.application.ReviewService; +import com.funeat.review.persistence.ReviewRepository; +import com.funeat.tag.persistence.TagRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.event.ApplicationEvents; +import org.springframework.test.context.event.RecordApplicationEvents; + +@SpringBootTest +@RecordApplicationEvents +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +@ExtendWith({MockitoExtension.class, DataClearExtension.class}) +public class EventTest { + + @Autowired + protected ApplicationEvents events; + + @Autowired + protected ReviewService reviewService; + + @Autowired + protected ProductRepository productRepository; + + @Autowired + protected CategoryRepository categoryRepository; + + @Autowired + protected TagRepository tagRepository; + + @Autowired + protected MemberRepository memberRepository; + + @Autowired + protected ReviewRepository reviewRepository; + + @AfterEach + void tearDown() { + events.clear(); + } + + protected Long ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(final Product product) { + return productRepository.save(product).getId(); + } + + protected Long ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(final Category category) { + return categoryRepository.save(category).getId(); + } + + protected Long ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(final Member member) { + return memberRepository.save(member).getId(); + } +} diff --git a/backend/src/test/java/com/funeat/common/RepositoryTest.java b/backend/src/test/java/com/funeat/common/RepositoryTest.java index b438ca41b..1ae1fff62 100644 --- a/backend/src/test/java/com/funeat/common/RepositoryTest.java +++ b/backend/src/test/java/com/funeat/common/RepositoryTest.java @@ -1,11 +1,11 @@ package com.funeat.common; +import com.funeat.banner.domain.Banner; +import com.funeat.banner.persistence.BannerRepository; import com.funeat.member.domain.Member; import com.funeat.member.domain.favorite.RecipeFavorite; import com.funeat.member.domain.favorite.ReviewFavorite; import com.funeat.member.persistence.MemberRepository; -import com.funeat.member.persistence.ProductBookmarkRepository; -import com.funeat.member.persistence.RecipeBookMarkRepository; import com.funeat.member.persistence.RecipeFavoriteRepository; import com.funeat.member.persistence.ReviewFavoriteRepository; import com.funeat.product.domain.Category; @@ -42,12 +42,6 @@ public abstract class RepositoryTest { @Autowired protected MemberRepository memberRepository; - @Autowired - protected ProductBookmarkRepository productBookmarkRepository; - - @Autowired - protected RecipeBookMarkRepository recipeBookMarkRepository; - @Autowired protected RecipeFavoriteRepository recipeFavoriteRepository; @@ -78,6 +72,9 @@ public abstract class RepositoryTest { @Autowired protected TagRepository tagRepository; + @Autowired + protected BannerRepository bannerRepository; + protected Long ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(final Product product) { return productRepository.save(product).getId(); } @@ -195,4 +192,10 @@ public abstract class RepositoryTest { protected void ๋ ˆ์‹œํ”ผ_์ข‹์•„์š”_์ €์žฅ(final RecipeFavorite recipeFavorite) { recipeFavoriteRepository.save(recipeFavorite); } + + protected void ๋ณต์ˆ˜_๋ฐฐ๋„ˆ_์ €์žฅ(final Banner... bannerToSave) { + final List banners = List.of(bannerToSave); + + bannerRepository.saveAll(banners); + } } diff --git a/backend/src/test/java/com/funeat/common/ServiceTest.java b/backend/src/test/java/com/funeat/common/ServiceTest.java index f8b58e5a2..23ee634f5 100644 --- a/backend/src/test/java/com/funeat/common/ServiceTest.java +++ b/backend/src/test/java/com/funeat/common/ServiceTest.java @@ -1,12 +1,14 @@ package com.funeat.common; import com.funeat.auth.application.AuthService; +import com.funeat.banner.application.BannerService; +import com.funeat.banner.domain.Banner; +import com.funeat.banner.persistence.BannerRepository; +import com.funeat.comment.persistence.CommentRepository; import com.funeat.member.application.TestMemberService; import com.funeat.member.domain.Member; import com.funeat.member.domain.favorite.ReviewFavorite; import com.funeat.member.persistence.MemberRepository; -import com.funeat.member.persistence.ProductBookmarkRepository; -import com.funeat.member.persistence.RecipeBookMarkRepository; import com.funeat.member.persistence.RecipeFavoriteRepository; import com.funeat.member.persistence.ReviewFavoriteRepository; import com.funeat.product.application.CategoryService; @@ -48,12 +50,6 @@ public abstract class ServiceTest { @Autowired protected MemberRepository memberRepository; - @Autowired - protected ProductBookmarkRepository productBookmarkRepository; - - @Autowired - protected RecipeBookMarkRepository recipeBookMarkRepository; - @Autowired protected RecipeFavoriteRepository recipeFavoriteRepository; @@ -84,6 +80,12 @@ public abstract class ServiceTest { @Autowired protected TagRepository tagRepository; + @Autowired + protected BannerRepository bannerRepository; + + @Autowired + protected CommentRepository commentRepository; + @Autowired protected AuthService authService; @@ -105,6 +107,9 @@ public abstract class ServiceTest { @Autowired protected TagService tagService; + @Autowired + protected BannerService bannerService; + protected Long ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(final Product product) { return productRepository.save(product).getId(); } @@ -200,4 +205,10 @@ public abstract class ServiceTest { productRecipeRepository.saveAll(productRecipes); } + + protected void ๋ณต์ˆ˜_๋ฐฐ๋„ˆ_์ €์žฅ(final Banner... bannerToSave) { + final List banners = List.of(bannerToSave); + + bannerRepository.saveAll(banners); + } } diff --git a/backend/src/test/java/com/funeat/common/TestImageUploader.java b/backend/src/test/java/com/funeat/common/TestImageUploader.java index 58d4ab6f8..642da2176 100644 --- a/backend/src/test/java/com/funeat/common/TestImageUploader.java +++ b/backend/src/test/java/com/funeat/common/TestImageUploader.java @@ -30,6 +30,10 @@ public String upload(final MultipartFile image) { } } + @Override + public void delete(final String fileName) { + } + private void deleteDirectory(Path directory) throws IOException { // ๋””๋ ‰ํ† ๋ฆฌ ๋‚ด๋ถ€ ํŒŒ์ผ ๋ฐ ๋””๋ ‰ํ† ๋ฆฌ ์‚ญ์ œ try (Stream pathStream = Files.walk(directory)) { diff --git a/backend/src/test/java/com/funeat/fixture/BannerFixture.java b/backend/src/test/java/com/funeat/fixture/BannerFixture.java new file mode 100644 index 000000000..9ab4443f5 --- /dev/null +++ b/backend/src/test/java/com/funeat/fixture/BannerFixture.java @@ -0,0 +1,26 @@ +package com.funeat.fixture; + +import com.funeat.banner.domain.Banner; + +public class BannerFixture { + + public static Banner ๋ฐฐ๋„ˆ1_์ƒ์„ฑ() { + return new Banner("๋ฐฐ๋„ˆ1๋งํฌ", "๋ฐฐ๋„ˆ1.jpeg"); + } + + public static Banner ๋ฐฐ๋„ˆ2_์ƒ์„ฑ() { + return new Banner("๋ฐฐ๋„ˆ2๋งํฌ", "๋ฐฐ๋„ˆ2.jpeg"); + } + + public static Banner ๋ฐฐ๋„ˆ3_์ƒ์„ฑ() { + return new Banner("๋ฐฐ๋„ˆ3๋งํฌ", "๋ฐฐ๋„ˆ3.jpeg"); + } + + public static Banner ๋ฐฐ๋„ˆ4_์ƒ์„ฑ() { + return new Banner("๋ฐฐ๋„ˆ4๋งํฌ", "๋ฐฐ๋„ˆ4.jpeg"); + } + + public static Banner ๋ฐฐ๋„ˆ5_์ƒ์„ฑ() { + return new Banner("๋ฐฐ๋„ˆ5๋งํฌ", "๋ฐฐ๋„ˆ5.jpeg"); + } +} diff --git a/backend/src/test/java/com/funeat/fixture/PageFixture.java b/backend/src/test/java/com/funeat/fixture/PageFixture.java index afae2d14a..773658f48 100644 --- a/backend/src/test/java/com/funeat/fixture/PageFixture.java +++ b/backend/src/test/java/com/funeat/fixture/PageFixture.java @@ -2,6 +2,7 @@ import com.funeat.common.dto.PageDto; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; @@ -18,6 +19,7 @@ public class PageFixture { public static final String ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ = "rating,desc"; public static final String ๊ณผ๊ฑฐ์ˆœ = "createdAt,asc"; public static final String ์ตœ์‹ ์ˆœ = "createdAt,desc"; + public static final String ์•„์ด๋””_๋‚ด๋ฆผ์ฐจ์ˆœ = "id,desc"; public static final Long PAGE_SIZE = 10L; public static final Long FIRST_PAGE = 0L; @@ -45,6 +47,30 @@ public class PageFixture { return new PageDto(totalDataCount, totalPages, firstPage, lastPage, requestPage, requestSize); } + public static Pageable ํŽ˜์ด์ง€์š”์ฒญ_์ข‹์•„์š”_๋‚ด๋ฆผ์ฐจ์ˆœ_์ƒ์„ฑ(final int page, final int size) { + final var sort = Sort.by(Direction.DESC, "favoriteCount"); + + return PageRequest.of(page, size, sort); + } + + public static Pageable ํŽ˜์ด์ง€์š”์ฒญ_์ตœ์‹ ์ˆœ_์ƒ์„ฑ(final int page, final int size) { + final var sort = Sort.by(Direction.DESC, "createdAt"); + + return PageRequest.of(page, size, sort); + } + + public static Pageable ํŽ˜์ด์ง€์š”์ฒญ_ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ_์ƒ์„ฑ(final int page, final int size) { + final var sort = Sort.by(Direction.ASC, "rating"); + + return PageRequest.of(page, size, sort); + } + + public static Pageable ํŽ˜์ด์ง€์š”์ฒญ_ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ_์ƒ์„ฑ(final int page, final int size) { + final var sort = Sort.by(Direction.DESC, "rating"); + + return PageRequest.of(page, size, sort); + } + public static Long ์ด_๋ฐ์ดํ„ฐ_๊ฐœ์ˆ˜(final Long count) { return count; } diff --git a/backend/src/test/java/com/funeat/fixture/ProductFixture.java b/backend/src/test/java/com/funeat/fixture/ProductFixture.java index 15b2fa27e..6bb7da0d9 100644 --- a/backend/src/test/java/com/funeat/fixture/ProductFixture.java +++ b/backend/src/test/java/com/funeat/fixture/ProductFixture.java @@ -130,6 +130,22 @@ public class ProductFixture { return new Product("์• ํ”Œ๋ง๊ณ ", 3000L, "image.png", "๋ง›์žˆ๋Š” ์• ํ”Œ๋ง๊ณ ", 5.0, category); } + public static Product ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ5000์›_๋ฆฌ๋ทฐ0๊ฐœ_์ƒ์„ฑ(final Category category) { + return new Product("์‚ผ๊ฐ๊น€๋ฐฅ", 5000L, "image.png", "๋ง›์žˆ๋Š” ์‚ผ๊ฐ๊น€๋ฐฅ", category, 0L); + } + + public static Product ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_๋ฆฌ๋ทฐ1๊ฐœ_์ƒ์„ฑ(final Category category) { + return new Product("์‚ผ๊ฐ๊น€๋ฐฅ", 2000L, "image.png", "๋ง›์žˆ๋Š” ์‚ผ๊ฐ๊น€๋ฐฅ", category, 1L); + } + + public static Product ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_๋ฆฌ๋ทฐ3๊ฐœ_์ƒ์„ฑ(final Category category) { + return new Product("์‚ผ๊ฐ๊น€๋ฐฅ", 1000L, "image.png", "๋ง›์žˆ๋Š” ์‚ผ๊ฐ๊น€๋ฐฅ", category, 3L); + } + + public static Product ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ3000์›_๋ฆฌ๋ทฐ5๊ฐœ_์ƒ์„ฑ(final Category category) { + return new Product("์‚ผ๊ฐ๊น€๋ฐฅ", 3000L, "image.png", "๋ง›์žˆ๋Š” ์‚ผ๊ฐ๊น€๋ฐฅ", category, 5L); + } + public static ProductRecipe ๋ ˆ์‹œํ”ผ_์•ˆ์—_๋“ค์–ด๊ฐ€๋Š”_์ƒํ’ˆ_์ƒ์„ฑ(final Product product, final Recipe recipe) { return new ProductRecipe(product, recipe); } diff --git a/backend/src/test/java/com/funeat/fixture/RecipeFixture.java b/backend/src/test/java/com/funeat/fixture/RecipeFixture.java index 2050727f3..2d3bb3deb 100644 --- a/backend/src/test/java/com/funeat/fixture/RecipeFixture.java +++ b/backend/src/test/java/com/funeat/fixture/RecipeFixture.java @@ -6,6 +6,7 @@ import com.funeat.recipe.domain.RecipeImage; import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeFavoriteRequest; +import java.time.LocalDateTime; import java.util.List; @SuppressWarnings("NonAsciiCharacters") @@ -33,6 +34,11 @@ public class RecipeFixture { return new Recipe("The most delicious recipes", "More rice, more rice, more rice.. Done!!", member, favoriteCount); } + public static Recipe ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(final Member member, final Long favoriteCount, final LocalDateTime createdAt) { + return new Recipe("The most delicious recipes", "More rice, more rice, more rice.. Done!!", + member, favoriteCount, createdAt); + } + public static RecipeFavorite ๋ ˆ์‹œํ”ผ_์ข‹์•„์š”_์ƒ์„ฑ(final Member member, final Recipe recipe, final Boolean favorite) { return new RecipeFavorite(member, recipe, favorite); } diff --git a/backend/src/test/java/com/funeat/fixture/ReviewFixture.java b/backend/src/test/java/com/funeat/fixture/ReviewFixture.java index 3ae53a3c7..fee2b0b7b 100644 --- a/backend/src/test/java/com/funeat/fixture/ReviewFixture.java +++ b/backend/src/test/java/com/funeat/fixture/ReviewFixture.java @@ -1,10 +1,17 @@ package com.funeat.fixture; +import static com.funeat.fixture.PageFixture.์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ; +import static com.funeat.fixture.PageFixture.์ตœ์‹ ์ˆœ; +import static com.funeat.fixture.PageFixture.ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ; +import static com.funeat.fixture.PageFixture.ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ; + import com.funeat.member.domain.Member; import com.funeat.product.domain.Product; import com.funeat.review.domain.Review; import com.funeat.review.dto.ReviewCreateRequest; import com.funeat.review.dto.ReviewFavoriteRequest; +import com.funeat.review.dto.SortingReviewRequest; +import java.time.LocalDateTime; import java.util.List; @SuppressWarnings("NonAsciiCharacters") @@ -21,6 +28,10 @@ public class ReviewFixture { public static final boolean ์žฌ๊ตฌ๋งคO = true; public static final boolean ์žฌ๊ตฌ๋งคX = false; + public static final Long ์ฒซ_๋ชฉ๋ก์„_๊ฐ€์ ธ์˜ด = 0L; + public static final boolean ๋‹ค์Œ_๋ฐ์ดํ„ฐ_์กด์žฌO = true; + public static final boolean ๋‹ค์Œ_๋ฐ์ดํ„ฐ_์กด์žฌX = false; + public static Review ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test1_ํ‰์ 1์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(final Member member, final Product product, final Long count) { return new Review(member, product, "test1", 1L, "test", true, count); } @@ -65,6 +76,15 @@ public class ReviewFixture { return new Review(member, product, "test5", 5L, "test", false, count); } + public static Review ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(final Member member, final Product product, final Long count, + final LocalDateTime createdAt) { + return new Review(member, product, "test5", 5L, "test", false, count, createdAt); + } + + public static Review ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€์—†์Œ_ํ‰์ 1์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(final Member member, final Product product, final Long count) { + return new Review(member, product, "", 1L, "test", false, count); + } + public static ReviewCreateRequest ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์ƒ์„ฑ(final Long rating, final List tagIds, final String content, final Boolean rebuy) { return new ReviewCreateRequest(rating, tagIds, content, rebuy); @@ -81,4 +101,24 @@ public class ReviewFixture { public static ReviewFavoriteRequest ๋ฆฌ๋ทฐ์ข‹์•„์š”์š”์ฒญ_์ƒ์„ฑ(final Boolean favorite) { return new ReviewFavoriteRequest(favorite); } + + public static SortingReviewRequest ๋ฆฌ๋ทฐ์ •๋ ฌ์š”์ฒญ_์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ_์ƒ์„ฑ(final Long lastReviewId) { + return new SortingReviewRequest(์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ, lastReviewId); + } + + public static SortingReviewRequest ๋ฆฌ๋ทฐ์ •๋ ฌ์š”์ฒญ_์ตœ์‹ ์ˆœ_์ƒ์„ฑ(final Long lastReviewId) { + return new SortingReviewRequest(์ตœ์‹ ์ˆœ, lastReviewId); + } + + public static SortingReviewRequest ๋ฆฌ๋ทฐ์ •๋ ฌ์š”์ฒญ_ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ_์ƒ์„ฑ(final Long lastReviewId) { + return new SortingReviewRequest(ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ, lastReviewId); + } + + public static SortingReviewRequest ๋ฆฌ๋ทฐ์ •๋ ฌ์š”์ฒญ_ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ_์ƒ์„ฑ(final Long lastReviewId) { + return new SortingReviewRequest(ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ, lastReviewId); + } + + public static SortingReviewRequest ๋ฆฌ๋ทฐ์ •๋ ฌ์š”์ฒญ_์กด์žฌํ•˜์ง€์•Š๋Š”์ •๋ ฌ_์ƒ์„ฑ() { + return new SortingReviewRequest("test,test", 1L); + } } diff --git a/backend/src/test/java/com/funeat/member/persistence/ReviewFavoriteRepositoryTest.java b/backend/src/test/java/com/funeat/member/persistence/ReviewFavoriteRepositoryTest.java index 73ed00553..fcb190d28 100644 --- a/backend/src/test/java/com/funeat/member/persistence/ReviewFavoriteRepositoryTest.java +++ b/backend/src/test/java/com/funeat/member/persistence/ReviewFavoriteRepositoryTest.java @@ -3,6 +3,7 @@ import static com.funeat.fixture.CategoryFixture.์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ; import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ; import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ; +import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„_๋ฉค๋ฒ„3_์ƒ์„ฑ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; @@ -11,6 +12,7 @@ import com.funeat.common.RepositoryTest; import com.funeat.member.domain.favorite.ReviewFavorite; +import java.util.List; import java.util.NoSuchElementException; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -106,4 +108,77 @@ class findByMemberAndReview_์‹คํŒจ_ํ…Œ์ŠคํŠธ { .isInstanceOf(NoSuchElementException.class); } } + + @Nested + class deleteByReview_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ํ•ด๋‹น_๋ฆฌ๋ทฐ์—_๋‹ฌ๋ฆฐ_์ข‹์•„์š”๋ฅผ_์‚ญ์ œํ• _์ˆ˜_์žˆ๋‹ค() { + // given + final var member1 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + final var member2 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ(); + final var member3 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„3_์ƒ์„ฑ(); + ๋ณต์ˆ˜_๋ฉค๋ฒ„_์ €์žฅ(member1, member2, member3); + + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); + ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member1, product, 0L); + final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member1, product, 0L); + ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1, review2); + + final var reviewFavorite1_1 = ReviewFavorite.create(member1, review1, true); + final var reviewFavorite1_2 = ReviewFavorite.create(member2, review1, true); + final var reviewFavorite1_3 = ReviewFavorite.create(member3, review1, true); + final var reviewFavorite2_1 = ReviewFavorite.create(member1, review2, true); + final var reviewFavorite2_2 = ReviewFavorite.create(member2, review2, true); + ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ข‹์•„์š”_์ €์žฅ(reviewFavorite1_1, reviewFavorite1_2, reviewFavorite1_3, reviewFavorite2_1, reviewFavorite2_2); + + final var expected = List.of(reviewFavorite2_1, reviewFavorite2_2); + + // when + reviewFavoriteRepository.deleteByReview(review1); + + // then + final var remainings = reviewFavoriteRepository.findAll(); + assertThat(remainings).usingRecursiveComparison() + .isEqualTo(expected); + } + } + + @Nested + class findByReview_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ๋ฆฌ๋ทฐ๋กœ_ํ•ด๋‹น_๋ฆฌ๋ทฐ์—_๋‹ฌ๋ฆฐ_์ข‹์•„์š”๋ฅผ_์กฐํšŒํ• _์ˆ˜_์žˆ๋‹ค() { + // given + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); + ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var review = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 0L); + ๋‹จ์ผ_๋ฆฌ๋ทฐ_์ €์žฅ(review); + + final var reviewFavorite = ReviewFavorite.create(member, review, true); + ๋‹จ์ผ_๋ฆฌ๋ทฐ_์ข‹์•„์š”_์ €์žฅ(reviewFavorite); + + final var expected = List.of(reviewFavorite); + + // when + final var actual = reviewFavoriteRepository.findByReview(review); + + // then + assertThat(actual).usingRecursiveComparison() + .ignoringExpectedNullFields() + .isEqualTo(expected); + } + } } diff --git a/backend/src/test/java/com/funeat/product/application/ProductServiceTest.java b/backend/src/test/java/com/funeat/product/application/ProductServiceTest.java index 5aa812dc6..9f5b88c5b 100644 --- a/backend/src/test/java/com/funeat/product/application/ProductServiceTest.java +++ b/backend/src/test/java/com/funeat/product/application/ProductServiceTest.java @@ -109,16 +109,16 @@ class ์ƒํ’ˆ_๊ฐœ์ˆ˜์—_๋Œ€ํ•œ_ํ…Œ์ŠคํŠธ { final var review1_4 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member1, product1, 0L); final var review2_1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member1, product2, 0L); final var review2_2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member1, product2, 0L); - final var review3_1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member1, product2, 0L); - final var review4_1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member1, product2, 0L); - final var review4_2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member1, product2, 0L); - final var review4_3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member1, product2, 0L); + final var review3_1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member1, product3, 0L); + final var review4_1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member1, product4, 0L); + final var review4_2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member1, product4, 0L); + final var review4_3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member1, product4, 0L); ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1_1, review1_2, review1_3, review1_4, review2_1, review2_2, review3_1, review4_1, review4_2, review4_3); - final var rankingProductDto1 = RankingProductDto.toDto(product2); - final var rankingProductDto2 = RankingProductDto.toDto(product3); - final var rankingProductDto3 = RankingProductDto.toDto(product4); + final var rankingProductDto1 = RankingProductDto.toDto(product3); + final var rankingProductDto2 = RankingProductDto.toDto(product4); + final var rankingProductDto3 = RankingProductDto.toDto(product2); final var rankingProductDtos = List.of(rankingProductDto1, rankingProductDto2, rankingProductDto3); final var expected = RankingProductsResponse.toResponse(rankingProductDtos); diff --git a/backend/src/test/java/com/funeat/product/domain/ProductTest.java b/backend/src/test/java/com/funeat/product/domain/ProductTest.java index ddfeaa2c7..7e012dd22 100644 --- a/backend/src/test/java/com/funeat/product/domain/ProductTest.java +++ b/backend/src/test/java/com/funeat/product/domain/ProductTest.java @@ -1,7 +1,6 @@ package com.funeat.product.domain; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; import org.junit.jupiter.api.DisplayNameGeneration; @@ -14,17 +13,17 @@ class ProductTest { @Nested - class updateAverageRating_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + class updateAverageRatingForInsert_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { @Test void ํ‰๊ท _ํ‰์ ์„_์—…๋ฐ์ดํŠธ_ํ• _์ˆ˜_์žˆ๋‹ค() { // given final var product = new Product("testName", 1000L, "testImage", "testContent", null); - final var reviewRating = 4L; final var reviewCount = 1L; + final var reviewRating = 4L; // when - product.updateAverageRating(reviewRating, reviewCount); + product.updateAverageRatingForInsert(reviewCount, reviewRating); final var actual = product.getAverageRating(); // then @@ -35,16 +34,16 @@ class updateAverageRating_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { void ํ‰๊ท _ํ‰์ ์„_์—ฌ๋Ÿฌ๋ฒˆ_์—…๋ฐ์ดํŠธ_ํ• _์ˆ˜_์žˆ๋‹ค() { // given final var product = new Product("testName", 1000L, "testImage", "testContent", null); - final var reviewRating1 = 4L; - final var reviewRating2 = 2L; final var reviewCount1 = 1L; final var reviewCount2 = 2L; + final var reviewRating1 = 4L; + final var reviewRating2 = 2L; // when - product.updateAverageRating(reviewRating1, reviewCount1); + product.updateAverageRatingForInsert(reviewCount1, reviewRating1); final var actual1 = product.getAverageRating(); - product.updateAverageRating(reviewRating2, reviewCount2); + product.updateAverageRatingForInsert(reviewCount2, reviewRating2); final var actual2 = product.getAverageRating(); // then @@ -58,39 +57,34 @@ class updateAverageRating_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { } @Nested - class updateAverageRating_์‹คํŒจ_ํ…Œ์ŠคํŠธ { + class updateAverageRatingForDelete_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { @Test - void ๋ฆฌ๋ทฐ_ํ‰์ ์—_null_๊ฐ’์ด_๋“ค์–ด์˜ค๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + void ๋ฆฌ๋ทฐ๊ฐ€_ํ•˜๋‚˜์ธ_์ƒํ’ˆ์˜_๋ฆฌ๋ทฐ๋ฅผ_์‚ญ์ œํ•˜๋ฉด_ํ‰๊ท ํ‰์ ์€_0์ ์ด_๋œ๋‹ค() { // given - final var product = new Product("testName", 1000L, "testImage", "testContent", null); - final var reviewCount = 1L; + final var product = new Product("testName", 1000L, "testImage", "testContent", 4.0, null, 1L); + final var reviewRating = 4L; // when - assertThatThrownBy(() -> product.updateAverageRating(null, reviewCount)) - .isInstanceOf(NullPointerException.class); - } - - @Test - void ๋ฆฌ๋ทฐ_ํ‰์ ์ด_0์ ์ด๋ผ๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•ด์•ผํ•˜๋Š”๋ฐ_๊ด€๋ จ_๋กœ์ง์ด_์—†์–ด_ํ†ต๊ณผํ•˜๊ณ _์žˆ๋‹ค() { - // given - final var product = new Product("testName", 1000L, "testImage", "testContent", null); - final var reviewRating = 0L; - final var reviewCount = 1L; + product.updateAverageRatingForDelete(reviewRating); + final var actual = product.getAverageRating(); - // when - product.updateAverageRating(reviewRating, reviewCount); + // then + assertThat(actual).isEqualTo(0.0); } @Test - void ๋ฆฌ๋ทฐ_๊ฐœ์ˆ˜๊ฐ€_0๊ฐœ๋ผ๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•ด์•ผํ•˜๋Š”๋ฐ_calculatedRating๊ฐ’์ด_infinity๊ฐ€_๋‚˜์™€_ํ†ต๊ณผํ•˜๊ณ _์žˆ๋‹ค() { + void ๋ฆฌ๋ทฐ๊ฐ€_์—ฌ๋Ÿฌ๊ฐœ์ธ_์ƒํ’ˆ์˜_๋ฆฌ๋ทฐ๋ฅผ_์‚ญ์ œํ•˜๋ฉด_ํ‰๊ท ํ‰์ ์ด_๊ฐฑ์‹ ๋œ๋‹ค() { // given - final var product = new Product("testName", 1000L, "testImage", "testContent", null); - final var reviewRating = 3L; - final var reviewCount = 0L; + final var product = new Product("testName", 1000L, "testImage", "testContent", 4.0, null, 4L); + final var reviewRating = 5L; // when - product.updateAverageRating(reviewRating, reviewCount); + product.updateAverageRatingForDelete(reviewRating); + final var actual = product.getAverageRating(); + + // then + assertThat(actual).isEqualTo(3.7); } } diff --git a/backend/src/test/java/com/funeat/product/persistence/ProductRepositoryTest.java b/backend/src/test/java/com/funeat/product/persistence/ProductRepositoryTest.java index dbcf77ace..a93c15f51 100644 --- a/backend/src/test/java/com/funeat/product/persistence/ProductRepositoryTest.java +++ b/backend/src/test/java/com/funeat/product/persistence/ProductRepositoryTest.java @@ -4,37 +4,23 @@ import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ; import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ; import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„_๋ฉค๋ฒ„3_์ƒ์„ฑ; -import static com.funeat.fixture.PageFixture.๊ฐ€๊ฒฉ_๋‚ด๋ฆผ์ฐจ์ˆœ; -import static com.funeat.fixture.PageFixture.๊ฐ€๊ฒฉ_์˜ค๋ฆ„์ฐจ์ˆœ; import static com.funeat.fixture.PageFixture.ํŽ˜์ด์ง€์š”์ฒญ_๊ธฐ๋ณธ_์ƒ์„ฑ; -import static com.funeat.fixture.PageFixture.ํŽ˜์ด์ง€์š”์ฒญ_์ƒ์„ฑ; -import static com.funeat.fixture.PageFixture.ํ‰๊ท _ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ; -import static com.funeat.fixture.PageFixture.ํ‰๊ท _ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_๋ง๊ณ ๋น™์ˆ˜_๊ฐ€๊ฒฉ5000์›_ํ‰์ 4์ _์ƒ์„ฑ; -import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 1์ _์ƒ์„ฑ; -import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ; -import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 4์ _์ƒ์„ฑ; -import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 5์ _์ƒ์„ฑ; -import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 1์ _์ƒ์„ฑ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 4์ _์ƒ์„ฑ; -import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ3000์›_ํ‰์ 1์ _์ƒ์„ฑ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ3000์›_ํ‰์ 5์ _์ƒ์„ฑ; -import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ4000์›_ํ‰์ 1์ _์ƒ์„ฑ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ4000์›_ํ‰์ 2์ _์ƒ์„ฑ; -import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ5000์›_ํ‰์ 1์ _์ƒ์„ฑ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์• ํ”Œ๋ง๊ณ _๊ฐ€๊ฒฉ3000์›_ํ‰์ 5์ _์ƒ์„ฑ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test1_ํ‰์ 1์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ; -import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test2_ํ‰์ 2์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; -import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; -import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; +import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ; import static org.assertj.core.api.Assertions.assertThat; import com.funeat.common.RepositoryTest; -import com.funeat.product.dto.ProductInCategoryDto; import com.funeat.product.dto.ProductReviewCountDto; +import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -44,134 +30,18 @@ class ProductRepositoryTest extends RepositoryTest { @Nested - class findByAllCategory_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { - - @Test - void ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ์„_ํ‰์ ์ด_๋†’์€_์ˆœ์œผ๋กœ_์ •๋ ฌํ•œ๋‹ค() { - // given - final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); - ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); - - final var product1 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); - final var product2 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); - final var product3 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); - final var product4 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 4์ _์ƒ์„ฑ(category); - final var product5 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 5์ _์ƒ์„ฑ(category); - ๋ณต์ˆ˜_์ƒํ’ˆ_์ €์žฅ(product1, product2, product3, product4, product5); - - final var page = ํŽ˜์ด์ง€์š”์ฒญ_์ƒ์„ฑ(0, 3, ํ‰๊ท _ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ); - - final var productInCategoryDto1 = ProductInCategoryDto.toDto(product5, 0L); - final var productInCategoryDto2 = ProductInCategoryDto.toDto(product4, 0L); - final var productInCategoryDto3 = ProductInCategoryDto.toDto(product3, 0L); - final var expected = List.of(productInCategoryDto1, productInCategoryDto2, productInCategoryDto3); - - // when - final var actual = productRepository.findAllByCategory(category, page).getContent(); - - // then - assertThat(actual).usingRecursiveComparison() - .isEqualTo(expected); - } - - @Test - void ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ์„_ํ‰์ ์ด_๋‚ฎ์€_์ˆœ์œผ๋กœ_์ •๋ ฌํ•œ๋‹ค() { - // given - final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); - ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); - - final var product1 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); - final var product2 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); - final var product3 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); - final var product4 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 4์ _์ƒ์„ฑ(category); - final var product5 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 5์ _์ƒ์„ฑ(category); - ๋ณต์ˆ˜_์ƒํ’ˆ_์ €์žฅ(product1, product2, product3, product4, product5); - - final var page = ํŽ˜์ด์ง€์š”์ฒญ_์ƒ์„ฑ(0, 3, ํ‰๊ท _ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ); - - final var productInCategoryDto1 = ProductInCategoryDto.toDto(product1, 0L); - final var productInCategoryDto2 = ProductInCategoryDto.toDto(product2, 0L); - final var productInCategoryDto3 = ProductInCategoryDto.toDto(product3, 0L); - final var expected = List.of(productInCategoryDto1, productInCategoryDto2, productInCategoryDto3); - - // when - final var actual = productRepository.findAllByCategory(category, page).getContent(); - - // then - assertThat(actual).usingRecursiveComparison() - .isEqualTo(expected); - } - - @Test - void ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ์„_๊ฐ€๊ฒฉ์ด_๋†’์€_์ˆœ์œผ๋กœ_์ •๋ ฌํ•œ๋‹ค() { - // given - final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); - ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); - - final var product1 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); - final var product2 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); - final var product3 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ3000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); - final var product4 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ4000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); - final var product5 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ5000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); - ๋ณต์ˆ˜_์ƒํ’ˆ_์ €์žฅ(product1, product2, product3, product4, product5); - - final var page = ํŽ˜์ด์ง€์š”์ฒญ_์ƒ์„ฑ(0, 3, ๊ฐ€๊ฒฉ_๋‚ด๋ฆผ์ฐจ์ˆœ); - - final var productInCategoryDto1 = ProductInCategoryDto.toDto(product5, 0L); - final var productInCategoryDto2 = ProductInCategoryDto.toDto(product4, 0L); - final var productInCategoryDto3 = ProductInCategoryDto.toDto(product3, 0L); - final var expected = List.of(productInCategoryDto1, productInCategoryDto2, productInCategoryDto3); - - // when - final var actual = productRepository.findAllByCategory(category, page).getContent(); - - // then - assertThat(actual).usingRecursiveComparison() - .isEqualTo(expected); - } - - @Test - void ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ์„_๊ฐ€๊ฒฉ์ด_๋‚ฎ์€_์ˆœ์œผ๋กœ_์ •๋ ฌํ•œ๋‹ค() { - // given - final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); - ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); - - final var product1 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); - final var product2 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); - final var product3 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ3000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); - final var product4 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ4000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); - final var product5 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ5000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); - ๋ณต์ˆ˜_์ƒํ’ˆ_์ €์žฅ(product1, product2, product3, product4, product5); - - final var page = ํŽ˜์ด์ง€์š”์ฒญ_์ƒ์„ฑ(0, 3, ๊ฐ€๊ฒฉ_์˜ค๋ฆ„์ฐจ์ˆœ); - - final var productInCategoryDto1 = ProductInCategoryDto.toDto(product1, 0L); - final var productInCategoryDto2 = ProductInCategoryDto.toDto(product2, 0L); - final var productInCategoryDto3 = ProductInCategoryDto.toDto(product3, 0L); - final var expected = List.of(productInCategoryDto1, productInCategoryDto2, productInCategoryDto3); - - // when - final var actual = productRepository.findAllByCategory(category, page).getContent(); - - // then - assertThat(actual).usingRecursiveComparison() - .isEqualTo(expected); - } - } - - @Nested - class findAllByCategoryOrderByReviewCountDesc_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + class findAllByAverageRatingGreaterThan3_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { @Test - void ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„_์ƒํ’ˆ์„_๋ฆฌ๋ทฐ์ˆ˜๊ฐ€_๋งŽ์€_์ˆœ์œผ๋กœ_์ •๋ ฌํ•œ๋‹ค() { + void ํ‰์ ์ด_3๋ณด๋‹ค_ํฐ_๋ชจ๋“ _์ƒํ’ˆ๋“ค๊ณผ_๋ฆฌ๋ทฐ_์ˆ˜๋ฅผ_์กฐํšŒํ•œ๋‹ค() { // given final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); - final var product1 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); - final var product2 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); - final var product3 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ3000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); - final var product4 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ4000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); + final var product1 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); + final var product2 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 4์ _์ƒ์„ฑ(category); + final var product3 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ3000์›_ํ‰์ 5์ _์ƒ์„ฑ(category); + final var product4 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ4000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); ๋ณต์ˆ˜_์ƒํ’ˆ_์ €์žฅ(product1, product2, product3, product4); final var member1 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); @@ -179,65 +49,52 @@ class findAllByCategoryOrderByReviewCountDesc_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { final var member3 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„3_์ƒ์„ฑ(); ๋ณต์ˆ˜_๋ฉค๋ฒ„_์ €์žฅ(member1, member2, member3); - final var review1_1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test1_ํ‰์ 1์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member1, product1, 0L); - final var review1_2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member2, product1, 0L); - final var review2_1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member3, product2, 0L); - final var review2_2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test2_ํ‰์ 2์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member1, product2, 0L); - final var review2_3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member2, product2, 0L); - final var review3_1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member1, product3, 0L); + final var review1_1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member1, product1, 0L, LocalDateTime.now().minusDays(2L)); + final var review1_2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member2, product1, 0L, LocalDateTime.now().minusDays(3L)); + final var review2_1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member3, product2, 0L, LocalDateTime.now().minusDays(10L)); + final var review2_2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member1, product2, 0L, LocalDateTime.now().minusDays(1L)); + final var review2_3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member2, product2, 0L, LocalDateTime.now().minusDays(9L)); + final var review3_1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member1, product3, 0L, LocalDateTime.now().minusDays(8L)); ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1_1, review1_2, review2_1, review2_2, review2_3, review3_1); - final var page = ํŽ˜์ด์ง€์š”์ฒญ_๊ธฐ๋ณธ_์ƒ์„ฑ(0, 3); - - final var productInCategoryDto1 = ProductInCategoryDto.toDto(product2, 3L); - final var productInCategoryDto2 = ProductInCategoryDto.toDto(product1, 2L); - final var productInCategoryDto3 = ProductInCategoryDto.toDto(product3, 1L); - final var expected = List.of(productInCategoryDto1, productInCategoryDto2, productInCategoryDto3); + final var productReviewCountDto1 = new ProductReviewCountDto(product2, 3L); + final var productReviewCountDto2 = new ProductReviewCountDto(product3, 1L); + final var expected = List.of(productReviewCountDto1, productReviewCountDto2); // when - final var actual = productRepository.findAllByCategoryOrderByReviewCountDesc(category, page) - .getContent(); + final var startDateTime = LocalDateTime.now().minusWeeks(2L); + final var endDateTime = LocalDateTime.now(); + final var actual = productRepository.findAllByAverageRatingGreaterThan3(startDateTime, endDateTime); // then assertThat(actual).usingRecursiveComparison() .isEqualTo(expected); } - } - - @Nested - class findAllByAverageRatingGreaterThan3_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { @Test - void ํ‰์ ์ด_3๋ณด๋‹ค_ํฐ_๋ชจ๋“ _์ƒํ’ˆ๋“ค๊ณผ_๋ฆฌ๋ทฐ_์ˆ˜๋ฅผ_์กฐํšŒํ•œ๋‹ค() { + void ๊ธฐ๊ฐ„_์•ˆ์—_๋ฆฌ๋ทฐ๊ฐ€_์กด์žฌํ•˜๋Š”_์ƒํ’ˆ์ด_์—†์œผ๋ฉด_๋นˆ_๋ฆฌ์ŠคํŠธ๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { // given final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); final var product1 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); final var product2 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 4์ _์ƒ์„ฑ(category); - final var product3 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ3000์›_ํ‰์ 5์ _์ƒ์„ฑ(category); - final var product4 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ4000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); - ๋ณต์ˆ˜_์ƒํ’ˆ_์ €์žฅ(product1, product2, product3, product4); + ๋ณต์ˆ˜_์ƒํ’ˆ_์ €์žฅ(product1, product2); final var member1 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); final var member2 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ(); - final var member3 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„3_์ƒ์„ฑ(); - ๋ณต์ˆ˜_๋ฉค๋ฒ„_์ €์žฅ(member1, member2, member3); + ๋ณต์ˆ˜_๋ฉค๋ฒ„_์ €์žฅ(member1, member2); - final var review1_1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test1_ํ‰์ 1์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member1, product1, 0L); - final var review1_2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member2, product1, 0L); - final var review2_1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member3, product2, 0L); - final var review2_2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member1, product2, 0L); - final var review2_3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member2, product2, 0L); - final var review3_1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member1, product3, 0L); - ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1_1, review1_2, review2_1, review2_2, review2_3, review3_1); + final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member1, product1, 0L, LocalDateTime.now().minusDays(15L)); + final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member2, product2, 0L, LocalDateTime.now().minusWeeks(3L)); + ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1, review2); - final var productReviewCountDto1 = new ProductReviewCountDto(product2, 3L); - final var productReviewCountDto2 = new ProductReviewCountDto(product3, 1L); - final var expected = List.of(productReviewCountDto1, productReviewCountDto2); + final var expected = Collections.emptyList(); // when - final var actual = productRepository.findAllByAverageRatingGreaterThan3(); + final var startDateTime = LocalDateTime.now().minusWeeks(2L); + final var endDateTime = LocalDateTime.now(); + final var actual = productRepository.findAllByAverageRatingGreaterThan3(startDateTime, endDateTime); // then assertThat(actual).usingRecursiveComparison() diff --git a/backend/src/test/java/com/funeat/recipe/application/RecipeServiceTest.java b/backend/src/test/java/com/funeat/recipe/application/RecipeServiceTest.java index fcd4e2f40..81badf4f6 100644 --- a/backend/src/test/java/com/funeat/recipe/application/RecipeServiceTest.java +++ b/backend/src/test/java/com/funeat/recipe/application/RecipeServiceTest.java @@ -24,6 +24,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import com.funeat.comment.domain.Comment; import com.funeat.common.ServiceTest; import com.funeat.common.dto.PageDto; import com.funeat.member.domain.Member; @@ -35,17 +36,24 @@ import com.funeat.product.domain.CategoryType; import com.funeat.product.domain.Product; import com.funeat.product.exception.ProductException.ProductNotFoundException; +import com.funeat.recipe.dto.RankingRecipeDto; +import com.funeat.recipe.dto.RankingRecipesResponse; +import com.funeat.recipe.dto.RecipeAuthorDto; +import com.funeat.recipe.dto.RecipeCommentCondition; +import com.funeat.recipe.dto.RecipeCommentCreateRequest; +import com.funeat.recipe.dto.RecipeCommentResponse; import com.funeat.recipe.dto.RecipeCreateRequest; import com.funeat.recipe.dto.RecipeDetailResponse; import com.funeat.recipe.dto.RecipeDto; import com.funeat.recipe.exception.RecipeException.RecipeNotFoundException; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - +import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; @SuppressWarnings("NonAsciiCharacters") class RecipeServiceTest extends ServiceTest { @@ -317,7 +325,7 @@ class getSortingRecipes_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { } @Test - void ๊ฟ€์กฐํ•ฉ์„_์ตœ์‹ ์ˆœ์œผ๋กœ_์ •๋ ฌํ• _์ˆ˜_์žˆ๋‹ค() { + void ๊ฟ€์กฐํ•ฉ์„_์ตœ์‹ ์ˆœ์œผ๋กœ_์ •๋ ฌํ• _์ˆ˜_์žˆ๋‹ค() throws InterruptedException { // given final var member1 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); final var member2 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ(); @@ -333,7 +341,9 @@ class getSortingRecipes_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { ๋ณต์ˆ˜_์ƒํ’ˆ_์ €์žฅ(product1, product2, product3); final var recipe1_1 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member1, 1L); + Thread.sleep(100); final var recipe1_2 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member1, 3L); + Thread.sleep(100); final var recipe1_3 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member1, 2L); ๋ณต์ˆ˜_๊ฟ€์กฐํ•ฉ_์ €์žฅ(recipe1_1, recipe1_2, recipe1_3); @@ -545,6 +555,342 @@ class likeRecipe_์‹คํŒจ_ํ…Œ์ŠคํŠธ { } } + @Nested + class getTop3Recipes_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Nested + class ๊ฟ€์กฐํ•ฉ_๊ฐœ์ˆ˜์—_๋Œ€ํ•œ_ํ…Œ์ŠคํŠธ { + + @Test + void ์ „์ฒด_๊ฟ€์กฐํ•ฉ์ด_ํ•˜๋‚˜๋„_์—†์–ด๋„_๋ฐ˜ํ™˜๊ฐ’์€_์žˆ์–ด์•ผํ•œ๋‹ค() { + // given + final var expected = RankingRecipesResponse.toResponse(Collections.emptyList()); + + // when + final var actual = recipeService.getTop3Recipes(); + + // then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + void ๋žญํ‚น_์กฐ๊ฑด์—_๋ถ€ํ•ฉํ•˜๋Š”_๊ฟ€์กฐํ•ฉ์ด_1๊ฐœ๋ฉด_๊ฟ€์กฐํ•ฉ์ด_1๊ฐœ_๋ฐ˜ํ™˜๋œ๋‹ค() { + // given + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var now = LocalDateTime.now(); + final var recipe = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 2L, now); + ๋‹จ์ผ_๊ฟ€์กฐํ•ฉ_์ €์žฅ(recipe); + + final var author = RecipeAuthorDto.toDto(member); + final var rankingRecipeDto = RankingRecipeDto.toDto(recipe, Collections.emptyList(), author); + final var rankingRecipesDtos = Collections.singletonList(rankingRecipeDto); + final var expected = RankingRecipesResponse.toResponse(rankingRecipesDtos); + + // when + final var actual = recipeService.getTop3Recipes(); + + // then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + void ๋žญํ‚น_์กฐ๊ฑด์—_๋ถ€ํ•ฉํ•˜๋Š”_๊ฟ€์กฐํ•ฉ์ด_2๊ฐœ๋ฉด_๊ฟ€์กฐํ•ฉ์ด_2๊ฐœ_๋ฐ˜ํ™˜๋œ๋‹ค() { + // given + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var now = LocalDateTime.now(); + final var recipe1 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 2L, now.minusDays(1L)); + final var recipe2 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 2L, now); + ๋ณต์ˆ˜_๊ฟ€์กฐํ•ฉ_์ €์žฅ(recipe1, recipe2); + + final var author = RecipeAuthorDto.toDto(member); + final var rankingRecipeDto1 = RankingRecipeDto.toDto(recipe1, Collections.emptyList(), author); + final var rankingRecipeDto2 = RankingRecipeDto.toDto(recipe2, Collections.emptyList(), author); + final var rankingRecipesDtos = List.of(rankingRecipeDto2, rankingRecipeDto1); + final var expected = RankingRecipesResponse.toResponse(rankingRecipesDtos); + + // when + final var actual = recipeService.getTop3Recipes(); + + // then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + void ์ „์ฒด_๊ฟ€์กฐํ•ฉ_์ค‘_๋žญํ‚น์ด_๋†’์€_์ƒ์œ„_3๊ฐœ_๊ฟ€์กฐํ•ฉ์„_๊ตฌํ• _์ˆ˜_์žˆ๋‹ค() { + // given + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var now = LocalDateTime.now(); + final var recipe1 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 4L, now.minusDays(10L)); + final var recipe2 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 6L, now.minusDays(10L)); + final var recipe3 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 5L, now); + final var recipe4 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 6L, now); + ๋ณต์ˆ˜_๊ฟ€์กฐํ•ฉ_์ €์žฅ(recipe1, recipe2, recipe3, recipe4); + + final var author = RecipeAuthorDto.toDto(member); + final var rankingRecipeDto1 = RankingRecipeDto.toDto(recipe1, Collections.emptyList(), author); + final var rankingRecipeDto2 = RankingRecipeDto.toDto(recipe2, Collections.emptyList(), author); + final var rankingRecipeDto3 = RankingRecipeDto.toDto(recipe3, Collections.emptyList(), author); + final var rankingRecipeDto4 = RankingRecipeDto.toDto(recipe4, Collections.emptyList(), author); + final var rankingRecipesDtos = List.of(rankingRecipeDto4, rankingRecipeDto3, rankingRecipeDto2); + final var expected = RankingRecipesResponse.toResponse(rankingRecipesDtos); + + // when + final var actual = recipeService.getTop3Recipes(); + + // then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + } + + @Nested + class ๊ฟ€์กฐํ•ฉ_๋žญํ‚น_์ ์ˆ˜์—_๋Œ€ํ•œ_ํ…Œ์ŠคํŠธ { + + @Test + void ๊ฟ€์กฐํ•ฉ_์ข‹์•„์š”_์ˆ˜๊ฐ€_๊ฐ™์œผ๋ฉด_์ตœ๊ทผ_์ƒ์„ฑ๋œ_๊ฟ€์กฐํ•ฉ์˜_๋žญํ‚น์„_๋”_๋†’๊ฒŒ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + // given + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var now = LocalDateTime.now(); + final var recipe1 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 10L, now.minusDays(9L)); + final var recipe2 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 10L, now.minusDays(4L)); + ๋ณต์ˆ˜_๊ฟ€์กฐํ•ฉ_์ €์žฅ(recipe1, recipe2); + + final var author = RecipeAuthorDto.toDto(member); + final var rankingRecipeDto1 = RankingRecipeDto.toDto(recipe1, Collections.emptyList(), author); + final var rankingRecipeDto2 = RankingRecipeDto.toDto(recipe2, Collections.emptyList(), author); + final var rankingRecipesDtos = List.of(rankingRecipeDto2, rankingRecipeDto1); + final var expected = RankingRecipesResponse.toResponse(rankingRecipesDtos); + + // when + final var actual = recipeService.getTop3Recipes(); + + // then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + void ๊ฟ€์กฐํ•ฉ_์ƒ์„ฑ_์ผ์ž๊ฐ€_๊ฐ™์œผ๋ฉด_์ข‹์•„์š”_์ˆ˜๊ฐ€_๋งŽ์€_๊ฟ€์กฐํ•ฉ์˜_๋žญํ‚น์„_๋”_๋†’๊ฒŒ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + // given + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var now = LocalDateTime.now(); + final var recipe1 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 2L, now.minusDays(1L)); + final var recipe2 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 4L, now.minusDays(1L)); + ๋ณต์ˆ˜_๊ฟ€์กฐํ•ฉ_์ €์žฅ(recipe1, recipe2); + + final var author = RecipeAuthorDto.toDto(member); + final var rankingRecipeDto1 = RankingRecipeDto.toDto(recipe1, Collections.emptyList(), author); + final var rankingRecipeDto2 = RankingRecipeDto.toDto(recipe2, Collections.emptyList(), author); + final var rankingRecipesDtos = List.of(rankingRecipeDto2, rankingRecipeDto1); + final var expected = RankingRecipesResponse.toResponse(rankingRecipesDtos); + + // when + final var actual = recipeService.getTop3Recipes(); + + // then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + } + } + + @Nested + class writeCommentOfRecipe_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ๊ฟ€์กฐํ•ฉ์—_๋Œ“๊ธ€์„_์ž‘์„ฑํ• _์ˆ˜_์žˆ๋‹ค() { + // given + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + final var product1 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); + final var product2 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ3000์›_ํ‰์ 4์ _์ƒ์„ฑ(category); + final var product3 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); + ๋ณต์ˆ˜_์ƒํ’ˆ_์ €์žฅ(product1, product2, product3); + final var author = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(author); + final var authorId = author.getId(); + + final var images = ์—ฌ๋Ÿฌ_์ด๋ฏธ์ง€_์ƒ์„ฑ(3); + + final var productIds = List.of(product1.getId(), product2.getId(), product3.getId()); + final var recipeCreateRequest = new RecipeCreateRequest("์ œ์ผ๋กœ ๋ง›์žˆ๋Š” ๋ ˆ์‹œํ”ผ", productIds, + "์šฐ์„  ๋ฐฅ์„ ๋„ฃ์–ด์š”. ๊ทธ๋ฆฌ๊ณ  ๋ฐฅ์„ ๋˜ ๋„ฃ์–ด์š”. ๊ทธ๋ฆฌ๊ณ  ๋ฐฅ์„ ๋˜ ๋„ฃ์œผ๋ฉด.. ๋!!"); + + final var savedMemberId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ()); + final var savedRecipeId = recipeService.create(authorId, images, recipeCreateRequest); + + // when + final var request = new RecipeCommentCreateRequest("๊ฟ€์กฐํ•ฉ ๋Œ“๊ธ€์ด์—์š”"); + final var savedCommentId = recipeService.writeCommentOfRecipe(savedMemberId, savedRecipeId, request); + + // then + final var result = commentRepository.findById(savedCommentId).get(); + final var savedRecipe = recipeRepository.findById(savedRecipeId).get(); + final var savedMember = memberRepository.findById(savedMemberId).get(); + + assertThat(result).usingRecursiveComparison() + .ignoringFields("id", "createdAt") + .isEqualTo(new Comment(savedRecipe, savedMember, request.getComment())); + } + } + + @Nested + class writeCommentOfRecipe_์‹คํŒจ_ํ…Œ์ŠคํŠธ { + + @Test + void ์กด์žฌํ•˜์ง€_์•Š์€_๋ฉค๋ฒ„๊ฐ€_๊ฟ€์กฐํ•ฉ์—_๋Œ“๊ธ€์„_์ž‘์„ฑํ•˜๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + // given + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + final var product1 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); + final var product2 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ3000์›_ํ‰์ 4์ _์ƒ์„ฑ(category); + final var product3 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); + ๋ณต์ˆ˜_์ƒํ’ˆ_์ €์žฅ(product1, product2, product3); + final var author = new Member("author", "image.png", "1"); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(author); + final var authorId = author.getId(); + + final var images = ์—ฌ๋Ÿฌ_์ด๋ฏธ์ง€_์ƒ์„ฑ(3); + + final var productIds = List.of(product1.getId(), product2.getId(), product3.getId()); + final var recipeCreateRequest = new RecipeCreateRequest("์ œ์ผ๋กœ ๋ง›์žˆ๋Š” ๋ ˆ์‹œํ”ผ", productIds, + "์šฐ์„  ๋ฐฅ์„ ๋„ฃ์–ด์š”. ๊ทธ๋ฆฌ๊ณ  ๋ฐฅ์„ ๋˜ ๋„ฃ์–ด์š”. ๊ทธ๋ฆฌ๊ณ  ๋ฐฅ์„ ๋˜ ๋„ฃ์œผ๋ฉด.. ๋!!"); + + final var notExistMemberId = 999999999L; + final var savedRecipeId = recipeService.create(authorId, images, recipeCreateRequest); + final var request = new RecipeCommentCreateRequest("๊ฟ€์กฐํ•ฉ ๋Œ“๊ธ€์ด์—์š”"); + + // when then + assertThatThrownBy(() -> recipeService.writeCommentOfRecipe(notExistMemberId, savedRecipeId, request)) + .isInstanceOf(MemberNotFoundException.class); + } + + @Test + void ์กด์žฌํ•˜์ง€_์•Š์€_๊ฟ€์กฐํ•ฉ์—_๋Œ“๊ธ€์„_์ž‘์„ฑํ•˜๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + // given + final var memberId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ()); + final var request = new RecipeCommentCreateRequest("๊ฟ€์กฐํ•ฉ ๋Œ“๊ธ€์ด์—์š”"); + final var notExistRecipeId = 999999999L; + + // when then + assertThatThrownBy(() -> recipeService.writeCommentOfRecipe(memberId, notExistRecipeId, request)) + .isInstanceOf(RecipeNotFoundException.class); + } + } + + @Nested + class getCommentsOfRecipe_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ๊ฟ€์กฐํ•ฉ์—_๋‹ฌ๋ฆฐ_๋Œ“๊ธ€๋“ค์„_์ปค์„œํŽ˜์ด์ง•์„_ํ†ตํ•ด_์กฐํšŒํ• _์ˆ˜_์žˆ๋‹ค_์ด_๋Œ“๊ธ€_15๊ฐœ_์ค‘_์ฒซํŽ˜์ด์ง€_๋Œ“๊ธ€_10๊ฐœ์กฐํšŒ() { + // given + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + final var product1 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); + final var product2 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ3000์›_ํ‰์ 4์ _์ƒ์„ฑ(category); + final var product3 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); + ๋ณต์ˆ˜_์ƒํ’ˆ_์ €์žฅ(product1, product2, product3); + final var author = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(author); + final var authorId = author.getId(); + + final var images = ์—ฌ๋Ÿฌ_์ด๋ฏธ์ง€_์ƒ์„ฑ(3); + + final var productIds = List.of(product1.getId(), product2.getId(), product3.getId()); + final var recipeCreateRequest = new RecipeCreateRequest("์ œ์ผ๋กœ ๋ง›์žˆ๋Š” ๋ ˆ์‹œํ”ผ", productIds, + "์šฐ์„  ๋ฐฅ์„ ๋„ฃ์–ด์š”. ๊ทธ๋ฆฌ๊ณ  ๋ฐฅ์„ ๋˜ ๋„ฃ์–ด์š”. ๊ทธ๋ฆฌ๊ณ  ๋ฐฅ์„ ๋˜ ๋„ฃ์œผ๋ฉด.. ๋!!"); + + final var savedMemberId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ()); + final var savedRecipeId = recipeService.create(authorId, images, recipeCreateRequest); + + for (int i = 1; i <= 15; i++) { + final var request = new RecipeCommentCreateRequest("๊ฟ€์กฐํ•ฉ ๋Œ“๊ธ€์ด์—์š”" + i); + recipeService.writeCommentOfRecipe(savedMemberId, savedRecipeId, request); + } + + // when + final var result = recipeService.getCommentsOfRecipe(savedRecipeId, + new RecipeCommentCondition(null, null)); + + // + final var savedRecipe = recipeRepository.findById(savedRecipeId).get(); + final var savedMember = memberRepository.findById(savedMemberId).get(); + + final var expectedCommentResponses = new ArrayList<>(); + for (int i = 0; i < result.getComments().size(); i++) { + expectedCommentResponses.add(RecipeCommentResponse.toResponse( + new Comment(savedRecipe, savedMember, "๊ฟ€์กฐํ•ฉ ๋Œ“๊ธ€์ด์—์š”" + (15 - i)))); + } + + assertThat(result.getHasNext()).isTrue(); + assertThat(result.getTotalElements()).isEqualTo(15); + assertThat(result.getComments()).hasSize(10); + assertThat(result.getComments()).usingRecursiveComparison() + .ignoringFields("id", "createdAt") + .isEqualTo(expectedCommentResponses); + } + + @Test + void ๊ฟ€์กฐํ•ฉ์—_๋‹ฌ๋ฆฐ_๋Œ“๊ธ€๋“ค์„_์ปค์„œํŽ˜์ด์ง•์„_ํ†ตํ•ด_์กฐํšŒํ• _์ˆ˜_์žˆ๋‹ค_์ด_๋Œ“๊ธ€_15๊ฐœ_์ค‘_๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€_๋Œ“๊ธ€_5๊ฐœ์กฐํšŒ() { + // given + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + final var product1 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); + final var product2 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ3000์›_ํ‰์ 4์ _์ƒ์„ฑ(category); + final var product3 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); + ๋ณต์ˆ˜_์ƒํ’ˆ_์ €์žฅ(product1, product2, product3); + final var author = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(author); + final var authorId = author.getId(); + + final var images = ์—ฌ๋Ÿฌ_์ด๋ฏธ์ง€_์ƒ์„ฑ(3); + + final var productIds = List.of(product1.getId(), product2.getId(), product3.getId()); + final var recipeCreateRequest = new RecipeCreateRequest("์ œ์ผ๋กœ ๋ง›์žˆ๋Š” ๋ ˆ์‹œํ”ผ", productIds, + "์šฐ์„  ๋ฐฅ์„ ๋„ฃ์–ด์š”. ๊ทธ๋ฆฌ๊ณ  ๋ฐฅ์„ ๋˜ ๋„ฃ์–ด์š”. ๊ทธ๋ฆฌ๊ณ  ๋ฐฅ์„ ๋˜ ๋„ฃ์œผ๋ฉด.. ๋!!"); + + final var savedMemberId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ()); + final var savedRecipeId = recipeService.create(authorId, images, recipeCreateRequest); + + for (int i = 1; i <= 15; i++) { + final var request = new RecipeCommentCreateRequest("๊ฟ€์กฐํ•ฉ ๋Œ“๊ธ€์ด์—์š”" + i); + recipeService.writeCommentOfRecipe(savedMemberId, savedRecipeId, request); + } + + // when + final var result = recipeService.getCommentsOfRecipe(savedRecipeId, + new RecipeCommentCondition(6L, 15L)); + + // + final var savedRecipe = recipeRepository.findById(savedRecipeId).get(); + final var savedMember = memberRepository.findById(savedMemberId).get(); + + final var expectedCommentResponses = new ArrayList<>(); + for (int i = 0; i < result.getComments().size(); i++) { + expectedCommentResponses.add(RecipeCommentResponse.toResponse( + new Comment(savedRecipe, savedMember, "๊ฟ€์กฐํ•ฉ ๋Œ“๊ธ€์ด์—์š”" + (5 - i)))); + } + + assertThat(result.getHasNext()).isFalse(); + assertThat(result.getTotalElements()).isEqualTo(15); + assertThat(result.getComments()).hasSize(5); + assertThat(result.getComments()).usingRecursiveComparison() + .ignoringFields("id", "createdAt") + .isEqualTo(expectedCommentResponses); + } + } + private void ํ•ด๋‹น๋ฉค๋ฒ„์˜_๊ฟ€์กฐํ•ฉ๊ณผ_ํŽ˜์ด์ง•_๊ฒฐ๊ณผ๋ฅผ_๊ฒ€์ฆํ•œ๋‹ค(final MemberRecipesResponse actual, final List expectedRecipesDtos, final PageDto expectedPage) { assertSoftly(soft -> { diff --git a/backend/src/test/java/com/funeat/recipe/domain/RecipeTest.java b/backend/src/test/java/com/funeat/recipe/domain/RecipeTest.java new file mode 100644 index 000000000..7a0d28030 --- /dev/null +++ b/backend/src/test/java/com/funeat/recipe/domain/RecipeTest.java @@ -0,0 +1,36 @@ +package com.funeat.recipe.domain; + +import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ; +import static com.funeat.fixture.RecipeFixture.๋ ˆ์‹œํ”ผ_์ƒ์„ฑ; +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDateTime; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RecipeTest { + + @Nested + class calculateRankingScore_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ๊ฟ€์กฐํ•ฉ_์ข‹์•„์š”_์ˆ˜์™€_๊ฟ€์กฐํ•ฉ_์ƒ์„ฑ_์‹œ๊ฐ„์œผ๋กœ_ํ•ด๋‹น_๊ฟ€์กฐํ•ฉ์˜_๋žญํ‚น_์ ์ˆ˜๋ฅผ_๊ตฌํ• _์ˆ˜_์žˆ๋‹ค() { + // given + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + final var favoriteCount = 4L; + final var recipe = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, favoriteCount, LocalDateTime.now().minusDays(1L)); + + final var expected = favoriteCount / Math.pow(2.0, 0.1); + + // when + final var actual = recipe.calculateRankingScore(); + + // then + assertThat(actual).isEqualTo(expected); + } + } +} diff --git a/backend/src/test/java/com/funeat/recipe/persistence/RecipeRepositoryTest.java b/backend/src/test/java/com/funeat/recipe/persistence/RecipeRepositoryTest.java index c7130d2ce..9c53177db 100644 --- a/backend/src/test/java/com/funeat/recipe/persistence/RecipeRepositoryTest.java +++ b/backend/src/test/java/com/funeat/recipe/persistence/RecipeRepositoryTest.java @@ -21,6 +21,7 @@ import static org.assertj.core.api.Assertions.assertThat; import com.funeat.common.RepositoryTest; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -119,7 +120,7 @@ class findAllRecipes_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { } @Test - void ๊ฟ€์กฐํ•ฉ์„_์ตœ์‹ ์ˆœ์œผ๋กœ_์ •๋ ฌํ•œ๋‹ค() { + void ๊ฟ€์กฐํ•ฉ์„_์ตœ์‹ ์ˆœ์œผ๋กœ_์ •๋ ฌํ•œ๋‹ค() throws InterruptedException { // given final var member1 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); final var member2 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ(); @@ -135,7 +136,9 @@ class findAllRecipes_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { ๋ณต์ˆ˜_์ƒํ’ˆ_์ €์žฅ(product1, product2, product3); final var recipe1_1 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member1, 1L); + Thread.sleep(100); final var recipe1_2 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member1, 3L); + Thread.sleep(100); final var recipe1_3 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member1, 2L); ๋ณต์ˆ˜_๊ฟ€์กฐํ•ฉ_์ €์žฅ(recipe1_1, recipe1_2, recipe1_3); @@ -257,25 +260,44 @@ class findRecipesByProduct_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { } @Nested - class findRecipesByOrderByFavoriteCountDesc_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + class findRecipesByFavoriteCountGreaterThanEqual_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { @Test - void ์ข‹์•„์š”์ˆœ์œผ๋กœ_์ƒ์œ„_3๊ฐœ์˜_๋ ˆ์‹œํ”ผ๋“ค์„_์กฐํšŒํ•œ๋‹ค() { + void ํŠน์ •_์ข‹์•„์š”_์ˆ˜_์ด์ƒ์ธ_๋ชจ๋“ _๊ฟ€์กฐํ•ฉ๋“ค์„_์กฐํšŒํ•œ๋‹ค() { // given final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); - final var recipe1 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 1L); - final var recipe2 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 2L); - final var recipe3 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 3L); - final var recipe4 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 4L); + final var recipe1 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 0L); + final var recipe2 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 1L); + final var recipe3 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 10L); + final var recipe4 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 100L); ๋ณต์ˆ˜_๊ฟ€์กฐํ•ฉ_์ €์žฅ(recipe1, recipe2, recipe3, recipe4); - final var page = ํŽ˜์ด์ง€์š”์ฒญ_๊ธฐ๋ณธ_์ƒ์„ฑ(0, 3); - final var expected = List.of(recipe4, recipe3, recipe2); + final var expected = List.of(recipe2, recipe3, recipe4); + + // when + final var actual = recipeRepository.findRecipesByFavoriteCountGreaterThanEqual(1L); + + // then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + void ํŠน์ •_์ข‹์•„์š”_์ˆ˜_์ด์ƒ์ธ_๊ฟ€์กฐํ•ฉ์ด_์—†์œผ๋ฉด_๋นˆ_๋ฆฌ์ŠคํŠธ๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + // given + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var recipe1 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 0L); + final var recipe2 = ๋ ˆ์‹œํ”ผ_์ƒ์„ฑ(member, 0L); + ๋ณต์ˆ˜_๊ฟ€์กฐํ•ฉ_์ €์žฅ(recipe1, recipe2); + + final var expected = Collections.emptyList(); // when - final var actual = recipeRepository.findRecipesByOrderByFavoriteCountDesc(page); + final var actual = recipeRepository.findRecipesByFavoriteCountGreaterThanEqual(1L); // then assertThat(actual).usingRecursiveComparison() diff --git a/backend/src/test/java/com/funeat/review/application/ReviewDeleteEventListenerTest.java b/backend/src/test/java/com/funeat/review/application/ReviewDeleteEventListenerTest.java new file mode 100644 index 000000000..1349eaa37 --- /dev/null +++ b/backend/src/test/java/com/funeat/review/application/ReviewDeleteEventListenerTest.java @@ -0,0 +1,110 @@ +package com.funeat.review.application; + +import static com.funeat.fixture.CategoryFixture.์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ; +import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ; +import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ; +import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ; +import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test1_ํ‰์ 1์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; +import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test2_ํ‰์ 2์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; +import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; + +import com.funeat.common.EventTest; +import com.funeat.common.ImageUploader; +import com.funeat.common.exception.CommonException.S3DeleteFailException; +import com.funeat.exception.CommonErrorCode; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; + +@SuppressWarnings("NonAsciiCharacters") +class ReviewDeleteEventListenerTest extends EventTest { + + @MockBean + private ImageUploader uploader; + + @Nested + class ๋ฆฌ๋ทฐ_์‚ญ์ œ_์ด๋ฒคํŠธ_๋ฐœํ–‰ { + + @Test + void ๋ฆฌ๋ทฐ_์ž‘์„ฑ์ž๊ฐ€_๋ฆฌ๋ทฐ_์‚ญ์ œ_์‹œ๋„์‹œ_๋ฆฌ๋ทฐ_์‚ญ์ œ_์ด๋ฒคํŠธ๊ฐ€_๋ฐœํ–‰๋œ๋‹ค() { + // given + final var author = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + final var authorId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(author); + + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); + ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var review = reviewRepository.save(๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test1_ํ‰์ 1์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(author, product, 0L)); + + // when + reviewService.deleteReview(review.getId(), authorId); + + // then + final var count = events.stream(ReviewDeleteEvent.class).count(); + assertThat(count).isEqualTo(1); + } + + @Test + void ๋ฆฌ๋ทฐ_์ž‘์„ฑ์ž๊ฐ€_์•„๋‹Œ_์‚ฌ๋žŒ์ด_๋ฆฌ๋ทฐ_์‚ญ์ œ_์‹œ๋„์‹œ_๋ฆฌ๋ทฐ_์‚ญ์ œ_์ด๋ฒคํŠธ๊ฐ€_๋ฐœํ–‰๋˜์ง€_์•Š๋Š”๋‹ค() { + // given + final var author = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + final var authorId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(author); + + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ(); + final var memberId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); + ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var review = reviewRepository.save(๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test2_ํ‰์ 2์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(author, product, 0L)); + + // when + try { + reviewService.deleteReview(review.getId(), memberId); + } catch (Exception ignored) { + } + + // then + final var count = events.stream(ReviewDeleteEvent.class).count(); + assertThat(count).isEqualTo(0); + } + } + + @Nested + class ์ด๋ฏธ์ง€_์‚ญ์ œ_๋กœ์ง_์ž‘๋™ { + + @Test + void ์ด๋ฏธ์ง€_์‚ญ์ œ_๋กœ์ง์ด_์‹คํŒจํ•ด๋„_๋ฉ”์ธ๋กœ์ง๊นŒ์ง€_๋กค๋ฐฑ๋˜์–ด์„œ๋Š”_์•ˆ๋œ๋‹ค() { + // given + final var author = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + final var authorId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(author); + + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); + ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var review = reviewRepository.save(๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(author, product, 0L)); + + doThrow(new S3DeleteFailException(CommonErrorCode.UNKNOWN_SERVER_ERROR_CODE)) + .when(uploader) + .delete(any()); + + // when + reviewService.deleteReview(review.getId(), authorId); + + // then + assertThat(reviewRepository.findById(review.getId())).isEmpty(); + } + } +} diff --git a/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java b/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java index 00c3ed691..93fd0e046 100644 --- a/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java +++ b/backend/src/test/java/com/funeat/review/application/ReviewServiceTest.java @@ -1,18 +1,15 @@ package com.funeat.review.application; +import static com.funeat.fixture.CategoryFixture.์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ; import static com.funeat.fixture.CategoryFixture.์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ; import static com.funeat.fixture.ImageFixture.์ด๋ฏธ์ง€_์ƒ์„ฑ; import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ; import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ; -import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„_๋ฉค๋ฒ„3_์ƒ์„ฑ; -import static com.funeat.fixture.PageFixture.์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ; import static com.funeat.fixture.PageFixture.์ตœ์‹ ์ˆœ; -import static com.funeat.fixture.PageFixture.ํŽ˜์ด์ง€์š”์ฒญ_๊ธฐ๋ณธ_์ƒ์„ฑ; import static com.funeat.fixture.PageFixture.ํŽ˜์ด์ง€์š”์ฒญ_์ƒ์„ฑ; -import static com.funeat.fixture.PageFixture.ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ; -import static com.funeat.fixture.PageFixture.ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ; +import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 4์ _์ƒ์„ฑ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 5์ _์ƒ์„ฑ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 1์ _์ƒ์„ฑ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 3์ _์ƒ์„ฑ; @@ -23,7 +20,12 @@ import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; +import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€์—†์Œ_ํ‰์ 1์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; +import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ์ •๋ ฌ์š”์ฒญ_์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ_์ƒ์„ฑ; +import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ์ •๋ ฌ์š”์ฒญ_์ตœ์‹ ์ˆœ_์ƒ์„ฑ; +import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ์ •๋ ฌ์š”์ฒญ_ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ_์ƒ์„ฑ; +import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ์ •๋ ฌ์š”์ฒญ_ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ_์ƒ์„ฑ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ์ข‹์•„์š”์š”์ฒญ_์ƒ์„ฑ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; import static com.funeat.fixture.TagFixture.ํƒœ๊ทธ_๋ง›์žˆ์–ด์š”_TASTE_์ƒ์„ฑ; @@ -39,9 +41,15 @@ import com.funeat.product.exception.ProductException.ProductNotFoundException; import com.funeat.review.domain.Review; import com.funeat.review.dto.MostFavoriteReviewResponse; +import com.funeat.review.dto.RankingReviewDto; +import com.funeat.review.dto.RankingReviewsResponse; +import com.funeat.review.dto.ReviewDetailResponse; import com.funeat.review.dto.SortingReviewDto; +import com.funeat.review.exception.ReviewException.NotAuthorOfReviewException; import com.funeat.review.exception.ReviewException.ReviewNotFoundException; import com.funeat.tag.domain.Tag; +import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -340,44 +348,36 @@ class sortingReviews_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { @Test void ์ข‹์•„์š”_๊ธฐ์ค€์œผ๋กœ_๋‚ด๋ฆผ์ฐจ์ˆœ_์ •๋ ฌ์„_ํ• _์ˆ˜_์žˆ๋‹ค() { // given - final var member1 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); - final var member2 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ(); - final var member3 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„3_์ƒ์„ฑ(); - ๋ณต์ˆ˜_๋ฉค๋ฒ„_์ €์žฅ(member1, member2, member3); + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + final var memberId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); - final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); final var productId = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); - final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member1, product, 351L); - final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member2, product, 24L); - final var review3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member3, product, 130L); + final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 351L); + final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 24L); + final var review3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 130L); ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1, review2, review3); - final var page = ํŽ˜์ด์ง€์š”์ฒญ_์ƒ์„ฑ(0, 2, ์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ); - final var member1Id = member1.getId(); + final var request = ๋ฆฌ๋ทฐ์ •๋ ฌ์š”์ฒญ_์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ_์ƒ์„ฑ(0L); - final var expected = Stream.of(review1, review3) - .map(review -> SortingReviewDto.toDto(review, member1)) - .collect(Collectors.toList()); + final var expected = List.of(review1.getId(), review3.getId(), review2.getId()); // when - final var actual = reviewService.sortingReviews(productId, page, member1Id).getReviews(); + final var actual = reviewService.sortingReviews(productId, memberId, request).getReviews(); // then - assertThat(actual).usingRecursiveComparison() - .isEqualTo(expected); + assertThat(actual).extracting(SortingReviewDto::getId) + .containsExactlyElementsOf(expected); } @Test - void ํ‰์ _๊ธฐ์ค€์œผ๋กœ_์˜ค๋ฆ„์ฐจ์ˆœ_์ •๋ ฌ์„_ํ• _์ˆ˜_์žˆ๋‹ค() { + void ์ตœ์‹ ์ˆœ์œผ๋กœ_์ •๋ ฌ์„_ํ• _์ˆ˜_์žˆ๋‹ค() throws InterruptedException { // given - final var member1 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); - final var member2 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ(); - final var member3 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„3_์ƒ์„ฑ(); - ๋ณต์ˆ˜_๋ฉค๋ฒ„_์ €์žฅ(member1, member2, member3); + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + final var memberId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); @@ -385,33 +385,30 @@ class sortingReviews_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); final var productId = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); - final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test2_ํ‰์ 2์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member1, product, 351L); - final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member2, product, 24L); - final var review3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member3, product, 130L); + final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 351L); + Thread.sleep(100); + final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 24L); + Thread.sleep(100); + final var review3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 130L); ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1, review2, review3); - final var page = ํŽ˜์ด์ง€์š”์ฒญ_์ƒ์„ฑ(0, 2, ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ); - final var member1Id = member1.getId(); + final var request = ๋ฆฌ๋ทฐ์ •๋ ฌ์š”์ฒญ_์ตœ์‹ ์ˆœ_์ƒ์„ฑ(3L); - final var expected = Stream.of(review1, review3) - .map(review -> SortingReviewDto.toDto(review, member1)) - .collect(Collectors.toList()); + final var expected = List.of(review2.getId(), review1.getId()); // when - final var actual = reviewService.sortingReviews(productId, page, member1Id).getReviews(); + final var actual = reviewService.sortingReviews(productId, memberId, request).getReviews(); // then - assertThat(actual).usingRecursiveComparison() - .isEqualTo(expected); + assertThat(actual).extracting(SortingReviewDto::getId) + .containsExactlyElementsOf(expected); } @Test - void ํ‰์ _๊ธฐ์ค€์œผ๋กœ_๋‚ด๋ฆผ์ฐจ์ˆœ_์ •๋ ฌ์„_ํ• _์ˆ˜_์žˆ๋‹ค() { + void ํ‰์ _๊ธฐ์ค€์œผ๋กœ_์˜ค๋ฆ„์ฐจ์ˆœ_์ •๋ ฌ์„_ํ• _์ˆ˜_์žˆ๋‹ค() { // given - final var member1 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); - final var member2 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ(); - final var member3 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„3_์ƒ์„ฑ(); - ๋ณต์ˆ˜_๋ฉค๋ฒ„_์ €์žฅ(member1, member2, member3); + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + final var memberId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); @@ -419,33 +416,28 @@ class sortingReviews_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); final var productId = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); - final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member1, product, 351L); - final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member2, product, 24L); - final var review3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member3, product, 130L); + final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test2_ํ‰์ 2์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 351L); + final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 24L); + final var review3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 130L); ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1, review2, review3); - final var page = ํŽ˜์ด์ง€์š”์ฒญ_์ƒ์„ฑ(0, 2, ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ); - final var member1Id = member1.getId(); + final var request = ๋ฆฌ๋ทฐ์ •๋ ฌ์š”์ฒญ_ํ‰์ _์˜ค๋ฆ„์ฐจ์ˆœ_์ƒ์„ฑ(0L); - final var expected = Stream.of(review2, review3) - .map(review -> SortingReviewDto.toDto(review, member1)) - .collect(Collectors.toList()); + final var expected = List.of(review1.getId(), review3.getId(), review2.getId()); // when - final var actual = reviewService.sortingReviews(productId, page, member1Id).getReviews(); + final var actual = reviewService.sortingReviews(productId, memberId, request).getReviews(); // then - assertThat(actual).usingRecursiveComparison() - .isEqualTo(expected); + assertThat(actual).extracting(SortingReviewDto::getId) + .containsExactlyElementsOf(expected); } @Test - void ์ตœ์‹ ์ˆœ์œผ๋กœ_์ •๋ ฌ์„_ํ• _์ˆ˜_์žˆ๋‹ค() { + void ํ‰์ _๊ธฐ์ค€์œผ๋กœ_๋‚ด๋ฆผ์ฐจ์ˆœ_์ •๋ ฌ์„_ํ• _์ˆ˜_์žˆ๋‹ค() { // given - final var member1 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); - final var member2 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ(); - final var member3 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„3_์ƒ์„ฑ(); - ๋ณต์ˆ˜_๋ฉค๋ฒ„_์ €์žฅ(member1, member2, member3); + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + final var memberId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); @@ -453,24 +445,21 @@ class sortingReviews_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); final var productId = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); - final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member1, product, 351L); - final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member2, product, 24L); - final var review3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member3, product, 130L); + final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 5L); + final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test2_ํ‰์ 2์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 24L); + final var review3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 13L); ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1, review2, review3); - final var page = ํŽ˜์ด์ง€์š”์ฒญ_์ƒ์„ฑ(0, 2, ์ตœ์‹ ์ˆœ); - final var member1Id = member1.getId(); + final var request = ๋ฆฌ๋ทฐ์ •๋ ฌ์š”์ฒญ_ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ_์ƒ์„ฑ(1L); - final var expected = Stream.of(review3, review2) - .map(review -> SortingReviewDto.toDto(review, member1)) - .collect(Collectors.toList()); + final var expected = List.of(review3.getId(), review2.getId()); // when - final var actual = reviewService.sortingReviews(productId, page, member1Id).getReviews(); + final var actual = reviewService.sortingReviews(productId, memberId, request).getReviews(); // then - assertThat(actual).usingRecursiveComparison() - .isEqualTo(expected); + assertThat(actual).extracting(SortingReviewDto::getId) + .containsExactlyElementsOf(expected); } } @@ -480,10 +469,8 @@ class sortingReviews_์‹คํŒจ_ํ…Œ์ŠคํŠธ { @Test void ์กด์žฌํ•˜์ง€_์•Š๋Š”_๋ฉค๋ฒ„๊ฐ€_์ƒํ’ˆ์—_์žˆ๋Š”_๋ฆฌ๋ทฐ๋“ค์„_์ •๋ ฌํ•˜๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { // given - final var member1 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); - final var member2 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ(); - final var member3 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„3_์ƒ์„ฑ(); - ๋ณต์ˆ˜_๋ฉค๋ฒ„_์ €์žฅ(member1, member2, member3); + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + final var wrongMemberId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member) + 3L; final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); @@ -491,26 +478,23 @@ class sortingReviews_์‹คํŒจ_ํ…Œ์ŠคํŠธ { final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); final var productId = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); - final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member1, product, 351L); - final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member2, product, 24L); - final var review3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member3, product, 130L); + final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 351L); + final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 24L); + final var review3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 130L); ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1, review2, review3); - final var page = ํŽ˜์ด์ง€์š”์ฒญ_๊ธฐ๋ณธ_์ƒ์„ฑ(0, 2); - final var wrongMemberId = member1.getId() + 3L; + final var request = ๋ฆฌ๋ทฐ์ •๋ ฌ์š”์ฒญ_ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ_์ƒ์„ฑ(1L); // when & then - assertThatThrownBy(() -> reviewService.sortingReviews(productId, page, wrongMemberId)) + assertThatThrownBy(() -> reviewService.sortingReviews(productId, wrongMemberId, request)) .isInstanceOf(MemberNotFoundException.class); } @Test void ๋ฉค๋ฒ„๊ฐ€_์กด์žฌํ•˜์ง€_์•Š๋Š”_์ƒํ’ˆ์—_์žˆ๋Š”_๋ฆฌ๋ทฐ๋“ค์„_์ •๋ ฌํ•˜๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { // given - final var member1 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); - final var member2 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ(); - final var member3 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„3_์ƒ์„ฑ(); - ๋ณต์ˆ˜_๋ฉค๋ฒ„_์ €์žฅ(member1, member2, member3); + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + final var memberId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); @@ -518,16 +502,15 @@ class sortingReviews_์‹คํŒจ_ํ…Œ์ŠคํŠธ { final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); final var wrongProductId = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product) + 1L; - final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member1, product, 351L); - final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member2, product, 24L); - final var review3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member3, product, 130L); + final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 351L); + final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 24L); + final var review3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 130L); ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1, review2, review3); - final var page = ํŽ˜์ด์ง€์š”์ฒญ_๊ธฐ๋ณธ_์ƒ์„ฑ(0, 2); - final var member1Id = member1.getId(); + final var request = ๋ฆฌ๋ทฐ์ •๋ ฌ์š”์ฒญ_ํ‰์ _๋‚ด๋ฆผ์ฐจ์ˆœ_์ƒ์„ฑ(1L); // when & then - assertThatThrownBy(() -> reviewService.sortingReviews(wrongProductId, page, member1Id)) + assertThatThrownBy(() -> reviewService.sortingReviews(wrongProductId, memberId, request)) .isInstanceOf(ProductNotFoundException.class); } } @@ -614,7 +597,7 @@ class updateProductImage_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { final var expected = review.getImage(); // when - reviewService.updateProductImage(reviewId); + reviewService.updateProductImage(product.getId()); final var actual = product.getImage(); // then @@ -643,7 +626,7 @@ class updateProductImage_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { final var expected = secondReview.getImage(); // when - reviewService.updateProductImage(secondReviewId); + reviewService.updateProductImage(product.getId()); final var actual = product.getImage(); // then @@ -672,7 +655,7 @@ class updateProductImage_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { final var expected = firstReview.getImage(); // when - reviewService.updateProductImage(secondReviewId); + reviewService.updateProductImage(product.getId()); final var actual = product.getImage(); // then @@ -701,7 +684,7 @@ class updateProductImage_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { final var expected = secondReview.getImage(); // when - reviewService.updateProductImage(secondReviewId); + reviewService.updateProductImage(product.getId()); final var actual = product.getImage(); // then @@ -722,11 +705,11 @@ class updateProductImage_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { final var firstReview = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€์—†์Œ_ํ‰์ 1์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 3L); final var firstReviewId = ๋‹จ์ผ_๋ฆฌ๋ทฐ_์ €์žฅ(firstReview); - reviewService.updateProductImage(firstReviewId); + reviewService.updateProductImage(product.getId()); final var secondReview = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€์—†์Œ_ํ‰์ 1์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 2L); final var secondReviewId = ๋‹จ์ผ_๋ฆฌ๋ทฐ_์ €์žฅ(secondReview); - reviewService.updateProductImage(secondReviewId); + reviewService.updateProductImage(product.getId()); final var thirdReview = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 1L); final var thirdReviewId = ๋‹จ์ผ_๋ฆฌ๋ทฐ_์ €์žฅ(thirdReview); @@ -734,7 +717,7 @@ class updateProductImage_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { final var expected = thirdReview.getImage(); // when - reviewService.updateProductImage(thirdReviewId); + reviewService.updateProductImage(product.getId()); final var actual = product.getImage(); // then @@ -755,7 +738,7 @@ class updateProductImage_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { final var firstReview = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€์—†์Œ_ํ‰์ 1์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 3L); final var firstReviewId = ๋‹จ์ผ_๋ฆฌ๋ทฐ_์ €์žฅ(firstReview); - reviewService.updateProductImage(firstReviewId); + reviewService.updateProductImage(product.getId()); final var secondReview = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€์—†์Œ_ํ‰์ 1์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 2L); final var secondReviewId = ๋‹จ์ผ_๋ฆฌ๋ทฐ_์ €์žฅ(secondReview); @@ -763,7 +746,7 @@ class updateProductImage_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { final var expected = secondReview.getImage(); // when - reviewService.updateProductImage(secondReviewId); + reviewService.updateProductImage(product.getId()); final var actual = product.getImage(); // then @@ -775,7 +758,7 @@ class updateProductImage_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { class updateProductImage_์‹คํŒจ_ํ…Œ์ŠคํŠธ { @Test - void ์กด์žฌํ•˜์ง€_์•Š๋Š”_๋ฆฌ๋ทฐ๋กœ_์ƒํ’ˆ_์—…๋ฐ์ดํŠธ๋ฅผ_์‹œ๋„ํ•˜๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + void ์กด์žฌํ•˜์ง€_์•Š๋Š”_์ƒํ’ˆ์œผ๋กœ_์ƒํ’ˆ_์—…๋ฐ์ดํŠธ๋ฅผ_์‹œ๋„ํ•˜๋ฉด_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { // given final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); @@ -786,13 +769,164 @@ class updateProductImage_์‹คํŒจ_ํ…Œ์ŠคํŠธ { final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); - final var review = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test1_ํ‰์ 1์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 0L); - final var wrongReviewId = ๋‹จ์ผ_๋ฆฌ๋ทฐ_์ €์žฅ(review) + 1L; + final var wrongProductId = 999L; // when & then - assertThatThrownBy(() -> reviewService.updateProductImage(wrongReviewId)) + assertThatThrownBy(() -> reviewService.updateProductImage(wrongProductId)) + .isInstanceOf(ProductNotFoundException.class); + } + } + + @Nested + class deleteReview_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ์ž์‹ ์ด_์ž‘์„ฑํ•œ_๋ฆฌ๋ทฐ๋ฅผ_์‚ญ์ œํ• _์ˆ˜_์žˆ๋‹ค() { + // given + final var author = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + final var authorId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(author); + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ(); + final var memberId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); + final var productId = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var tag1 = ํƒœ๊ทธ_๋ง›์žˆ์–ด์š”_TASTE_์ƒ์„ฑ(); + final var tag2 = ํƒœ๊ทธ_์•„์นจ์‹์‚ฌ_ETC_์ƒ์„ฑ(); + ๋ณต์ˆ˜_ํƒœ๊ทธ_์ €์žฅ(tag1, tag2); + + final var tagIds = ํƒœ๊ทธ_์•„์ด๋””_๋ณ€ํ™˜(tag1, tag2); + final var image = ์ด๋ฏธ์ง€_์ƒ์„ฑ(); + final var reviewCreateRequest1 = ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(2L, tagIds); + final var reviewCreateRequest2 = ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(4L, tagIds); + + reviewService.create(productId, authorId, image, reviewCreateRequest1); + reviewService.create(productId, authorId, image, reviewCreateRequest2); + + final var reviews = reviewRepository.findAll(); + final var rating2_review = reviews.stream() + .filter(it -> it.getRating() == 2L) + .findFirst() + .get(); + + final var favoriteRequest = ๋ฆฌ๋ทฐ์ข‹์•„์š”์š”์ฒญ_์ƒ์„ฑ(true); + reviewService.likeReview(rating2_review.getId(), authorId, favoriteRequest); + reviewService.likeReview(rating2_review.getId(), memberId, favoriteRequest); + + // when + reviewService.deleteReview(rating2_review.getId(), authorId); + + // then + final var tags = reviewTagRepository.findByReview(rating2_review); + final var favorites = reviewFavoriteRepository.findByReview(rating2_review); + final var findReview = reviewRepository.findById(rating2_review.getId()); + final var findProduct = productRepository.findById(productId).get(); + + assertSoftly(soft -> { + soft.assertThat(tags).isEmpty(); + soft.assertThat(favorites).isEmpty(); + soft.assertThat(findReview).isEmpty(); + soft.assertThat(findProduct.getAverageRating()).isEqualTo(4.0); + soft.assertThat(findProduct.getReviewCount()).isEqualTo(1); + }); + } + } + + @Nested + class deleteReview_์‹คํŒจ_ํ…Œ์ŠคํŠธ { + + @Test + void ์กด์žฌํ•˜์ง€_์•Š๋Š”_์‚ฌ์šฉ์ž๊ฐ€_๋ฆฌ๋ทฐ๋ฅผ_์‚ญ์ œํ•˜๋ คํ•˜๋ฉด_์—๋Ÿฌ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + // given + final var author = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + final var authorId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(author); + + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); + final var productId = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var tag1 = ํƒœ๊ทธ_๋ง›์žˆ์–ด์š”_TASTE_์ƒ์„ฑ(); + final var tag2 = ํƒœ๊ทธ_์•„์นจ์‹์‚ฌ_ETC_์ƒ์„ฑ(); + ๋ณต์ˆ˜_ํƒœ๊ทธ_์ €์žฅ(tag1, tag2); + + final var tagIds = ํƒœ๊ทธ_์•„์ด๋””_๋ณ€ํ™˜(tag1, tag2); + final var image = ์ด๋ฏธ์ง€_์ƒ์„ฑ(); + final var reviewCreateRequest = ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(4L, tagIds); + reviewService.create(productId, authorId, image, reviewCreateRequest); + + final var review = reviewRepository.findAll().get(0); + final var reviewId = review.getId(); + + final var wrongMemberId = 999L; + + // when & then + assertThatThrownBy(() -> reviewService.deleteReview(reviewId, wrongMemberId)) + .isInstanceOf(MemberNotFoundException.class); + } + + @Test + void ์กด์žฌํ•˜์ง€_์•Š๋Š”_๋ฆฌ๋ทฐ๋ฅผ_์‚ญ์ œํ•˜๋ คํ•˜๋ฉด_์—๋Ÿฌ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + // given + final var author = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + final var authorId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(author); + + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); + final var productId = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var tag1 = ํƒœ๊ทธ_๋ง›์žˆ์–ด์š”_TASTE_์ƒ์„ฑ(); + final var tag2 = ํƒœ๊ทธ_์•„์นจ์‹์‚ฌ_ETC_์ƒ์„ฑ(); + ๋ณต์ˆ˜_ํƒœ๊ทธ_์ €์žฅ(tag1, tag2); + + final var tagIds = ํƒœ๊ทธ_์•„์ด๋””_๋ณ€ํ™˜(tag1, tag2); + final var image = ์ด๋ฏธ์ง€_์ƒ์„ฑ(); + final var reviewCreateRequest = ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(4L, tagIds); + reviewService.create(productId, authorId, image, reviewCreateRequest); + + final var wrongReviewId = 999L; + + // when & then + assertThatThrownBy(() -> reviewService.deleteReview(wrongReviewId, authorId)) .isInstanceOf(ReviewNotFoundException.class); } + + @Test + void ์ž์‹ ์ด_์ž‘์„ฑํ•˜์ง€_์•Š์€_๋ฆฌ๋ทฐ๋ฅผ_์‚ญ์ œํ•˜๋ คํ•˜๋ฉด_์—๋Ÿฌ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + // given + final var author = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + final var authorId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(author); + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ(); + final var memberId = ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); + final var productId = ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var tag1 = ํƒœ๊ทธ_๋ง›์žˆ์–ด์š”_TASTE_์ƒ์„ฑ(); + final var tag2 = ํƒœ๊ทธ_์•„์นจ์‹์‚ฌ_ETC_์ƒ์„ฑ(); + ๋ณต์ˆ˜_ํƒœ๊ทธ_์ €์žฅ(tag1, tag2); + + final var tagIds = ํƒœ๊ทธ_์•„์ด๋””_๋ณ€ํ™˜(tag1, tag2); + final var image = ์ด๋ฏธ์ง€_์ƒ์„ฑ(); + final var reviewCreateRequest = ๋ฆฌ๋ทฐ์ถ”๊ฐ€์š”์ฒญ_์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(4L, tagIds); + reviewService.create(productId, authorId, image, reviewCreateRequest); + + final var review = reviewRepository.findAll().get(0); + final var reviewId = review.getId(); + + // when & then + assertThatThrownBy(() -> reviewService.deleteReview(reviewId, memberId)) + .isInstanceOf(NotAuthorOfReviewException.class); + } } @Nested @@ -890,6 +1024,198 @@ class getMostFavoriteReview_์‹คํŒจ_ํ…Œ์ŠคํŠธ { } } + @Nested + class getReviewDetail_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ๋ฆฌ๋ทฐ_์ƒ์„ธ_์ •๋ณด๋ฅผ_์กฐํšŒํ•œ๋‹ค() { + // given + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 5์ _์ƒ์„ฑ(category); + ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var review = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test2_ํ‰์ 2์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 0L); + ๋‹จ์ผ_๋ฆฌ๋ทฐ_์ €์žฅ(review); + + // when + final var actual = reviewService.getReviewDetail(review.getId()); + + // then + final var expected = ReviewDetailResponse.toResponse(review); + + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + } + + @Nested + class getReviewDetail_์‹คํŒจ_ํ…Œ์ŠคํŠธ { + + @Test + void ์กด์žฌํ•˜์ง€_์•Š๋Š”_๋ฆฌ๋ทฐ๋ฅผ_์กฐํšŒํ• ๋•Œ_์˜ˆ์™ธ๊ฐ€_๋ฐœ์ƒํ•œ๋‹ค() { + // given + final var notExistReviewId = 999999L; + + // when & then + assertThatThrownBy(() -> reviewService.getReviewDetail(notExistReviewId)) + .isInstanceOf(ReviewNotFoundException.class); + } + } + + @Nested + class getTopReviews_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Nested + class ๋ฆฌ๋ทฐ_๊ฐœ์ˆ˜์—_๋Œ€ํ•œ_ํ…Œ์ŠคํŠธ { + + @Test + void ์ „์ฒด_๋ฆฌ๋ทฐ๊ฐ€_ํ•˜๋‚˜๋„_์—†์–ด๋„_๋ฐ˜ํ™˜๊ฐ’์€_์žˆ์–ด์•ผํ•œ๋‹ค() { + // given + final var expected = RankingReviewsResponse.toResponse(Collections.emptyList()); + + // when + final var actual = reviewService.getTopReviews(); + + // then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + void ์ „์ฒด_๋ฆฌ๋ทฐ๊ฐ€_1๊ฐœ_์ด์ƒ_3๊ฐœ_๋ฏธ๋งŒ์ด๋ผ๋„_๋ฆฌ๋ทฐ๊ฐ€_๋‚˜์™€์•ผํ•œ๋‹ค() { + // given + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 4์ _์ƒ์„ฑ(category); + ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var now = LocalDateTime.now(); + final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 2L, now.minusDays(1L)); + final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 2L, now); + ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1, review2); + + final var rankingReviewDto1 = RankingReviewDto.toDto(review1); + final var rankingReviewDto2 = RankingReviewDto.toDto(review2); + final var rankingReviewDtos = List.of(rankingReviewDto2, rankingReviewDto1); + final var expected = RankingReviewsResponse.toResponse(rankingReviewDtos); + + // when + final var actual = reviewService.getTopReviews(); + + // then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + void ์ „์ฒด_๋ฆฌ๋ทฐ_์ค‘_๋žญํ‚น์ด_๋†’์€_์ƒ์œ„_3๊ฐœ_๋ฆฌ๋ทฐ๋ฅผ_๊ตฌํ• _์ˆ˜_์žˆ๋‹ค() { + // given + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 4์ _์ƒ์„ฑ(category); + ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var now = LocalDateTime.now(); + final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 4L, now.minusDays(3L)); + final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 6L, now.minusDays(2L)); + final var review3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 4L, now); + final var review4 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 5L, now); + ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1, review2, review3, review4); + + final var rankingReviewDto1 = RankingReviewDto.toDto(review1); + final var rankingReviewDto2 = RankingReviewDto.toDto(review2); + final var rankingReviewDto3 = RankingReviewDto.toDto(review3); + final var rankingReviewDto4 = RankingReviewDto.toDto(review4); + final var rankingReviewDtos = List.of(rankingReviewDto4, rankingReviewDto3, rankingReviewDto2); + final var expected = RankingReviewsResponse.toResponse(rankingReviewDtos); + + // when + final var actual = reviewService.getTopReviews(); + + // then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + } + + @Nested + class ๋ฆฌ๋ทฐ_๋žญํ‚น_์ ์ˆ˜์—_๋Œ€ํ•œ_ํ…Œ์ŠคํŠธ { + + @Test + void ๋ฆฌ๋ทฐ_์ข‹์•„์š”_์ˆ˜๊ฐ€_๊ฐ™์œผ๋ฉด_์ตœ๊ทผ_์ƒ์„ฑ๋œ_๋ฆฌ๋ทฐ์˜_๋žญํ‚น์„_๋”_๋†’๊ฒŒ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + // given + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 4์ _์ƒ์„ฑ(category); + ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var now = LocalDateTime.now(); + final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 10L, now.minusDays(9L)); + final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 10L, now.minusDays(4L)); + ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1, review2); + + final var rankingReviewDto1 = RankingReviewDto.toDto(review1); + final var rankingReviewDto2 = RankingReviewDto.toDto(review2); + final var rankingReviewDtos = List.of(rankingReviewDto2, rankingReviewDto1); + final var expected = RankingReviewsResponse.toResponse(rankingReviewDtos); + + // when + final var actual = reviewService.getTopReviews(); + + // then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + void ๋ฆฌ๋ทฐ_์ƒ์„ฑ_์ผ์ž๊ฐ€_๊ฐ™์œผ๋ฉด_์ข‹์•„์š”_์ˆ˜๊ฐ€_๋งŽ์€_๋ฆฌ๋ทฐ์˜_๋žญํ‚น์„_๋”_๋†’๊ฒŒ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + // given + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 4์ _์ƒ์„ฑ(category); + ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var now = LocalDateTime.now(); + final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 2L, now.minusDays(1L)); + final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 4L, now.minusDays(1L)); + ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1, review2); + + final var rankingReviewDto1 = RankingReviewDto.toDto(review1); + final var rankingReviewDto2 = RankingReviewDto.toDto(review2); + final var rankingReviewDtos = List.of(rankingReviewDto2, rankingReviewDto1); + final var expected = RankingReviewsResponse.toResponse(rankingReviewDtos); + + // when + final var actual = reviewService.getTopReviews(); + + // then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + } + } + private List ํƒœ๊ทธ_์•„์ด๋””_๋ณ€ํ™˜(final Tag... tags) { return Stream.of(tags) .map(Tag::getId) diff --git a/backend/src/test/java/com/funeat/review/domain/ReviewTest.java b/backend/src/test/java/com/funeat/review/domain/ReviewTest.java new file mode 100644 index 000000000..a9b02876b --- /dev/null +++ b/backend/src/test/java/com/funeat/review/domain/ReviewTest.java @@ -0,0 +1,40 @@ +package com.funeat.review.domain; + +import static com.funeat.fixture.CategoryFixture.์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ; +import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ; +import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 1์ _์ƒ์„ฑ; +import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ; +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDateTime; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ReviewTest { + + @Nested + class calculateRankingScore_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ๋ฆฌ๋ทฐ_์ข‹์•„์š”_์ˆ˜์™€_๋ฆฌ๋ทฐ_์ƒ์„ฑ_์‹œ๊ฐ„์œผ๋กœ_ํ•ด๋‹น_๋ฆฌ๋ทฐ์˜_๋žญํ‚น_์ ์ˆ˜๋ฅผ_๊ตฌํ• _์ˆ˜_์žˆ๋‹ค() { + // given + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 1์ _์ƒ์„ฑ(category); + final var favoriteCount = 4L; + final var review = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, favoriteCount, LocalDateTime.now().minusDays(1L)); + + final var expected = favoriteCount / Math.pow(2.0, 0.5); + + // when + final var actual = review.calculateRankingScore(); + + // then + assertThat(actual).isEqualTo(expected); + } + } +} diff --git a/backend/src/test/java/com/funeat/review/persistence/ReviewRepositoryTest.java b/backend/src/test/java/com/funeat/review/persistence/ReviewRepositoryTest.java index d0198db1a..46e4645f4 100644 --- a/backend/src/test/java/com/funeat/review/persistence/ReviewRepositoryTest.java +++ b/backend/src/test/java/com/funeat/review/persistence/ReviewRepositoryTest.java @@ -5,21 +5,18 @@ import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ; import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ; import static com.funeat.fixture.MemberFixture.๋ฉค๋ฒ„_๋ฉค๋ฒ„3_์ƒ์„ฑ; -import static com.funeat.fixture.PageFixture.์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ; -import static com.funeat.fixture.PageFixture.ํŽ˜์ด์ง€์š”์ฒญ_์ƒ์„ฑ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 1์ _์ƒ์„ฑ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ; import static com.funeat.fixture.ProductFixture.์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 3์ _์ƒ์„ฑ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test1_ํ‰์ 1์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; -import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test1_ํ‰์ 1์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ; import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; -import static com.funeat.fixture.ReviewFixture.๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; import com.funeat.common.RepositoryTest; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -66,77 +63,6 @@ class countByProduct_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { } } - @Nested - class findReviewsByProduct_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { - - @Test - void ํŠน์ •_์ƒํ’ˆ์—_๋Œ€ํ•œ_์ข‹์•„์š”_๊ธฐ์ค€_๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ_์ •๋ ฌํ•œ๋‹ค() { - // given - final var member1 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); - final var member2 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ(); - final var member3 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„3_์ƒ์„ฑ(); - ๋ณต์ˆ˜_๋ฉค๋ฒ„_์ €์žฅ(member1, member2, member3); - - final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); - ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); - - final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); - ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); - - final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member1, product, 351L); - final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member2, product, 24L); - final var review3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member3, product, 130L); - ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1, review2, review3); - - final var page = ํŽ˜์ด์ง€์š”์ฒญ_์ƒ์„ฑ(0, 2, ์ข‹์•„์š”์ˆ˜_๋‚ด๋ฆผ์ฐจ์ˆœ); - - final var expected = List.of(review1, review3); - - // when - final var actual = reviewRepository.findReviewsByProduct(page, product).getContent(); - - // then - assertThat(actual).usingRecursiveComparison() - .isEqualTo(expected); - } - } - - @Nested - class findTop3ByOrderByFavoriteCountDesc_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { - - @Test - void ์ „์ฒด_๋ฆฌ๋ทฐ_๋ชฉ๋ก์—์„œ_๊ฐ€์žฅ_์ข‹์•„์š”๊ฐ€_๋†’์€_์ƒ์œ„_3๊ฐœ์˜_๋ฆฌ๋ทฐ๋ฅผ_๊ฐ€์ ธ์˜จ๋‹ค() { - // given - final var member1 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); - final var member2 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„2_์ƒ์„ฑ(); - final var member3 = ๋ฉค๋ฒ„_๋ฉค๋ฒ„3_์ƒ์„ฑ(); - ๋ณต์ˆ˜_๋ฉค๋ฒ„_์ €์žฅ(member1, member2, member3); - - final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); - ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); - - final var product1 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); - final var product2 = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ2000์›_ํ‰์ 3์ _์ƒ์„ฑ(category); - ๋ณต์ˆ˜_์ƒํ’ˆ_์ €์žฅ(product1, product2); - - final var review1_1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member1, product1, 5L); - final var review1_2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member2, product1, 351L); - final var review1_3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member3, product1, 130L); - final var review2_2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member2, product2, 247L); - final var review3_2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test1_ํ‰์ 1์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member3, product2, 83L); - ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1_1, review1_2, review1_3, review2_2, review3_2); - - final var expected = List.of(review1_2, review2_2, review1_3); - - // when - final var actual = reviewRepository.findTop3ByOrderByFavoriteCountDesc(); - - // then - assertThat(actual).usingRecursiveComparison() - .isEqualTo(expected); - } - } - @Nested class findPopularReviewWithImage_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { @@ -235,4 +161,61 @@ class findTopByProductOrderByFavoriteCountDescIdDesc_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { assertThat(actual.get()).isEqualTo(review2); } } + + @Nested + class findReviewsByFavoriteCountGreaterThanEqual_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ํŠน์ •_์ข‹์•„์š”_์ˆ˜_์ด์ƒ์ธ_๋ชจ๋“ _๋ฆฌ๋ทฐ๋“ค์„_์กฐํšŒํ•œ๋‹ค() { + // given + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); + ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 1L); + final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 0L); + final var review3 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 100L); + ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1, review2, review3); + + final var expected = List.of(review1, review3); + + // when + final var actual = reviewRepository.findReviewsByFavoriteCountGreaterThanEqual(1L); + + // then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + void ํŠน์ •_์ข‹์•„์š”_์ˆ˜_์ด์ƒ์ธ_๋ฆฌ๋ทฐ๊ฐ€_์—†์œผ๋ฉด_๋นˆ_๋ฆฌ์ŠคํŠธ๋ฅผ_๋ฐ˜ํ™˜ํ•œ๋‹ค() { + // given + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_๊ฐ„ํŽธ์‹์‚ฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ1000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); + ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 0L); + final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test4_ํ‰์ 4์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 0L); + ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1, review2); + + final var expected = Collections.emptyList(); + + // when + final var actual = reviewRepository.findReviewsByFavoriteCountGreaterThanEqual(1L); + + // then + assertThat(actual).usingRecursiveComparison() + .isEqualTo(expected); + } + } } diff --git a/backend/src/test/java/com/funeat/review/persistence/ReviewTagRepositoryTest.java b/backend/src/test/java/com/funeat/review/persistence/ReviewTagRepositoryTest.java index baab6abdb..4f5bbf152 100644 --- a/backend/src/test/java/com/funeat/review/persistence/ReviewTagRepositoryTest.java +++ b/backend/src/test/java/com/funeat/review/persistence/ReviewTagRepositoryTest.java @@ -72,6 +72,80 @@ class findTop3TagsByReviewIn_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { } } + @Nested + class deleteByReview_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ํ•ด๋‹น_๋ฆฌ๋ทฐ์—_๋‹ฌ๋ฆฐ_ํƒœ๊ทธ๋ฅผ_์‚ญ์ œํ• _์ˆ˜_์žˆ๋‹ค() { + // given + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ3000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); + ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var tag1 = ํƒœ๊ทธ_๋ง›์žˆ์–ด์š”_TASTE_์ƒ์„ฑ(); + ๋‹จ์ผ_ํƒœ๊ทธ_์ €์žฅ(tag1); + + final var review1 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 0L); + final var review2 = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test3_ํ‰์ 3์ _์žฌ๊ตฌ๋งคX_์ƒ์„ฑ(member, product, 0L); + ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_์ €์žฅ(review1, review2); + + final var reviewTag1_1 = ๋ฆฌ๋ทฐ_ํƒœ๊ทธ_์ƒ์„ฑ(review1, tag1); + final var reviewTag2_1 = ๋ฆฌ๋ทฐ_ํƒœ๊ทธ_์ƒ์„ฑ(review2, tag1); + ๋ณต์ˆ˜_๋ฆฌ๋ทฐ_ํƒœ๊ทธ_์ €์žฅ(reviewTag1_1, reviewTag2_1); + + final var expected = List.of(reviewTag2_1); + + // when + reviewTagRepository.deleteByReview(review1); + + // then + final var remainings = reviewTagRepository.findAll(); + assertThat(remainings).usingRecursiveComparison() + .isEqualTo(expected); + } + } + + @Nested + class findByReview_์„ฑ๊ณต_ํ…Œ์ŠคํŠธ { + + @Test + void ํ•ด๋‹น_๋ฆฌ๋ทฐ์—_๋‹ฌ๋ฆฐ_ํƒœ๊ทธ๋ฅผ_ํ™•์ธํ• _์ˆ˜_์žˆ๋‹ค() { + // given + final var member = ๋ฉค๋ฒ„_๋ฉค๋ฒ„1_์ƒ์„ฑ(); + ๋‹จ์ผ_๋ฉค๋ฒ„_์ €์žฅ(member); + + final var category = ์นดํ…Œ๊ณ ๋ฆฌ_์ฆ‰์„์กฐ๋ฆฌ_์ƒ์„ฑ(); + ๋‹จ์ผ_์นดํ…Œ๊ณ ๋ฆฌ_์ €์žฅ(category); + + final var product = ์ƒํ’ˆ_์‚ผ๊ฐ๊น€๋ฐฅ_๊ฐ€๊ฒฉ3000์›_ํ‰์ 2์ _์ƒ์„ฑ(category); + ๋‹จ์ผ_์ƒํ’ˆ_์ €์žฅ(product); + + final var tag1 = ํƒœ๊ทธ_๋ง›์žˆ์–ด์š”_TASTE_์ƒ์„ฑ(); + ๋‹จ์ผ_ํƒœ๊ทธ_์ €์žฅ(tag1); + + final var review = ๋ฆฌ๋ทฐ_์ด๋ฏธ์ง€test5_ํ‰์ 5์ _์žฌ๊ตฌ๋งคO_์ƒ์„ฑ(member, product, 0L); + ๋‹จ์ผ_๋ฆฌ๋ทฐ_์ €์žฅ(review); + + final var reviewTag = ๋ฆฌ๋ทฐ_ํƒœ๊ทธ_์ƒ์„ฑ(review, tag1); + ๋‹จ์ผ_๋ฆฌ๋ทฐ_ํƒœ๊ทธ_์ €์žฅ(reviewTag); + + final var expected = List.of(reviewTag); + + // when + final var actual = reviewTagRepository.findByReview(review); + + // then + assertThat(actual).usingRecursiveComparison() + .ignoringExpectedNullFields() + .isEqualTo(expected); + } + } + private ReviewTag ๋ฆฌ๋ทฐ_ํƒœ๊ทธ_์ƒ์„ฑ(final Review review, final Tag tag) { return ReviewTag.createReviewTag(review, tag); } diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index 44ef68eb7..5d2dd0387 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -30,7 +30,16 @@ cloud: bucket: testBucket folder: testFolder cloudfrontPath: testCloudfrontPath + image: + food: foodimage + store: storeimage back-office: id: test key: test + +server: + servlet: + session: + cookie: + name: SESSION diff --git a/frontend/.storybook/preview-body.html b/frontend/.storybook/preview-body.html index c74febeca..a37b26cbd 100644 --- a/frontend/.storybook/preview-body.html +++ b/frontend/.storybook/preview-body.html @@ -100,6 +100,30 @@ d="M3 4V1h2v3h3v2H5v3H3V6H0V4m6 6V7h3V4h7l1.8 2H21c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V10m10 9c4.45 0 6.69-5.38 3.54-8.54C13.39 7.31 8 9.55 8 14c0 2.76 2.24 5 5 5m-3.2-5c0 2.85 3.45 4.28 5.46 2.26c2.02-2.01.59-5.46-2.26-5.46A3.21 3.21 0 0 0 9.8 14Z" /> + + + + + + + + + + + + + + +
+ diff --git a/frontend/package.json b/frontend/package.json index 51619c2d1..335158204 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,9 +13,10 @@ "test:coverage": "jest --watchAll --coverage" }, "dependencies": { - "@fun-eat/design-system": "^0.3.13", + "@fun-eat/design-system": "^0.4.1", "@tanstack/react-query": "^4.32.6", "@tanstack/react-query-devtools": "^4.32.6", + "browser-image-compression": "^2.0.2", "dayjs": "^1.11.9", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/frontend/public/index.html b/frontend/public/index.html index 0359ca16d..36782ea87 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -27,6 +27,7 @@
+
diff --git a/frontend/src/apis/index.ts b/frontend/src/apis/index.ts index 73d9e4928..c258c1615 100644 --- a/frontend/src/apis/index.ts +++ b/frontend/src/apis/index.ts @@ -9,3 +9,5 @@ export const memberApi = new ApiClient('/members'); export const recipeApi = new ApiClient('/recipes'); export const searchApi = new ApiClient('/search'); export const logoutApi = new ApiClient('/logout'); +export const reviewApi = new ApiClient('/reviews'); +export const bannerApi = new ApiClient('/banners'); diff --git a/frontend/src/components/Common/Banner/Banner.tsx b/frontend/src/components/Common/Banner/Banner.tsx new file mode 100644 index 000000000..89af08c6c --- /dev/null +++ b/frontend/src/components/Common/Banner/Banner.tsx @@ -0,0 +1,26 @@ +import { Link } from '@fun-eat/design-system'; +import styled from 'styled-components'; + +import { useBannerQuery } from '@/hooks/queries/banner'; + +const Banner = () => { + const { data: banners } = useBannerQuery(); + const { link, image } = banners[Math.floor(Math.random() * banners.length)]; + + if (!link) { + return ; + } + + return ( + + + + ); +}; + +export default Banner; + +const BannerImage = styled.img` + width: 100%; + height: auto; +`; diff --git a/frontend/src/components/Common/Carousel/Carousel.stories.tsx b/frontend/src/components/Common/Carousel/Carousel.stories.tsx deleted file mode 100644 index 8d7eccf6f..000000000 --- a/frontend/src/components/Common/Carousel/Carousel.stories.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import Carousel from './Carousel'; - -import { RecipeItem } from '@/components/Recipe'; -import mockRecipe from '@/mocks/data/recipes.json'; - -const meta: Meta = { - title: 'common/Carousel', - component: Carousel, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - carouselList: [ - { - id: 0, - children:
1
, - }, - { - id: 1, - children:
2
, - }, - { - id: 2, - children:
3
, - }, - ], - }, -}; - -export const RecipeRanking: Story = { - args: { - carouselList: [ - { - id: 0, - children: , - }, - { - id: 1, - children: , - }, - { - id: 2, - children: , - }, - ], - }, -}; diff --git a/frontend/src/components/Common/Carousel/Carousel.tsx b/frontend/src/components/Common/Carousel/Carousel.tsx deleted file mode 100644 index 038b5cefa..000000000 --- a/frontend/src/components/Common/Carousel/Carousel.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useEffect, useState } from 'react'; -import styled from 'styled-components'; - -import type { CarouselChildren } from '@/types/common'; - -interface CarouselProps { - carouselList: CarouselChildren[]; -} - -const Carousel = ({ carouselList }: CarouselProps) => { - const extendedCarouselList = [...carouselList, carouselList[0]]; - const [currentIndex, setCurrentIndex] = useState(0); - - const CAROUSEL_WIDTH = window.innerWidth; - - const showNextSlide = () => { - setCurrentIndex((prev) => (prev === carouselList.length ? 0 : prev + 1)); - }; - - useEffect(() => { - const timer = setInterval(showNextSlide, 2000); - - return () => clearInterval(timer); - }, [currentIndex]); - - return ( - - - {extendedCarouselList.map(({ id, children }, index) => ( - - {children} - - ))} - - - ); -}; - -export default Carousel; - -const CarouselContainer = styled.div` - display: flex; - width: 100%; - border: 1px solid ${({ theme }) => theme.colors.gray2}; - border-radius: 10px; - overflow: hidden; -`; - -const CarouselWrapper = styled.ul` - display: flex; -`; - -const CarouselItem = styled.li` - height: fit-content; -`; diff --git a/frontend/src/components/Common/ImageUploader/ImageUploader.tsx b/frontend/src/components/Common/ImageUploader/ImageUploader.tsx index bd85c20e3..b139a1b4c 100644 --- a/frontend/src/components/Common/ImageUploader/ImageUploader.tsx +++ b/frontend/src/components/Common/ImageUploader/ImageUploader.tsx @@ -1,4 +1,4 @@ -import { Button } from '@fun-eat/design-system'; +import { Button, useToastActionContext } from '@fun-eat/design-system'; import type { ChangeEventHandler } from 'react'; import styled from 'styled-components'; @@ -13,6 +13,7 @@ interface ReviewImageUploaderProps { const ImageUploader = ({ previewImage, uploadImage, deleteImage }: ReviewImageUploaderProps) => { const { inputRef, handleKeydown } = useEnterKeyDown(); + const { toast } = useToastActionContext(); const handleImageUpload: ChangeEventHandler = (event) => { if (!event.target.files) { @@ -22,7 +23,7 @@ const ImageUploader = ({ previewImage, uploadImage, deleteImage }: ReviewImageUp const imageFile = event.target.files[0]; if (imageFile.size > IMAGE_MAX_SIZE) { - alert('์ด๋ฏธ์ง€ ํฌ๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ์ปค์š”. 5MB ์ดํ•˜์˜ ์ด๋ฏธ์ง€๋ฅผ ๊ณจ๋ผ์ฃผ์„ธ์š”.'); + toast.error('์ด๋ฏธ์ง€ ํฌ๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ์ปค์š”. 5MB ์ดํ•˜์˜ ์ด๋ฏธ์ง€๋ฅผ ๊ณจ๋ผ์ฃผ์„ธ์š”.'); event.target.value = ''; return; } diff --git a/frontend/src/components/Common/Input/Input.stories.tsx b/frontend/src/components/Common/Input/Input.stories.tsx deleted file mode 100644 index 48e8856a3..000000000 --- a/frontend/src/components/Common/Input/Input.stories.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import Input from './Input'; -import SvgIcon from '../Svg/SvgIcon'; - -const meta: Meta = { - title: 'common/Input', - component: Input, - argTypes: { - rightIcon: { - control: { type: 'boolean' }, - mapping: { false: '', true: }, - }, - }, - args: { - customWidth: '300px', - isError: false, - rightIcon: false, - errorMessage: '', - }, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = {}; - -export const WithPlaceholder: Story = { - args: { - placeholder: '์ƒํ’ˆ ์ด๋ฆ„์„ ๊ฒ€์ƒ‰ํ•˜์„ธ์š”.', - }, -}; - -export const WithIcon: Story = { - args: { - placeholder: '์ƒํ’ˆ ์ด๋ฆ„์„ ๊ฒ€์ƒ‰ํ•˜์„ธ์š”.', - rightIcon: true, - }, -}; - -export const Error: Story = { - args: { - isError: true, - errorMessage: '10๊ธ€์ž ์ด๋‚ด๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.', - }, -}; - -export const Disabled: Story = { - render: () => , -}; diff --git a/frontend/src/components/Common/Input/Input.tsx b/frontend/src/components/Common/Input/Input.tsx deleted file mode 100644 index 59f86743e..000000000 --- a/frontend/src/components/Common/Input/Input.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Text, theme } from '@fun-eat/design-system'; -import type { ComponentPropsWithRef, ForwardedRef, ReactNode } from 'react'; -import { forwardRef } from 'react'; -import styled from 'styled-components'; - -interface InputProps extends ComponentPropsWithRef<'input'> { - /** - * Input ์ปดํฌ๋„ŒํŠธ์˜ ๋„ˆ๋น„๊ฐ’์ž…๋‹ˆ๋‹ค. - */ - customWidth?: string; - /** - * Input value์— ์—๋Ÿฌ๊ฐ€ ์žˆ๋Š”์ง€ ์—ฌ๋ถ€์ž…๋‹ˆ๋‹ค. - */ - isError?: boolean; - /** - * Input ์ปดํฌ๋„ŒํŠธ ์˜ค๋ฅธ์ชฝ์— ์œ„์น˜ํ•  ์•„์ด์ฝ˜์ž…๋‹ˆ๋‹ค. - */ - rightIcon?: ReactNode; - /** - * isError๊ฐ€ true์ผ ๋•Œ ๋ณด์—ฌ์ค„ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€์ž…๋‹ˆ๋‹ค. - */ - errorMessage?: string; -} - -const Input = forwardRef( - ( - { customWidth = '300px', isError = false, rightIcon, errorMessage, ...props }: InputProps, - ref: ForwardedRef - ) => { - return ( - <> - - - {rightIcon && {rightIcon}} - - {isError && {errorMessage}} - - ); - } -); - -Input.displayName = 'Input'; - -export default Input; - -type InputContainerStyleProps = Pick; -type CustomInputStyleProps = Pick; - -const InputContainer = styled.div` - position: relative; - max-width: ${({ customWidth }) => customWidth}; - text-align: center; -`; - -const CustomInput = styled.input` - width: 100%; - height: 40px; - padding: 10px 0 10px 12px; - color: ${({ isError }) => (isError ? theme.colors.error : theme.textColors.default)}; - border: 1px solid ${({ isError }) => (isError ? theme.colors.error : theme.borderColors.default)}; - border-radius: 5px; - - &:focus { - border: 2px solid ${({ isError }) => (isError ? theme.colors.error : theme.borderColors.strong)}; - outline: none; - } - - &:disabled { - border: 1px solid ${({ theme }) => theme.borderColors.disabled}; - background: ${({ theme }) => theme.colors.gray1}; - } - - &::placeholder { - color: ${theme.textColors.disabled}; - font-size: ${theme.fontSizes.sm}; - } -`; - -const IconWrapper = styled.div` - position: absolute; - top: 0; - right: 0; - display: flex; - align-items: center; - height: 100%; - margin-right: 8px; -`; - -const ErrorMessage = styled(Text)` - color: ${theme.colors.error}; - font-size: ${theme.fontSizes.xs}; -`; diff --git a/frontend/src/components/Common/Loading/Loading.tsx b/frontend/src/components/Common/Loading/Loading.tsx index 4ef7d374e..7c58614ae 100644 --- a/frontend/src/components/Common/Loading/Loading.tsx +++ b/frontend/src/components/Common/Loading/Loading.tsx @@ -40,7 +40,7 @@ const rotate = keyframes` } 100% { - transform: rotate(-360deg); + transform: rotate(360deg); } `; diff --git a/frontend/src/components/Common/SectionTitle/SectionTitle.stories.tsx b/frontend/src/components/Common/SectionTitle/SectionTitle.stories.tsx index edec003ca..05883aea3 100644 --- a/frontend/src/components/Common/SectionTitle/SectionTitle.stories.tsx +++ b/frontend/src/components/Common/SectionTitle/SectionTitle.stories.tsx @@ -13,13 +13,11 @@ type Story = StoryObj; export const Default: Story = { args: { name: '์‚ฌ์ด๋‹ค', - bookmark: false, }, }; export const Bookmarked: Story = { args: { name: '์‚ฌ์ด๋‹ค', - bookmark: true, }, }; diff --git a/frontend/src/components/Common/SectionTitle/SectionTitle.tsx b/frontend/src/components/Common/SectionTitle/SectionTitle.tsx index 106c60759..c9d649ddb 100644 --- a/frontend/src/components/Common/SectionTitle/SectionTitle.tsx +++ b/frontend/src/components/Common/SectionTitle/SectionTitle.tsx @@ -1,4 +1,5 @@ -import { Button, Heading, theme } from '@fun-eat/design-system'; +import { Button, Heading, Link, theme } from '@fun-eat/design-system'; +import { Link as RouterLink } from 'react-router-dom'; import styled from 'styled-components'; import { SvgIcon } from '@/components/Common'; @@ -6,10 +7,10 @@ import { useRoutePage } from '@/hooks/common'; interface SectionTitleProps { name: string; - bookmark?: boolean; + link?: string; } -const SectionTitle = ({ name, bookmark = false }: SectionTitleProps) => { +const SectionTitle = ({ name, link }: SectionTitleProps) => { const { routeBack } = useRoutePage(); return ( @@ -18,18 +19,15 @@ const SectionTitle = ({ name, bookmark = false }: SectionTitleProps) => { - - {name} - + {link ? ( + + {name} + + ) : ( + {name} + )} + {link && } - {bookmark && ( - - )} ); }; @@ -45,9 +43,12 @@ const SectionTitleContainer = styled.div` const SectionTitleWrapper = styled.div` display: flex; align-items: center; - column-gap: 16px; svg { padding-top: 2px; } `; + +const ProductName = styled(Heading)` + margin: 0 5px 0 16px; +`; diff --git a/frontend/src/components/Common/Skeleton/Skeleton.stories.tsx b/frontend/src/components/Common/Skeleton/Skeleton.stories.tsx deleted file mode 100644 index e8952c316..000000000 --- a/frontend/src/components/Common/Skeleton/Skeleton.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import Skeleton from './Skeleton'; - -const meta: Meta = { - title: 'common/Skeleton', - component: Skeleton, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - width: 100, - height: 100, - }, -}; diff --git a/frontend/src/components/Common/Skeleton/Skeleton.tsx b/frontend/src/components/Common/Skeleton/Skeleton.tsx deleted file mode 100644 index 857d03079..000000000 --- a/frontend/src/components/Common/Skeleton/Skeleton.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import type { ComponentPropsWithoutRef } from 'react'; -import styled from 'styled-components'; - -interface SkeletonProps extends ComponentPropsWithoutRef<'div'> { - width?: string | number; - height?: string | number; -} - -const Skeleton = ({ width, height }: SkeletonProps) => { - return ; -}; - -export default Skeleton; - -export const SkeletonContainer = styled.div` - position: absolute; - width: ${({ width }) => (typeof width === 'number' ? width + 'px' : width)}; - height: ${({ height }) => (typeof height === 'number' ? height + 'px' : height)}; - border-radius: 8px; - background: linear-gradient(-90deg, #dddddd, #f7f7f7, #dddddd, #f7f7f7); - background-size: 400%; - overflow: hidden; - animation: skeleton-gradient 5s infinite ease-out; - - @keyframes skeleton-gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } - } -`; diff --git a/frontend/src/components/Common/SortOptionList/SortOptionList.stories.tsx b/frontend/src/components/Common/SortOptionList/SortOptionList.stories.tsx index 779e68942..5e7c2f935 100644 --- a/frontend/src/components/Common/SortOptionList/SortOptionList.stories.tsx +++ b/frontend/src/components/Common/SortOptionList/SortOptionList.stories.tsx @@ -17,7 +17,7 @@ type Story = StoryObj; export const Default: Story = { render: () => { - const { ref, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); + const { isOpen, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); const { selectedOption, selectSortOption } = useSortOption(PRODUCT_SORT_OPTIONS[0]); useEffect(() => { @@ -25,7 +25,7 @@ export const Default: Story = { }, []); return ( - + { + + + + + + + + + + + + + + + ); }; diff --git a/frontend/src/components/Common/Toast/Toast.stories.tsx b/frontend/src/components/Common/Toast/Toast.stories.tsx deleted file mode 100644 index 383c43751..000000000 --- a/frontend/src/components/Common/Toast/Toast.stories.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; - -import Toast from './Toast'; - -import ToastProvider from '@/contexts/ToastContext'; -import { useToastActionContext } from '@/hooks/context'; - -const meta: Meta = { - title: 'common/Toast', - component: Toast, - decorators: [ - (Story) => ( - - - - ), - ], -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - render: () => { - const { toast } = useToastActionContext(); - const handleClick = () => { - toast.success('์„ฑ๊ณต'); - }; - return ( -
- -
- ); - }, -}; - -export const Error: Story = { - render: () => { - const { toast } = useToastActionContext(); - const handleClick = () => { - toast.error('์‹คํŒจ'); - }; - return ( -
- -
- ); - }, -}; diff --git a/frontend/src/components/Common/Toast/Toast.tsx b/frontend/src/components/Common/Toast/Toast.tsx deleted file mode 100644 index 28571c6af..000000000 --- a/frontend/src/components/Common/Toast/Toast.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Text, useTheme } from '@fun-eat/design-system'; -import styled from 'styled-components'; - -import { useToast } from '@/hooks/common'; -import { fadeOut, slideIn } from '@/styles/animations'; - -interface ToastProps { - id: number; - message: string; - isError?: boolean; -} - -const Toast = ({ id, message, isError = false }: ToastProps) => { - const theme = useTheme(); - const isShown = useToast(id); - - return ( - - {message} - - ); -}; - -export default Toast; - -type ToastStyleProps = Pick & { isAnimating?: boolean }; - -const ToastWrapper = styled.div` - position: relative; - width: 100%; - height: 55px; - max-width: 560px; - border-radius: 10px; - background: ${({ isError, theme }) => (isError ? theme.colors.error : theme.colors.black)}; - animation: ${({ isAnimating }) => (isAnimating ? slideIn : fadeOut)} 0.3s ease-in-out forwards; -`; - -const Message = styled(Text)` - margin-left: 20px; - line-height: 55px; -`; diff --git a/frontend/src/components/Common/index.ts b/frontend/src/components/Common/index.ts index 7d9a7c747..f5b218215 100644 --- a/frontend/src/components/Common/index.ts +++ b/frontend/src/components/Common/index.ts @@ -10,17 +10,14 @@ export { default as TabMenu } from './TabMenu/TabMenu'; export { default as TagList } from './TagList/TagList'; export { default as SectionTitle } from './SectionTitle/SectionTitle'; export { default as ScrollButton } from './ScrollButton/ScrollButton'; -export { default as Input } from './Input/Input'; export { default as ImageUploader } from './ImageUploader/ImageUploader'; export { default as ErrorBoundary } from './ErrorBoundary/ErrorBoundary'; export { default as ErrorComponent } from './ErrorComponent/ErrorComponent'; export { default as Loading } from './Loading/Loading'; export { default as MarkedText } from './MarkedText/MarkedText'; export { default as NavigableSectionTitle } from './NavigableSectionTitle/NavigableSectionTitle'; -export { default as Carousel } from './Carousel/Carousel'; export { default as RegisterButton } from './RegisterButton/RegisterButton'; -export { default as Toast } from './Toast/Toast'; export { default as CategoryItem } from './CategoryItem/CategoryItem'; export { default as CategoryFoodList } from './CategoryFoodList/CategoryFoodList'; export { default as CategoryStoreList } from './CategoryStoreList/CategoryStoreList'; -export { default as Skeleton } from './Skeleton/Skeleton'; +export { default as Banner } from './Banner/Banner'; diff --git a/frontend/src/components/Members/MemberModifyInput/MemberModifyInput.tsx b/frontend/src/components/Members/MemberModifyInput/MemberModifyInput.tsx index 39a78e6c5..6a03e4507 100644 --- a/frontend/src/components/Members/MemberModifyInput/MemberModifyInput.tsx +++ b/frontend/src/components/Members/MemberModifyInput/MemberModifyInput.tsx @@ -1,9 +1,7 @@ -import { Heading, Spacing, Text, useTheme } from '@fun-eat/design-system'; +import { Heading, Spacing, Text, Input, useTheme } from '@fun-eat/design-system'; import type { ChangeEventHandler } from 'react'; import styled from 'styled-components'; -import { Input } from '@/components/Common'; - const MIN_LENGTH = 1; const MAX_LENGTH = 10; diff --git a/frontend/src/components/Members/MemberRecipeList/MemberRecipeList.tsx b/frontend/src/components/Members/MemberRecipeList/MemberRecipeList.tsx index 14558353f..740daddff 100644 --- a/frontend/src/components/Members/MemberRecipeList/MemberRecipeList.tsx +++ b/frontend/src/components/Members/MemberRecipeList/MemberRecipeList.tsx @@ -10,15 +10,15 @@ import { useInfiniteMemberRecipeQuery } from '@/hooks/queries/members'; import useDisplaySlice from '@/utils/displaySlice'; interface MemberRecipeListProps { - isMemberPage?: boolean; + isPreview?: boolean; } -const MemberRecipeList = ({ isMemberPage = false }: MemberRecipeListProps) => { +const MemberRecipeList = ({ isPreview = false }: MemberRecipeListProps) => { const scrollRef = useRef(null); const { fetchNextPage, hasNextPage, data } = useInfiniteMemberRecipeQuery(); const memberRecipes = data?.pages.flatMap((page) => page.recipes); - const recipeToDisplay = useDisplaySlice(isMemberPage, memberRecipes); + const recipeToDisplay = useDisplaySlice(isPreview, memberRecipes); useIntersectionObserver(fetchNextPage, scrollRef, hasNextPage); @@ -40,7 +40,7 @@ const MemberRecipeList = ({ isMemberPage = false }: MemberRecipeListProps) => { return ( - {!isMemberPage && ( + {!isPreview && ( ์ด {totalRecipeCount}๊ฐœ์˜ ๊ฟ€์กฐํ•ฉ์„ ๋‚จ๊ฒผ์–ด์š”! @@ -50,7 +50,7 @@ const MemberRecipeList = ({ isMemberPage = false }: MemberRecipeListProps) => { {recipeToDisplay?.map((recipe) => (
  • - +
  • ))} diff --git a/frontend/src/components/Members/MemberReviewItem/MemberReviewItem.stories.tsx b/frontend/src/components/Members/MemberReviewItem/MemberReviewItem.stories.tsx new file mode 100644 index 000000000..3856648f5 --- /dev/null +++ b/frontend/src/components/Members/MemberReviewItem/MemberReviewItem.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import MemberReviewItem from './MemberReviewItem'; + +const meta: Meta = { + title: 'members/MemberReviewItem', + component: MemberReviewItem, + args: { + review: { + reviewId: 1, + productId: 5, + productName: '๊ตฌ์šด๊ฐ์ž์Šฌ๋ฆผ๋ช…๋ž€๋งˆ์š”', + content: + 'ํ• ๋จธ๋‹ˆ๊ฐ€ ๋จน์„ ๊ฑฐ ๊ฐ™์€ ๋ง›์ž…๋‹ˆ๋‹ค. 1960๋…„ ์ „์Ÿ ๋•Œ ๋ง› ๋ณด๊ณ  ์‹ถ์—ˆ๋Š”๋ฐ ๊ทธ๋•Œ๋Š” ๋„ˆ๋ฌด ๊ฐ€๋‚œํ•ด์„œ ๋จน์„ ์ˆ˜ ์—†์—ˆ๋Š”๋ฐ์š” ์ด๊ฒƒ๋ณด๋‹ค ๊ธด ๋ฆฌ๋ทฐ๋„ ์ž˜๋ ค ๋ณด์ธ๋‹ต๋‹ˆ๋‹ค', + rating: 4.0, + favoriteCount: 1256, + categoryType: 'food', + }, + isPreview: true, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/frontend/src/components/Members/MemberReviewItem/MemberReviewItem.tsx b/frontend/src/components/Members/MemberReviewItem/MemberReviewItem.tsx new file mode 100644 index 000000000..3024b982a --- /dev/null +++ b/frontend/src/components/Members/MemberReviewItem/MemberReviewItem.tsx @@ -0,0 +1,122 @@ +import { useTheme, Spacing, Text, Button, useToastActionContext } from '@fun-eat/design-system'; +import type { MouseEventHandler } from 'react'; +import styled from 'styled-components'; + +import { SvgIcon } from '@/components/Common'; +import { useDeleteReview } from '@/hooks/queries/members'; +import type { MemberReview } from '@/types/review'; + +interface MemberReviewItemProps { + review: MemberReview; + isPreview: boolean; +} + +const MemberReviewItem = ({ review, isPreview }: MemberReviewItemProps) => { + const theme = useTheme(); + + const { mutate } = useDeleteReview(); + + const { toast } = useToastActionContext(); + + const { reviewId, productName, content, rating, favoriteCount } = review; + + const handleReviewDelete: MouseEventHandler = (e) => { + e.preventDefault(); + + const result = window.confirm('๋ฆฌ๋ทฐ๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?'); + if (!result) { + return; + } + + mutate(reviewId, { + onSuccess: () => { + toast.success('๋ฆฌ๋ทฐ๋ฅผ ์‚ญ์ œํ–ˆ์Šต๋‹ˆ๋‹ค.'); + }, + onError: (error) => { + if (error instanceof Error) { + toast.error(error.message); + return; + } + + toast.error('๋ฆฌ๋ทฐ ์ข‹์•„์š”๋ฅผ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'); + }, + }); + }; + + return ( + + + + {productName} + + {!isPreview && ( + + )} + + + {content} + + + + + + + {favoriteCount} + + + + + + {rating.toFixed(1)} + + + + + ); +}; + +export default MemberReviewItem; + +const ReviewRankingItemContainer = styled.div` + display: flex; + flex-direction: column; + gap: 4px; + padding: 12px 0; + border-bottom: ${({ theme }) => `1px solid ${theme.borderColors.disabled}`}; +`; + +const ProductNameIconWrapper = styled.div` + display: flex; + justify-content: space-between; +`; + +const ReviewText = styled(Text)` + display: -webkit-inline-box; + text-overflow: ellipsis; + overflow: hidden; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +`; + +const FavoriteStarWrapper = styled.div` + display: flex; + gap: 4px; +`; + +const FavoriteIconWrapper = styled.div` + display: flex; + gap: 4px; + align-items: center; +`; + +const RatingIconWrapper = styled.div` + display: flex; + gap: 2px; + align-items: center; + + & > svg { + padding-bottom: 2px; + } +`; diff --git a/frontend/src/components/Members/MemberReviewList/MemberReviewList.tsx b/frontend/src/components/Members/MemberReviewList/MemberReviewList.tsx index 50398fedf..b622d9f65 100644 --- a/frontend/src/components/Members/MemberReviewList/MemberReviewList.tsx +++ b/frontend/src/components/Members/MemberReviewList/MemberReviewList.tsx @@ -3,21 +3,22 @@ import { useRef } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import styled from 'styled-components'; -import { ReviewRankingItem } from '@/components/Rank'; +import MemberReviewItem from '../MemberReviewItem/MemberReviewItem'; + import { PATH } from '@/constants/path'; import { useIntersectionObserver } from '@/hooks/common'; import { useInfiniteMemberReviewQuery } from '@/hooks/queries/members'; import useDisplaySlice from '@/utils/displaySlice'; interface MemberReviewListProps { - isMemberPage?: boolean; + isPreview?: boolean; } -const MemberReviewList = ({ isMemberPage = false }: MemberReviewListProps) => { +const MemberReviewList = ({ isPreview = false }: MemberReviewListProps) => { const scrollRef = useRef(null); const { fetchNextPage, hasNextPage, data } = useInfiniteMemberReviewQuery(); const memberReviews = data.pages.flatMap((page) => page.reviews); - const reviewsToDisplay = useDisplaySlice(isMemberPage, memberReviews); + const reviewsToDisplay = useDisplaySlice(isPreview, memberReviews); useIntersectionObserver(fetchNextPage, scrollRef, hasNextPage); @@ -39,21 +40,17 @@ const MemberReviewList = ({ isMemberPage = false }: MemberReviewListProps) => { return ( - {!isMemberPage && ( + {!isPreview && ( ์ด {totalReviewCount}๊ฐœ์˜ ๋ฆฌ๋ทฐ๋ฅผ ๋‚จ๊ฒผ์–ด์š”! )} - {reviewsToDisplay.map((reviewRanking) => ( -
  • - - + {reviewsToDisplay.map((review) => ( +
  • + +
  • ))} diff --git a/frontend/src/components/Members/index.ts b/frontend/src/components/Members/index.ts index a295e2728..4e31460ee 100644 --- a/frontend/src/components/Members/index.ts +++ b/frontend/src/components/Members/index.ts @@ -2,3 +2,4 @@ export { default as MembersInfo } from './MembersInfo/MembersInfo'; export { default as MemberReviewList } from './MemberReviewList/MemberReviewList'; export { default as MemberRecipeList } from './MemberRecipeList/MemberRecipeList'; export { default as MemberModifyInput } from './MemberModifyInput/MemberModifyInput'; +export { default as MemberReviewItem } from './MemberReviewItem/MemberReviewItem'; diff --git a/frontend/src/components/Product/ProductDetailItem/ProductDetailItem.tsx b/frontend/src/components/Product/ProductDetailItem/ProductDetailItem.tsx index 52cf05ada..b91b97e2b 100644 --- a/frontend/src/components/Product/ProductDetailItem/ProductDetailItem.tsx +++ b/frontend/src/components/Product/ProductDetailItem/ProductDetailItem.tsx @@ -20,7 +20,7 @@ const ProductDetailItem = ({ category, productDetail }: ProductDetailItemProps) return ( - {image !== null ? ( + {image ? ( {name} ) : category === CATEGORY_TYPE.FOOD ? ( @@ -42,7 +42,7 @@ const ProductDetailItem = ({ category, productDetail }: ProductDetailItemProps) ํ‰๊ท  ํ‰์  - {averageRating} + {averageRating.toFixed(1)} diff --git a/frontend/src/components/Product/ProductItem/ProductItem.tsx b/frontend/src/components/Product/ProductItem/ProductItem.tsx index 74fe7accb..d961894d3 100644 --- a/frontend/src/components/Product/ProductItem/ProductItem.tsx +++ b/frontend/src/components/Product/ProductItem/ProductItem.tsx @@ -1,9 +1,12 @@ -import { Text, useTheme } from '@fun-eat/design-system'; +import { Text, Skeleton, useTheme } from '@fun-eat/design-system'; import { memo, useState } from 'react'; +import { useParams } from 'react-router-dom'; import styled from 'styled-components'; import PreviewImage from '@/assets/characters.svg'; -import { Skeleton, SvgIcon } from '@/components/Common'; +import PBPreviewImage from '@/assets/samgakgimbab.svg'; +import { SvgIcon } from '@/components/Common'; +import { CATEGORY_TYPE } from '@/constants'; import type { Product } from '@/types/product'; interface ProductItemProps { @@ -12,12 +15,13 @@ interface ProductItemProps { const ProductItem = ({ product }: ProductItemProps) => { const theme = useTheme(); + const { category } = useParams(); const { name, price, image, averageRating, reviewCount } = product; const [isImageLoading, setIsImageLoading] = useState(true); return ( - {image !== null ? ( + {image ? ( <> { /> {isImageLoading && } - ) : ( + ) : category === CATEGORY_TYPE.FOOD ? ( + ) : ( + )} @@ -43,7 +49,7 @@ const ProductItem = ({ product }: ProductItemProps) => { - {averageRating} + {averageRating.toFixed(1)} diff --git a/frontend/src/components/Rank/RecipeRankingItem/RecipeRankingItem.tsx b/frontend/src/components/Rank/RecipeRankingItem/RecipeRankingItem.tsx index 81086851d..308f2d3fd 100644 --- a/frontend/src/components/Rank/RecipeRankingItem/RecipeRankingItem.tsx +++ b/frontend/src/components/Rank/RecipeRankingItem/RecipeRankingItem.tsx @@ -1,10 +1,11 @@ -import { Spacing, Text, useTheme } from '@fun-eat/design-system'; +import { Spacing, Text, Skeleton, useTheme } from '@fun-eat/design-system'; import { useState } from 'react'; import styled from 'styled-components'; import RecipePreviewImage from '@/assets/plate.svg'; -import { Skeleton, SvgIcon } from '@/components/Common'; +import { SvgIcon } from '@/components/Common'; import type { RecipeRanking } from '@/types/ranking'; +import { getRelativeDate } from '@/utils/date'; interface RecipeRankingItemProps { rank: number; @@ -18,6 +19,7 @@ const RecipeRankingItem = ({ rank, recipe }: RecipeRankingItemProps) => { title, author: { nickname, profileImage }, favoriteCount, + createdAt, } = recipe; const [isImageLoading, setIsImageLoading] = useState(true); @@ -49,6 +51,10 @@ const RecipeRankingItem = ({ rank, recipe }: RecipeRankingItemProps) => { {favoriteCount} + + + {getRelativeDate(createdAt)} + diff --git a/frontend/src/components/Rank/RecipeRankingList/RecipeRankingList.tsx b/frontend/src/components/Rank/RecipeRankingList/RecipeRankingList.tsx index 76397964d..368bff096 100644 --- a/frontend/src/components/Rank/RecipeRankingList/RecipeRankingList.tsx +++ b/frontend/src/components/Rank/RecipeRankingList/RecipeRankingList.tsx @@ -1,9 +1,8 @@ -import { Link, Text } from '@fun-eat/design-system'; +import { Carousel, Link, Text } from '@fun-eat/design-system'; import { Link as RouterLink } from 'react-router-dom'; import RecipeRankingItem from '../RecipeRankingItem/RecipeRankingItem'; -import { Carousel } from '@/components/Common'; import { PATH } from '@/constants/path'; import { useGA } from '@/hooks/common'; import { useRecipeRankingQuery } from '@/hooks/queries/rank'; diff --git a/frontend/src/components/Rank/ReviewRankingItem/ReviewRankingItem.stories.tsx b/frontend/src/components/Rank/ReviewRankingItem/ReviewRankingItem.stories.tsx index ca874ebf6..099f34737 100644 --- a/frontend/src/components/Rank/ReviewRankingItem/ReviewRankingItem.stories.tsx +++ b/frontend/src/components/Rank/ReviewRankingItem/ReviewRankingItem.stories.tsx @@ -15,6 +15,7 @@ const meta: Meta = { rating: 4.0, favoriteCount: 1256, categoryType: 'food', + createdAt: '2021-08-01T00:00:00.000Z', }, }, }; diff --git a/frontend/src/components/Rank/ReviewRankingItem/ReviewRankingItem.tsx b/frontend/src/components/Rank/ReviewRankingItem/ReviewRankingItem.tsx index 70fd89c32..205cfb03b 100644 --- a/frontend/src/components/Rank/ReviewRankingItem/ReviewRankingItem.tsx +++ b/frontend/src/components/Rank/ReviewRankingItem/ReviewRankingItem.tsx @@ -1,16 +1,19 @@ -import { Spacing, Text, theme } from '@fun-eat/design-system'; +import { Spacing, Text, useTheme } from '@fun-eat/design-system'; import { memo } from 'react'; import styled from 'styled-components'; import { SvgIcon } from '@/components/Common'; import type { ReviewRanking } from '@/types/ranking'; +import { getRelativeDate } from '@/utils/date'; interface ReviewRankingItemProps { reviewRanking: ReviewRanking; } const ReviewRankingItem = ({ reviewRanking }: ReviewRankingItemProps) => { - const { productName, content, rating, favoriteCount } = reviewRanking; + const theme = useTheme(); + + const { productName, content, rating, favoriteCount, createdAt } = reviewRanking; return ( @@ -34,6 +37,9 @@ const ReviewRankingItem = ({ reviewRanking }: ReviewRankingItemProps) => { {rating.toFixed(1)} + + {getRelativeDate(createdAt)} + ); @@ -46,7 +52,7 @@ const ReviewRankingItemContainer = styled.div` flex-direction: column; gap: 4px; padding: 12px; - border: 1px solid ${({ theme }) => theme.borderColors.disabled}; + border: ${({ theme }) => `1px solid ${theme.borderColors.disabled}`}; border-radius: ${({ theme }) => theme.borderRadius.sm}; `; @@ -78,3 +84,7 @@ const RatingIconWrapper = styled.div` padding-bottom: 2px; } `; + +const ReviewDate = styled(Text)` + margin-left: auto; +`; diff --git a/frontend/src/components/Rank/ReviewRankingList/ReviewRankingList.tsx b/frontend/src/components/Rank/ReviewRankingList/ReviewRankingList.tsx index 7b6c8272c..99f10d5f3 100644 --- a/frontend/src/components/Rank/ReviewRankingList/ReviewRankingList.tsx +++ b/frontend/src/components/Rank/ReviewRankingList/ReviewRankingList.tsx @@ -28,7 +28,7 @@ const ReviewRankingList = ({ isHomePage = false }: ReviewRankingListProps) => {
  • diff --git a/frontend/src/components/Recipe/CommentForm/CommentForm.stories.tsx b/frontend/src/components/Recipe/CommentForm/CommentForm.stories.tsx new file mode 100644 index 000000000..e65b87225 --- /dev/null +++ b/frontend/src/components/Recipe/CommentForm/CommentForm.stories.tsx @@ -0,0 +1,13 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import CommentForm from './CommentForm'; + +const meta: Meta = { + title: 'recipe/CommentForm', + component: CommentForm, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/frontend/src/components/Recipe/CommentForm/CommentForm.tsx b/frontend/src/components/Recipe/CommentForm/CommentForm.tsx new file mode 100644 index 000000000..5eb0ac1c7 --- /dev/null +++ b/frontend/src/components/Recipe/CommentForm/CommentForm.tsx @@ -0,0 +1,105 @@ +import { Button, Spacing, Text, Textarea, useTheme, useToastActionContext } from '@fun-eat/design-system'; +import type { ChangeEventHandler, FormEventHandler, RefObject } from 'react'; +import { useState } from 'react'; +import styled from 'styled-components'; + +import { SvgIcon } from '@/components/Common'; +import { useScroll } from '@/hooks/common'; +import { useRecipeCommentMutation } from '@/hooks/queries/recipe'; + +interface CommentFormProps { + recipeId: number; + scrollTargetRef: RefObject; +} + +const MAX_COMMENT_LENGTH = 200; + +const CommentForm = ({ recipeId, scrollTargetRef }: CommentFormProps) => { + const [commentValue, setCommentValue] = useState(''); + const { mutate } = useRecipeCommentMutation(recipeId); + + const theme = useTheme(); + const { toast } = useToastActionContext(); + + const { scrollToPosition } = useScroll(); + + const handleCommentInput: ChangeEventHandler = (e) => { + setCommentValue(e.target.value); + }; + + const handleSubmitComment: FormEventHandler = (e) => { + e.preventDefault(); + + mutate( + { comment: commentValue }, + { + onSuccess: () => { + setCommentValue(''); + scrollToPosition(scrollTargetRef); + toast.success('๋Œ“๊ธ€์ด ๋“ฑ๋ก๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + }, + onError: (error) => { + if (error instanceof Error) { + toast.error(error.message); + return; + } + + toast.error('๋Œ“๊ธ€์„ ๋“ฑ๋กํ•˜๋Š”๋ฐ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.'); + }, + } + ); + }; + + return ( + +
    + + + + + + + + {commentValue.length}์ž / {MAX_COMMENT_LENGTH}์ž + +
    + ); +}; + +export default CommentForm; + +const CommentFormContainer = styled.div` + position: fixed; + bottom: 0; + width: calc(100% - 40px); + max-width: 540px; + padding: 16px 0; + background: ${({ theme }) => theme.backgroundColors.default}; +`; + +const Form = styled.form` + display: flex; + gap: 4px; + justify-content: space-around; + align-items: center; +`; + +const CommentTextarea = styled(Textarea)` + height: 50px; + padding: 8px; + font-size: 1.4rem; +`; + +const SubmitButton = styled(Button)` + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; +`; diff --git a/frontend/src/components/Recipe/CommentItem/CommentItem.stories.tsx b/frontend/src/components/Recipe/CommentItem/CommentItem.stories.tsx new file mode 100644 index 000000000..70bf1f9a6 --- /dev/null +++ b/frontend/src/components/Recipe/CommentItem/CommentItem.stories.tsx @@ -0,0 +1,18 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import CommentItem from './CommentItem'; + +import comments from '@/mocks/data/comments.json'; + +const meta: Meta = { + title: 'recipe/CommentItem', + component: CommentItem, + args: { + recipeComment: comments.comments[0], + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/frontend/src/components/Recipe/CommentItem/CommentItem.tsx b/frontend/src/components/Recipe/CommentItem/CommentItem.tsx new file mode 100644 index 000000000..847194b75 --- /dev/null +++ b/frontend/src/components/Recipe/CommentItem/CommentItem.tsx @@ -0,0 +1,50 @@ +import { Divider, Spacing, Text, useTheme } from '@fun-eat/design-system'; +import styled from 'styled-components'; + +import type { Comment } from '@/types/recipe'; +import { getFormattedDate } from '@/utils/date'; + +interface CommentItemProps { + recipeComment: Comment; +} + +const CommentItem = ({ recipeComment }: CommentItemProps) => { + const theme = useTheme(); + const { author, comment, createdAt } = recipeComment; + + return ( + <> + + +
    + + {author.nickname} ๋‹˜ + + + {getFormattedDate(createdAt)} + +
    +
    + {comment} + + + + ); +}; + +export default CommentItem; + +const AuthorWrapper = styled.div` + display: flex; + gap: 12px; + align-items: center; +`; + +const AuthorProfileImage = styled.img` + border: 1px solid ${({ theme }) => theme.colors.primary}; + border-radius: 50%; +`; + +const CommentContent = styled(Text)` + margin: 16px 0; +`; diff --git a/frontend/src/components/Recipe/CommentList/CommentList.stories.tsx b/frontend/src/components/Recipe/CommentList/CommentList.stories.tsx new file mode 100644 index 000000000..ebad218de --- /dev/null +++ b/frontend/src/components/Recipe/CommentList/CommentList.stories.tsx @@ -0,0 +1,13 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import CommentList from './CommentList'; + +const meta: Meta = { + title: 'recipe/CommentList', + component: CommentList, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/frontend/src/components/Recipe/CommentList/CommentList.tsx b/frontend/src/components/Recipe/CommentList/CommentList.tsx new file mode 100644 index 000000000..d44f33c34 --- /dev/null +++ b/frontend/src/components/Recipe/CommentList/CommentList.tsx @@ -0,0 +1,37 @@ +import { Heading, Spacing, Text, theme } from '@fun-eat/design-system'; +import { useRef } from 'react'; + +import CommentItem from '../CommentItem/CommentItem'; + +import { useIntersectionObserver } from '@/hooks/common'; +import { useInfiniteRecipeCommentQuery } from '@/hooks/queries/recipe'; + +interface CommentListProps { + recipeId: number; +} + +const CommentList = ({ recipeId }: CommentListProps) => { + const scrollRef = useRef(null); + + const { fetchNextPage, hasNextPage, data } = useInfiniteRecipeCommentQuery(Number(recipeId)); + useIntersectionObserver(fetchNextPage, scrollRef, hasNextPage); + + const [{ totalElements }] = data.pages.flatMap((page) => page); + const comments = data.pages.flatMap((page) => page.comments); + + return ( + <> + + ๋Œ“๊ธ€ ({totalElements}๊ฐœ) + + + {totalElements === 0 && ๊ฟ€์กฐํ•ฉ์˜ ์ฒซ๋ฒˆ์งธ ๋Œ“๊ธ€์„ ๋‹ฌ์•„๋ณด์„ธ์š”!} + {comments.map((comment) => ( + + ))} +
    + + ); +}; + +export default CommentList; diff --git a/frontend/src/components/Recipe/RecipeFavorite/RecipeFavorite.tsx b/frontend/src/components/Recipe/RecipeFavoriteButton/RecipeFavoriteButton.tsx similarity index 53% rename from frontend/src/components/Recipe/RecipeFavorite/RecipeFavorite.tsx rename to frontend/src/components/Recipe/RecipeFavoriteButton/RecipeFavoriteButton.tsx index 882ca0eed..4685676cd 100644 --- a/frontend/src/components/Recipe/RecipeFavorite/RecipeFavorite.tsx +++ b/frontend/src/components/Recipe/RecipeFavoriteButton/RecipeFavoriteButton.tsx @@ -1,5 +1,4 @@ import { theme, Button, Text } from '@fun-eat/design-system'; -import { useState } from 'react'; import styled from 'styled-components'; import { SvgIcon } from '@/components/Common'; @@ -12,24 +11,11 @@ interface RecipeFavoriteProps { recipeId: number; } -const RecipeFavorite = ({ recipeId, favorite, favoriteCount }: RecipeFavoriteProps) => { - const [isFavorite, setIsFavorite] = useState(favorite); - const [currentFavoriteCount, setCurrentFavoriteCount] = useState(favoriteCount); +const RecipeFavoriteButton = ({ recipeId, favorite, favoriteCount }: RecipeFavoriteProps) => { const { mutate } = useRecipeFavoriteMutation(Number(recipeId)); const handleToggleFavorite = async () => { - mutate( - { favorite: !isFavorite }, - { - onSuccess: () => { - setIsFavorite((prev) => !prev); - setCurrentFavoriteCount((prev) => (isFavorite ? prev - 1 : prev + 1)); - }, - onError: () => { - alert('๊ฟ€์กฐํ•ฉ ์ข‹์•„์š”๋ฅผ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'); - }, - } - ); + mutate({ favorite: !favorite }); }; const [debouncedToggleFavorite] = useTimeout(handleToggleFavorite, 200); @@ -37,18 +23,18 @@ const RecipeFavorite = ({ recipeId, favorite, favoriteCount }: RecipeFavoritePro return ( - {currentFavoriteCount} + {favoriteCount} ); }; -export default RecipeFavorite; +export default RecipeFavoriteButton; const FavoriteButton = styled(Button)` display: flex; diff --git a/frontend/src/components/Recipe/RecipeItem/RecipeItem.tsx b/frontend/src/components/Recipe/RecipeItem/RecipeItem.tsx index 5846bd872..f7e7f229f 100644 --- a/frontend/src/components/Recipe/RecipeItem/RecipeItem.tsx +++ b/frontend/src/components/Recipe/RecipeItem/RecipeItem.tsx @@ -1,9 +1,9 @@ -import { Heading, Text, useTheme } from '@fun-eat/design-system'; +import { Heading, Text, Skeleton, useTheme } from '@fun-eat/design-system'; import { Fragment, memo, useState } from 'react'; import styled from 'styled-components'; import PreviewImage from '@/assets/plate.svg'; -import { Skeleton, SvgIcon } from '@/components/Common'; +import { SvgIcon } from '@/components/Common'; import type { MemberRecipe, Recipe } from '@/types/recipe'; import { getFormattedDate } from '@/utils/date'; diff --git a/frontend/src/components/Recipe/RecipeNameInput/RecipeNameInput.tsx b/frontend/src/components/Recipe/RecipeNameInput/RecipeNameInput.tsx index 3ab0d8bc8..093f31143 100644 --- a/frontend/src/components/Recipe/RecipeNameInput/RecipeNameInput.tsx +++ b/frontend/src/components/Recipe/RecipeNameInput/RecipeNameInput.tsx @@ -1,8 +1,7 @@ -import { Heading, Spacing, Text, useTheme } from '@fun-eat/design-system'; +import { Heading, Input, Spacing, Text, useTheme } from '@fun-eat/design-system'; import type { ChangeEventHandler } from 'react'; import styled from 'styled-components'; -import { Input } from '@/components/Common'; import { useRecipeFormActionContext } from '@/hooks/context'; const MIN_LENGTH = 1; diff --git a/frontend/src/components/Recipe/RecipeRegisterForm/RecipeRegisterForm.tsx b/frontend/src/components/Recipe/RecipeRegisterForm/RecipeRegisterForm.tsx index 238de3574..dbc7f5254 100644 --- a/frontend/src/components/Recipe/RecipeRegisterForm/RecipeRegisterForm.tsx +++ b/frontend/src/components/Recipe/RecipeRegisterForm/RecipeRegisterForm.tsx @@ -1,4 +1,4 @@ -import { Button, Divider, Heading, Spacing, Text, useTheme } from '@fun-eat/design-system'; +import { Button, Divider, Heading, Spacing, Text, useTheme, useToastActionContext } from '@fun-eat/design-system'; import type { FormEventHandler } from 'react'; import styled from 'styled-components'; @@ -23,6 +23,7 @@ const RecipeRegisterForm = ({ closeRecipeDialog }: RecipeRegisterFormProps) => { const recipeFormValue = useRecipeFormValueContext(); const { resetRecipeFormValue } = useRecipeFormActionContext(); + const { toast } = useToastActionContext(); const formData = useFormData({ imageKey: 'images', @@ -48,15 +49,16 @@ const RecipeRegisterForm = ({ closeRecipeDialog }: RecipeRegisterFormProps) => { mutate(formData, { onSuccess: () => { resetAndCloseForm(); + toast.success('๐Ÿฏ ๊ฟ€์กฐํ•ฉ์ด ๋“ฑ๋ก ๋์–ด์š”'); }, onError: (error) => { resetAndCloseForm(); if (error instanceof Error) { - alert(error.message); + toast.error(error.message); return; } - alert('๊ฟ€์กฐํ•ฉ ๋“ฑ๋ก์„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”'); + toast.error('๊ฟ€์กฐํ•ฉ ๋“ฑ๋ก์„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”'); }, }); }; diff --git a/frontend/src/components/Recipe/RecipeUsedProducts/RecipeUsedProducts.tsx b/frontend/src/components/Recipe/RecipeUsedProducts/RecipeUsedProducts.tsx index 8ee6faec4..00739f3fe 100644 --- a/frontend/src/components/Recipe/RecipeUsedProducts/RecipeUsedProducts.tsx +++ b/frontend/src/components/Recipe/RecipeUsedProducts/RecipeUsedProducts.tsx @@ -1,11 +1,11 @@ -import { Badge, Button, Heading, Text, useTheme } from '@fun-eat/design-system'; +import { Badge, Button, Heading, Text, Input, useTheme } from '@fun-eat/design-system'; import { useQueryErrorResetBoundary } from '@tanstack/react-query'; import { Suspense, useState } from 'react'; import styled from 'styled-components'; import SearchedProductList from './SearchedProductList'; -import { ErrorBoundary, ErrorComponent, Input, Loading, SvgIcon } from '@/components/Common'; +import { ErrorBoundary, ErrorComponent, Loading, SvgIcon } from '@/components/Common'; import { useDebounce } from '@/hooks/common'; import { useRecipeFormActionContext } from '@/hooks/context'; import { useSearch } from '@/hooks/search'; diff --git a/frontend/src/components/Recipe/index.ts b/frontend/src/components/Recipe/index.ts index f0ecdf5f6..ff9566ece 100644 --- a/frontend/src/components/Recipe/index.ts +++ b/frontend/src/components/Recipe/index.ts @@ -4,4 +4,7 @@ export { default as RecipeUsedProducts } from './RecipeUsedProducts/RecipeUsedPr export { default as RecipeItem } from './RecipeItem/RecipeItem'; export { default as RecipeList } from './RecipeList/RecipeList'; export { default as RecipeRegisterForm } from './RecipeRegisterForm/RecipeRegisterForm'; -export { default as RecipeFavorite } from './RecipeFavorite/RecipeFavorite'; +export { default as RecipeFavoriteButton } from './RecipeFavoriteButton/RecipeFavoriteButton'; +export { default as CommentItem } from './CommentItem/CommentItem'; +export { default as CommentForm } from './CommentForm/CommentForm'; +export { default as CommentList } from './CommentList/CommentList'; diff --git a/frontend/src/components/Review/ReviewFavoriteButton/ReviewFavoriteButton.tsx b/frontend/src/components/Review/ReviewFavoriteButton/ReviewFavoriteButton.tsx new file mode 100644 index 000000000..128872492 --- /dev/null +++ b/frontend/src/components/Review/ReviewFavoriteButton/ReviewFavoriteButton.tsx @@ -0,0 +1,69 @@ +import { Text, Button, useTheme } from '@fun-eat/design-system'; +import { useState } from 'react'; +import styled from 'styled-components'; + +import { SvgIcon } from '@/components/Common'; +import { useTimeout } from '@/hooks/common'; +import { useReviewFavoriteMutation } from '@/hooks/queries/review'; + +interface ReviewFavoriteButtonProps { + productId: number; + reviewId: number; + favorite: boolean; + favoriteCount: number; +} + +const ReviewFavoriteButton = ({ productId, reviewId, favorite, favoriteCount }: ReviewFavoriteButtonProps) => { + const theme = useTheme(); + + const initialFavoriteState = { + isFavorite: favorite, + currentFavoriteCount: favoriteCount, + }; + + const [favoriteInfo, setFavoriteInfo] = useState(initialFavoriteState); + const { isFavorite, currentFavoriteCount } = favoriteInfo; + + const { mutate } = useReviewFavoriteMutation(productId, reviewId); + + const handleToggleFavorite = async () => { + setFavoriteInfo((prev) => ({ + isFavorite: !prev.isFavorite, + currentFavoriteCount: isFavorite ? prev.currentFavoriteCount - 1 : prev.currentFavoriteCount + 1, + })); + + mutate( + { favorite: !isFavorite }, + { + onError: () => { + setFavoriteInfo(initialFavoriteState); + }, + } + ); + }; + + const [debouncedToggleFavorite] = useTimeout(handleToggleFavorite, 200); + + return ( + + + + {currentFavoriteCount} + + + ); +}; + +export default ReviewFavoriteButton; + +const FavoriteButton = styled(Button)` + display: flex; + align-items: center; + padding: 0; + column-gap: 8px; +`; diff --git a/frontend/src/components/Review/ReviewItem/ReviewItem.tsx b/frontend/src/components/Review/ReviewItem/ReviewItem.tsx index 112824ada..bbd38647e 100644 --- a/frontend/src/components/Review/ReviewItem/ReviewItem.tsx +++ b/frontend/src/components/Review/ReviewItem/ReviewItem.tsx @@ -1,10 +1,10 @@ -import { Badge, Button, Text, useTheme } from '@fun-eat/design-system'; -import { memo, useState } from 'react'; +import { Badge, Text, useTheme } from '@fun-eat/design-system'; +import { memo } from 'react'; import styled from 'styled-components'; +import ReviewFavoriteButton from '../ReviewFavoriteButton/ReviewFavoriteButton'; + import { SvgIcon, TagList } from '@/components/Common'; -import { useTimeout } from '@/hooks/common'; -import { useReviewFavoriteMutation } from '@/hooks/queries/review'; import type { Review } from '@/types/review'; import { getRelativeDate } from '@/utils/date'; @@ -14,35 +14,10 @@ interface ReviewItemProps { } const ReviewItem = ({ productId, review }: ReviewItemProps) => { - const { id, userName, profileImage, image, rating, tags, content, createdAt, rebuy, favoriteCount, favorite } = - review; - const [isFavorite, setIsFavorite] = useState(favorite); - const [currentFavoriteCount, setCurrentFavoriteCount] = useState(favoriteCount); - const { mutate } = useReviewFavoriteMutation(productId, id); - const theme = useTheme(); - const handleToggleFavorite = async () => { - mutate( - { favorite: !isFavorite }, - { - onSuccess: () => { - setIsFavorite((prev) => !prev); - setCurrentFavoriteCount((prev) => (isFavorite ? prev - 1 : prev + 1)); - }, - onError: (error) => { - if (error instanceof Error) { - alert(error.message); - return; - } - - alert('๋ฆฌ๋ทฐ ์ข‹์•„์š”๋ฅผ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'); - }, - } - ); - }; - - const [debouncedToggleFavorite] = useTimeout(handleToggleFavorite, 200); + const { id, userName, profileImage, image, rating, tags, content, createdAt, rebuy, favorite, favoriteCount } = + review; return ( @@ -76,17 +51,7 @@ const ReviewItem = ({ productId, review }: ReviewItemProps) => { {image && } {content} - - - - {currentFavoriteCount} - - + ); }; @@ -138,10 +103,3 @@ const ReviewImage = styled.img` const ReviewContent = styled(Text)` white-space: pre-wrap; `; - -const FavoriteButton = styled(Button)` - display: flex; - align-items: center; - padding: 0; - column-gap: 8px; -`; diff --git a/frontend/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx b/frontend/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx index 32207d184..7c3ac6ec1 100644 --- a/frontend/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx +++ b/frontend/src/components/Review/ReviewRegisterForm/ReviewRegisterForm.tsx @@ -1,4 +1,4 @@ -import { Button, Divider, Heading, Spacing, Text, theme } from '@fun-eat/design-system'; +import { Button, Divider, Heading, Spacing, Text, theme, useToastActionContext } from '@fun-eat/design-system'; import type { FormEventHandler, RefObject } from 'react'; import styled from 'styled-components'; @@ -29,10 +29,11 @@ interface ReviewRegisterFormProps { const ReviewRegisterForm = ({ productId, targetRef, closeReviewDialog, initTabMenu }: ReviewRegisterFormProps) => { const { scrollToPosition } = useScroll(); - const { previewImage, imageFile, uploadImage, deleteImage } = useImageUploader(); + const { isImageUploading, previewImage, imageFile, uploadImage, deleteImage } = useImageUploader(); const reviewFormValue = useReviewFormValueContext(); const { resetReviewFormValue } = useReviewFormActionContext(); + const { toast } = useToastActionContext(); const { data: productDetail } = useProductDetailQuery(productId); const { mutate, isLoading } = useReviewRegisterFormMutation(productId); @@ -41,7 +42,8 @@ const ReviewRegisterForm = ({ productId, targetRef, closeReviewDialog, initTabMe reviewFormValue.rating > MIN_RATING_SCORE && reviewFormValue.tagIds.length >= MIN_SELECTED_TAGS_COUNT && reviewFormValue.tagIds.length <= MIN_DISPLAYED_TAGS_LENGTH && - reviewFormValue.content.length > MIN_CONTENT_LENGTH; + reviewFormValue.content.length > MIN_CONTENT_LENGTH && + !isImageUploading; const formData = useFormData({ imageKey: 'image', @@ -64,15 +66,16 @@ const ReviewRegisterForm = ({ productId, targetRef, closeReviewDialog, initTabMe resetAndCloseForm(); initTabMenu(); scrollToPosition(targetRef); + toast.success('๐Ÿ“ ๋ฆฌ๋ทฐ๊ฐ€ ๋“ฑ๋ก ๋์–ด์š”'); }, onError: (error) => { resetAndCloseForm(); if (error instanceof Error) { - alert(error.message); + toast.error(error.message); return; } - alert('๋ฆฌ๋ทฐ ๋“ฑ๋ก์„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”'); + toast.error('๋ฆฌ๋ทฐ ๋“ฑ๋ก์„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”'); }, }); }; diff --git a/frontend/src/components/Review/index.ts b/frontend/src/components/Review/index.ts index ac1fa9cd4..399ee5e30 100644 --- a/frontend/src/components/Review/index.ts +++ b/frontend/src/components/Review/index.ts @@ -4,3 +4,4 @@ export { default as ReviewTagItem } from './ReviewTagItem/ReviewTagItem'; export { default as ReviewTagList } from './ReviewTagList/ReviewTagList'; export { default as ReviewRegisterForm } from './ReviewRegisterForm/ReviewRegisterForm'; export { default as BestReviewItem } from './BestReviewItem/BestReviewItem'; +export { default as ReviewFavoriteButton } from './ReviewFavoriteButton/ReviewFavoriteButton'; diff --git a/frontend/src/constants/index.ts b/frontend/src/constants/index.ts index 81b964c6c..8865b2802 100644 --- a/frontend/src/constants/index.ts +++ b/frontend/src/constants/index.ts @@ -63,7 +63,13 @@ export const CATEGORY_TYPE = { export const IMAGE_MAX_SIZE = 5 * 1024 * 1024; -export const ENVIRONMENT = window.location.href.includes('dev') ? 'dev' : 'prod'; +export const ENVIRONMENT = window.location.href.includes('dev') + ? 'dev' + : process.env.NODE_ENV === 'production' + ? 'prod' + : 'local'; export const IMAGE_URL = ENVIRONMENT === 'dev' ? process.env.S3_DEV_CLOUDFRONT_PATH : process.env.S3_PROD_CLOUDFRONT_PATH; + +export const PREVIOUS_PATH_LOCAL_STORAGE_KEY = `funeat-previous-path-${ENVIRONMENT}`; diff --git a/frontend/src/constants/path.ts b/frontend/src/constants/path.ts index 6fd8735c8..f729a74b2 100644 --- a/frontend/src/constants/path.ts +++ b/frontend/src/constants/path.ts @@ -4,5 +4,6 @@ export const PATH = { PRODUCT_LIST: '/products', MEMBER: '/members', RECIPE: '/recipes', + REVIEW: '/reviews', LOGIN: '/login', } as const; diff --git a/frontend/src/contexts/ToastContext.tsx b/frontend/src/contexts/ToastContext.tsx deleted file mode 100644 index 0bfdb0837..000000000 --- a/frontend/src/contexts/ToastContext.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import type { PropsWithChildren } from 'react'; -import { createContext, useState } from 'react'; -import { createPortal } from 'react-dom'; -import styled from 'styled-components'; - -import { Toast } from '@/components/Common'; - -interface ToastState { - id: number; - message: string; - isError?: boolean; -} - -interface ToastValue { - toasts: ToastState[]; -} -interface ToastAction { - toast: { - success: (message: string) => void; - error: (message: string) => void; - }; - deleteToast: (id: number) => void; -} - -export const ToastValueContext = createContext(null); -export const ToastActionContext = createContext(null); - -const ToastProvider = ({ children }: PropsWithChildren) => { - const [toasts, setToasts] = useState([]); - - const showToast = (id: number, message: string, isError?: boolean) => { - setToasts([...toasts, { id, message, isError }]); - }; - - const deleteToast = (id: number) => { - setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id)); - }; - - const toast = { - success: (message: string) => showToast(Number(Date.now()), message), - error: (message: string) => showToast(Number(Date.now()), message, true), - }; - - const toastValue = { - toasts, - }; - - const toastAction = { - toast, - deleteToast, - }; - - return ( - - - {children} - {createPortal( - - {toasts.map(({ id, message, isError }) => ( - - ))} - , - document.getElementById('toast-container') as HTMLElement - )} - - - ); -}; - -export default ToastProvider; - -const ToastContainer = styled.div` - position: fixed; - z-index: 1000; - display: flex; - flex-direction: column; - align-items: center; - width: calc(100% - 20px); - transform: translate(0, -10px); -`; diff --git a/frontend/src/hooks/common/index.ts b/frontend/src/hooks/common/index.ts index 61c985f67..2cbf28c1f 100644 --- a/frontend/src/hooks/common/index.ts +++ b/frontend/src/hooks/common/index.ts @@ -10,6 +10,4 @@ export { default as useTimeout } from './useTimeout'; export { default as useRouteChangeTracker } from './useRouteChangeTracker'; export { default as useTabMenu } from './useTabMenu'; export { default as useScrollRestoration } from './useScrollRestoration'; -export { default as useToast } from './useToast'; export { default as useGA } from './useGA'; - diff --git a/frontend/src/hooks/common/useImageUploader.ts b/frontend/src/hooks/common/useImageUploader.ts index c5783ad59..1f56a9923 100644 --- a/frontend/src/hooks/common/useImageUploader.ts +++ b/frontend/src/hooks/common/useImageUploader.ts @@ -1,19 +1,49 @@ +import { useToastActionContext } from '@fun-eat/design-system'; +import imageCompression from 'browser-image-compression'; import { useState } from 'react'; const isImageFile = (file: File) => file.type !== 'image/png' && file.type !== 'image/jpeg'; +const options = { + maxSizeMB: 1, + maxWidthOrHeight: 1920, + useWebWorker: true, +}; + const useImageUploader = () => { + const { toast } = useToastActionContext(); + const [imageFile, setImageFile] = useState(null); + const [isImageUploading, setIsImageUploading] = useState(false); const [previewImage, setPreviewImage] = useState(''); - const uploadImage = (imageFile: File) => { + const uploadImage = async (imageFile: File) => { if (isImageFile(imageFile)) { - alert('์ด๋ฏธ์ง€ ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.'); + toast.error('์ด๋ฏธ์ง€ ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.'); return; } setPreviewImage(URL.createObjectURL(imageFile)); - setImageFile(imageFile); + + try { + setIsImageUploading(true); + + const compressedFile = await imageCompression(imageFile, options); + const compressedImageFilePromise = imageCompression.getFilefromDataUrl( + await imageCompression.getDataUrlFromFile(compressedFile), + compressedFile.name + ); + compressedImageFilePromise + .then((result) => { + setImageFile(result); + }) + .then(() => { + setIsImageUploading(false); + toast.success('์ด๋ฏธ์ง€๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ๋“ฑ๋ก ๋์Šต๋‹ˆ๋‹ค'); + }); + } catch (error) { + console.log(error); + } }; const deleteImage = () => { @@ -23,6 +53,7 @@ const useImageUploader = () => { }; return { + isImageUploading, previewImage, imageFile, uploadImage, diff --git a/frontend/src/hooks/common/useToast.ts b/frontend/src/hooks/common/useToast.ts deleted file mode 100644 index f95f33ef9..000000000 --- a/frontend/src/hooks/common/useToast.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; - -import { useToastActionContext } from '../context'; - -const useToast = (id: number) => { - const { deleteToast } = useToastActionContext(); - const [isShown, setIsShown] = useState(true); - - const showTimeoutRef = useRef(null); - const deleteTimeoutRef = useRef(null); - - useEffect(() => { - showTimeoutRef.current = window.setTimeout(() => setIsShown(false), 2000); - - return () => { - if (showTimeoutRef.current) { - clearTimeout(showTimeoutRef.current); - } - }; - }, []); - - useEffect(() => { - if (!isShown) { - deleteTimeoutRef.current = window.setTimeout(() => deleteToast(id), 2000); - } - - return () => { - if (deleteTimeoutRef.current) { - clearTimeout(deleteTimeoutRef.current); - } - }; - }, [isShown]); - - return isShown; -}; - -export default useToast; diff --git a/frontend/src/hooks/context/index.ts b/frontend/src/hooks/context/index.ts index dd03253c9..56470cfbb 100644 --- a/frontend/src/hooks/context/index.ts +++ b/frontend/src/hooks/context/index.ts @@ -4,5 +4,3 @@ export { default as useReviewFormActionContext } from './useReviewFormActionCont export { default as useReviewFormValueContext } from './useReviewFormValueContext'; export { default as useRecipeFormActionContext } from './useRecipeFormActionContext'; export { default as useRecipeFormValueContext } from './useRecipeFormValueContext'; -export { default as useToastActionContext } from './useToastActionContext'; -export { default as useToastValueContext } from './useToastValueContext'; diff --git a/frontend/src/hooks/context/useToastActionContext.ts b/frontend/src/hooks/context/useToastActionContext.ts deleted file mode 100644 index e0d7e31a2..000000000 --- a/frontend/src/hooks/context/useToastActionContext.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useContext } from 'react'; - -import { ToastActionContext } from '@/contexts/ToastContext'; - -const useToastActionContext = () => { - const toastAction = useContext(ToastActionContext); - if (toastAction === null || toastAction === undefined) { - throw new Error('useToastActionContext๋Š” Toast Provider ์•ˆ์—์„œ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.'); - } - - return toastAction; -}; - -export default useToastActionContext; diff --git a/frontend/src/hooks/context/useToastValueContext.ts b/frontend/src/hooks/context/useToastValueContext.ts deleted file mode 100644 index ca4b65ca3..000000000 --- a/frontend/src/hooks/context/useToastValueContext.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useContext } from 'react'; - -import { ToastValueContext } from '@/contexts/ToastContext'; - -const useToastValueContext = () => { - const toastValue = useContext(ToastValueContext); - if (toastValue === null || toastValue === undefined) { - throw new Error('useToastValueContext๋Š” Toast Provider ์•ˆ์—์„œ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.'); - } - - return toastValue; -}; - -export default useToastValueContext; diff --git a/frontend/src/hooks/queries/banner/index.ts b/frontend/src/hooks/queries/banner/index.ts new file mode 100644 index 000000000..92fc0eb88 --- /dev/null +++ b/frontend/src/hooks/queries/banner/index.ts @@ -0,0 +1 @@ +export { default as useBannerQuery } from './useBannerQuery'; diff --git a/frontend/src/hooks/queries/banner/useBannerQuery.ts b/frontend/src/hooks/queries/banner/useBannerQuery.ts new file mode 100644 index 000000000..00fc09c42 --- /dev/null +++ b/frontend/src/hooks/queries/banner/useBannerQuery.ts @@ -0,0 +1,16 @@ +import { useSuspendedQuery } from '../useSuspendedQuery'; + +import { bannerApi } from '@/apis'; +import type { Banner } from '@/types/banner'; + +const fetchBanner = async () => { + const response = await bannerApi.get({}); + const data: Banner[] = await response.json(); + return data; +}; + +const useBannerQuery = () => { + return useSuspendedQuery(['banner'], () => fetchBanner()); +}; + +export default useBannerQuery; diff --git a/frontend/src/hooks/queries/members/index.ts b/frontend/src/hooks/queries/members/index.ts index 9e45e5239..cbd6e7468 100644 --- a/frontend/src/hooks/queries/members/index.ts +++ b/frontend/src/hooks/queries/members/index.ts @@ -3,3 +3,4 @@ export { default as useMemberQuery } from './useMemberQuery'; export { default as useInfiniteMemberRecipeQuery } from './useInfiniteMemberRecipeQuery'; export { default as useMemberModifyMutation } from './useMemberModifyMutation'; export { default as useLogoutMutation } from './useLogoutMutation'; +export { default as useDeleteReview } from './useDeleteReview'; diff --git a/frontend/src/hooks/queries/members/useDeleteReview.ts b/frontend/src/hooks/queries/members/useDeleteReview.ts new file mode 100644 index 000000000..a88169bce --- /dev/null +++ b/frontend/src/hooks/queries/members/useDeleteReview.ts @@ -0,0 +1,20 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { memberApi } from '@/apis'; + +const headers = { 'Content-Type': 'application/json' }; + +const deleteReview = async (reviewId: number) => { + return memberApi.delete({ params: `/reviews/${reviewId}`, credentials: true }, headers); +}; + +const useDeleteReview = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (reviewId: number) => deleteReview(reviewId), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['member', 'review'] }), + }); +}; + +export default useDeleteReview; diff --git a/frontend/src/hooks/queries/members/useLogoutMutation.ts b/frontend/src/hooks/queries/members/useLogoutMutation.ts index 3e586adc7..8aed88849 100644 --- a/frontend/src/hooks/queries/members/useLogoutMutation.ts +++ b/frontend/src/hooks/queries/members/useLogoutMutation.ts @@ -1,3 +1,4 @@ +import { useToastActionContext } from '@fun-eat/design-system'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; @@ -8,6 +9,8 @@ const useLogoutMutation = () => { const navigate = useNavigate(); const queryClient = useQueryClient(); + const { toast } = useToastActionContext(); + return useMutation({ mutationFn: () => logoutApi.post({ credentials: true }), onSuccess: () => { @@ -16,10 +19,10 @@ const useLogoutMutation = () => { }, onError: (error) => { if (error instanceof Error) { - alert(error.message); + toast.error(error.message); return; } - alert('๋กœ๊ทธ์•„์›ƒ์„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'); + toast.error('๋กœ๊ทธ์•„์›ƒ์„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'); }, }); }; diff --git a/frontend/src/hooks/queries/product/useInfiniteProductReviewsQuery.ts b/frontend/src/hooks/queries/product/useInfiniteProductReviewsQuery.ts index 0fb4d604b..8142c77e0 100644 --- a/frontend/src/hooks/queries/product/useInfiniteProductReviewsQuery.ts +++ b/frontend/src/hooks/queries/product/useInfiniteProductReviewsQuery.ts @@ -6,7 +6,7 @@ import type { ProductReviewResponse } from '@/types/response'; const fetchProductReviews = async (pageParam: number, productId: number, sort: string) => { const res = await productApi.get({ params: `/${productId}/reviews`, - queries: `?sort=${sort}&page=${pageParam}`, + queries: `?sort=${sort}&lastReviewId=${pageParam}`, credentials: true, }); @@ -20,9 +20,8 @@ const useInfiniteProductReviewsQuery = (productId: number, sort: string) => { ({ pageParam = 0 }) => fetchProductReviews(pageParam, productId, sort), { getNextPageParam: (prevResponse: ProductReviewResponse) => { - const isLast = prevResponse.page.lastPage; - const nextPage = prevResponse.page.requestPage + 1; - return isLast ? undefined : nextPage; + const lastCursor = prevResponse.reviews.length ? prevResponse.reviews[prevResponse.reviews.length - 1].id : 0; + return prevResponse.hasNext ? lastCursor : undefined; }, } ); diff --git a/frontend/src/hooks/queries/product/useInfiniteProductsQuery.ts b/frontend/src/hooks/queries/product/useInfiniteProductsQuery.ts index d8b008126..1b9fdd57f 100644 --- a/frontend/src/hooks/queries/product/useInfiniteProductsQuery.ts +++ b/frontend/src/hooks/queries/product/useInfiniteProductsQuery.ts @@ -6,7 +6,7 @@ import type { CategoryProductResponse } from '@/types/response'; const fetchProducts = async (pageParam: number, categoryId: number, sort = 'reviewCount,desc') => { const res = await categoryApi.get({ params: `/${categoryId}/products`, - queries: `?page=${pageParam}&sort=${sort}`, + queries: `?lastProductId=${pageParam}&sort=${sort}`, }); const data: CategoryProductResponse = await res.json(); @@ -19,9 +19,10 @@ const useInfiniteProductsQuery = (categoryId: number, sort = 'reviewCount,desc') ({ pageParam = 0 }) => fetchProducts(pageParam, categoryId, sort), { getNextPageParam: (prevResponse: CategoryProductResponse) => { - const isLast = prevResponse.page.lastPage; - const nextPage = prevResponse.page.requestPage + 1; - return isLast ? undefined : nextPage; + const lastCursor = prevResponse.products.length + ? prevResponse.products[prevResponse.products.length - 1].id + : 0; + return prevResponse.hasNext ? lastCursor : undefined; }, } ); diff --git a/frontend/src/hooks/queries/recipe/index.ts b/frontend/src/hooks/queries/recipe/index.ts index ef871dadd..0cd5db9f6 100644 --- a/frontend/src/hooks/queries/recipe/index.ts +++ b/frontend/src/hooks/queries/recipe/index.ts @@ -2,3 +2,5 @@ export { default as useRecipeDetailQuery } from './useRecipeDetailQuery'; export { default as useRecipeRegisterFormMutation } from './useRecipeRegisterFormMutation'; export { default as useRecipeFavoriteMutation } from './useRecipeFavoriteMutation'; export { default as useInfiniteRecipesQuery } from './useInfiniteRecipesQuery'; +export { default as useInfiniteRecipeCommentQuery } from './useInfiniteRecipeCommentQuery'; +export { default as useRecipeCommentMutation } from './useRecipeCommentMutation'; diff --git a/frontend/src/hooks/queries/recipe/useInfiniteRecipeCommentQuery.ts b/frontend/src/hooks/queries/recipe/useInfiniteRecipeCommentQuery.ts new file mode 100644 index 000000000..068520c8a --- /dev/null +++ b/frontend/src/hooks/queries/recipe/useInfiniteRecipeCommentQuery.ts @@ -0,0 +1,39 @@ +import { useSuspendedInfiniteQuery } from '../useSuspendedInfiniteQuery'; + +import { recipeApi } from '@/apis'; +import type { CommentResponse } from '@/types/response'; + +interface PageParam { + lastId: number; + totalElements: number | null; +} + +const fetchRecipeComments = async (pageParam: PageParam, recipeId: number) => { + const { lastId, totalElements } = pageParam; + const queries = totalElements === null ? '' : `?lastId=${lastId}&totalElements=${totalElements}`; + + const response = await recipeApi.get({ + params: `/${recipeId}/comments`, + queries: queries, + credentials: true, + }); + const data: CommentResponse = await response.json(); + return data; +}; + +const useInfiniteRecipeCommentQuery = (recipeId: number) => { + return useSuspendedInfiniteQuery( + ['recipeComment', recipeId], + ({ pageParam = { lastId: 0, totalElements: null } }) => fetchRecipeComments(pageParam, recipeId), + { + getNextPageParam: (prevResponse: CommentResponse) => { + const lastId = prevResponse.comments.length ? prevResponse.comments[prevResponse.comments.length - 1].id : 0; + const totalElements = prevResponse.totalElements; + const lastCursor = { lastId: lastId, totalElements: totalElements }; + return prevResponse.hasNext ? lastCursor : undefined; + }, + } + ); +}; + +export default useInfiniteRecipeCommentQuery; diff --git a/frontend/src/hooks/queries/recipe/useRecipeCommentMutation.ts b/frontend/src/hooks/queries/recipe/useRecipeCommentMutation.ts new file mode 100644 index 000000000..fc599b15e --- /dev/null +++ b/frontend/src/hooks/queries/recipe/useRecipeCommentMutation.ts @@ -0,0 +1,24 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { recipeApi } from '@/apis'; + +interface RecipeCommentRequestBody { + comment: string; +} + +const headers = { 'Content-Type': 'application/json' }; + +const postRecipeComment = (recipeId: number, body: RecipeCommentRequestBody) => { + return recipeApi.post({ params: `/${recipeId}/comments`, credentials: true }, headers, body); +}; + +const useRecipeCommentMutation = (recipeId: number) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (body: RecipeCommentRequestBody) => postRecipeComment(recipeId, body), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['recipeComment', recipeId] }), + }); +}; + +export default useRecipeCommentMutation; diff --git a/frontend/src/hooks/queries/recipe/useRecipeFavoriteMutation.ts b/frontend/src/hooks/queries/recipe/useRecipeFavoriteMutation.ts index c97b392af..38da1ec54 100644 --- a/frontend/src/hooks/queries/recipe/useRecipeFavoriteMutation.ts +++ b/frontend/src/hooks/queries/recipe/useRecipeFavoriteMutation.ts @@ -1,7 +1,8 @@ +import { useToastActionContext } from '@fun-eat/design-system'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { recipeApi } from '@/apis'; -import type { RecipeFavoriteRequestBody } from '@/types/recipe'; +import type { RecipeFavoriteRequestBody, RecipeDetail } from '@/types/recipe'; const headers = { 'Content-Type': 'application/json' }; @@ -11,10 +12,42 @@ const patchRecipeFavorite = (recipeId: number, body: RecipeFavoriteRequestBody) const useRecipeFavoriteMutation = (recipeId: number) => { const queryClient = useQueryClient(); + const { toast } = useToastActionContext(); + + const queryKey = ['recipeDetail', recipeId]; return useMutation({ mutationFn: (body: RecipeFavoriteRequestBody) => patchRecipeFavorite(recipeId, body), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['recipeDetail', recipeId] }), + onMutate: async (newFavoriteRequest) => { + await queryClient.cancelQueries({ queryKey: queryKey }); + + const previousRequest = queryClient.getQueryData(queryKey); + + if (previousRequest) { + queryClient.setQueryData(queryKey, () => ({ + ...previousRequest, + favorite: newFavoriteRequest.favorite, + favoriteCount: newFavoriteRequest.favorite + ? previousRequest.favoriteCount + 1 + : previousRequest.favoriteCount - 1, + })); + } + + return { previousRequest }; + }, + onError: (error, _, context) => { + queryClient.setQueryData(queryKey, context?.previousRequest); + + if (error instanceof Error) { + toast.error(error.message); + return; + } + + toast.error('์ข‹์•„์š”๋ฅผ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'); + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: queryKey }); + }, }); }; diff --git a/frontend/src/hooks/queries/review/index.ts b/frontend/src/hooks/queries/review/index.ts index e8bb44d4a..78fd628d2 100644 --- a/frontend/src/hooks/queries/review/index.ts +++ b/frontend/src/hooks/queries/review/index.ts @@ -1,3 +1,4 @@ export { default as useReviewTagsQuery } from './useReviewTagsQuery'; export { default as useReviewFavoriteMutation } from './useReviewFavoriteMutation'; export { default as useReviewRegisterFormMutation } from './useReviewRegisterFormMutation'; +export { default as useReviewDetailQuery } from './useReviewDetailQuery'; diff --git a/frontend/src/hooks/queries/review/useReviewDetailQuery.ts b/frontend/src/hooks/queries/review/useReviewDetailQuery.ts new file mode 100644 index 000000000..4b043a93b --- /dev/null +++ b/frontend/src/hooks/queries/review/useReviewDetailQuery.ts @@ -0,0 +1,16 @@ +import { useSuspendedQuery } from '../useSuspendedQuery'; + +import { reviewApi } from '@/apis'; +import type { ReviewDetail } from '@/types/review'; + +const fetchReviewDetail = async (reviewId: number) => { + const response = await reviewApi.get({ params: `/${reviewId}` }); + const data: ReviewDetail = await response.json(); + return data; +}; + +const useReviewDetailQuery = (reviewId: number) => { + return useSuspendedQuery(['review', reviewId, 'detail'], () => fetchReviewDetail(reviewId)); +}; + +export default useReviewDetailQuery; diff --git a/frontend/src/hooks/queries/review/useReviewFavoriteMutation.ts b/frontend/src/hooks/queries/review/useReviewFavoriteMutation.ts index 0d35ab610..c80a8c262 100644 --- a/frontend/src/hooks/queries/review/useReviewFavoriteMutation.ts +++ b/frontend/src/hooks/queries/review/useReviewFavoriteMutation.ts @@ -1,3 +1,4 @@ +import { useToastActionContext } from '@fun-eat/design-system'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { productApi } from '@/apis'; @@ -11,10 +12,21 @@ const patchReviewFavorite = (productId: number, reviewId: number, body: ReviewFa const useReviewFavoriteMutation = (productId: number, reviewId: number) => { const queryClient = useQueryClient(); + const { toast } = useToastActionContext(); + + const queryKey = ['product', productId, 'review']; return useMutation({ mutationFn: (body: ReviewFavoriteRequestBody) => patchReviewFavorite(productId, reviewId, body), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['product', productId, 'review'] }), + onError: (error) => { + if (error instanceof Error) { + toast.error(error.message); + return; + } + + toast.error('์ข‹์•„์š”๋ฅผ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'); + }, + onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKey }), }); }; diff --git a/frontend/src/hooks/search/useSearch.ts b/frontend/src/hooks/search/useSearch.ts index 4c438d4ec..bf0452206 100644 --- a/frontend/src/hooks/search/useSearch.ts +++ b/frontend/src/hooks/search/useSearch.ts @@ -1,3 +1,4 @@ +import { useToastActionContext } from '@fun-eat/design-system'; import type { ChangeEventHandler, FormEventHandler, MouseEventHandler } from 'react'; import { useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; @@ -14,6 +15,8 @@ const useSearch = () => { const [isSubmitted, setIsSubmitted] = useState(!!currentSearchQuery); const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(searchQuery.length > 0); + const { toast } = useToastActionContext(); + const { gaEvent } = useGA(); const focusInput = () => { @@ -35,7 +38,7 @@ const useSearch = () => { const trimmedSearchQuery = searchQuery.trim(); if (!trimmedSearchQuery) { - alert('๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'); + toast.error('๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”'); focusInput(); resetSearchQuery(); return; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 7cb7bc771..0299216de 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -8,7 +8,6 @@ import { RouterProvider } from 'react-router-dom'; import { SvgSprite } from './components/Common'; import { ENVIRONMENT } from './constants'; -import ToastProvider from './contexts/ToastContext'; import router from './router'; import GlobalStyle from './styles/globalStyle'; @@ -43,9 +42,7 @@ root.render( - - ...loading

    } /> -
    + ...loading

    } />
    diff --git a/frontend/src/mocks/browser.ts b/frontend/src/mocks/browser.ts index f7bb4bf35..b0b74505b 100644 --- a/frontend/src/mocks/browser.ts +++ b/frontend/src/mocks/browser.ts @@ -9,6 +9,7 @@ import { recipeHandlers, searchHandlers, logoutHandlers, + bannerHandlers, } from './handlers'; export const worker = setupWorker( @@ -19,5 +20,6 @@ export const worker = setupWorker( ...memberHandlers, ...recipeHandlers, ...searchHandlers, - ...logoutHandlers + ...logoutHandlers, + ...bannerHandlers ); diff --git a/frontend/src/mocks/data/banners.json b/frontend/src/mocks/data/banners.json new file mode 100644 index 000000000..0883f4c22 --- /dev/null +++ b/frontend/src/mocks/data/banners.json @@ -0,0 +1,17 @@ +[ + { + "id": 3, + "link": "https://www.youtube.com/embed/3QlYJ0VY7zg", + "image": "https://i.ytimg.com/vi/3QlYJ0VY7zg/maxresdefault.jpg" + }, + { + "id": 2, + "link": "https://www.youtube.com/embed/3QlYJ0VY7zg", + "image": "https://i.ytimg.com/vi/3QlYJ0VY7zg/maxresdefault.jpg" + }, + { + "id": 1, + "link": "https://www.youtube.com/embed/3QlYJ0VY7zg", + "image": "https://i.ytimg.com/vi/3QlYJ0VY7zg/maxresdefault.jpg" + } +] diff --git a/frontend/src/mocks/data/comments.json b/frontend/src/mocks/data/comments.json new file mode 100644 index 000000000..d2f029c98 --- /dev/null +++ b/frontend/src/mocks/data/comments.json @@ -0,0 +1,33 @@ +{ + "hasNext": false, + "totalElements": 3, + "comments": [ + { + "author": { + "nickname": "ํŽ€์ž‡", + "profileImage": "https://github.com/woowacourse-teams/2023-fun-eat/assets/78616893/1f0fd418-131c-4cf8-b540-112d762b7c34" + }, + "comment": "์ €๋„ ๋จน์–ด๋ดค๋Š”๋ฐ ๋ง›์žˆ์—ˆ์–ด์š”. ์ €๋„ ๋จน์–ด๋ดค๋Š”๋ฐ ๋ง›์žˆ์—ˆ์–ด์š”. ์ €๋„ ๋จน์–ด๋ดค๋Š”๋ฐ ๋ง›์žˆ์—ˆ์–ด์š”. ์ €๋„ ๋จน์–ด๋ดค๋Š”๋ฐ ๋ง›์žˆ์—ˆ์–ด์š”. ์ €๋„ ๋จน์–ด๋ดค๋Š”๋ฐ ๋ง›์žˆ์—ˆ์–ด์š”. ์ €๋„ ๋จน์–ด๋ดค๋Š”๋ฐ ๋ง›์žˆ์—ˆ์–ด์š”. ", + "createdAt": "2023-08-09T10:10:10", + "id": 1 + }, + { + "author": { + "nickname": "ํŽ€์ž‡", + "profileImage": "https://github.com/woowacourse-teams/2023-fun-eat/assets/78616893/1f0fd418-131c-4cf8-b540-112d762b7c34" + }, + "comment": "string", + "createdAt": "2023-08-09T10:10:10", + "id": 1 + }, + { + "author": { + "nickname": "ํŽ€์ž‡", + "profileImage": "https://github.com/woowacourse-teams/2023-fun-eat/assets/78616893/1f0fd418-131c-4cf8-b540-112d762b7c34" + }, + "comment": "string", + "createdAt": "2023-08-09T10:10:10", + "id": 1 + } + ] +} diff --git a/frontend/src/mocks/data/pbProducts.json b/frontend/src/mocks/data/pbProducts.json index 12bad382c..cd4491c33 100644 --- a/frontend/src/mocks/data/pbProducts.json +++ b/frontend/src/mocks/data/pbProducts.json @@ -1,12 +1,5 @@ { - "page": { - "totalDataCount": 99, - "totalPages": 10, - "firstPage": true, - "lastPage": false, - "requestPage": 1, - "requestSize": 10 - }, + "hasNext": false, "products": [ { "id": 11, @@ -44,7 +37,7 @@ "id": 5, "name": "PB ๋ฒ„ํ„ฐ๋ง", "price": 1000, - "image": "https://cdn.pixabay.com/photo/2016/03/23/15/00/ice-cream-1274894_1280.jpg", + "image": "", "averageRating": 4.0, "reviewCount": 100 }, diff --git a/frontend/src/mocks/data/productDetail.json b/frontend/src/mocks/data/productDetail.json index 386c7ae71..2695b51b1 100644 --- a/frontend/src/mocks/data/productDetail.json +++ b/frontend/src/mocks/data/productDetail.json @@ -5,7 +5,6 @@ "image": "https://i.namu.wiki/i/9wnvUaEa1EkDqG-M0Pbwfdf19FJQQXV_-bnlU2SYaNcG05y2wbabiIrfrGES1M4xSgDjY39RwOvLNggDd3Huuw.webp", "content": "ํ• ๋จธ๋‹ˆ๊ฐ€ ๋จน์„ ๊ฑฐ ๊ฐ™์€ ๋ง›์ž…๋‹ˆ๋‹ค.\n1960๋…„ ์ „์Ÿ ๋•Œ ๋ง› ๋ณด๊ณ  ์‹ถ์—ˆ๋Š”๋ฐ ๊ทธ๋•Œ๋Š” ๋„ˆ๋ฌด ๊ฐ€๋‚œํ•ด์„œ ๋จน์„ ์ˆ˜ ์—†์—ˆ๋Š”๋ฐ, ๋ง›์žˆ์–ด์š”.", "averageRating": 4.5, - "bookmark": false, "reviewCount": 100, "tags": [ { diff --git a/frontend/src/mocks/data/productDetails.json b/frontend/src/mocks/data/productDetails.json index e3c68dab0..c386ce680 100644 --- a/frontend/src/mocks/data/productDetails.json +++ b/frontend/src/mocks/data/productDetails.json @@ -6,7 +6,6 @@ "image": "https://github.com/woowacourse-teams/2023-fun-eat/assets/78616893/1f0fd418-131c-4cf8-b540-112d762b7c34", "content": "ํ• ๋จธ๋‹ˆ๊ฐ€ ๋จน์„ ๊ฑฐ ๊ฐ™์€ ๋ง›์ž…๋‹ˆ๋‹ค.\n1960๋…„ ์ „์Ÿ ๋•Œ ๋ง› ๋ณด๊ณ  ์‹ถ์—ˆ๋Š”๋ฐ ๊ทธ๋•Œ๋Š” ๋„ˆ๋ฌด ๊ฐ€๋‚œํ•ด์„œ ๋จน์„ ์ˆ˜ ์—†์—ˆ๋Š”๋ฐ, ๋ง›์žˆ์–ด์š”.", "averageRating": 4.5, - "bookmark": false, "reviewCount": 100, "tags": [ { @@ -33,7 +32,6 @@ "image": "https://github.com/woowacourse-teams/2023-fun-eat/assets/78616893/1f0fd418-131c-4cf8-b540-112d762b7c34", "content": "ํ• ๋จธ๋‹ˆ๊ฐ€ ๋จน์„ ๊ฑฐ ๊ฐ™์€ ๋ง›์ž…๋‹ˆ๋‹ค. 1960๋…„ ์ „์Ÿ ๋•Œ ๋ง› ๋ณด๊ณ  ์‹ถ์—ˆ๋Š”๋ฐ ๊ทธ๋•Œ๋Š” ๋„ˆ๋ฌด ๊ฐ€๋‚œํ•ด์„œ ๋จน์„ ์ˆ˜ ์—†์—ˆ๋Š”๋ฐ, ๋ง›์žˆ์–ด์š”.", "averageRating": 4.0, - "bookmark": true, "reviewCount": 55, "tags": [ { diff --git a/frontend/src/mocks/data/products.json b/frontend/src/mocks/data/products.json index 8d78d4a97..e68a13fbf 100644 --- a/frontend/src/mocks/data/products.json +++ b/frontend/src/mocks/data/products.json @@ -1,12 +1,5 @@ { - "page": { - "totalDataCount": 99, - "totalPages": 10, - "firstPage": true, - "lastPage": false, - "requestPage": 1, - "requestSize": 10 - }, + "hasNext": false, "products": [ { "id": 1, diff --git a/frontend/src/mocks/data/recipeDetail.json b/frontend/src/mocks/data/recipeDetail.json index 41c5c1fce..a8ba54c3d 100644 --- a/frontend/src/mocks/data/recipeDetail.json +++ b/frontend/src/mocks/data/recipeDetail.json @@ -1,6 +1,6 @@ { "id": 3, - "images": [], + "images": ["https://github.com/woowacourse-teams/2023-fun-eat/assets/78616893/1f0fd418-131c-4cf8-b540-112d762b7c34"], "title": "์ดˆํŠน๊ธ‰๋ถˆ๋‹ญ์ฝ˜์น˜์ฆˆ", "content": "๋ง›์žˆ๋Š” ๋ถˆ๋‹ญ์ฝ˜์น˜์ฆˆ๋ฎ๋ฐฅ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ๋Š” 1๋ฒˆ. ์–ด์ฉŒ๊ณ ์ €์ฉŒ๊ณ  2๋ฒˆ. ์–ด์ฉŒ๊ณ ์ €์ฉŒ๊ณ  3๋ฒˆ ๋ง›์žˆ๋Š” ๋ถˆ๋‹ญ์ฝ˜์น˜์ฆˆ๋ฎ๋ฐฅ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ๋Š” 1๋ฒˆ. ์–ด์ฉŒ๊ณ ์ €์ฉŒ๊ณ  2๋ฒˆ. ์–ด์ฉŒ๊ณ ์ €์ฉŒ๊ณ \n\n 3๋ฒˆ๋ง›์žˆ๋Š” ๋ถˆ๋‹ญ์ฝ˜์น˜์ฆˆ๋ฎ๋ฐฅ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ๋Š” 1๋ฒˆ. ์–ด์ฉŒ๊ณ ์ €์ฉŒ๊ณ  2๋ฒˆ. ์–ด์ฉŒ๊ณ ์ €์ฉŒ๊ณ  3๋ฒˆ๋ง›์žˆ๋Š” ๋ถˆ๋‹ญ์ฝ˜์น˜์ฆˆ๋ฎ๋ฐฅ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ๋Š” 1๋ฒˆ. ์–ด์ฉŒ๊ณ ์ €์ฉŒ๊ณ  2๋ฒˆ. ์–ด์ฉŒ๊ณ ์ €์ฉŒ๊ณ  3๋ฒˆ๋ง›์žˆ๋Š” ๋ถˆ๋‹ญ์ฝ˜์น˜์ฆˆ๋ฎ๋ฐฅ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ๋Š” 1๋ฒˆ. ์–ด์ฉŒ๊ณ ์ €์ฉŒ๊ณ  2๋ฒˆ. ์–ด์ฉŒ๊ณ ์ €์ฉŒ๊ณ  3๋ฒˆ๋ง›์žˆ๋Š” ๋ถˆ๋‹ญ์ฝ˜์น˜์ฆˆ๋ฎ๋ฐฅ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ๋Š” 1๋ฒˆ. ์–ด์ฉŒ๊ณ ์ €์ฉŒ๊ณ  2๋ฒˆ. ์–ด์ฉŒ๊ณ ์ €์ฉŒ๊ณ  3๋ฒˆ๋ง›์žˆ๋Š” ๋ถˆ๋‹ญ์ฝ˜์น˜์ฆˆ๋ฎ๋ฐฅ์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ๋Š” 1๋ฒˆ. ์–ด์ฉŒ๊ณ ์ €์ฉŒ๊ณ  2๋ฒˆ. ์–ด์ฉŒ๊ณ ์ €์ฉŒ๊ณ  3๋ฒˆ", "author": { diff --git a/frontend/src/mocks/data/recipeRankingList.json b/frontend/src/mocks/data/recipeRankingList.json index c08391be2..fbb93ff10 100644 --- a/frontend/src/mocks/data/recipeRankingList.json +++ b/frontend/src/mocks/data/recipeRankingList.json @@ -8,7 +8,8 @@ "profileImage": "https://github.com/woowacourse-teams/2023-fun-eat/assets/78616893/991f7b69-53bf-4d03-96e1-988c34d010ed", "nickname": "funeat" }, - "favoriteCount": 153 + "favoriteCount": 153, + "createdAt": "2023-08-09T10:10:10" }, { "id": 2, @@ -18,7 +19,8 @@ "profileImage": "https://github.com/woowacourse-teams/2023-fun-eat/assets/78616893/991f7b69-53bf-4d03-96e1-988c34d010ed", "nickname": "funeat" }, - "favoriteCount": 153 + "favoriteCount": 153, + "createdAt": "2023-08-09T10:10:10" }, { "id": 3, @@ -28,7 +30,8 @@ "profileImage": "https://github.com/woowacourse-teams/2023-fun-eat/assets/78616893/991f7b69-53bf-4d03-96e1-988c34d010ed", "nickname": "funeat" }, - "favoriteCount": 153 + "favoriteCount": 153, + "createdAt": "2023-08-09T10:10:10" } ] } diff --git a/frontend/src/mocks/data/reviewDetail.json b/frontend/src/mocks/data/reviewDetail.json new file mode 100644 index 000000000..9d0bcc097 --- /dev/null +++ b/frontend/src/mocks/data/reviewDetail.json @@ -0,0 +1,27 @@ +{ + "id": 1, + "userName": "ํŽ€์ž‡", + "profileImage": "https://github.com/woowacourse-teams/2023-fun-eat/assets/78616893/1f0fd418-131c-4cf8-b540-112d762b7c34", + "image": "https://i.namu.wiki/i/9wnvUaEa1EkDqG-M0Pbwfdf19FJQQXV_-bnlU2SYaNcG05y2wbabiIrfrGES1M4xSgDjY39RwOvLNggDd3Huuw.webp", + "rating": 4.5, + "tags": [ + { + "id": 5, + "name": "๋‹จ์ง ๋‹จ์ง ", + "tagType": "TASTE" + }, + { + "id": 1, + "name": "๋ง๊ณ ๋ง๊ณ ", + "tagType": "TASTE" + } + ], + "content": "๋ง›์žˆ์–ด์šฉ~!~!", + "rebuy": true, + "favoriteCount": 1320, + "favorite": true, + "createdAt": "2023-10-13T00:00:00", + "categoryType": "food", + "productId": 1, + "productName": "์น ์„ฑ ์‚ฌ์ด๋‹ค" +} diff --git a/frontend/src/mocks/data/reviewRankingList.json b/frontend/src/mocks/data/reviewRankingList.json index 3a5cc0f73..8f9577454 100644 --- a/frontend/src/mocks/data/reviewRankingList.json +++ b/frontend/src/mocks/data/reviewRankingList.json @@ -7,7 +7,8 @@ "content": "ํ• ๋จธ๋‹ˆ๊ฐ€ ๋จน์„ ๊ฑฐ ๊ฐ™์€ ๋ง›์ž…๋‹ˆ๋‹ค. 1960๋…„ ์ „์Ÿ ๋•Œ ๋ง› ๋ณด๊ณ  ์‹ถ์—ˆ๋Š”๋ฐ ๊ทธ๋•Œ๋Š” ๋„ˆ๋ฌด ๊ฐ€๋‚œํ•ด์„œ ๋จน์„ ์ˆ˜ ์—†์—ˆ๋Š”๋ฐ์š” ์ด๊ฒƒ๋ณด๋‹ค ๊ธด ๋ฆฌ๋ทฐ๋„ ์ž˜๋ ค ๋ณด์ธ๋‹ต๋‹ˆ๋‹ค", "rating": 4.0, "favoriteCount": 1256, - "categoryType": "food" + "categoryType": "food", + "createdAt": "2023-08-09T10:10:10" }, { "reviewId": 1, @@ -16,7 +17,8 @@ "content": "ํ•˜์–€ ์งœํŒŒ๊ฒŒํ‹ฐ๋ผ๋‹ˆ ๋ง์ด ์•ˆ๋œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์—ˆ์ฃ . ์‹ค์ œ๋กœ ๋ง›์„ ๋ณด๋‹ˆ๊นŒ ๊นŒ๋งŒ ์งœํŒŒ๊ฒŒํ‹ฐ๋ž‘ ๋ง›์ด ๋ญ”๊ฐ€ ๋‹ค๋ฅผ๊ฒŒ ์—†๋„ค์š”.", "rating": 4.4, "favoriteCount": 870, - "categoryType": "food" + "categoryType": "food", + "createdAt": "2023-08-09T10:10:10" } ] } diff --git a/frontend/src/mocks/data/reviews.json b/frontend/src/mocks/data/reviews.json index 54b9719ce..5f9d129ee 100644 --- a/frontend/src/mocks/data/reviews.json +++ b/frontend/src/mocks/data/reviews.json @@ -1,12 +1,5 @@ { - "page": { - "totalDataCount": 99, - "totalPages": 10, - "firstPage": true, - "lastPage": false, - "requestPage": 1, - "requestSize": 10 - }, + "hasNext": false, "reviews": [ { "id": 1, diff --git a/frontend/src/mocks/handlers/bannerHandlers.ts b/frontend/src/mocks/handlers/bannerHandlers.ts new file mode 100644 index 000000000..20bf92f77 --- /dev/null +++ b/frontend/src/mocks/handlers/bannerHandlers.ts @@ -0,0 +1,9 @@ +import { rest } from 'msw'; + +import banners from '../data/banners.json'; + +export const bannerHandlers = [ + rest.get('/api/banners', (req, res, ctx) => { + return res(ctx.status(200), ctx.json(banners)); + }), +]; diff --git a/frontend/src/mocks/handlers/index.ts b/frontend/src/mocks/handlers/index.ts index c255590ef..b8c5b8b38 100644 --- a/frontend/src/mocks/handlers/index.ts +++ b/frontend/src/mocks/handlers/index.ts @@ -6,3 +6,4 @@ export * from './memberHandlers'; export * from './recipeHandlers'; export * from './searchHandlers'; export * from './logoutHandlers'; +export * from './bannerHandlers'; diff --git a/frontend/src/mocks/handlers/memberHandlers.ts b/frontend/src/mocks/handlers/memberHandlers.ts index 2abb3d7fb..31ee93536 100644 --- a/frontend/src/mocks/handlers/memberHandlers.ts +++ b/frontend/src/mocks/handlers/memberHandlers.ts @@ -55,4 +55,8 @@ export const memberHandlers = [ return res(ctx.status(200), ctx.json(mockMemberRecipes)); }), + + rest.delete('/api/members/reviews/:reviewId', (req, res, ctx) => { + return res(ctx.status(204)); + }), ]; diff --git a/frontend/src/mocks/handlers/productHandlers.ts b/frontend/src/mocks/handlers/productHandlers.ts index bc230c1fa..32f3feadc 100644 --- a/frontend/src/mocks/handlers/productHandlers.ts +++ b/frontend/src/mocks/handlers/productHandlers.ts @@ -25,7 +25,6 @@ export const productHandlers = [ rest.get('/api/categories/:categoryId/products', (req, res, ctx) => { const sortOptions = req.url.searchParams.get('sort'); const categoryId = req.params.categoryId; - const page = Number(req.url.searchParams.get('page')); if (sortOptions === null) { return res(ctx.status(400)); @@ -37,7 +36,7 @@ export const productHandlers = [ let products = commonProducts; - if (Number(categoryId) >= 7 && Number(categoryId) <= 9) { + if (Number(categoryId) >= 6 && Number(categoryId) <= 9) { products = pbProducts; } @@ -53,11 +52,7 @@ export const productHandlers = [ sortOrder === 'asc' ? cur[key] - next[key] : next[key] - cur[key] ), }; - return res( - ctx.status(200), - ctx.json({ page: sortedProducts.page, products: products.products.slice(page * 10, (page + 1) * 10) }), - ctx.delay(500) - ); + return res(ctx.status(200), ctx.json(sortedProducts), ctx.delay(500)); }), rest.get('/api/products/:productId', (req, res, ctx) => { diff --git a/frontend/src/mocks/handlers/recipeHandlers.ts b/frontend/src/mocks/handlers/recipeHandlers.ts index 0a37b22f0..fff47a879 100644 --- a/frontend/src/mocks/handlers/recipeHandlers.ts +++ b/frontend/src/mocks/handlers/recipeHandlers.ts @@ -1,11 +1,18 @@ import { rest } from 'msw'; import { isRecipeSortOption, isSortOrder } from './utils'; +import comments from '../data/comments.json'; import recipeDetail from '../data/recipeDetail.json'; import mockRecipes from '../data/recipes.json'; export const recipeHandlers = [ rest.get('/api/recipes/:recipeId', (req, res, ctx) => { + const { mockSessionId } = req.cookies; + + if (!mockSessionId) { + return res(ctx.status(401)); + } + return res(ctx.status(200), ctx.json(recipeDetail), ctx.delay(1000)); }), @@ -88,4 +95,12 @@ export const recipeHandlers = [ ctx.json({ ...sortedRecipes, recipes: sortedRecipes.recipes.slice(page * 5, (page + 1) * 5) }) ); }), + + rest.get('/api/recipes/:recipeId/comments', (req, res, ctx) => { + return res(ctx.status(200), ctx.json(comments)); + }), + + rest.post('/api/recipes/:recipeId/comments', (req, res, ctx) => { + return res(ctx.status(201)); + }), ]; diff --git a/frontend/src/mocks/handlers/reviewHandlers.ts b/frontend/src/mocks/handlers/reviewHandlers.ts index 455714f52..656891e55 100644 --- a/frontend/src/mocks/handlers/reviewHandlers.ts +++ b/frontend/src/mocks/handlers/reviewHandlers.ts @@ -1,6 +1,7 @@ import { rest } from 'msw'; import { isReviewSortOption, isSortOrder } from './utils'; +import mockReviewDetail from '../data/reviewDetail.json'; import mockReviewRanking from '../data/reviewRankingList.json'; import mockReviews from '../data/reviews.json'; import mockReviewTags from '../data/reviewTagList.json'; @@ -41,7 +42,7 @@ export const reviewHandlers = [ return res( ctx.status(200), - ctx.json({ page: sortedReviews.page, reviews: sortedReviews.reviews }), + ctx.json({ hasNext: sortedReviews.hasNext, reviews: sortedReviews.reviews }), ctx.delay(1000) ); }), @@ -73,4 +74,8 @@ export const reviewHandlers = [ rest.get('/api/tags', (_, res, ctx) => { return res(ctx.status(200), ctx.json(mockReviewTags)); }), + + rest.get('/api/reviews/:reviewId', (_, res, ctx) => { + return res(ctx.status(200), ctx.json(mockReviewDetail)); + }), ]; diff --git a/frontend/src/pages/AuthPage.tsx b/frontend/src/pages/AuthPage.tsx index a840cb978..9f7974e45 100644 --- a/frontend/src/pages/AuthPage.tsx +++ b/frontend/src/pages/AuthPage.tsx @@ -2,8 +2,10 @@ import { useEffect, useState } from 'react'; import { Navigate, useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { loginApi } from '@/apis'; +import { PREVIOUS_PATH_LOCAL_STORAGE_KEY } from '@/constants'; import { PATH } from '@/constants/path'; import { useMemberQuery } from '@/hooks/queries/members'; +import { getLocalStorage, removeLocalStorage } from '@/utils/localStorage'; export const AuthPage = () => { const { authProvider } = useParams(); @@ -14,10 +16,6 @@ export const AuthPage = () => { const [location, setLocation] = useState(''); const navigate = useNavigate(); - if (member) { - return ; - } - const getSessionId = async () => { const response = await loginApi.get({ params: `/oauth2/code/${authProvider}`, @@ -51,9 +49,17 @@ export const AuthPage = () => { return; } + const previousPath = getLocalStorage(PREVIOUS_PATH_LOCAL_STORAGE_KEY); + const redirectLocation = previousPath ? previousPath : location; + + navigate(redirectLocation, { replace: true }); + removeLocalStorage(PREVIOUS_PATH_LOCAL_STORAGE_KEY); refetchMember(); - navigate(location, { replace: true }); }, [location]); + if (member) { + return ; + } + return <>; }; diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 436b893ec..d48e4a255 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -1,15 +1,23 @@ -import { Heading, Spacing } from '@fun-eat/design-system'; +import { Heading, Spacing, Text, useTheme } from '@fun-eat/design-system'; import { useQueryErrorResetBoundary } from '@tanstack/react-query'; import { Suspense } from 'react'; import styled from 'styled-components'; -import { Loading, ErrorBoundary, ErrorComponent, CategoryFoodList, CategoryStoreList } from '@/components/Common'; +import { + Loading, + ErrorBoundary, + ErrorComponent, + CategoryFoodList, + CategoryStoreList, + SvgIcon, + Banner, +} from '@/components/Common'; import { ProductRankingList, ReviewRankingList, RecipeRankingList } from '@/components/Rank'; -import { IMAGE_URL } from '@/constants'; import channelTalk from '@/service/channelTalk'; export const HomePage = () => { const { reset } = useQueryErrorResetBoundary(); + const theme = useTheme(); channelTalk.loadScript(); @@ -20,7 +28,7 @@ export const HomePage = () => { return ( <>
    - +
    @@ -41,7 +49,12 @@ export const HomePage = () => { ๐Ÿฏ ๊ฟ€์กฐํ•ฉ ๋žญํ‚น - + + + + ๊ฟ€์กฐํ•ฉ ๋žญํ‚น์€ ์ž์ฒด ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ธฐ๋ฐ˜์œผ๋กœ ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค. + + }> @@ -51,9 +64,14 @@ export const HomePage = () => { - ๐Ÿ‘‘ ์ƒํ’ˆ ๋žญํ‚น + ๐Ÿ™ ์ƒํ’ˆ ๋žญํ‚น - + + + + ์ƒํ’ˆ ๋žญํ‚น์€ 2์ฃผ ๋‹จ์œ„๋กœ ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค. + + }> @@ -65,7 +83,12 @@ export const HomePage = () => { ๐Ÿ“ ๋ฆฌ๋ทฐ ๋žญํ‚น - + + + + ๋ฆฌ๋ทฐ ๋žญํ‚น์€ ์ž์ฒด ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ธฐ๋ฐ˜์œผ๋กœ ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค. + + }> @@ -77,11 +100,6 @@ export const HomePage = () => { ); }; -const Banner = styled.img` - width: 100%; - height: auto; -`; - const SectionWrapper = styled.section` padding: 0 20px; `; @@ -100,3 +118,14 @@ const CategoryListWrapper = styled.div` display: none; } `; + +const RankingInfoWrapper = styled.div` + display: flex; + align-items: center; + gap: 2px; + margin: 8px 0 16px; + + & > svg { + padding-bottom: 2px; + } +`; diff --git a/frontend/src/pages/IntegratedSearchPage.tsx b/frontend/src/pages/IntegratedSearchPage.tsx index 77f781f32..a74f7a776 100644 --- a/frontend/src/pages/IntegratedSearchPage.tsx +++ b/frontend/src/pages/IntegratedSearchPage.tsx @@ -1,9 +1,9 @@ -import { Button, Heading, Spacing, Text } from '@fun-eat/design-system'; +import { Button, Heading, Spacing, Text, Input } from '@fun-eat/design-system'; import { useQueryErrorResetBoundary } from '@tanstack/react-query'; import { Suspense, useEffect, useState } from 'react'; import styled from 'styled-components'; -import { ErrorBoundary, ErrorComponent, Input, Loading, SvgIcon, TabMenu } from '@/components/Common'; +import { ErrorBoundary, ErrorComponent, Loading, SvgIcon, TabMenu } from '@/components/Common'; import { RecommendList, ProductSearchResultList, RecipeSearchResultList } from '@/components/Search'; import { SEARCH_TAB_VARIANTS } from '@/constants'; import { useDebounce, useTabMenu } from '@/hooks/common'; diff --git a/frontend/src/pages/MemberModifyPage.tsx b/frontend/src/pages/MemberModifyPage.tsx index c6c43a8c3..4e6624e59 100644 --- a/frontend/src/pages/MemberModifyPage.tsx +++ b/frontend/src/pages/MemberModifyPage.tsx @@ -1,4 +1,4 @@ -import { Button, Spacing } from '@fun-eat/design-system'; +import { Button, Spacing, useToastActionContext } from '@fun-eat/design-system'; import type { ChangeEventHandler, FormEventHandler } from 'react'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -16,6 +16,7 @@ export const MemberModifyPage = () => { const { mutate } = useMemberModifyMutation(); const { previewImage, imageFile, uploadImage } = useImageUploader(); + const { toast } = useToastActionContext(); const [nickname, setNickname] = useState(member?.nickname ?? ''); const navigate = useNavigate(); @@ -43,7 +44,7 @@ export const MemberModifyPage = () => { const imageFile = event.target.files[0]; if (imageFile.size > IMAGE_MAX_SIZE) { - alert('์ด๋ฏธ์ง€ ํฌ๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ์ปค์š”. 5MB ์ดํ•˜์˜ ์ด๋ฏธ์ง€๋ฅผ ๊ณจ๋ผ์ฃผ์„ธ์š”.'); + toast.error('์ด๋ฏธ์ง€ ํฌ๊ธฐ๊ฐ€ ๋„ˆ๋ฌด ์ปค์š”. 5MB ์ดํ•˜์˜ ์ด๋ฏธ์ง€๋ฅผ ๊ณจ๋ผ์ฃผ์„ธ์š”.'); event.target.value = ''; return; } @@ -60,11 +61,11 @@ export const MemberModifyPage = () => { }, onError: (error) => { if (error instanceof Error) { - alert(error.message); + toast.error(error.message); return; } - alert('ํšŒ์›์ •๋ณด ์ˆ˜์ •์„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'); + toast.error('ํšŒ์›์ •๋ณด ์ˆ˜์ •์„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.'); }, }); }; diff --git a/frontend/src/pages/MemberPage.tsx b/frontend/src/pages/MemberPage.tsx index 7026e4768..58aa6d8e4 100644 --- a/frontend/src/pages/MemberPage.tsx +++ b/frontend/src/pages/MemberPage.tsx @@ -20,14 +20,14 @@ export const MemberPage = () => { }> - + }> - + diff --git a/frontend/src/pages/ProductDetailPage.tsx b/frontend/src/pages/ProductDetailPage.tsx index f41985856..5b6bbca20 100644 --- a/frontend/src/pages/ProductDetailPage.tsx +++ b/frontend/src/pages/ProductDetailPage.tsx @@ -1,7 +1,7 @@ -import { BottomSheet, Spacing, useBottomSheet, Text, Link } from '@fun-eat/design-system'; +import { BottomSheet, Spacing, useBottomSheet, Text, Button } from '@fun-eat/design-system'; import { useQueryErrorResetBoundary } from '@tanstack/react-query'; import { useState, useRef, Suspense } from 'react'; -import { useParams, Link as RouterLink } from 'react-router-dom'; +import { useParams, useLocation, useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import { @@ -17,12 +17,13 @@ import { } from '@/components/Common'; import { ProductDetailItem, ProductRecipeList } from '@/components/Product'; import { BestReviewItem, ReviewList, ReviewRegisterForm } from '@/components/Review'; -import { RECIPE_SORT_OPTIONS, REVIEW_SORT_OPTIONS } from '@/constants'; +import { PREVIOUS_PATH_LOCAL_STORAGE_KEY, RECIPE_SORT_OPTIONS, REVIEW_SORT_OPTIONS } from '@/constants'; import { PATH } from '@/constants/path'; import ReviewFormProvider from '@/contexts/ReviewFormContext'; import { useGA, useSortOption, useTabMenu } from '@/hooks/common'; import { useMemberQuery } from '@/hooks/queries/members'; import { useProductDetailQuery } from '@/hooks/queries/product'; +import { setLocalStorage } from '@/utils/localStorage'; const LOGIN_ERROR_MESSAGE_REVIEW = '๋กœ๊ทธ์ธ ํ›„ ์ƒํ’ˆ ๋ฆฌ๋ทฐ๋ฅผ ๋ณผ ์ˆ˜ ์žˆ์–ด์š”.\nํŽ€์ž‡์— ๊ฐ€์ž…ํ•˜๊ณ  ํŽธ์˜์  ์ƒํ’ˆ ๋ฆฌ๋ทฐ๋ฅผ ํ™•์ธํ•ด๋ณด์„ธ์š” ๐Ÿ˜Š'; @@ -31,6 +32,9 @@ const LOGIN_ERROR_MESSAGE_RECIPE = export const ProductDetailPage = () => { const { category, productId } = useParams(); + const { pathname } = useLocation(); + const navigate = useNavigate(); + const { data: member } = useMemberQuery(); const { data: productDetail } = useProductDetailQuery(Number(productId)); @@ -40,17 +44,17 @@ export const ProductDetailPage = () => { const tabRef = useRef(null); const { selectedOption, selectSortOption } = useSortOption(REVIEW_SORT_OPTIONS[0]); - const { ref, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); + const { isOpen, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); const [activeSheet, setActiveSheet] = useState<'registerReview' | 'sortOption'>('sortOption'); const { gaEvent } = useGA(); const productDetailPageRef = useRef(null); - if (!category) { + if (!category || !productId) { return null; } - const { name, bookmark, reviewCount } = productDetail; + const { name, reviewCount } = productDetail; const tabMenus = [`๋ฆฌ๋ทฐ ${reviewCount}`, '๊ฟ€์กฐํ•ฉ']; const sortOptions = isReviewTab ? REVIEW_SORT_OPTIONS : RECIPE_SORT_OPTIONS; @@ -73,9 +77,14 @@ export const ProductDetailPage = () => { selectSortOption(currentSortOption); }; + const handleLoginButtonClick = () => { + setLocalStorage(PREVIOUS_PATH_LOCAL_STORAGE_KEY, pathname); + navigate(PATH.LOGIN); + }; + return ( - + @@ -107,9 +116,15 @@ export const ProductDetailPage = () => { {isReviewTab ? LOGIN_ERROR_MESSAGE_REVIEW : LOGIN_ERROR_MESSAGE_RECIPE} - + ๋กœ๊ทธ์ธํ•˜๋Ÿฌ ๊ฐ€๊ธฐ - + )} @@ -121,7 +136,7 @@ export const ProductDetailPage = () => { /> - + {activeSheet === 'registerReview' ? ( theme.colors.gray4}; - border-radius: 8px; `; const ReviewRegisterButtonWrapper = styled.div` diff --git a/frontend/src/pages/ProductListPage.tsx b/frontend/src/pages/ProductListPage.tsx index 11cf6231b..adaaeff1c 100644 --- a/frontend/src/pages/ProductListPage.tsx +++ b/frontend/src/pages/ProductListPage.tsx @@ -28,7 +28,7 @@ export const ProductListPage = () => { const { category } = useParams(); const productListRef = useRef(null); - const { ref, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); + const { isOpen, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); const { selectedOption, selectSortOption } = useSortOption(PRODUCT_SORT_OPTIONS[0]); const { reset } = useQueryErrorResetBoundary(); const { gaEvent } = useGA(); @@ -68,7 +68,7 @@ export const ProductListPage = () => { - + { const { recipeId } = useParams(); + const scrollTargetRef = useRef(null); + const { data: recipeDetail } = useRecipeDetailQuery(Number(recipeId)); + const { reset } = useQueryErrorResetBoundary(); + const { id, images, title, content, author, products, totalPrice, favoriteCount, favorite, createdAt } = recipeDetail; return ( - + <> {images.length > 0 ? ( @@ -40,7 +46,7 @@ export const RecipeDetailPage = () => { {getFormattedDate(createdAt)}
    - + @@ -65,23 +71,38 @@ export const RecipeDetailPage = () => { {content} - - + + + + + }> +
    + +
    +
    +
    + + + + ); }; -const RecipeDetailPageContainer = styled.div` - padding: 20px 20px 0; -`; - const RecipeImageContainer = styled.ul` display: flex; flex-direction: column; gap: 20px; align-items: center; + + & > li { + width: 312px; + margin: 0 auto; + } `; const RecipeImage = styled.img` + width: 100%; + height: auto; border-radius: 10px; object-fit: cover; `; diff --git a/frontend/src/pages/RecipePage.tsx b/frontend/src/pages/RecipePage.tsx index fe7dc1a36..fb7899a1b 100644 --- a/frontend/src/pages/RecipePage.tsx +++ b/frontend/src/pages/RecipePage.tsx @@ -1,7 +1,7 @@ import { BottomSheet, Heading, Link, Spacing, useBottomSheet } from '@fun-eat/design-system'; import { useQueryErrorResetBoundary } from '@tanstack/react-query'; -import { Suspense, useRef, useState } from 'react'; -import { Link as RouterLink } from 'react-router-dom'; +import { Suspense, useEffect, useRef, useState } from 'react'; +import { Link as RouterLink, useLocation } from 'react-router-dom'; import styled from 'styled-components'; import { @@ -15,10 +15,11 @@ import { SvgIcon, } from '@/components/Common'; import { RecipeList, RecipeRegisterForm } from '@/components/Recipe'; -import { RECIPE_SORT_OPTIONS } from '@/constants'; +import { PREVIOUS_PATH_LOCAL_STORAGE_KEY, RECIPE_SORT_OPTIONS } from '@/constants'; import { PATH } from '@/constants/path'; import RecipeFormProvider from '@/contexts/RecipeFormContext'; import { useGA, useSortOption } from '@/hooks/common'; +import { setLocalStorage } from '@/utils/localStorage'; const RECIPE_PAGE_TITLE = '๐Ÿฏ ๊ฟ€์กฐํ•ฉ'; const REGISTER_RECIPE = '๊ฟ€์กฐํ•ฉ ์ž‘์„ฑํ•˜๊ธฐ'; @@ -27,12 +28,17 @@ const REGISTER_RECIPE_AFTER_LOGIN = '๋กœ๊ทธ์ธ ํ›„ ๊ฟ€์กฐํ•ฉ์„ ์ž‘์„ฑํ•  ์ˆ˜ export const RecipePage = () => { const [activeSheet, setActiveSheet] = useState<'registerRecipe' | 'sortOption'>('sortOption'); const { selectedOption, selectSortOption } = useSortOption(RECIPE_SORT_OPTIONS[0]); - const { ref, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); + const { isOpen, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet(); const { reset } = useQueryErrorResetBoundary(); const { gaEvent } = useGA(); + const { pathname } = useLocation(); const recipeRef = useRef(null); + useEffect(() => { + setLocalStorage(PREVIOUS_PATH_LOCAL_STORAGE_KEY, pathname); + }, []); + const handleOpenRegisterRecipeSheet = () => { setActiveSheet('registerRecipe'); handleOpenBottomSheet(); @@ -72,7 +78,7 @@ export const RecipePage = () => { /> - + {activeSheet === 'sortOption' ? ( { + const { reviewId } = useParams(); + const { data: reviewDetail } = useReviewDetailQuery(Number(reviewId)); + + const { + productName, + categoryType, + productId, + profileImage, + userName, + rating, + createdAt, + rebuy, + image, + tags, + content, + favoriteCount, + } = reviewDetail; + + const theme = useTheme(); + + return ( + + + + + + + +
    + {userName} + + {Array.from({ length: 5 }, (_, index) => ( + + ))} + + {getRelativeDate(createdAt)} + + +
    +
    + {rebuy && ( + + ๐Ÿ˜ ๋˜ ์‚ด๋ž˜์š” + + )} +
    + {image && } + + {content} + + + + {favoriteCount} + + +
    +
    + ); +}; + +const ReviewDetailPageContainer = styled.div` + padding: 20px 20px 0; +`; + +const ReviewItemContainer = styled.div` + display: flex; + flex-direction: column; + row-gap: 20px; +`; + +const ReviewerWrapper = styled.div` + display: flex; + justify-content: space-between; + align-items: center; +`; + +const ReviewerInfoWrapper = styled.div` + display: flex; + align-items: center; + column-gap: 10px; +`; + +const RebuyBadge = styled(Badge)` + font-weight: ${({ theme }) => theme.fontWeights.bold}; +`; + +const ReviewerImage = styled.img` + border: 2px solid ${({ theme }) => theme.colors.primary}; + border-radius: 50%; + object-fit: cover; +`; + +const RatingIconWrapper = styled.div` + display: flex; + align-items: center; + margin-left: -2px; + + & > span { + margin-left: 12px; + } +`; + +const ReviewImage = styled.img` + align-self: center; +`; + +const ReviewContent = styled(Text)` + white-space: pre-wrap; +`; + +const FavoriteWrapper = styled.div` + display: flex; + align-items: center; + padding: 0; + column-gap: 8px; +`; diff --git a/frontend/src/pages/SearchPage.tsx b/frontend/src/pages/SearchPage.tsx index b14c4d6a4..be5cf7971 100644 --- a/frontend/src/pages/SearchPage.tsx +++ b/frontend/src/pages/SearchPage.tsx @@ -1,10 +1,10 @@ -import { Button, Heading, Spacing, Text, useTheme } from '@fun-eat/design-system'; +import { Button, Heading, Spacing, Text, Input, useTheme } from '@fun-eat/design-system'; import { useQueryErrorResetBoundary } from '@tanstack/react-query'; import { Suspense, useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import styled from 'styled-components'; -import { ErrorBoundary, ErrorComponent, Input, Loading, SvgIcon } from '@/components/Common'; +import { ErrorBoundary, ErrorComponent, Loading, SvgIcon } from '@/components/Common'; import { RecommendList, ProductSearchResultList, RecipeSearchResultList } from '@/components/Search'; import { SEARCH_PAGE_VARIANTS } from '@/constants'; import { useDebounce, useRoutePage } from '@/hooks/common'; diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index 0103d0fc7..9f39193e0 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -1,4 +1,4 @@ -import { createBrowserRouter } from 'react-router-dom'; +import { Navigate, createBrowserRouter } from 'react-router-dom'; import App from './App'; @@ -8,6 +8,7 @@ import CategoryProvider from '@/contexts/CategoryContext'; import NotFoundPage from '@/pages/NotFoundPage'; const router = createBrowserRouter([ + /** ๋กœ๊ทธ์ธ์ด ์•ˆ๋˜์—ˆ๋‹ค๋ฉด ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ */ { path: '/', element: ( @@ -15,17 +16,8 @@ const router = createBrowserRouter([ ), - errorElement: , + errorElement: , children: [ - { - path: `${PATH.RECIPE}/:recipeId`, - async lazy() { - const { RecipeDetailPage } = await import( - /* webpackChunkName: "RecipeDetailPage" */ '@/pages/RecipeDetailPage' - ); - return { Component: RecipeDetailPage }; - }, - }, { path: PATH.MEMBER, async lazy() { @@ -62,6 +54,28 @@ const router = createBrowserRouter([ }, ], }, + /** ๋กœ๊ทธ์ธ์ด ์•ˆ๋˜์—ˆ๋‹ค๋ฉด ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•˜๋ฉด์„œ ํ—ค๋”๋งŒ ์žˆ๋Š” ๋ ˆ์ด์•„์›ƒ */ + { + path: '/', + element: ( + + + + ), + errorElement: , + children: [ + { + path: `${PATH.RECIPE}/:recipeId`, + async lazy() { + const { RecipeDetailPage } = await import( + /* webpackChunkName: "RecipeDetailPage" */ '@/pages/RecipeDetailPage' + ); + return { Component: RecipeDetailPage }; + }, + }, + ], + }, + /** ํ—ค๋”์™€ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐ”๊ฐ€ ์žˆ๋Š” ๊ธฐ๋ณธ ๋ ˆ์ด์•„์›ƒ */ { path: '/', element: ( @@ -78,8 +92,18 @@ const router = createBrowserRouter([ return { Component: HomePage }; }, }, + { + path: `${PATH.REVIEW}/:reviewId`, + async lazy() { + const { ReviewDetailPage } = await import( + /* webpackChunkName: "ReviewDetailPage" */ '@/pages/ReviewDetailPage' + ); + return { Component: ReviewDetailPage }; + }, + }, ], }, + /** ํ—ค๋”, ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ชจ๋‘ ์—†๋Š” ๋ ˆ์ด์•„์›ƒ */ { path: '/', element: , @@ -101,6 +125,7 @@ const router = createBrowserRouter([ }, ], }, + /** ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐ” ์—†์ด ํ—ค๋”๋งŒ ์žˆ๋Š” ๋ ˆ์ด์•„์›ƒ */ { path: '/', element: ( @@ -121,6 +146,7 @@ const router = createBrowserRouter([ }, ], }, + /** ๋„ค๋น„๊ฒŒ์ด์…˜๊ณผ ํ—ค๋”(๊ฒ€์ƒ‰ ์•„์ด์ฝ˜์ด ์—†๋Š”)๊ฐ€ ์žˆ๋Š” ๋ ˆ์ด์•„์›ƒ */ { path: '/', element: ( diff --git a/frontend/src/types/banner.ts b/frontend/src/types/banner.ts new file mode 100644 index 000000000..ae3ebad74 --- /dev/null +++ b/frontend/src/types/banner.ts @@ -0,0 +1,5 @@ +export interface Banner { + id: number; + link: string; + image: string; +} diff --git a/frontend/src/types/product.ts b/frontend/src/types/product.ts index 391cb103d..b3389a32f 100644 --- a/frontend/src/types/product.ts +++ b/frontend/src/types/product.ts @@ -17,7 +17,6 @@ export interface ProductDetail { content: string; averageRating: number; reviewCount: number; - bookmark: boolean; tags: Tag[]; } diff --git a/frontend/src/types/ranking.ts b/frontend/src/types/ranking.ts index 9f0707f68..42cfe29ca 100644 --- a/frontend/src/types/ranking.ts +++ b/frontend/src/types/ranking.ts @@ -1,3 +1,4 @@ +import type { CategoryVariant } from './common'; import type { Member } from './member'; import type { Product } from './product'; @@ -10,7 +11,8 @@ export interface ReviewRanking { content: string; rating: number; favoriteCount: number; - categoryType: string; + categoryType: CategoryVariant; + createdAt: string; } export interface RecipeRanking { @@ -19,4 +21,5 @@ export interface RecipeRanking { title: string; author: Member; favoriteCount: number; + createdAt: string; } diff --git a/frontend/src/types/recipe.ts b/frontend/src/types/recipe.ts index 3fec2ba91..da336d7b5 100644 --- a/frontend/src/types/recipe.ts +++ b/frontend/src/types/recipe.ts @@ -39,3 +39,10 @@ export interface RecipeFavoriteRequestBody { type RecipeProductWithPrice = Pick; export type RecipeProduct = Omit; + +export interface Comment { + id: number; + author: Member; + comment: string; + createdAt: string; +} diff --git a/frontend/src/types/response.ts b/frontend/src/types/response.ts index 7ab144bc0..b2935328a 100644 --- a/frontend/src/types/response.ts +++ b/frontend/src/types/response.ts @@ -1,7 +1,7 @@ import type { Product } from './product'; import type { ProductRanking, RecipeRanking, ReviewRanking } from './ranking'; -import type { MemberRecipe, Recipe } from './recipe'; -import type { Review } from './review'; +import type { Comment, MemberRecipe, Recipe } from './recipe'; +import type { Review, ReviewDetail } from './review'; import type { ProductSearchResult, ProductSearchAutocomplete } from './search'; export interface Page { @@ -14,11 +14,11 @@ export interface Page { } export interface CategoryProductResponse { - page: Page; + hasNext: boolean; products: Product[]; } export interface ProductReviewResponse { - page: Page; + hasNext: boolean; reviews: Review[]; } @@ -63,3 +63,9 @@ export interface MemberRecipeResponse { page: Page; recipes: MemberRecipe[]; } + +export interface CommentResponse { + hasNext: boolean; + totalElements: number | null; + comments: Comment[]; +} diff --git a/frontend/src/types/review.ts b/frontend/src/types/review.ts index debf6f65e..dd0273ddf 100644 --- a/frontend/src/types/review.ts +++ b/frontend/src/types/review.ts @@ -1,4 +1,4 @@ -import type { Tag, TagVariants } from './common'; +import type { CategoryVariant, Tag, TagVariants } from './common'; export interface Review { id: number; @@ -14,6 +14,22 @@ export interface Review { favorite: boolean; } +export interface ReviewDetail extends Review { + categoryType: CategoryVariant; + productId: number; + productName: string; +} + +export interface MemberReview { + reviewId: number; + productId: number; + productName: string; + content: string; + rating: number; + favoriteCount: number; + categoryType: CategoryVariant; +} + export interface ReviewTag { tagType: TagVariants; tags: Tag[]; diff --git a/frontend/src/types/search.ts b/frontend/src/types/search.ts index 75d5d816d..78f293f7f 100644 --- a/frontend/src/types/search.ts +++ b/frontend/src/types/search.ts @@ -1,7 +1,8 @@ +import type { CategoryVariant } from './common'; import type { Product } from './product'; export interface ProductSearchResult extends Product { - categoryType: string; + categoryType: CategoryVariant; } export type ProductSearchAutocomplete = Pick; diff --git a/frontend/src/utils/localStorage.ts b/frontend/src/utils/localStorage.ts new file mode 100644 index 000000000..91ce0c762 --- /dev/null +++ b/frontend/src/utils/localStorage.ts @@ -0,0 +1,26 @@ +export const getLocalStorage = (key: string) => { + const item = localStorage.getItem(key); + + if (item) { + try { + return JSON.parse(item); + } catch (error) { + return item; + } + } + + return null; +}; + +export const setLocalStorage = (key: string, newValue: unknown) => { + if (typeof newValue === 'string') { + localStorage.setItem(key, newValue); + return; + } + + localStorage.setItem(key, JSON.stringify(newValue)); +}; + +export const removeLocalStorage = (key: string) => { + localStorage.removeItem(key); +}; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index da508d2b9..b0955c2e1 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1375,10 +1375,10 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9" integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A== -"@fun-eat/design-system@^0.3.13": - version "0.3.13" - resolved "https://registry.yarnpkg.com/@fun-eat/design-system/-/design-system-0.3.13.tgz#fbb48efff05c95883889dff280e118204de6d459" - integrity sha512-+wlTfWAJ3Z0ZmnJ2GyxX+HSQB8eB3g9PY8Blemv8nAk5ppuWbB9UKjnhebNgdtbtq+AN4HezKmbNl1Y+prxcWA== +"@fun-eat/design-system@^0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@fun-eat/design-system/-/design-system-0.4.1.tgz#a486b58e9cc4db2535e5ec5370b711cfc5ca78b3" + integrity sha512-nmAu+H0qTmR161WrOVUD5pE66W7MtZl26+fASMBLZw7al9khTUwXkiI6vMr3sGaglih0TsB0bRSiMxWKtV4DlA== "@humanwhocodes/config-array@^0.11.11": version "0.11.11" @@ -4497,6 +4497,13 @@ browser-assert@^1.2.1: resolved "https://registry.yarnpkg.com/browser-assert/-/browser-assert-1.2.1.tgz#9aaa5a2a8c74685c2ae05bfe46efd606f068c200" integrity sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ== +browser-image-compression@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/browser-image-compression/-/browser-image-compression-2.0.2.tgz#4d5ef8882e9e471d6d923715ceb9034499d14eaa" + integrity sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw== + dependencies: + uzip "0.20201231.0" + browserify-zlib@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" @@ -11068,6 +11075,11 @@ uuid@^9.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== +uzip@0.20201231.0: + version "0.20201231.0" + resolved "https://registry.yarnpkg.com/uzip/-/uzip-0.20201231.0.tgz#9e64b065b9a8ebf26eb7583fe8e77e1d9a15ed14" + integrity sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng== + v8-to-istanbul@^9.0.0, v8-to-istanbul@^9.0.1: version "9.1.2" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.2.tgz#51168df21c8ca01c83285f27316549b2c51a5b46"