diff --git a/src/docs/asciidoc/post-api.adoc b/src/docs/asciidoc/post-api.adoc index 57c4c52..d89a00d 100644 --- a/src/docs/asciidoc/post-api.adoc +++ b/src/docs/asciidoc/post-api.adoc @@ -343,4 +343,23 @@ include::{snippetsDir}/loadUserPickAllPopular/1/http-response.adoc[] include::{snippetsDir}/loadUserPickAllPopular/1/response-fields.adoc[] +--- + +=== **14. 해시태그 추천 - "해시태그 기반 상품 조회** + +게시글 조회 api 이지만, Request Body가 필요하여 "POST" 메서드로 구현하였습니다. + +==== Request +include::{snippetsDir}/loadPostProducts/1/http-request.adoc[] + +==== Request Body Parameters +include::{snippetsDir}/loadPostProducts/1/request-fields.adoc[] + +==== 성공 Response +include::{snippetsDir}/loadPostProducts/1/http-response.adoc[] + +==== Response Body Fields +include::{snippetsDir}/loadPostProducts/1/response-fields.adoc[] + + --- \ No newline at end of file diff --git a/src/main/java/com/ftm/server/adapter/in/web/post/controller/LoadProductsByHashTagController.java b/src/main/java/com/ftm/server/adapter/in/web/post/controller/LoadProductsByHashTagController.java new file mode 100644 index 0000000..284979d --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/post/controller/LoadProductsByHashTagController.java @@ -0,0 +1,39 @@ +package com.ftm.server.adapter.in.web.post.controller; + +import com.ftm.server.adapter.in.web.post.dto.request.LoadProductsByHashTagRequest; +import com.ftm.server.adapter.in.web.post.dto.response.LoadProductsByHashTagResponse; +import com.ftm.server.application.port.in.post.LoadProductsByHashTagUseCase; +import com.ftm.server.common.response.ApiResponse; +import com.ftm.server.common.response.enums.SuccessResponseCode; +import com.ftm.server.infrastructure.security.UserPrincipal; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class LoadProductsByHashTagController { + + private final LoadProductsByHashTagUseCase useCase; + + @PostMapping("/api/posts/products") + public ResponseEntity loadProductsByHashTag( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @RequestBody(required = false) LoadProductsByHashTagRequest request) { + + List result = + useCase + .execute( + userPrincipal == null ? null : userPrincipal.getId(), + request == null ? null : request.getHashTagList()) + .stream() + .map(LoadProductsByHashTagResponse::from) + .toList(); + + return ResponseEntity.ok().body(ApiResponse.success(SuccessResponseCode.OK, result)); + } +} diff --git a/src/main/java/com/ftm/server/adapter/in/web/post/dto/request/LoadProductsByHashTagRequest.java b/src/main/java/com/ftm/server/adapter/in/web/post/dto/request/LoadProductsByHashTagRequest.java new file mode 100644 index 0000000..ca66c80 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/post/dto/request/LoadProductsByHashTagRequest.java @@ -0,0 +1,12 @@ +package com.ftm.server.adapter.in.web.post.dto.request; + +import com.ftm.server.domain.enums.ProductHashtag; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class LoadProductsByHashTagRequest { + private final List hashTagList; +} diff --git a/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/LoadProductsByHashTagResponse.java b/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/LoadProductsByHashTagResponse.java new file mode 100644 index 0000000..cfd4a25 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/LoadProductsByHashTagResponse.java @@ -0,0 +1,28 @@ +package com.ftm.server.adapter.in.web.post.dto.response; + +import com.ftm.server.application.vo.post.LoadProductsByHashTagVo; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class LoadProductsByHashTagResponse { + private final Long productId; + private final String productName; + private final String productImage; + private final Boolean likeYn; + private final String brand; + private final Long recommendedCount; + private final Long postId; + + public static LoadProductsByHashTagResponse from(LoadProductsByHashTagVo vo) { + return new LoadProductsByHashTagResponse( + vo.getProductId(), + vo.getProductName(), + vo.getProductImage(), + vo.getLikeYn(), + vo.getBrand(), + vo.getRecommendedCount(), + vo.getPostId()); + } +} diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/adapter/post/PostDomainPersistenceAdapter.java b/src/main/java/com/ftm/server/adapter/out/persistence/adapter/post/PostDomainPersistenceAdapter.java index 34c099c..5c2b875 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/adapter/post/PostDomainPersistenceAdapter.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/adapter/post/PostDomainPersistenceAdapter.java @@ -10,6 +10,7 @@ import com.ftm.server.common.exception.CustomException; import com.ftm.server.common.response.enums.ErrorResponseCode; import com.ftm.server.domain.entity.*; +import com.ftm.server.domain.enums.ProductHashtag; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -36,7 +37,8 @@ public class PostDomainPersistenceAdapter DeletePostProductPort, DeletePostProductImagePort, LoadPostWithBookmarkCountPort, - LoadUserForPostDomainPort { + LoadUserForPostDomainPort, + LoadProductLikePort { private final PostRepository postRepository; private final PostImageRepository postImageRepository; @@ -44,6 +46,7 @@ public class PostDomainPersistenceAdapter private final PostProductImageRepository postProductImageRepository; private final UserRepository userRepository; private final UserImageRepository userImageRepository; + private final ProductLikeRepository productLikeRepository; private final PostMapper postMapper; private final PostImageMapper postImageMapper; @@ -237,6 +240,26 @@ public List loadPostProductsByPostIds(FindByIdsQuery query) { .toList(); } + @Override + public List loadPostProductsByHashTags(FindByProductHashTagsQuery query) { + return postProductRepository + .findByHashtags( + query.getProductHashtagList().stream() + .map(ProductHashtag::name) + .toList() + .toArray(new String[0])) + .stream() + .map(p -> postProductMapper.toDomainEntity(p)) + .toList(); + } + + @Override + public List loadAllPostProduct() { + return postProductRepository.findAllByLatest().stream() + .map(p -> postProductMapper.toDomainEntity(p)) + .toList(); + } + @Override public List loadPostProductImagesByPostProductIds(FindByIdsQuery query) { List postProductImageJpaEntities = @@ -369,4 +392,10 @@ public List loadPostListByUsers(FindByUserIdsQuery query) { public List loadPostAndAuthorName(FindByIdsQuery query) { return userRepository.findUserNameByUserIds(query.getIds()); } + + @Override + public List findProductLikeByUser( + Long userId, List productIds) { + return productLikeRepository.findProductLikeByUser(userId, productIds); + } } diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/model/ProductLikeJpaEntity.java b/src/main/java/com/ftm/server/adapter/out/persistence/model/ProductLikeJpaEntity.java new file mode 100644 index 0000000..509de71 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/persistence/model/ProductLikeJpaEntity.java @@ -0,0 +1,37 @@ +package com.ftm.server.adapter.out.persistence.model; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "product_like") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ProductLikeJpaEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_product_id") + private PostProductJpaEntity postProduct; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private UserJpaEntity user; + + @Builder(access = AccessLevel.PRIVATE) + private ProductLikeJpaEntity(PostProductJpaEntity postProduct, UserJpaEntity user) { + this.postProduct = postProduct; + this.user = user; + } + + public static ProductLikeJpaEntity from( + PostProductJpaEntity postProductJpaEntity, UserJpaEntity userJpaEntity) { + return ProductLikeJpaEntity.builder() + .postProduct(postProductJpaEntity) + .user(userJpaEntity) + .build(); + } +} diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostProductCustomRepository.java b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostProductCustomRepository.java new file mode 100644 index 0000000..69ccdde --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostProductCustomRepository.java @@ -0,0 +1,3 @@ +package com.ftm.server.adapter.out.persistence.repository; + +public interface PostProductCustomRepository {} diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostProductCustomRepositoryImpl.java b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostProductCustomRepositoryImpl.java new file mode 100644 index 0000000..6d318db --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostProductCustomRepositoryImpl.java @@ -0,0 +1,33 @@ +package com.ftm.server.adapter.out.persistence.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class PostProductCustomRepositoryImpl implements PostProductCustomRepository { + + // private final JPAQueryFactory queryFactory; + // + // @Override + // public List findByHashtags(List postHashtagList) { + // + // List targetHashtags = + // List.of(ProductHashtag.values()).stream().map(a->a.name()).toList(); + // + // List result = queryFactory + // .selectFrom(postProductJpaEntity) + // .where( + // Expressions.booleanTemplate( + // "{0} @> cast({1} as product_hashtag[])", + // postProductJpaEntity.hashtags, + // targetHashtags.toArray(new String[0]) + // ) + // ) + // .orderBy(postProductJpaEntity.createdAt.desc()) + // .fetch(); + // + // return result; + // + // } +} diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostProductRepository.java b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostProductRepository.java index aff7e50..60aacb4 100644 --- a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostProductRepository.java +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostProductRepository.java @@ -8,7 +8,8 @@ import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; -public interface PostProductRepository extends JpaRepository { +public interface PostProductRepository + extends JpaRepository, PostProductCustomRepository { List findAllByPost(PostJpaEntity post); @@ -17,4 +18,18 @@ public interface PostProductRepository extends JpaRepository postProductIds); + + @Query("SELECT p FROM PostProductJpaEntity p order by p.createdAt desc ") + List findAllByLatest(); + + @Query( + value = + """ + SELECT * + FROM post_product + WHERE hashtags @> CAST(:hashtags AS product_hashtag[]) + ORDER BY created_at DESC + """, + nativeQuery = true) + List findByHashtags(@Param("hashtags") String[] hashtags); } diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/repository/ProductLikeRepository.java b/src/main/java/com/ftm/server/adapter/out/persistence/repository/ProductLikeRepository.java new file mode 100644 index 0000000..d796f4b --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/ProductLikeRepository.java @@ -0,0 +1,29 @@ +package com.ftm.server.adapter.out.persistence.repository; + +import com.ftm.server.adapter.out.persistence.model.ProductLikeJpaEntity; +import com.ftm.server.application.vo.post.LoadProductAndUserLikeVo; +import io.lettuce.core.dynamic.annotation.Param; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface ProductLikeRepository extends JpaRepository { + + @Query( + """ + SELECT new com.ftm.server.application.vo.post.LoadProductAndUserLikeVo( + p.id, + :userId, + CASE WHEN pl.id IS NOT NULL THEN true ELSE false END + ) + FROM PostProductJpaEntity p + LEFT JOIN ProductLikeJpaEntity pl + ON pl.postProduct.id = p.id + AND pl.user.id = :userId + WHERE p.id in (:postProductIds) + """) + List findProductLikeByUser( + @Param("userId") Long userId, @Param("postProductIds") List postProductIds); +} diff --git a/src/main/java/com/ftm/server/application/port/in/post/LoadProductsByHashTagUseCase.java b/src/main/java/com/ftm/server/application/port/in/post/LoadProductsByHashTagUseCase.java new file mode 100644 index 0000000..e162f44 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/in/post/LoadProductsByHashTagUseCase.java @@ -0,0 +1,12 @@ +package com.ftm.server.application.port.in.post; + +import com.ftm.server.application.vo.post.LoadProductsByHashTagVo; +import com.ftm.server.common.annotation.UseCase; +import com.ftm.server.domain.enums.ProductHashtag; +import java.util.List; + +@UseCase +public interface LoadProductsByHashTagUseCase { + + List execute(Long userId, List hashtagList); +} diff --git a/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostProductPort.java b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostProductPort.java index a7930e1..aff20d1 100644 --- a/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostProductPort.java +++ b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostProductPort.java @@ -2,6 +2,7 @@ import com.ftm.server.application.query.FindByIdsQuery; import com.ftm.server.application.query.FindByPostIdQuery; +import com.ftm.server.application.query.FindByProductHashTagsQuery; import com.ftm.server.common.annotation.Port; import com.ftm.server.domain.entity.PostProduct; import java.util.List; @@ -14,4 +15,8 @@ public interface LoadPostProductPort { List loadPostProductsByIds(FindByIdsQuery query); List loadPostProductsByPostIds(FindByIdsQuery query); + + List loadPostProductsByHashTags(FindByProductHashTagsQuery query); + + List loadAllPostProduct(); } diff --git a/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadProductLikePort.java b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadProductLikePort.java new file mode 100644 index 0000000..4c1d59f --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadProductLikePort.java @@ -0,0 +1,10 @@ +package com.ftm.server.application.port.out.persistence.post; + +import com.ftm.server.application.vo.post.LoadProductAndUserLikeVo; +import com.ftm.server.common.annotation.Port; +import java.util.List; + +@Port +public interface LoadProductLikePort { + List findProductLikeByUser(Long userId, List productIds); +} diff --git a/src/main/java/com/ftm/server/application/query/FindByProductHashTagsQuery.java b/src/main/java/com/ftm/server/application/query/FindByProductHashTagsQuery.java new file mode 100644 index 0000000..e21fc3c --- /dev/null +++ b/src/main/java/com/ftm/server/application/query/FindByProductHashTagsQuery.java @@ -0,0 +1,12 @@ +package com.ftm.server.application.query; + +import com.ftm.server.domain.enums.ProductHashtag; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class FindByProductHashTagsQuery { + List productHashtagList; +} diff --git a/src/main/java/com/ftm/server/application/service/post/LoadProductsByHashTagService.java b/src/main/java/com/ftm/server/application/service/post/LoadProductsByHashTagService.java new file mode 100644 index 0000000..f4ed955 --- /dev/null +++ b/src/main/java/com/ftm/server/application/service/post/LoadProductsByHashTagService.java @@ -0,0 +1,95 @@ +package com.ftm.server.application.service.post; + +import com.ftm.server.application.port.in.post.LoadProductsByHashTagUseCase; +import com.ftm.server.application.port.out.persistence.post.LoadPostProductImagePort; +import com.ftm.server.application.port.out.persistence.post.LoadPostProductPort; +import com.ftm.server.application.port.out.persistence.post.LoadProductLikePort; +import com.ftm.server.application.query.FindByIdsQuery; +import com.ftm.server.application.query.FindByProductHashTagsQuery; +import com.ftm.server.application.vo.post.LoadProductAndUserLikeVo; +import com.ftm.server.application.vo.post.LoadProductsByHashTagVo; +import com.ftm.server.domain.entity.PostProduct; +import com.ftm.server.domain.entity.PostProductImage; +import com.ftm.server.domain.enums.ProductHashtag; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class LoadProductsByHashTagService implements LoadProductsByHashTagUseCase { + + private final LoadPostProductPort loadPostProductPort; + private final LoadProductLikePort loadProductLikePort; + private final LoadPostProductImagePort loadPostProductImagePort; + + @Override + public List execute(Long userId, List hashtagList) { + + // 1. postProduct 해시태그 별 정보 조회 + List postProducts = null; + if (hashtagList == null || hashtagList.isEmpty()) { + postProducts = loadPostProductPort.loadAllPostProduct(); + } else { + postProducts = + loadPostProductPort.loadPostProductsByHashTags( + new FindByProductHashTagsQuery(hashtagList)); + } + + Map postProductMap = + postProducts.stream() + .collect( + Collectors.toMap( + PostProduct::getId, // key: productId + Function.identity() // value: 해당 PostProduct 객체 자체 + )); + + // 2. 대상 product id 추출 + List productIds = postProducts.stream().map(PostProduct::getId).toList(); + + // 3. 좋아요 여부 확인 + List loadProductAndUserLikeVos = null; + if (userId == null) { + loadProductAndUserLikeVos = + productIds.stream() + .map(p -> new LoadProductAndUserLikeVo(p, null, false)) + .toList(); + } else { + loadProductAndUserLikeVos = + loadProductLikePort.findProductLikeByUser(userId, productIds); + } + Map productLikeMap = + loadProductAndUserLikeVos.stream() + .collect( + Collectors.toMap( + LoadProductAndUserLikeVo::getProductId, // key: productId + Function.identity() // value: 해당 PostProduct 객체 자체 + )); + + // 4. 이미지 가져오기 + List postProductImages = + loadPostProductImagePort.loadPostProductImagesByPostProductIds( + FindByIdsQuery.from(productIds)); + + Map productImageMap = + postProductImages.stream() + .collect( + Collectors.toMap( + PostProductImage::getPostProductId, // key: productId + Function.identity() // value: 해당 PostProduct 객체 자체 + )); + + // 5. 결과값 merge + return productIds.stream() + .map( + p -> + LoadProductsByHashTagVo.from( + postProductMap.get(p), + productLikeMap.get(p), + productImageMap.getOrDefault(p, null))) + .toList(); + } +} diff --git a/src/main/java/com/ftm/server/application/vo/post/LoadProductAndUserLikeVo.java b/src/main/java/com/ftm/server/application/vo/post/LoadProductAndUserLikeVo.java new file mode 100644 index 0000000..f47f180 --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/post/LoadProductAndUserLikeVo.java @@ -0,0 +1,12 @@ +package com.ftm.server.application.vo.post; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class LoadProductAndUserLikeVo { + private final Long productId; + private final Long userId; + private final Boolean likeYn; +} diff --git a/src/main/java/com/ftm/server/application/vo/post/LoadProductsByHashTagVo.java b/src/main/java/com/ftm/server/application/vo/post/LoadProductsByHashTagVo.java new file mode 100644 index 0000000..6cb3b22 --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/post/LoadProductsByHashTagVo.java @@ -0,0 +1,35 @@ +package com.ftm.server.application.vo.post; + +import com.ftm.server.common.consts.PropertiesHolder; +import com.ftm.server.domain.entity.PostProduct; +import com.ftm.server.domain.entity.PostProductImage; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public class LoadProductsByHashTagVo { + + private final Long productId; + private final String productName; + private final String brand; + private final Long recommendedCount; + private final Long postId; + private final String productImage; + private final Boolean likeYn; + + public static LoadProductsByHashTagVo from( + PostProduct postProduct, LoadProductAndUserLikeVo vo, PostProductImage imageUrl) { + String productImage = + imageUrl == null ? PropertiesHolder.PRODUCT_DEFAULT_IMAGE : imageUrl.getObjectKey(); + productImage = PropertiesHolder.CDN_PATH + "/" + productImage; + return new LoadProductsByHashTagVo( + postProduct.getId(), + postProduct.getName(), + postProduct.getBrand(), + postProduct.getRecommendedCount(), + postProduct.getPostId(), + productImage, + vo.getLikeYn()); + } +} diff --git a/src/main/java/com/ftm/server/domain/entity/ProductLike.java b/src/main/java/com/ftm/server/domain/entity/ProductLike.java new file mode 100644 index 0000000..3d284f1 --- /dev/null +++ b/src/main/java/com/ftm/server/domain/entity/ProductLike.java @@ -0,0 +1,13 @@ +package com.ftm.server.domain.entity; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ProductLike extends BaseTime { + private Long id; + private Long postProduct; + private Long user; +} diff --git a/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java b/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java index 30c4dac..006dc04 100644 --- a/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java +++ b/src/main/java/com/ftm/server/infrastructure/security/SecurityConfig.java @@ -69,7 +69,8 @@ public class SecurityConfig { "/api/users/social", "/api/grooming/tests/submission", "/api/grooming/tests", - "api/users/me/recover" + "/api/users/me/recover", + "/api/posts/products" }; private static final String[] ANONYMOUS_MATCHERS = {"/docs/**"}; diff --git a/src/test/java/com/ftm/server/post/LoadProductsByHashTagTest.java b/src/test/java/com/ftm/server/post/LoadProductsByHashTagTest.java new file mode 100644 index 0000000..5754f11 --- /dev/null +++ b/src/test/java/com/ftm/server/post/LoadProductsByHashTagTest.java @@ -0,0 +1,152 @@ +package com.ftm.server.post; + +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.hamcrest.Matchers.hasSize; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.JsonFieldType.*; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.ftm.server.BaseTest; +import com.ftm.server.adapter.in.web.post.dto.request.SavePostProductRequest; +import com.ftm.server.adapter.in.web.post.dto.request.SavePostRequest; +import com.ftm.server.application.command.post.SavePostCommand; +import com.ftm.server.application.command.post.SavePostProductCommand; +import com.ftm.server.application.port.out.persistence.post.SavePostPort; +import com.ftm.server.application.port.out.persistence.post.SavePostProductPort; +import com.ftm.server.domain.entity.Post; +import com.ftm.server.domain.entity.PostProduct; +import com.ftm.server.domain.entity.User; +import com.ftm.server.domain.enums.PostHashtag; +import com.ftm.server.domain.enums.ProductHashtag; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.restdocs.snippet.Attributes; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +public class LoadProductsByHashTagTest extends BaseTest { + + @Autowired private SavePostPort savePostPort; + + @Autowired private SavePostProductPort savePostProductPort; + private final List responseFields = + List.of( + fieldWithPath("status").type(NUMBER).description("응답 상태"), + fieldWithPath("code").type(STRING).description("상태 코드"), + fieldWithPath("message").type(STRING).description("메시지"), + fieldWithPath("data") + .type(ARRAY) + .optional() + .description("응답 데이터 : 대상 상품이 없는 경우 빈 배열"), + fieldWithPath("data[].productId").type(NUMBER).description("상품 id"), + fieldWithPath("data[].productName").type(STRING).description("상품 이름"), + fieldWithPath("data[].productImage").type(STRING).description("상품 이미지 url"), + fieldWithPath("data[].likeYn").type(BOOLEAN).description("사용자 추천 버튼 누름 여부"), + fieldWithPath("data[].brand").type(STRING).description("브랜드"), + fieldWithPath("data[].recommendedCount").type(NUMBER).description("추천수"), + fieldWithPath("data[].postId").type(NUMBER).description("상품이 속한 게시글 id")); + + private final List requestFields = + List.of( + fieldWithPath("hashTagList") + .type(ARRAY) + .optional() + .description( + "사용자가 선택한 해시태그 목록. 빈 배열이 전달되거나 field 전체가 전달되지 않을 경우, 전체 상품을 최신순으로 반환함.") + .attributes( + new Attributes.Attribute( + "constraint", + "게시글 상품 해시태그 목록 조회 api response 중 result.$.hashtags.name 값을 전달"))); + + private ResultActions getResultActions() throws Exception { + Map> req = new HashMap<>(); + req.put("hashTagList", List.of(ProductHashtag.HAND_CARE.name())); + return mockMvc.perform( + RestDocumentationRequestBuilders.post("/api/posts/products") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(req))); + } + + private RestDocumentationResultHandler getDocument(Integer identifier) { + return document( + "loadPostProducts/" + identifier, + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint(), getModifiedHeader()), + requestFields(requestFields), + responseFields(responseFields), + resource( + ResourceSnippetParameters.builder() + .tag("해시태그 추천") + .summary("해시태그 추천 - 해시태그 기반 상품 조회 api") + .description("해시태그 기반 상품 조회 api 입니다") + .requestFields(requestFields) + .responseFields(responseFields) + .build())); + } + + @Test + @Transactional + @DisplayName("테스트 성공") + public void test1() throws Exception { + // given + BaseTest.SessionAndUser sessionAndUser = createUserAndLoginAndReturnUser(); // 로그인 처리 + + User user = sessionAndUser.user(); + + // test 용 post 생성 + Post post = + savePostPort.savePost( + Post.create( + SavePostCommand.from( + user.getId(), + new SavePostRequest( + "test1", + List.of( + PostHashtag.SUN_CARE, + PostHashtag.CLEANSING), + "content1", + new ArrayList<>()), + new ArrayList<>(), + new ArrayList<>()))); + + savePostProductPort.savePostProducts( + List.of( + PostProduct.create( + SavePostProductCommand.from( + new SavePostProductRequest( + -1, + "상품 3번", + "이니스프리", + List.of( + ProductHashtag.HAND_CARE, + ProductHashtag.INCENSE))) + .withPostId(post.getId())))); + + // when + ResultActions resultActions = getResultActions(); + + // then + resultActions + .andExpect(status().is(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data", hasSize(1))); + + // documentation + resultActions.andDo(getDocument(1)); + } +}