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
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package project.flipnote.bookmark.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import project.flipnote.bookmark.controller.docs.BookmarkControllerDocs;
import project.flipnote.bookmark.model.BookmarkResponse;
import project.flipnote.bookmark.model.BookmarkSearchRequest;
import project.flipnote.bookmark.model.BookmarkTargetResponse;
import project.flipnote.bookmark.model.BookmarkTargetType;
import project.flipnote.bookmark.service.BookmarkService;
import project.flipnote.common.model.response.IdResponse;
import project.flipnote.common.model.response.PagingResponse;
import project.flipnote.common.security.dto.AuthPrinciple;

@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/bookmarks/{targetType}")
public class BookmarkController implements BookmarkControllerDocs {

private final BookmarkService bookmarkService;

@PostMapping("/{targetId}")
public ResponseEntity<IdResponse> addBookmark(
@PathVariable("targetType") BookmarkTargetType targetType,
@PathVariable("targetId") Long targetId,
@AuthenticationPrincipal AuthPrinciple authPrinciple
) {
IdResponse res = bookmarkService.addBookmark(authPrinciple.userId(), targetType.toDomainType(), targetId);

return ResponseEntity.status(HttpStatus.CREATED).body(res);
}

@DeleteMapping("/{targetId}")
public ResponseEntity<IdResponse> deleteBookmark(
@PathVariable("targetType") BookmarkTargetType targetType,
@PathVariable("targetId") Long targetId,
@AuthenticationPrincipal AuthPrinciple authPrinciple
) {
IdResponse res = bookmarkService.deleteBookmark(authPrinciple.userId(), targetType.toDomainType(), targetId);

return ResponseEntity.ok(res);
}

@GetMapping
public ResponseEntity<PagingResponse<BookmarkResponse<BookmarkTargetResponse>>> getBookmarks(
@PathVariable("targetType") BookmarkTargetType targetType,
@Valid @ModelAttribute BookmarkSearchRequest req,
@AuthenticationPrincipal AuthPrinciple authPrinciple
) {
PagingResponse<BookmarkResponse<BookmarkTargetResponse>> res
= bookmarkService.getBookmarks(authPrinciple.userId(), targetType.toDomainType(), req);

return ResponseEntity.ok(res);
}
Comment thread
dungbik marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package project.flipnote.bookmark.controller.docs;

import org.springframework.http.ResponseEntity;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import project.flipnote.bookmark.model.BookmarkResponse;
import project.flipnote.bookmark.model.BookmarkSearchRequest;
import project.flipnote.bookmark.model.BookmarkTargetResponse;
import project.flipnote.bookmark.model.BookmarkTargetType;
import project.flipnote.common.model.response.IdResponse;
import project.flipnote.common.model.response.PagingResponse;
import project.flipnote.common.security.dto.AuthPrinciple;

@Tag(name = "Bookmark", description = "Bookmark API")
public interface BookmarkControllerDocs {

@Operation(summary = "즐겨찾기 추가", security = {@SecurityRequirement(name = "access-token")})
ResponseEntity<IdResponse> addBookmark(BookmarkTargetType targetType, Long targetId, AuthPrinciple authPrinciple);

@Operation(summary = "즐겨찾기 제거", security = {@SecurityRequirement(name = "access-token")})
ResponseEntity<IdResponse> deleteBookmark(
BookmarkTargetType targetType,
Long targetId,
AuthPrinciple authPrinciple
);

@Operation(summary = "즐겨찾기 목록 조회", security = {@SecurityRequirement(name = "access-token")})
ResponseEntity<PagingResponse<BookmarkResponse<BookmarkTargetResponse>>> getBookmarks(
BookmarkTargetType targetType,
BookmarkSearchRequest req,
AuthPrinciple authPrinciple
);
}
59 changes: 59 additions & 0 deletions src/main/java/project/flipnote/bookmark/entity/Bookmark.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package project.flipnote.bookmark.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import project.flipnote.common.entity.BaseEntity;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(
name = "bookmarks",
indexes = {
@Index(
name = "idx_bookmarks_targettype_userid_targetid",
columnList = "target_type, user_id, target_id"
)
},
uniqueConstraints = {
@UniqueConstraint(
name = "uk_bookmarks_targettype_userid_targetid",
columnNames = {"target_type", "user_id", "target_id"}
)
}
)
@Entity
public class Bookmark extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private BookmarkTargetType targetType;

@Column(nullable = false)
private Long targetId;

@Column(nullable = false)
private Long userId;

@Builder
public Bookmark(BookmarkTargetType targetType, Long targetId, Long userId) {
this.targetType = targetType;
this.targetId = targetId;
this.userId = userId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package project.flipnote.bookmark.entity;

public enum BookmarkTargetType {
CARD_SET
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package project.flipnote.bookmark.exception;

import org.springframework.http.HttpStatus;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import project.flipnote.common.exception.ErrorCode;

@Getter
@RequiredArgsConstructor
public enum BookmarkErrorCode implements ErrorCode {
BOOKMARK_TARGET_NOT_FOUND(HttpStatus.NOT_FOUND, "BOOKMARK_001", "즐겨찾기 대상이 존재하지 않습니다."),
BOOKMARK_ALREADY_EXISTS(HttpStatus.CONFLICT, "BOOKMARK_002", "이미 즐겨찾기 되어 있습니다."),
BOOKMARK_FETCHER_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, "BOOKMARK_003", "현재 즐겨찾기 할 수 없는 대상입니다."),
BOOKMARK_NOT_EXISTS(HttpStatus.NOT_FOUND, "BOOKMARK_004", "즐겨찾기가 되어 있지 않습니다.");

private final HttpStatus httpStatus;
private final String code;
private final String message;

@Override
public int getStatus() {
return httpStatus.value();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package project.flipnote.bookmark.model;

import java.time.LocalDateTime;

import com.fasterxml.jackson.annotation.JsonFormat;

import lombok.AllArgsConstructor;
import lombok.Data;

@AllArgsConstructor
@Data
public class BookmarkResponse<T extends BookmarkTargetResponse> {
private T target;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime bookmarkedAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package project.flipnote.bookmark.model;

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;

import lombok.Getter;
import lombok.Setter;
import project.flipnote.common.model.request.PagingRequest;

@Getter
@Setter
public class BookmarkSearchRequest extends PagingRequest {

@Override
public PageRequest getPageRequest() {
return PageRequest.of(getPage() - 1, getSize(), Sort.by(Sort.Direction.DESC, "id"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package project.flipnote.bookmark.model;

public abstract class BookmarkTargetResponse {
public abstract Long getId();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package project.flipnote.bookmark.model;

public enum BookmarkTargetType {
card_sets;

public project.flipnote.bookmark.entity.BookmarkTargetType toDomainType() {
return switch (this) {
case card_sets -> project.flipnote.bookmark.entity.BookmarkTargetType.CARD_SET;
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package project.flipnote.bookmark.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import project.flipnote.cardset.model.CardSetSummaryResponse;

@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
@Data
public class CardSetBookmarkResponse extends BookmarkTargetResponse {
private Long id;
private String name;

public static CardSetBookmarkResponse from(CardSetSummaryResponse res) {
return new CardSetBookmarkResponse(res.cardSetId(), res.name());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package project.flipnote.bookmark.repository;

import java.util.Optional;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

import project.flipnote.bookmark.entity.Bookmark;
import project.flipnote.bookmark.entity.BookmarkTargetType;

public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {
boolean existsByTargetTypeAndUserIdAndTargetId(BookmarkTargetType targetType, Long userId, Long targetId);

Optional<Bookmark> findByTargetTypeAndUserIdAndTargetId(BookmarkTargetType targetType, Long userId, Long targetId);

Page<Bookmark> findAllByTargetTypeAndUserId(BookmarkTargetType targetType, Long userId, Pageable pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package project.flipnote.bookmark.service;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import project.flipnote.bookmark.entity.BookmarkTargetType;
import project.flipnote.bookmark.exception.BookmarkErrorCode;
import project.flipnote.bookmark.model.BookmarkTargetResponse;
import project.flipnote.bookmark.repository.BookmarkRepository;
import project.flipnote.common.exception.BizException;

@RequiredArgsConstructor
@Service
public class BookmarkPolicyService {

private final BookmarkRepository bookmarkRepository;
private final BookmarkTargetFetchService<BookmarkTargetResponse> bookmarkTargetFetchService;

public void validateTargetExists(BookmarkTargetType targetType, Long targetId) {
if (!bookmarkTargetFetchService.existsByTypeAndId(targetType, targetId)) {
throw new BizException(BookmarkErrorCode.BOOKMARK_TARGET_NOT_FOUND);
}
}

public void validateBookmarkNotExists(BookmarkTargetType targetType, Long userId, Long targetId) {
if (bookmarkRepository.existsByTargetTypeAndUserIdAndTargetId(targetType, userId, targetId)) {
throw new BizException(BookmarkErrorCode.BOOKMARK_ALREADY_EXISTS);
}
}
}
Loading
Loading