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: 7 additions & 2 deletions src/main/java/com/creatorhub/constant/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ public enum ErrorCode {
INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, "M003", "비밀번호가 올바르지 않습니다."),

// Creator 관련 에러
ALREADY_CREATOR(HttpStatus.CONFLICT, "C001", "이미 존재하는 작가입니다."),
CREATOR_NOT_FOUND(HttpStatus.NOT_FOUND, "C002", "존재하는 않는 작가입니다."),
ALREADY_CREATOR(HttpStatus.CONFLICT, "MC001", "이미 존재하는 작가입니다."),
CREATOR_NOT_FOUND(HttpStatus.NOT_FOUND, "MC002", "존재하는 않는 작가입니다."),

// Creation 관련 에러
ALREADY_CREATION(HttpStatus.CONFLICT, "C001", "이미 존재하는 작품입니다."),
CREATION_NOT_FOUND(HttpStatus.NOT_FOUND, "C002", "존재하는 않는 작품입니다."),
CREATION_FAVORITE_NOT_FOUND(HttpStatus.NOT_FOUND, "C003", "관심 작품으로 등록되어있지 않습니다."),

// Authorization 관련 에러
ACCESS_DENIED(HttpStatus.FORBIDDEN, "A001", "접근이 제한되었습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.creatorhub.controller;


import com.creatorhub.dto.CreationFavoriteRequest;
import com.creatorhub.dto.CreationFavoriteResponse;
import com.creatorhub.dto.FavoriteCreationItem;
import com.creatorhub.security.auth.CustomUserPrincipal;
import com.creatorhub.service.CreationFavoriteService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/creations/favorites")
public class CreationFavoriteController {
private final CreationFavoriteService creationFavoriteService;

/**
* 관심 등록
*/
@PostMapping("/create")
public ResponseEntity<CreationFavoriteResponse> favorite(
Comment on lines +26 to +27

Choose a reason for hiding this comment

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

url 명에 create 를 빼고, 대신 POST "/api/creations/favorites" 이면 생성으로 약속하는게 어떨까요?
(보통 관례)
아래 delete 도 마찬가지 입니다!

@AuthenticationPrincipal CustomUserPrincipal principal,
@Valid @RequestBody CreationFavoriteRequest req
) {
CreationFavoriteResponse creationFavoriteResponse =
creationFavoriteService.favorite(principal.id(), req.creationId());

return ResponseEntity.ok(creationFavoriteResponse);
}

/**
* 관심 해제
*/
@DeleteMapping("/delete/{creationId}")
public ResponseEntity<CreationFavoriteResponse> unfavorite(
@AuthenticationPrincipal CustomUserPrincipal principal,
@PathVariable Long creationId
) {
CreationFavoriteResponse creationFavoriteResponse =
creationFavoriteService.unfavorite(principal.id(), creationId);

return ResponseEntity.ok(creationFavoriteResponse);
}

/**
* 내 관심작품 목록
*/
@GetMapping("/me")
public ResponseEntity<Page<FavoriteCreationItem>> myFavorites(
@AuthenticationPrincipal CustomUserPrincipal principal,
Pageable pageable
) {
Page<FavoriteCreationItem> favoriteCreationItem =
creationFavoriteService.getMyFavorites(principal.id(), pageable);

return ResponseEntity.ok(favoriteCreationItem);
}
}
9 changes: 9 additions & 0 deletions src/main/java/com/creatorhub/dto/CreationFavoriteRequest.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 CreationFavoriteRequest(
@NotNull(message = "creationId가 존재하지 않습니다.")
Long creationId
) {
}
11 changes: 11 additions & 0 deletions src/main/java/com/creatorhub/dto/CreationFavoriteResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.creatorhub.dto;

public record CreationFavoriteResponse(
Long creationId,
Integer favoriteCount,
boolean favorited
) {
public static CreationFavoriteResponse of(Long creationId, Integer favoriteCount, boolean favorited) {
return new CreationFavoriteResponse(creationId, favoriteCount, favorited);
}
}
13 changes: 13 additions & 0 deletions src/main/java/com/creatorhub/dto/FavoriteCreationItem.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.creatorhub.dto;

import java.time.LocalDateTime;

public record FavoriteCreationItem(
Long creationId,
String title,
String plot,
boolean isPublic,
Integer favoriteCount,
LocalDateTime favoritedAt
) {
}
3 changes: 3 additions & 0 deletions src/main/java/com/creatorhub/entity/Creation.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public class Creation extends BaseEntity {
@Column(nullable = false)
private boolean isPublic;

@Column
private Integer favoriteCount;

// 연재요일은 자주 바뀌지 않으므로 별도의 엔티티가 아닌 @ElementCollection사용
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(
Expand Down
57 changes: 57 additions & 0 deletions src/main/java/com/creatorhub/entity/CreationFavorite.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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 = "creation_favorite",
indexes = {
// 내 관심작품 목록 조회 (최신순)
@Index(
name = "idx_creation_favorite_member_created_at",
columnList = "member_id, created_at"
),
// 작품별 관심자 조회 (알림 대상 추출)
@Index(
name = "idx_creation_favorite_creation",
columnList = "creation_id"
)
}
)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SQLDelete(sql = "UPDATE creation_favorite SET deleted_at = NOW() WHERE id = ?")
@SQLRestriction("deleted_at IS NULL")
public class CreationFavorite 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 = "creation_id", nullable = false)
private Creation creation;

@Builder(access = AccessLevel.PRIVATE)
private CreationFavorite(Member member, Creation creation) {
this.member = member;
this.creation = creation;
}

public static CreationFavorite create(Member member, Creation creation) {
return CreationFavorite.builder()
.member(member)
.creation(creation)
.build();
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/creatorhub/exception/CreationException.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 CreationException extends RuntimeException {
private final ErrorCode errorCode;

public CreationException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}

public CreationException(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 CreationFavoriteNotFoundException extends CreatorException {

Choose a reason for hiding this comment

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

CreationException 을 상속하는게 맞을 것 같아요 🙂


public CreationFavoriteNotFoundException() {
super(ErrorCode.CREATION_FAVORITE_NOT_FOUND);
}

public CreationFavoriteNotFoundException(String message) {
super(ErrorCode.CREATION_FAVORITE_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 CreationNotFoundException extends CreatorException {

public CreationNotFoundException() {
super(ErrorCode.CREATION_NOT_FOUND);
}

public CreationNotFoundException(String message) {
super(ErrorCode.CREATION_NOT_FOUND, message);
}
}
18 changes: 18 additions & 0 deletions src/main/java/com/creatorhub/exception/GlobalExceptionHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,24 @@ public ResponseEntity<ErrorResponse> handleCreatorException(
.body(errorResponse);
}

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

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

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

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


/**
* 요청 데이터 검증 오류 처리 (@Valid 실패)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.creatorhub.repository;

import com.creatorhub.entity.CreationFavorite;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface CreationFavoriteRepository extends JpaRepository<CreationFavorite, Long> {

Optional<CreationFavorite> findByMemberIdAndCreationId(Long memberId, Long creationId);

boolean existsByMemberIdAndCreationId(Long memberId, Long creationId);

Page<CreationFavorite> findByMemberIdOrderByCreatedAtDesc(Long memberId, Pageable pageable);
}
21 changes: 20 additions & 1 deletion src/main/java/com/creatorhub/repository/CreationRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

import com.creatorhub.entity.Creation;
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 CreationRepository extends JpaRepository<Creation, Long> {
public interface CreationRepository extends JpaRepository<Creation, Long> {

// favoriteCount값이 0 아래로 더 이상 감소되지 않도록 처리
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
UPDATE Creation c
SET c.favoriteCount =
CASE
WHEN :delta < 0 AND COALESCE(c.favoriteCount, 0) <= 0
THEN 0
ELSE COALESCE(c.favoriteCount, 0) + :delta
END
WHERE c.id = :creationId
""")
Comment on lines +12 to +22

Choose a reason for hiding this comment

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

이 부분은 왜 이렇게 했는지 한 번 설명해주시면 좋을 것 같아요!
정리한번 해보셨으면 좋겠네요 :)

void updateFavoriteCountSafely(@Param("creationId") Long creationId,
@Param("delta") int delta);
}

Loading