-
Notifications
You must be signed in to change notification settings - Fork 0
[#31] 회차별 좋아요 등록/해제 #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature/30
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package com.creatorhub.controller; | ||
|
|
||
| import com.creatorhub.dto.EpisodeLikeRequest; | ||
| import com.creatorhub.dto.EpisodeLikeResponse; | ||
| import com.creatorhub.security.auth.CustomUserPrincipal; | ||
| import com.creatorhub.service.EpisodeLikeService; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.*; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/api/episodes/likes") | ||
| public class EpisodeLikeController { | ||
| private final EpisodeLikeService episodeLikeService; | ||
|
|
||
| @PostMapping("/create") | ||
| public ResponseEntity<EpisodeLikeResponse> like( | ||
| @AuthenticationPrincipal CustomUserPrincipal principal, | ||
| @Valid @RequestBody EpisodeLikeRequest req | ||
| ) { | ||
| EpisodeLikeResponse episodeLikeResponse | ||
| = episodeLikeService.like(principal.id(), req.episodeId()); | ||
|
|
||
| return ResponseEntity.ok(episodeLikeResponse); | ||
| } | ||
|
|
||
| @DeleteMapping("/delete/{episodeId}") | ||
| public ResponseEntity<EpisodeLikeResponse> unlike( | ||
| @AuthenticationPrincipal CustomUserPrincipal principal, | ||
| @PathVariable Long episodeId | ||
| ) { | ||
| EpisodeLikeResponse episodeLikeResponse | ||
| = episodeLikeService.unlike(principal.id(), episodeId); | ||
|
|
||
| return ResponseEntity.ok(episodeLikeResponse); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.creatorhub.dto; | ||
|
|
||
| import jakarta.validation.constraints.NotNull; | ||
|
|
||
| public record EpisodeLikeRequest( | ||
| @NotNull(message = "episodeId가 존재하지 않습니다.") | ||
| Long episodeId | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.creatorhub.dto; | ||
|
|
||
| public record EpisodeLikeResponse( | ||
| Long episodeId, | ||
| Integer likeCount, | ||
| boolean liked | ||
| ) { | ||
| public static EpisodeLikeResponse of(Long episodeId, Integer likeCount, boolean liked) { | ||
| return new EpisodeLikeResponse(episodeId, likeCount, liked); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package com.creatorhub.entity; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import lombok.AccessLevel; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import org.hibernate.annotations.SQLDelete; | ||
| import org.hibernate.annotations.SQLRestriction; | ||
|
|
||
| @Entity | ||
| @Table( | ||
| name = "episode_like", | ||
| indexes = { | ||
| @Index(name = "idx_episode_like_episode", columnList = "episode_id"), | ||
| @Index(name = "idx_episode_like_member_created_at", columnList = "member_id, created_at") | ||
| } | ||
| ) | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @SQLDelete(sql = "UPDATE episode_like SET deleted_at = NOW() WHERE id = ?") | ||
| @SQLRestriction("deleted_at IS NULL") | ||
| public class EpisodeLike extends BaseTimeEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY, optional = false) | ||
| @JoinColumn(name = "member_id", nullable = false) | ||
| private Member member; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY, optional = false) | ||
| @JoinColumn(name = "episode_id", nullable = false) | ||
| private Episode episode; | ||
|
|
||
| @Builder(access = AccessLevel.PRIVATE) | ||
| private EpisodeLike(Member member, Episode episode) { | ||
| this.member = member; | ||
| this.episode = episode; | ||
| } | ||
|
|
||
| public static EpisodeLike like(Member member, Episode episode) { | ||
| return EpisodeLike.builder() | ||
| .member(member) | ||
| .episode(episode) | ||
| .build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.creatorhub.exception; | ||
|
|
||
| import com.creatorhub.constant.ErrorCode; | ||
|
|
||
| public class AlreadyEpisodeLikeException extends CreatorException { | ||
|
|
||
| public AlreadyEpisodeLikeException() { | ||
| super(ErrorCode.ALREADY_EPISODE_LIKE); | ||
| } | ||
|
|
||
| public AlreadyEpisodeLikeException(String message) { | ||
| super(ErrorCode.ALREADY_EPISODE_LIKE, message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.creatorhub.exception; | ||
|
|
||
| import com.creatorhub.constant.ErrorCode; | ||
| import lombok.Getter; | ||
|
|
||
| @Getter | ||
| public class EpisodeException extends RuntimeException { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. EpisodeException 이게 사용중인 곳이 없어서 사용하는지 한번 확인부탁드려요! |
||
| private final ErrorCode errorCode; | ||
|
|
||
| public EpisodeException(ErrorCode errorCode) { | ||
| super(errorCode.getMessage()); | ||
| this.errorCode = errorCode; | ||
| } | ||
|
|
||
| public EpisodeException(ErrorCode errorCode, String message) { | ||
| super(message); | ||
| this.errorCode = errorCode; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.creatorhub.exception; | ||
|
|
||
| import com.creatorhub.constant.ErrorCode; | ||
|
|
||
| public class EpisodeLikeNotFoundException extends CreatorException { | ||
|
|
||
| public EpisodeLikeNotFoundException() { | ||
| super(ErrorCode.EPISODE_LIKE_NOT_FOUND); | ||
| } | ||
|
|
||
| public EpisodeLikeNotFoundException(String message) { | ||
| super(ErrorCode.EPISODE_LIKE_NOT_FOUND, message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.creatorhub.exception; | ||
|
|
||
| import com.creatorhub.constant.ErrorCode; | ||
|
|
||
| public class EpisodeNotFoundException extends CreatorException { | ||
|
|
||
| public EpisodeNotFoundException() { | ||
| super(ErrorCode.EPISODE_NOT_FOUND); | ||
| } | ||
|
|
||
| public EpisodeNotFoundException(String message) { | ||
| super(ErrorCode.EPISODE_NOT_FOUND, message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| package com.creatorhub.repository; | ||
|
|
||
| import com.creatorhub.entity.EpisodeLike; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
|
|
||
| import java.util.Optional; | ||
|
|
||
| public interface EpisodeLikeRepository extends JpaRepository<EpisodeLike, Long> { | ||
|
|
||
| boolean existsByMemberIdAndEpisodeId(Long memberId, Long episodeId); | ||
|
|
||
| Optional<EpisodeLike> findByMemberIdAndEpisodeId(Long memberId, Long episodeId); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,24 @@ | |
|
|
||
| import com.creatorhub.entity.Episode; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Modifying; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; | ||
|
|
||
| public interface EpisodeRepository extends JpaRepository<Episode, Long> { | ||
| boolean existsByCreationIdAndEpisodeNum(Long creationId, Integer episodeNum); | ||
|
|
||
| @Modifying(clearAutomatically = true, flushAutomatically = true) | ||
| @Query(""" | ||
| UPDATE Episode e | ||
| SET e.likeCount = | ||
| CASE | ||
| WHEN :delta < 0 AND COALESCE(e.likeCount, 0) <= 0 | ||
| THEN 0 | ||
| ELSE COALESCE(e.likeCount, 0) + :delta | ||
| END | ||
| WHERE e.id = :episodeId | ||
| """) | ||
| void updateLikeCountSafely(@Param("episodeId") Long episodeId, | ||
| @Param("delta") int delta); | ||
|
Comment on lines
+12
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. deleted_at IS NULL |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| package com.creatorhub.service; | ||
|
|
||
| import com.creatorhub.dto.EpisodeLikeResponse; | ||
| import com.creatorhub.entity.Episode; | ||
| import com.creatorhub.entity.EpisodeLike; | ||
| import com.creatorhub.entity.Member; | ||
| import com.creatorhub.exception.AlreadyEpisodeLikeException; | ||
| import com.creatorhub.exception.EpisodeLikeNotFoundException; | ||
| import com.creatorhub.exception.EpisodeNotFoundException; | ||
| import com.creatorhub.exception.MemberNotFoundException; | ||
| import com.creatorhub.repository.EpisodeLikeRepository; | ||
| import com.creatorhub.repository.EpisodeRepository; | ||
| import com.creatorhub.repository.MemberRepository; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class EpisodeLikeService { | ||
| private final EpisodeLikeRepository episodeLikeRepository; | ||
| private final EpisodeRepository episodeRepository; | ||
| private final MemberRepository memberRepository; | ||
|
|
||
| @Transactional | ||
| public EpisodeLikeResponse like(Long memberId, Long episodeId) { | ||
| Member member = memberRepository.findById(memberId) | ||
| .orElseThrow(MemberNotFoundException::new); | ||
|
|
||
| Episode episode = episodeRepository.findById(episodeId) | ||
| .orElseThrow(EpisodeNotFoundException::new); | ||
|
|
||
| // 이미 좋아요 | ||
| if (episodeLikeRepository.existsByMemberIdAndEpisodeId(memberId, episodeId)) { | ||
| throw new AlreadyEpisodeLikeException(); | ||
| } | ||
|
|
||
| episodeLikeRepository.save(EpisodeLike.like(member, episode)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. member_id, episode_id 유니크 조건이 있으면 중복 인서트를 방지할 수 있을 것 같아요. 혹은 다른 좋은 방법이 있으면 적용해보면 좋아요. |
||
|
|
||
| episodeRepository.updateLikeCountSafely(episodeId, +1); | ||
|
|
||
| Integer newCount = episodeRepository.findById(episodeId) | ||
| .map(ep -> ep.getLikeCount() == null ? 0 : ep.getLikeCount()) | ||
| .orElse(0); | ||
|
Comment on lines
+42
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기도 굳이 다시 쿼리하지 않아도 업뎅트 성공했다면 likeCount + delta 로 반환해도될것 같아요. |
||
|
|
||
| return EpisodeLikeResponse.of(episodeId, newCount, true); | ||
| } | ||
|
|
||
| @Transactional | ||
| public EpisodeLikeResponse unlike(Long memberId, Long episodeId) { | ||
| EpisodeLike like = episodeLikeRepository.findByMemberIdAndEpisodeId(memberId, episodeId) | ||
| .orElseThrow(EpisodeLikeNotFoundException::new); | ||
|
|
||
| episodeLikeRepository.delete(like); | ||
|
|
||
| episodeRepository.updateLikeCountSafely(episodeId, -1); | ||
|
|
||
| Integer newCount = episodeRepository.findById(episodeId) | ||
| .map(ep -> ep.getLikeCount() == null ? 0 : ep.getLikeCount()) | ||
| .orElse(0); | ||
|
|
||
| return EpisodeLikeResponse.of(episodeId, newCount, false); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기와 같은 리뷰드립니다. https://github.com/f-lab-edu/creatorhub-server/pull/33/files#r2682316808