Skip to content
Open
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
9 changes: 8 additions & 1 deletion src/main/java/com/creatorhub/constant/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ public enum ErrorCode {
// Creation 관련 에러
ALREADY_CREATION(HttpStatus.CONFLICT, "C001", "이미 존재하는 작품입니다."),
CREATION_NOT_FOUND(HttpStatus.NOT_FOUND, "C002", "존재하는 않는 작품입니다."),
CREATION_FAVORITE_NOT_FOUND(HttpStatus.NOT_FOUND, "C003", "관심 작품으로 등록되어있지 않습니다."),
ALREADY_CREATION_FAVORITE(HttpStatus.CONFLICT, "C003", "이미 관심작품으로 등록한 작품입니다."),
CREATION_FAVORITE_NOT_FOUND(HttpStatus.NOT_FOUND, "C004", "관심 작품으로 등록되어있지 않습니다."),

// Episode 관련 에러
ALREADY_EPISODE(HttpStatus.CONFLICT, "E001", "이미 존재하는 회차입니다."),
EPISODE_NOT_FOUND(HttpStatus.NOT_FOUND, "E002", "존재하는 않는 회차입니다."),
ALREADY_EPISODE_LIKE(HttpStatus.CONFLICT, "E003", "이미 '좋아요'한 회차입니다."),
EPISODE_LIKE_NOT_FOUND(HttpStatus.NOT_FOUND, "E004", "'좋아요'를 하지 않은 회차입니다."),

// Authorization 관련 에러
ACCESS_DENIED(HttpStatus.FORBIDDEN, "A001", "접근이 제한되었습니다."),
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/com/creatorhub/controller/EpisodeLikeController.java
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
Comment on lines +19 to +33

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

) {
EpisodeLikeResponse episodeLikeResponse
= episodeLikeService.unlike(principal.id(), episodeId);

return ResponseEntity.ok(episodeLikeResponse);
}
}
9 changes: 9 additions & 0 deletions src/main/java/com/creatorhub/dto/EpisodeLikeRequest.java
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
) {
}
11 changes: 11 additions & 0 deletions src/main/java/com/creatorhub/dto/EpisodeLikeResponse.java
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);
}
}
49 changes: 49 additions & 0 deletions src/main/java/com/creatorhub/entity/EpisodeLike.java
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);
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/creatorhub/exception/EpisodeException.java
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 {

Choose a reason for hiding this comment

The 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
Expand Up @@ -53,7 +53,7 @@ public ResponseEntity<ErrorResponse> handleCreatorException(
}

/**
* Creator 관련 예외 처리
* Creation 관련 예외 처리
*/
@ExceptionHandler(CreationException.class)
public ResponseEntity<ErrorResponse> handleCreationException(
Expand All @@ -70,6 +70,24 @@ public ResponseEntity<ErrorResponse> handleCreationException(
.body(errorResponse);
}

/**
* Episode 관련 예외 처리
*/
@ExceptionHandler(EpisodeException.class)
public ResponseEntity<ErrorResponse> handleEpisodeException(
EpisodeException ex,
HttpServletRequest request) {

log.warn("EpisodeException occurred - Message: {}", ex.getMessage());

ErrorResponse errorResponse =
ErrorResponse.of(ex.getErrorCode(), request.getRequestURI());

return ResponseEntity
.status(ex.getErrorCode().getHttpStatus())
.body(errorResponse);
}


/**
* 요청 데이터 검증 오류 처리 (@Valid 실패)
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/com/creatorhub/repository/EpisodeLikeRepository.java
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);
}
17 changes: 17 additions & 0 deletions src/main/java/com/creatorhub/repository/EpisodeRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deleted_at IS NULL
조건은 필요없을지 고민이 필요해보여요!

}
64 changes: 64 additions & 0 deletions src/main/java/com/creatorhub/service/EpisodeLikeService.java
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));

Choose a reason for hiding this comment

The 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

Choose a reason for hiding this comment

The 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);
}
}