Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/docs/asciidoc/post-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]



---
Original file line number Diff line number Diff line change
@@ -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)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,8 +22,12 @@ public class LoadPostDetailController {
private final LoadPostDetailUseCase loadPostDetailUseCase;

@GetMapping("/api/posts/{postId}")
public ResponseEntity<ApiResponse<?>> loadPostDetail(@PathVariable Long postId) {
PostDetailVo vo = loadPostDetailUseCase.execute(FindByIdQuery.of(postId));
public ResponseEntity<ApiResponse<?>> 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)));
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class LoadPostDetailResponse {
private final List<String> 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;
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ public class PostDomainPersistenceAdapter
LoadUserForPostDomainPort,
LoadProductLikePort,
SaveProductLikePort,
DeleteProductLikePort {
DeleteProductLikePort,
LoadPostLikePort,
SavePostLikePort,
DeletePostLikePort {

private final PostRepository postRepository;
private final PostImageRepository postImageRepository;
Expand All @@ -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;
Expand Down Expand Up @@ -437,4 +441,42 @@ public List<LoadProductAndUserLikeVo> findProductLikeByUser(
Long userId, List<Long> productIds) {
return productLikeRepository.findProductLikeByUser(userId, productIds);
}

@Override
public void deletePostLike(Long postLikeId) {
productLikeRepository.deleteById(postLikeId);
}

@Override
public List<LoadPostAndUserLikeVo> findPostLikeByUser(Long userId, List<Long> postIds) {
return postLikeRepository.findPostLikeByUser(userId, postIds);
}

@Override
public LoadPostAndUserLikeVo findPostLikeByUser(Long userId, Long postId) {
return postLikeRepository.findPostLikeByUser(userId, postId);
}

@Override
public Optional<Long> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<PostLikeJpaEntity, Long> {

@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<LoadPostAndUserLikeVo> findPostLikeByUser(
@Param("userId") Long userId, @Param("postIds") List<Long> 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<Long> findByUserAndAndPost(@Param("userId") Long userId, @Param("postId") Long postId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,18 @@ public List<ProductIdAndScoreVo> 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<Double> normalizedScore =
postJpaEntity
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
@UseCase
public interface LoadPostDetailUseCase {

PostDetailVo execute(FindByIdQuery query);
PostDetailVo execute(Long userId, FindByIdQuery query);
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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<LoadPostAndUserLikeVo> findPostLikeByUser(Long userId, List<Long> postIds);

LoadPostAndUserLikeVo findPostLikeByUser(Long userId, Long postId);

Optional<Long> findOneByUserAndPost(Long userId, Long postId);
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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<Long> 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;
}
}
}
Loading
Loading