diff --git a/src/docs/asciidoc/post-api.adoc b/src/docs/asciidoc/post-api.adoc index 3aefaee..143e53b 100644 --- a/src/docs/asciidoc/post-api.adoc +++ b/src/docs/asciidoc/post-api.adoc @@ -388,4 +388,30 @@ include::{snippetsDir}/loadPostProducts/1/http-response.adoc[] include::{snippetsDir}/loadPostProducts/1/response-fields.adoc[] +--- + + +=== **16. 그루밍 라운지 - 게시글 좋아요 api** + +게시글에 대한 좋아요 생성/삭제를 위한 api입니다. +좋아요가 눌려져 있지 않은 상태에서 호출되면 좋아요가 생성되며, 좋아요가 눌려져 있는 상태에서 api가 호출되면, 기존 좋아요가 삭제됩니다. + + +==== Request +include::{snippetsDir}/createPostLike/1/http-request.adoc[] + +==== Request Path Parameters +include::{snippetsDir}/createPostLike/1/path-parameters.adoc[] + +==== 성공 Response +include::{snippetsDir}/createPostLike/1/http-response.adoc[] + +==== Response Body Fields +include::{snippetsDir}/createPostLike/1/response-fields.adoc[] + +==== 실패 Response +include::{snippetsDir}/createPostLike/2/http-response.adoc[] + + + --- \ No newline at end of file diff --git a/src/main/java/com/ftm/server/adapter/in/web/post/controller/CreatePostLikeController.java b/src/main/java/com/ftm/server/adapter/in/web/post/controller/CreatePostLikeController.java new file mode 100644 index 0000000..8975c36 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/post/controller/CreatePostLikeController.java @@ -0,0 +1,29 @@ +package com.ftm.server.adapter.in.web.post.controller; + +import com.ftm.server.adapter.in.web.post.dto.response.CreatePostLikeResponse; +import com.ftm.server.application.port.in.post.CreatePostLikeUseCase; +import com.ftm.server.common.response.ApiResponse; +import com.ftm.server.common.response.enums.SuccessResponseCode; +import com.ftm.server.infrastructure.security.UserPrincipal; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +public class CreatePostLikeController { + + private final CreatePostLikeUseCase createPostLikeUseCase; + + @PostMapping("/api/posts/{postId}/like") + public ResponseEntity createProductLike( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @PathVariable(name = "postId") Long postId) { + Boolean isCreated = createPostLikeUseCase.execute(userPrincipal.getId(), postId); + return ResponseEntity.ok( + ApiResponse.success(SuccessResponseCode.OK, new CreatePostLikeResponse(isCreated))); + } +} diff --git a/src/main/java/com/ftm/server/adapter/in/web/post/controller/LoadPostDetailController.java b/src/main/java/com/ftm/server/adapter/in/web/post/controller/LoadPostDetailController.java index 49e47e2..459d461 100644 --- a/src/main/java/com/ftm/server/adapter/in/web/post/controller/LoadPostDetailController.java +++ b/src/main/java/com/ftm/server/adapter/in/web/post/controller/LoadPostDetailController.java @@ -6,9 +6,11 @@ import com.ftm.server.application.vo.post.PostDetailVo; import com.ftm.server.common.response.ApiResponse; import com.ftm.server.common.response.enums.SuccessResponseCode; +import com.ftm.server.infrastructure.security.UserPrincipal; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @@ -20,8 +22,12 @@ public class LoadPostDetailController { private final LoadPostDetailUseCase loadPostDetailUseCase; @GetMapping("/api/posts/{postId}") - public ResponseEntity> loadPostDetail(@PathVariable Long postId) { - PostDetailVo vo = loadPostDetailUseCase.execute(FindByIdQuery.of(postId)); + public ResponseEntity> loadPostDetail( + @AuthenticationPrincipal UserPrincipal userPrincipal, @PathVariable Long postId) { + PostDetailVo vo = + loadPostDetailUseCase.execute( + userPrincipal == null ? null : userPrincipal.getId(), + FindByIdQuery.of(postId)); return ResponseEntity.status(HttpStatus.OK) .body(ApiResponse.success(SuccessResponseCode.OK, LoadPostDetailResponse.from(vo))); diff --git a/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/CreatePostLikeResponse.java b/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/CreatePostLikeResponse.java new file mode 100644 index 0000000..aff2e87 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/CreatePostLikeResponse.java @@ -0,0 +1,11 @@ +package com.ftm.server.adapter.in.web.post.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class CreatePostLikeResponse { + + private final Boolean isCreated; +} diff --git a/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/LoadPostDetailResponse.java b/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/LoadPostDetailResponse.java index fc6a798..27fa8f8 100644 --- a/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/LoadPostDetailResponse.java +++ b/src/main/java/com/ftm/server/adapter/in/web/post/dto/response/LoadPostDetailResponse.java @@ -17,6 +17,7 @@ public class LoadPostDetailResponse { private final List hashtags; private final Integer viewCount; private final Integer likeCount; + private final Boolean userLikeYn; @JsonFormat(pattern = "yyyy-MM-dd HH:mm", shape = JsonFormat.Shape.STRING) private final LocalDateTime createdAt; @@ -42,6 +43,7 @@ private LoadPostDetailResponse(PostDetailVo postDetailVo) { this.writer = PostWriterResponse.from(postDetailVo.getUser(), postDetailVo.getUserImage()); this.postProducts = postDetailVo.getProducts().stream().map(PostProductResponse::from).toList(); + this.userLikeYn = postDetailVo.getUserLikeYn(); } public static LoadPostDetailResponse from(PostDetailVo postDetailVo) { 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 bac1d6f..9edf47e 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 @@ -40,7 +40,10 @@ public class PostDomainPersistenceAdapter LoadUserForPostDomainPort, LoadProductLikePort, SaveProductLikePort, - DeleteProductLikePort { + DeleteProductLikePort, + LoadPostLikePort, + SavePostLikePort, + DeletePostLikePort { private final PostRepository postRepository; private final PostImageRepository postImageRepository; @@ -49,6 +52,7 @@ public class PostDomainPersistenceAdapter private final UserRepository userRepository; private final UserImageRepository userImageRepository; private final ProductLikeRepository productLikeRepository; + private final PostLikeRepository postLikeRepository; private final PostMapper postMapper; private final PostImageMapper postImageMapper; @@ -437,4 +441,42 @@ public List findProductLikeByUser( Long userId, List productIds) { return productLikeRepository.findProductLikeByUser(userId, productIds); } + + @Override + public void deletePostLike(Long postLikeId) { + productLikeRepository.deleteById(postLikeId); + } + + @Override + public List findPostLikeByUser(Long userId, List postIds) { + return postLikeRepository.findPostLikeByUser(userId, postIds); + } + + @Override + public LoadPostAndUserLikeVo findPostLikeByUser(Long userId, Long postId) { + return postLikeRepository.findPostLikeByUser(userId, postId); + } + + @Override + public Optional findOneByUserAndPost(Long userId, Long postId) { + return postLikeRepository.findByUserAndAndPost(userId, postId); + } + + @Override + public void savePostLike(PostLike postLike) { + Long postId = postLike.getPost(); + Long userId = postLike.getUser(); + + UserJpaEntity userJpaEntity = + userRepository + .findByIdAndIsDeleted(userId, false) + .orElseThrow(() -> new CustomException(ErrorResponseCode.USER_NOT_FOUND)); + PostJpaEntity postJpaEntity = + postRepository + .findById(postId) + .orElseThrow(() -> new CustomException(ErrorResponseCode.POST_NOT_FOUND)); + + PostLikeJpaEntity postLikeJpaEntity = PostLikeJpaEntity.from(postJpaEntity, userJpaEntity); + postLikeRepository.save(postLikeJpaEntity); + } } diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/model/PostLikeJpaEntity.java b/src/main/java/com/ftm/server/adapter/out/persistence/model/PostLikeJpaEntity.java new file mode 100644 index 0000000..5c3598c --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/persistence/model/PostLikeJpaEntity.java @@ -0,0 +1,36 @@ +package com.ftm.server.adapter.out.persistence.model; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "post_like") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PostLikeJpaEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private PostJpaEntity post; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private UserJpaEntity user; + + @Builder(access = AccessLevel.PRIVATE) + private PostLikeJpaEntity(PostJpaEntity post, UserJpaEntity user) { + this.post = post; + this.user = user; + } + + public static PostLikeJpaEntity from(PostJpaEntity postJpaEntity, UserJpaEntity userJpaEntity) { + return PostLikeJpaEntity.builder().post(postJpaEntity).user(userJpaEntity).build(); + } +} diff --git a/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostLikeRepository.java b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostLikeRepository.java new file mode 100644 index 0000000..f7fdbf1 --- /dev/null +++ b/src/main/java/com/ftm/server/adapter/out/persistence/repository/PostLikeRepository.java @@ -0,0 +1,47 @@ +package com.ftm.server.adapter.out.persistence.repository; + +import com.ftm.server.adapter.out.persistence.model.PostLikeJpaEntity; +import com.ftm.server.application.vo.post.LoadPostAndUserLikeVo; +import io.lettuce.core.dynamic.annotation.Param; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface PostLikeRepository extends JpaRepository { + + @Query( + """ + SELECT new com.ftm.server.application.vo.post.LoadPostAndUserLikeVo( + p.id, + :userId, + (pl.id IS NOT NULL) + ) + FROM PostJpaEntity p + LEFT JOIN PostLikeJpaEntity pl + ON pl.post.id = p.id AND pl.user.id = :userId + WHERE p.id IN :postIds + """) + List findPostLikeByUser( + @Param("userId") Long userId, @Param("postIds") List postIds); + + @Query( + """ + SELECT new com.ftm.server.application.vo.post.LoadPostAndUserLikeVo( + p.id, + :userId, + (pl.id IS NOT NULL) + ) + FROM PostJpaEntity p + LEFT JOIN PostLikeJpaEntity pl + ON pl.post.id = p.id AND pl.user.id = :userId + WHERE p.id = :postId + """) + LoadPostAndUserLikeVo findPostLikeByUser( + @Param("userId") Long userId, @Param("postIds") Long postId); + + @Query("select p.id from PostLikeJpaEntity p where p.user.id =:userId and p.post.id =:postId") + Optional findByUserAndAndPost(@Param("userId") Long userId, @Param("postId") Long postId); +} 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 index 4414d5e..4c66353 100644 --- 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 @@ -30,10 +30,18 @@ public List findAllByPopularity() { .join(postProductJpaEntity.post, postJpaEntity) .fetchOne(); + if (maxValues == null) { + // 데이터 자체가 없으면 빈 리스트 반환 + return List.of(); + } + Integer maxView = maxValues.get(postJpaEntity.viewCount.max()); maxView = maxView == null || maxView == 0 ? 1 : maxView; - Integer maxLike = maxValues.get(postProductJpaEntity.recommendedCount.max()).intValue(); - maxLike = maxLike == null || maxLike == 0 ? 1 : maxLike; + Integer maxLike = + maxValues.get(postProductJpaEntity.recommendedCount.max()) == null + ? 0 + : maxValues.get(postProductJpaEntity.recommendedCount.max()).intValue(); + maxLike = maxLike == 0 ? 1 : maxLike; NumberExpression normalizedScore = postJpaEntity diff --git a/src/main/java/com/ftm/server/application/port/in/post/CreatePostLikeUseCase.java b/src/main/java/com/ftm/server/application/port/in/post/CreatePostLikeUseCase.java new file mode 100644 index 0000000..cb762b1 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/in/post/CreatePostLikeUseCase.java @@ -0,0 +1,8 @@ +package com.ftm.server.application.port.in.post; + +import com.ftm.server.common.annotation.UseCase; + +@UseCase +public interface CreatePostLikeUseCase { + Boolean execute(Long userId, Long postId); +} diff --git a/src/main/java/com/ftm/server/application/port/in/post/LoadPostDetailUseCase.java b/src/main/java/com/ftm/server/application/port/in/post/LoadPostDetailUseCase.java index 9b76b15..c0d5706 100644 --- a/src/main/java/com/ftm/server/application/port/in/post/LoadPostDetailUseCase.java +++ b/src/main/java/com/ftm/server/application/port/in/post/LoadPostDetailUseCase.java @@ -7,5 +7,5 @@ @UseCase public interface LoadPostDetailUseCase { - PostDetailVo execute(FindByIdQuery query); + PostDetailVo execute(Long userId, FindByIdQuery query); } diff --git a/src/main/java/com/ftm/server/application/port/out/persistence/post/DeletePostLikePort.java b/src/main/java/com/ftm/server/application/port/out/persistence/post/DeletePostLikePort.java new file mode 100644 index 0000000..8075cf9 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/out/persistence/post/DeletePostLikePort.java @@ -0,0 +1,9 @@ +package com.ftm.server.application.port.out.persistence.post; + +import com.ftm.server.common.annotation.Port; + +@Port +public interface DeletePostLikePort { + + void deletePostLike(Long postLikeId); +} diff --git a/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostLikePort.java b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostLikePort.java new file mode 100644 index 0000000..60f8900 --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/out/persistence/post/LoadPostLikePort.java @@ -0,0 +1,15 @@ +package com.ftm.server.application.port.out.persistence.post; + +import com.ftm.server.application.vo.post.LoadPostAndUserLikeVo; +import com.ftm.server.common.annotation.Port; +import java.util.List; +import java.util.Optional; + +@Port +public interface LoadPostLikePort { + List findPostLikeByUser(Long userId, List postIds); + + LoadPostAndUserLikeVo findPostLikeByUser(Long userId, Long postId); + + Optional findOneByUserAndPost(Long userId, Long postId); +} diff --git a/src/main/java/com/ftm/server/application/port/out/persistence/post/SavePostLikePort.java b/src/main/java/com/ftm/server/application/port/out/persistence/post/SavePostLikePort.java new file mode 100644 index 0000000..7ccaefe --- /dev/null +++ b/src/main/java/com/ftm/server/application/port/out/persistence/post/SavePostLikePort.java @@ -0,0 +1,9 @@ +package com.ftm.server.application.port.out.persistence.post; + +import com.ftm.server.common.annotation.Port; +import com.ftm.server.domain.entity.PostLike; + +@Port +public interface SavePostLikePort { + void savePostLike(PostLike postLike); +} diff --git a/src/main/java/com/ftm/server/application/service/post/CreatePostLikeService.java b/src/main/java/com/ftm/server/application/service/post/CreatePostLikeService.java new file mode 100644 index 0000000..914a637 --- /dev/null +++ b/src/main/java/com/ftm/server/application/service/post/CreatePostLikeService.java @@ -0,0 +1,53 @@ +package com.ftm.server.application.service.post; + +import com.ftm.server.application.port.in.post.CreatePostLikeUseCase; +import com.ftm.server.application.port.out.persistence.post.*; +import com.ftm.server.application.query.FindByIdQuery; +import com.ftm.server.common.exception.CustomException; +import com.ftm.server.common.response.enums.ErrorResponseCode; +import com.ftm.server.domain.entity.Post; +import com.ftm.server.domain.entity.PostLike; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CreatePostLikeService implements CreatePostLikeUseCase { + + private final UpdatePostPort updatePostPort; + private final LoadPostPort loadPostPort; + private final LoadPostLikePort loadPostLikePort; + private final SavePostLikePort savePostLikePort; + private final DeletePostLikePort deletePostLikePort; + + @Transactional + public Boolean execute(Long userId, Long postId) { + + Post post = + loadPostPort + .loadPost(FindByIdQuery.of(postId)) + .orElseThrow(() -> new CustomException(ErrorResponseCode.POST_NOT_FOUND)); + + // 이미 등록된 좋아요 있는지 확인 + Optional optionalPostLikeId = loadPostLikePort.findOneByUserAndPost(userId, postId); + + // 없으면 좋아요 생성 + if (optionalPostLikeId.isEmpty()) { + PostLike postLike = PostLike.create(postId, userId); + savePostLikePort.savePostLike(postLike); + post.plusLikeCount(); // 게시글 좋아요 숫자 증가 + updatePostPort.updatePost(post); + return true; + } + + // 있으면 삭제 + else { + post.minusLikeCount(); + updatePostPort.updatePost(post); // 게시글 좋아요 숫자 감소 + deletePostLikePort.deletePostLike(optionalPostLikeId.get()); + return false; + } + } +} diff --git a/src/main/java/com/ftm/server/application/service/post/LoadPostDetailService.java b/src/main/java/com/ftm/server/application/service/post/LoadPostDetailService.java index 602677d..7f57671 100644 --- a/src/main/java/com/ftm/server/application/service/post/LoadPostDetailService.java +++ b/src/main/java/com/ftm/server/application/service/post/LoadPostDetailService.java @@ -28,10 +28,11 @@ public class LoadPostDetailService implements LoadPostDetailUseCase { private final LoadUserForPostPort loadUserForPostPort; private final LoadUserImageForPostPort loadUserImageForPostPort; private final UpdatePostPort updatePostPort; + private final LoadPostLikePort loadPostLikePort; @Override @Transactional - public PostDetailVo execute(FindByIdQuery query) { + public PostDetailVo execute(Long userId, FindByIdQuery query) { // 게시글 조회 Post post = loadPostPort @@ -76,7 +77,11 @@ public PostDetailVo execute(FindByIdQuery query) { Collectors.toMap( PostProductImage::getPostProductId, Function.identity())); + Boolean userLikeYn = + userId != null + && loadPostLikePort.findPostLikeByUser(userId, query.getId()).getLikeYn(); + return PostDetailVo.from( - post, user, userImage, postImages, postProducts, postProductImageMap); + post, user, userImage, postImages, postProducts, postProductImageMap, userLikeYn); } } diff --git a/src/main/java/com/ftm/server/application/vo/post/LoadPostAndUserLikeVo.java b/src/main/java/com/ftm/server/application/vo/post/LoadPostAndUserLikeVo.java new file mode 100644 index 0000000..b7a619e --- /dev/null +++ b/src/main/java/com/ftm/server/application/vo/post/LoadPostAndUserLikeVo.java @@ -0,0 +1,12 @@ +package com.ftm.server.application.vo.post; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class LoadPostAndUserLikeVo { + private final Long postId; + private final Long userId; + private final Boolean likeYn; +} diff --git a/src/main/java/com/ftm/server/application/vo/post/PostDetailVo.java b/src/main/java/com/ftm/server/application/vo/post/PostDetailVo.java index ffda1b9..0a9a48d 100644 --- a/src/main/java/com/ftm/server/application/vo/post/PostDetailVo.java +++ b/src/main/java/com/ftm/server/application/vo/post/PostDetailVo.java @@ -22,13 +22,15 @@ public class PostDetailVo { private final UserImage userImage; private final List postImages; private final List products; + private final Boolean userLikeYn; private PostDetailVo( Post post, User user, UserImage userImage, List postImages, - List products) { + List products, + Boolean userLikeYn) { this.postId = post.getId(); this.title = post.getTitle(); this.content = post.getContent(); @@ -41,6 +43,7 @@ private PostDetailVo( this.userImage = userImage; this.postImages = postImages; this.products = products; + this.userLikeYn = userLikeYn; } public static PostDetailVo from( @@ -49,12 +52,14 @@ public static PostDetailVo from( UserImage userImage, List postImages, List postProducts, - Map postProductImageMap) { + Map postProductImageMap, + Boolean userLikeYn) { return new PostDetailVo( post, user, userImage, postImages, - PostProductDetailVo.listFrom(postProducts, postProductImageMap)); + PostProductDetailVo.listFrom(postProducts, postProductImageMap), + userLikeYn); } } diff --git a/src/main/java/com/ftm/server/domain/entity/Post.java b/src/main/java/com/ftm/server/domain/entity/Post.java index 2216656..21d0cd9 100644 --- a/src/main/java/com/ftm/server/domain/entity/Post.java +++ b/src/main/java/com/ftm/server/domain/entity/Post.java @@ -124,4 +124,12 @@ public void validateDeleted() { throw new CustomException(ErrorResponseCode.POST_NOT_FOUND); } } + + public void plusLikeCount() { + this.likeCount += 1; + } + + public void minusLikeCount() { + this.likeCount = this.likeCount >= 1 ? this.likeCount - 1 : 0; + } } diff --git a/src/main/java/com/ftm/server/domain/entity/PostLike.java b/src/main/java/com/ftm/server/domain/entity/PostLike.java new file mode 100644 index 0000000..1d02a3c --- /dev/null +++ b/src/main/java/com/ftm/server/domain/entity/PostLike.java @@ -0,0 +1,21 @@ +package com.ftm.server.domain.entity; + +import lombok.*; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@Builder(access = AccessLevel.PRIVATE) +public class PostLike extends BaseTime { + private Long id; + private Long post; + private Long user; + + public static PostLike create(Long post, Long user) { + return PostLike.builder().post(post).user(user).build(); + } + + public static PostLike create(Long id, Long post, Long user) { + return PostLike.builder().id(id).post(post).user(user).build(); + } +} diff --git a/src/main/java/com/ftm/server/infrastructure/scheduler/SchedulerConfig.java b/src/main/java/com/ftm/server/infrastructure/scheduler/SchedulerConfig.java index 91f4a40..32f80e3 100644 --- a/src/main/java/com/ftm/server/infrastructure/scheduler/SchedulerConfig.java +++ b/src/main/java/com/ftm/server/infrastructure/scheduler/SchedulerConfig.java @@ -3,6 +3,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; @@ -10,6 +11,7 @@ @Slf4j @Configuration @EnableScheduling +@Profile("!test") public class SchedulerConfig { @Bean diff --git a/src/test/java/com/ftm/server/post/CreatePostLikeTest.java b/src/test/java/com/ftm/server/post/CreatePostLikeTest.java new file mode 100644 index 0000000..79cb2ea --- /dev/null +++ b/src/test/java/com/ftm/server/post/CreatePostLikeTest.java @@ -0,0 +1,132 @@ +package com.ftm.server.post; + +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.JsonFieldType.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +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.SavePostRequest; +import com.ftm.server.application.command.post.SavePostCommand; +import com.ftm.server.application.port.out.persistence.post.SavePostPort; +import com.ftm.server.common.response.enums.ErrorResponseCode; +import com.ftm.server.domain.entity.Post; +import com.ftm.server.domain.entity.User; +import java.util.ArrayList; +import java.util.List; +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.mock.web.MockHttpSession; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.restdocs.request.ParameterDescriptor; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +public class CreatePostLikeTest extends BaseTest { + + @Autowired private SavePostPort savePostPort; + + private final ParameterDescriptor pathParameters = + parameterWithName("postId").description("게시글 ID"); + + private final List responseFields = + List.of( + fieldWithPath("status").type(NUMBER).description("응답 상태"), + fieldWithPath("code").type(STRING).description("상태 코드"), + fieldWithPath("message").type(STRING).description("메시지"), + fieldWithPath("data").type(OBJECT).optional().description("응답 데이터"), + fieldWithPath("data.isCreated") + .type(BOOLEAN) + .description("좋아요 생성 여부. true면 좋아요 생성, false 면 좋아요 취소")); + + private ResultActions getResultActions(Long postId, MockHttpSession session) throws Exception { + return mockMvc.perform( + RestDocumentationRequestBuilders.post("/api/posts/{postId}/like", postId) + .session(session)); + } + + private RestDocumentationResultHandler getDocument(Integer identifier) { + return document( + "createPostLike/" + identifier, + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint(), getModifiedHeader()), + pathParameters(pathParameters), + responseFields(responseFields), + resource( + ResourceSnippetParameters.builder() + .tag("유저픽 게시글") + .summary("해시태그 상품 추천 - 게시글 좋아요 api") + .description("해시태그 상품 추천 - 게시글 좋아요 api 입니다.") + .responseFields(responseFields) + .build())); + } + + @Test + @Transactional + @DisplayName("테스트 성공") + public void test1() throws Exception { + // given + SessionAndUser sessionAndUser = createUserAndLoginAndReturnUser(); // 로그인 처리 + + User user = sessionAndUser.user(); + + MockHttpSession session = login("test@gmail.com"); + + // test 용 post 생성 + Post post = + savePostPort.savePost( + Post.create( + SavePostCommand.from( + user.getId(), + new SavePostRequest( + "test-title", + List.of(), + "content", + new ArrayList<>()), + new ArrayList<>(), + new ArrayList<>()))); + + // when + ResultActions resultActions = getResultActions(post.getId(), session); + + // then + resultActions + .andExpect(status().is(HttpStatus.OK.value())) + .andExpect(jsonPath("$.data.isCreated").value(true)); + + // documentation + resultActions.andDo(getDocument(1)); + } + + @Test + @Transactional + @DisplayName("테스트 실패") + public void test2() throws Exception { + // given + SessionAndUser sessionAndUser = createUserAndLoginAndReturnUser(); // 로그인 처리 + + MockHttpSession session = login("test@gmail.com"); + + // when + ResultActions resultActions = getResultActions(1000L, session); + + // then + resultActions + .andExpect(status().is(HttpStatus.NOT_FOUND.value())) + .andExpect(jsonPath("$.code").value(ErrorResponseCode.POST_NOT_FOUND.getCode())); + + // documentation + resultActions.andDo(getDocument(2)); + } +} diff --git a/src/test/java/com/ftm/server/post/LoadPostDetailTest.java b/src/test/java/com/ftm/server/post/LoadPostDetailTest.java index 0a3dc3b..f03b3bd 100644 --- a/src/test/java/com/ftm/server/post/LoadPostDetailTest.java +++ b/src/test/java/com/ftm/server/post/LoadPostDetailTest.java @@ -33,6 +33,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpSession; import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; @@ -64,6 +65,7 @@ public class LoadPostDetailTest extends BaseTest { fieldWithPath("data.hashtags[]").type(ARRAY).description("게시글 해시태그 목록"), fieldWithPath("data.viewCount").type(NUMBER).description("게시글 조회수"), fieldWithPath("data.likeCount").type(NUMBER).description("게시글 좋아요 수"), + fieldWithPath("data.userLikeYn").type(BOOLEAN).description("사용자 게시글 좋아요 여부"), fieldWithPath("data.createdAt").type(STRING).description("게시글 생성 날짜"), fieldWithPath("data.updatedAt").type(STRING).description("게시글 수정 날짜"), fieldWithPath("data.postImages[]").type(ARRAY).description("게시글 이미지 목록 정보"), @@ -102,8 +104,10 @@ public class LoadPostDetailTest extends BaseTest { private Long savedPostId; - private ResultActions getResultActions(Long postId) throws Exception { - return mockMvc.perform(RestDocumentationRequestBuilders.get("/api/posts/{postId}", postId)); + private ResultActions getResultActions(Long postId, MockHttpSession session) throws Exception { + return mockMvc.perform( + RestDocumentationRequestBuilders.get("/api/posts/{postId}", postId) + .session(session)); } private RestDocumentationResultHandler getDocument(Integer identifier) { @@ -123,6 +127,7 @@ private RestDocumentationResultHandler getDocument(Integer identifier) { } @BeforeEach + @Transactional void setUp() throws Exception { User user = createTestUser("test@gmail.com", "test1234!"); @@ -150,8 +155,10 @@ void setUp() throws Exception { @Test @Transactional void 유저픽_게시글_상세_조회_성공() throws Exception { + // when - ResultActions resultActions = getResultActions(savedPostId); + MockHttpSession session = login("test@gmail.com"); + ResultActions resultActions = getResultActions(savedPostId, session); // then resultActions.andExpect(status().isOk()).andDo(print()); @@ -160,11 +167,21 @@ void setUp() throws Exception { resultActions.andDo(getDocument(1)); } + @Test + @Transactional + void 유저픽_게시글_상세_조회_성공2() throws Exception { + // when + ResultActions resultActions = getResultActions(savedPostId, new MockHttpSession()); + + // then + resultActions.andExpect(status().isOk()).andDo(print()); + } + @Test @Transactional void 유저픽_게시글_상세_조회_실패() throws Exception { // when - ResultActions resultActions = getResultActions(1000L); + ResultActions resultActions = getResultActions(1000L, new MockHttpSession()); // then resultActions diff --git a/src/test/java/com/ftm/server/post/LoadPostHashTagsTest.java b/src/test/java/com/ftm/server/post/LoadPostHashTagsTest.java index 19e2fc1..dbf1bfa 100644 --- a/src/test/java/com/ftm/server/post/LoadPostHashTagsTest.java +++ b/src/test/java/com/ftm/server/post/LoadPostHashTagsTest.java @@ -18,6 +18,7 @@ import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; public class LoadPostHashTagsTest extends BaseTest { @@ -68,6 +69,7 @@ private RestDocumentationResultHandler getDocument(Integer identifier) { } @Test + @Transactional void 게시글_해시태그_목록_조회_성공() throws Exception { // when ResultActions resultActions = getResultActions(); diff --git a/src/test/java/com/ftm/server/post/LoadPostProductHashTagsTest.java b/src/test/java/com/ftm/server/post/LoadPostProductHashTagsTest.java index 44dc710..0637dae 100644 --- a/src/test/java/com/ftm/server/post/LoadPostProductHashTagsTest.java +++ b/src/test/java/com/ftm/server/post/LoadPostProductHashTagsTest.java @@ -19,6 +19,7 @@ import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; public class LoadPostProductHashTagsTest extends BaseTest { @@ -70,6 +71,7 @@ private RestDocumentationResultHandler getDocument(Integer identifier) { } @Test + @Transactional void 게시글_상품_해시태그_목록_조회_성공() throws Exception { // when ResultActions resultActions = getResultActions();