From f13e1b56aa8d5a7de7a92d0b539806b35673152a Mon Sep 17 00:00:00 2001 From: joansdev Date: Mon, 2 Jun 2025 17:52:17 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EC=A2=8B?= =?UTF-8?q?=EC=95=84=EC=9A=94=20=EC=B6=94=EA=B0=80/=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../post/controller/PostController.java | 20 +++++ .../domain/post/dto/PostLikeResponseDTO.java | 22 ++++++ .../domain/post/dto/PostResponseDto.java | 25 ++++-- .../feeda/domain/post/entity/Post.java | 13 ++-- .../post/repository/PostLikeRepository.java | 6 ++ .../domain/post/service/PostService.java | 9 ++- .../domain/post/service/PostServiceImpl.java | 78 +++++++++++++------ 7 files changed, 133 insertions(+), 40 deletions(-) create mode 100644 src/main/java/com/example/feeda/domain/post/dto/PostLikeResponseDTO.java diff --git a/src/main/java/com/example/feeda/domain/post/controller/PostController.java b/src/main/java/com/example/feeda/domain/post/controller/PostController.java index 565dec6..d7a1bf0 100644 --- a/src/main/java/com/example/feeda/domain/post/controller/PostController.java +++ b/src/main/java/com/example/feeda/domain/post/controller/PostController.java @@ -1,5 +1,6 @@ package com.example.feeda.domain.post.controller; +import com.example.feeda.domain.post.dto.PostLikeResponseDTO; import com.example.feeda.domain.post.dto.PostRequestDto; import com.example.feeda.domain.post.dto.PostResponseDto; import com.example.feeda.domain.post.service.PostService; @@ -25,6 +26,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; + @RestController @Validated @RequestMapping("/api/posts") @@ -42,6 +44,24 @@ public ResponseEntity createPost(@RequestBody PostRequestDto re return new ResponseEntity<>(post, HttpStatus.CREATED); } + @PostMapping("/{id}/likes") + public ResponseEntity makeLikes( + @PathVariable Long id, + @AuthenticationPrincipal JwtPayload jwtPayload) { + + Long profileId = jwtPayload.getProfileId(); + return new ResponseEntity<>(postService.makeLikes(id, jwtPayload), HttpStatus.OK); + } + + @DeleteMapping("/{id}/likes") + public ResponseEntity deleteLikes( + @PathVariable Long id, + @AuthenticationPrincipal JwtPayload jwtPayload + ) { + postService.deleteLikes(id, jwtPayload.getProfileId()); + return new ResponseEntity<>(HttpStatus.OK); + } + @GetMapping("/{id}") public ResponseEntity findPostById(@PathVariable @NotNull Long id) { return new ResponseEntity<>(postService.findPostById(id), HttpStatus.OK); diff --git a/src/main/java/com/example/feeda/domain/post/dto/PostLikeResponseDTO.java b/src/main/java/com/example/feeda/domain/post/dto/PostLikeResponseDTO.java new file mode 100644 index 0000000..adba5b0 --- /dev/null +++ b/src/main/java/com/example/feeda/domain/post/dto/PostLikeResponseDTO.java @@ -0,0 +1,22 @@ +package com.example.feeda.domain.post.dto; + +import com.example.feeda.domain.post.entity.PostLike; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +public class PostLikeResponseDTO { + + private final Long id; + private final Long postId; + private final Long profileId; + private final LocalDateTime createdAt; + + public PostLikeResponseDTO(PostLike postLike) { + this.id = postLike.getId(); + this.postId = postLike.getPost().getId(); + this.profileId = postLike.getProfile().getId(); + this.createdAt = postLike.getCreatedAt(); + } +} diff --git a/src/main/java/com/example/feeda/domain/post/dto/PostResponseDto.java b/src/main/java/com/example/feeda/domain/post/dto/PostResponseDto.java index 2989d5e..7bfa9fb 100644 --- a/src/main/java/com/example/feeda/domain/post/dto/PostResponseDto.java +++ b/src/main/java/com/example/feeda/domain/post/dto/PostResponseDto.java @@ -7,6 +7,8 @@ import jakarta.persistence.Id; import lombok.Getter; +import java.time.LocalDateTime; + @Getter public class PostResponseDto { @@ -18,14 +20,23 @@ public class PostResponseDto { private final String category; - public PostResponseDto(Long id, String title, String content, String category) { - this.id = id; - this.title = title; - this.content = content; - this.category = category; + private final Long likes; + + private final LocalDateTime createdAt; + + private final LocalDateTime updatedAt; + + public PostResponseDto(Post post, Long likes) { + this.id = post.getId(); + this.title = post.getTitle(); + this.content = post.getContent(); + this.category = post.getCategory(); + this.likes = likes; + this.createdAt = post.getCreatedAt(); + this.updatedAt = post.getUpdatedAt(); } - public static PostResponseDto toDto(Post post) { - return new PostResponseDto(post.getId(), post.getTitle(), post.getContent(), post.getCategory()); + public static PostResponseDto toDto(Post post, Long likes) { + return new PostResponseDto(post, likes); } } diff --git a/src/main/java/com/example/feeda/domain/post/entity/Post.java b/src/main/java/com/example/feeda/domain/post/entity/Post.java index f1367d6..af81853 100644 --- a/src/main/java/com/example/feeda/domain/post/entity/Post.java +++ b/src/main/java/com/example/feeda/domain/post/entity/Post.java @@ -13,7 +13,6 @@ import jakarta.validation.constraints.Size; import lombok.Getter; - @Getter @Entity @Table(name = "posts") @@ -39,21 +38,19 @@ public class Post extends BaseEntity { @JoinColumn(name = "profile_id") private Profile profile; - public Post(String title, String content, String category, Profile profile) { + public void update(String title, String content, String category) { this.title = title; this.content = content; this.category = category; - this.profile = profile; } - protected Post() { - - } - - public void update(String title, String content, String category) { + public Post(String title, String content, String category, Profile profile) { this.title = title; this.content = content; this.category = category; + this.profile = profile; } + protected Post() { + } } diff --git a/src/main/java/com/example/feeda/domain/post/repository/PostLikeRepository.java b/src/main/java/com/example/feeda/domain/post/repository/PostLikeRepository.java index 55f9c69..799142e 100644 --- a/src/main/java/com/example/feeda/domain/post/repository/PostLikeRepository.java +++ b/src/main/java/com/example/feeda/domain/post/repository/PostLikeRepository.java @@ -1,7 +1,13 @@ package com.example.feeda.domain.post.repository; +import com.example.feeda.domain.post.entity.Post; import com.example.feeda.domain.post.entity.PostLike; +import com.example.feeda.domain.profile.entity.Profile; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface PostLikeRepository extends JpaRepository { + Optional findByPostAndProfile(Post post, Profile profile); + long countByPost(Post post); // PostLike 갯수 } diff --git a/src/main/java/com/example/feeda/domain/post/service/PostService.java b/src/main/java/com/example/feeda/domain/post/service/PostService.java index 40c1f9e..05a8436 100644 --- a/src/main/java/com/example/feeda/domain/post/service/PostService.java +++ b/src/main/java/com/example/feeda/domain/post/service/PostService.java @@ -1,5 +1,6 @@ package com.example.feeda.domain.post.service; +import com.example.feeda.domain.post.dto.PostLikeResponseDTO; import com.example.feeda.domain.post.dto.PostRequestDto; import com.example.feeda.domain.post.dto.PostResponseDto; import com.example.feeda.security.jwt.JwtPayload; @@ -8,7 +9,6 @@ public interface PostService { - PostResponseDto createPost(PostRequestDto postRequestDto, JwtPayload jwtPayload); @@ -21,4 +21,9 @@ PostResponseDto createPost(PostRequestDto postRequestDto, PostResponseDto updatePost(Long id, PostRequestDto requestDto, JwtPayload jwtPayload); void deletePost(Long id, JwtPayload jwtPayload); -} + + PostLikeResponseDTO makeLikes(Long id, JwtPayload jwtPayload); + + void deleteLikes(Long id, Long profileId); + +} \ No newline at end of file diff --git a/src/main/java/com/example/feeda/domain/post/service/PostServiceImpl.java b/src/main/java/com/example/feeda/domain/post/service/PostServiceImpl.java index 7fa17a7..df5d72a 100644 --- a/src/main/java/com/example/feeda/domain/post/service/PostServiceImpl.java +++ b/src/main/java/com/example/feeda/domain/post/service/PostServiceImpl.java @@ -2,15 +2,20 @@ import com.example.feeda.domain.follow.entity.Follows; import com.example.feeda.domain.follow.repository.FollowsRepository; +import com.example.feeda.domain.post.dto.PostLikeResponseDTO; import com.example.feeda.domain.post.dto.PostRequestDto; import com.example.feeda.domain.post.dto.PostResponseDto; import com.example.feeda.domain.post.entity.Post; +import com.example.feeda.domain.post.entity.PostLike; +import com.example.feeda.domain.post.repository.PostLikeRepository; import com.example.feeda.domain.post.repository.PostRepository; import com.example.feeda.domain.profile.entity.Profile; import com.example.feeda.domain.profile.repository.ProfileRepository; import com.example.feeda.security.jwt.JwtPayload; + import java.util.List; import java.util.Optional; + import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -21,30 +26,57 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.server.ResponseStatusException; + @Service @RequiredArgsConstructor public class PostServiceImpl implements PostService { private final PostRepository postRepository; - private final ProfileRepository profileRepository; + private final ProfileRepository profileRepository; // 현재 로그인 사용자 정보를 찾기 위해 필요 private final FollowsRepository followsRepository; + private final PostLikeRepository postLikeRepository; @Override public PostResponseDto createPost(PostRequestDto postRequestDto, JwtPayload jwtPayload) { Profile profile = profileRepository.findById(jwtPayload.getProfileId()) - .orElseThrow( - () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 프로필입니다.")); + .orElseThrow( + () -> new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 프로필입니다.")); Post post = new Post(postRequestDto.getTitle(), postRequestDto.getContent(), - postRequestDto.getCategory(), profile); - + postRequestDto.getCategory(), profile); + Post savedPost = postRepository.save(post); - return new PostResponseDto(savedPost.getId(), - savedPost.getTitle(), - savedPost.getContent(), - savedPost.getCategory()); + return new PostResponseDto(savedPost, 0L); + } + + @Override + @Transactional + public PostLikeResponseDTO makeLikes(Long id, JwtPayload jwtPayload) { + Post post = postRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "게시글이 존재하지 않습니다.")); + Profile profile = profileRepository.findById(jwtPayload.getProfileId()).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "프로필이 존재하지 않습니다.")); + ; + + // 중복 좋아요 방지 + postLikeRepository.findByPostAndProfile(post, profile).ifPresent(like -> { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "이미 좋아요를 눌렀습니다."); + }); + + PostLike savePost = postLikeRepository.save(new PostLike(post, profile)); + + return new PostLikeResponseDTO(savePost); + } + + @Override + public void deleteLikes(Long id, Long profileId) { + Post post = postRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "게시글이 존재하지 않습니다.")); + Profile profile = profileRepository.findById(profileId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 프로필")); + Optional byPostAndProfile = postLikeRepository.findByPostAndProfile(post, profile); + PostLike postLike = byPostAndProfile.get(); + postLikeRepository.delete(postLike); + + } @Override @@ -58,8 +90,9 @@ public PostResponseDto findPostById(Long id) { Post findPost = optionalPost.get(); - return new PostResponseDto(id, findPost.getTitle(), findPost.getContent(), - findPost.getCategory()); + Long likeCount = postLikeRepository.countByPost(findPost); + + return new PostResponseDto(findPost, likeCount); } @Override @@ -67,7 +100,7 @@ public PostResponseDto findPostById(Long id) { public Page findAll(Pageable pageable, String keyword) { return postRepository.findAllByTitleContaining(keyword, pageable) - .map(PostResponseDto::toDto); + .map(post -> PostResponseDto.toDto(post, postLikeRepository.countByPost(post))); } @Override @@ -75,17 +108,17 @@ public Page findAll(Pageable pageable, String keyword) { public Page findFollowingAllPost(Pageable pageable, JwtPayload jwtPayload) { Page followings = followsRepository.findAllByFollowers_Id( - jwtPayload.getProfileId(), pageable); + jwtPayload.getProfileId(), pageable); List followingProfileIds = followings.stream() - .map(following -> following.getFollowings().getId()) - .toList(); + .map(following -> following.getFollowings().getId()) + .toList(); Pageable newPageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), - Sort.Direction.DESC, "updatedAt"); + Sort.Direction.DESC, "updatedAt"); return postRepository.findAllByProfile_IdIn(followingProfileIds, newPageable) - .map(PostResponseDto::toDto); + .map(post -> PostResponseDto.toDto(post, postLikeRepository.countByPost(post))); } @Override @@ -93,7 +126,7 @@ public Page findFollowingAllPost(Pageable pageable, JwtPayload public PostResponseDto updatePost(Long id, PostRequestDto requestDto, JwtPayload jwtPayload) { Post findPost = postRepository.findById(id) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 게시글")); + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 게시글")); if (!findPost.getProfile().getId().equals(jwtPayload.getProfileId())) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "권한이 없습니다."); @@ -102,10 +135,9 @@ public PostResponseDto updatePost(Long id, PostRequestDto requestDto, JwtPayload findPost.update(requestDto.getTitle(), requestDto.getCategory(), requestDto.getCategory()); Post savedPost = postRepository.save(findPost); - return new PostResponseDto(savedPost.getId(), - savedPost.getTitle(), - savedPost.getContent(), - savedPost.getCategory()); + Long likeCount = postLikeRepository.countByPost(findPost); + + return new PostResponseDto(savedPost, likeCount); } @Override @@ -113,7 +145,7 @@ public PostResponseDto updatePost(Long id, PostRequestDto requestDto, JwtPayload public void deletePost(Long id, JwtPayload jwtPayload) { Post findPost = postRepository.findById(id) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 게시글")); + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 게시글")); if (!findPost.getProfile().getId().equals(jwtPayload.getProfileId())) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "권한이 없습니다."); From 9a3b2d9a47d7f1fe8338735eff3eb44757ae6aa1 Mon Sep 17 00:00:00 2001 From: joansdev Date: Mon, 2 Jun 2025 19:22:32 +0900 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=20=EC=A0=9C=EA=B1=B0:=20=EC=A1=B0?= =?UTF-8?q?=EA=B1=B4=EB=B6=80=20+=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/post/controller/PostController.java | 6 ++++-- .../feeda/domain/post/service/PostServiceImpl.java | 13 ++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/feeda/domain/post/controller/PostController.java b/src/main/java/com/example/feeda/domain/post/controller/PostController.java index d7a1bf0..3ad4249 100644 --- a/src/main/java/com/example/feeda/domain/post/controller/PostController.java +++ b/src/main/java/com/example/feeda/domain/post/controller/PostController.java @@ -58,8 +58,10 @@ public ResponseEntity deleteLikes( @PathVariable Long id, @AuthenticationPrincipal JwtPayload jwtPayload ) { - postService.deleteLikes(id, jwtPayload.getProfileId()); - return new ResponseEntity<>(HttpStatus.OK); + Long profileId = jwtPayload.getProfileId(); + postService.deleteLikes(id, profileId); + + return ResponseEntity.ok().build(); } @GetMapping("/{id}") diff --git a/src/main/java/com/example/feeda/domain/post/service/PostServiceImpl.java b/src/main/java/com/example/feeda/domain/post/service/PostServiceImpl.java index df5d72a..8dc4a44 100644 --- a/src/main/java/com/example/feeda/domain/post/service/PostServiceImpl.java +++ b/src/main/java/com/example/feeda/domain/post/service/PostServiceImpl.java @@ -70,13 +70,16 @@ public PostLikeResponseDTO makeLikes(Long id, JwtPayload jwtPayload) { @Override public void deleteLikes(Long id, Long profileId) { - Post post = postRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "게시글이 존재하지 않습니다.")); - Profile profile = profileRepository.findById(profileId).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 프로필")); - Optional byPostAndProfile = postLikeRepository.findByPostAndProfile(post, profile); - PostLike postLike = byPostAndProfile.get(); - postLikeRepository.delete(postLike); + Post post = postRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "게시글이 존재하지 않습니다.")); + Profile profile = profileRepository.findById(profileId) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "존재하지 않는 프로필")); + PostLike postLike = postLikeRepository.findByPostAndProfile(post, profile) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "해당 사용자의 좋아요가 존재하지 않습니다.")); + + postLikeRepository.delete(postLike); } @Override