Skip to content

[#145] 좋아요 추가, 삭제 api 구현 #160

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

Merged
merged 7 commits into from
Apr 17, 2023
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
40 changes: 38 additions & 2 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,42 @@ include::{snippets}/bookshelf-controller-slice-test/find-summary-bookshelf-by-us

include::{snippets}/bookshelf-controller-slice-test/find-summary-bookshelf-by-user-id/response-fields.adoc[]

=== 책장 좋아요 추가

==== Request

include::{snippets}/bookshelf-like-controller-slice-test/create-like/http-request.adoc[]

==== Path Parameter

include::{snippets}/bookshelf-like-controller-slice-test/create-like/path-parameters.adoc[]

==== Request Header

include::{snippets}/bookshelf-like-controller-slice-test/create-like/request-headers.adoc[]

==== Response

include::{snippets}/bookshelf-like-controller-test/create-like/http-response.adoc[]

=== 책장 좋아요 취소

==== Request

include::{snippets}/bookshelf-like-controller-slice-test/delete-like/http-request.adoc[]

==== Path Parameter

include::{snippets}/bookshelf-like-controller-slice-test/delete-like/path-parameters.adoc[]

==== Request Header

include::{snippets}/bookshelf-like-controller-slice-test/delete-like/request-headers.adoc[]

==== Response

include::{snippets}/bookshelf-like-controller-test/delete-like/http-response.adoc[]

== Book - 책

=== 책 검색
Expand Down Expand Up @@ -473,7 +509,6 @@ include::{snippets}/book-controller-slice-test/test-find-recent-query/http-respo

include::{snippets}/book-controller-slice-test/test-find-recent-query/response-fields.adoc[]


=== 책 상세 정보

==== Request
Expand Down Expand Up @@ -888,13 +923,14 @@ include::{snippets}/book-group-comment-controller-slice-test/delete-book-group-c

include::{snippets}/book-group-comment-controller-slice-test/delete-book-group-comment_-should-return-ok/path-parameters.adoc[]


=== 모임 검색

==== Request

include::{snippets}/book-group-controller-slice-test/find-all-book-groups-by-query/http-request.adoc[]

==== Request Header

include::{snippets}/book-group-controller-slice-test/find-all-book-groups-by-query/request-headers.adoc[]

==== Request Parameter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.dadok.gaerval.domain.bookshelf.api;

import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
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 com.dadok.gaerval.domain.bookshelf.service.BookshelfLikeService;
import com.dadok.gaerval.global.config.security.UserPrincipal;

import lombok.RequiredArgsConstructor;

@RequestMapping("/api/bookshelves/{bookshelfId}/like")
@RestController
@RequiredArgsConstructor
public class BookshelfLikeController {

private final BookshelfLikeService bookshelfLikeService;

/***
* <Pre>
* 책장 좋아요 추가
* </Pre>
* @param bookshelfId
* @param userPrincipal
* @return status : ok
*/
@PostMapping()
@PreAuthorize(value = "hasAnyRole('ROLE_ADMIN', 'ROLE_USER')")
public ResponseEntity<Void> createLike(@PathVariable Long bookshelfId,
@AuthenticationPrincipal UserPrincipal userPrincipal) {
bookshelfLikeService.createBookshelfLike(userPrincipal.getUserId(), bookshelfId);
return ResponseEntity.ok().build();
}

/***
* <Pre>
* 책장 좋아요 해제
* </Pre>
* @param bookshelfId
* @param userPrincipal
* @return status : ok
*/
@DeleteMapping()
@PreAuthorize(value = "hasAnyRole('ROLE_ADMIN', 'ROLE_USER')")
public ResponseEntity<Void> deleteLike(@PathVariable Long bookshelfId,
@AuthenticationPrincipal UserPrincipal userPrincipal) {
bookshelfLikeService.deleteBookshelfLike(userPrincipal.getUserId(), bookshelfId);
return ResponseEntity.ok().build();
}
Comment on lines +27 to +54
Copy link
Collaborator

Choose a reason for hiding this comment

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

만약에, 집 컴퓨터와 폰으로 좋아요를 안 누른 상태의 같은 페이지를 열고 집 컴퓨터로만 좋아요를 눌렀다고 가정해볼게요.
해당 상태로 폰을 들고 밖으로 나가서 페이지를 본다면 해당 책장에 좋아요가 안눌려있겠죠?
이 때에 다시 좋아요를 누른다면 서버의 좋아요 상태는 어떻게 바뀌어야 할까요?
클라이언트는 무엇을 보고 어떤 메소드를 선택해서 요청을 보내야 할까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

집 컴퓨터와 폰으로 좋아요를 안 누른 상태의 같은 페이지를 열고 집 컴퓨터로만 좋아요를 눌렀다고 가정해볼게요.
해당 상태로 폰을 들고 밖으로 나가서 페이지를 본다면 해당 책장에 좋아요가 안눌려있겠죠?

이부분은 무슨 말씀일까요?

Copy link
Collaborator

Choose a reason for hiding this comment

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

@youngjijang

같은 페이지를 다른 브라우저에서 보고 있을 때,

  • 집브라우저는 좋아요가 눌린상태
  • 모바일 브라우저는 좋아요가 안눌린상태,
    서로 다른 상태를 가지고있을 때 모바일 브라우저로 다시 좋아요를 누른다면
    프론트엔드와 백엔드는 어떤 액션을 취해야 하는지 생각을 여쭤본겁니다!

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.dadok.gaerval.domain.bookshelf.entity;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
Expand Down Expand Up @@ -54,6 +56,10 @@ public class Bookshelf extends BaseTimeColumn {
@OneToMany(mappedBy = "bookshelf", cascade = CascadeType.ALL, orphanRemoval = true)
private final List<BookshelfItem> bookshelfItems = new ArrayList<>();

@JsonManagedReference
@OneToMany(mappedBy = "bookshelf", cascade = CascadeType.PERSIST, orphanRemoval = true)
private final Set<BookshelfLike> bookshelfLikes = new HashSet<>();
Comment on lines +59 to +61
Copy link
Collaborator

Choose a reason for hiding this comment

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

혹시, 좋아요의 개수는 매번 카운트 해서 보여주기로 했나요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

아직 이야기된것은 없으나 그럴거라 예상합니다. 책장 상세 조회 응답에 추가할 예정입니다.

Copy link
Collaborator

Choose a reason for hiding this comment

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

혹시 그렇다면

  • 매번 count 쿼리를 사용할것인지
  • 좋아요 수를 저장할 컬럼을 추가할것인지
    고민해보고 말씀해주세요!


@Column(name = "job_id", nullable = true)
private Long jobId;

Expand All @@ -77,6 +83,11 @@ public void addBookShelfItem(BookshelfItem bookshelfItem) {
bookshelfItems.add(bookshelfItem);
}

public void addBookShelfLike(BookshelfLike bookshelfLike) {
CommonValidator.validateNotnull(bookshelfLike, "bookshelfLike");
bookshelfLikes.add(bookshelfLike);
}

public void changeIsPublic(Boolean isPublic) {
CommonValidator.validateNotnull(isPublic, "isPublic");
this.isPublic = isPublic;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.dadok.gaerval.domain.bookshelf.entity;

import java.util.Objects;

import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;

import com.dadok.gaerval.domain.bookshelf.exception.BookshelfUserNotMatchedException;
import com.dadok.gaerval.domain.user.entity.User;
import com.dadok.gaerval.global.common.JacocoExcludeGenerated;
import com.dadok.gaerval.global.common.entity.BaseTimeColumn;
import com.dadok.gaerval.global.util.CommonValidator;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity(name = "bookshelf_likes")
@Table(
uniqueConstraints = {
@UniqueConstraint(name = "bookshelf_id_user_id_unique_key",
columnNames = {"bookshelf_id", "user_id"})
}
)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class BookshelfLike extends BaseTimeColumn {

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

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false, name = "user_id")
private User user;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false, name = "bookshelf_id")
private Bookshelf bookshelf;

private BookshelfLike(User user, Bookshelf bookshelf) {
CommonValidator.validateNotnull(bookshelf, "bookshelf");
CommonValidator.validateNotnull(user, "user");
validateNotOwner(user, bookshelf);
this.user = user;
this.bookshelf = bookshelf;
bookshelf.addBookShelfLike(this);
}

public static BookshelfLike create(User user, Bookshelf bookshelf) {
return new BookshelfLike(user, bookshelf);
}

private void validateNotOwner(User user, Bookshelf bookshelf) {
if (Objects.equals(bookshelf.getUser().getId(), user.getId())) {
throw new BookshelfUserNotMatchedException();
}
}

@Override
@JacocoExcludeGenerated
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
BookshelfLike that = (BookshelfLike)o;
return user.equals(that.user) && bookshelf.equals(that.bookshelf);
}

@Override
@JacocoExcludeGenerated
public int hashCode() {
return Objects.hash(user, bookshelf);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.dadok.gaerval.domain.bookshelf.exception;

import static com.dadok.gaerval.global.error.ErrorCode.*;

import com.dadok.gaerval.global.error.exception.BusinessException;

public class AlreadyExistsBookshelfLikeException extends BusinessException {

public AlreadyExistsBookshelfLikeException(Long bookshelfId) {
super(ALREADY_EXISTS_BOOKSHELF_LIKE, String.format(ALREADY_EXISTS_BOOKSHELF_LIKE.getMessage(), bookshelfId));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.dadok.gaerval.domain.bookshelf.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;

import com.dadok.gaerval.domain.bookshelf.entity.BookshelfLike;

public interface BookshelfLikeRepository extends JpaRepository<BookshelfLike, Long>, BookshelfLikeSupport {

Optional<BookshelfLike> findByUserIdAndBookshelfId(Long userId, Long bookshelfId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.dadok.gaerval.domain.bookshelf.repository;

public interface BookshelfLikeSupport {

boolean existsLike(Long bookshelfId, Long userId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.dadok.gaerval.domain.bookshelf.repository;

import static com.dadok.gaerval.domain.bookshelf.entity.QBookshelfLike.*;

import com.querydsl.jpa.impl.JPAQueryFactory;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class BookshelfLikeSupportImpl implements BookshelfLikeSupport {

private final JPAQueryFactory query;

@Override
public boolean existsLike(Long bookshelfId, Long userId) {
Integer fetchOne = query.selectOne().from(bookshelfLike)
.where(bookshelfLike.bookshelf.id.eq(bookshelfId), bookshelfLike.user.id.eq(userId))
.fetchFirst();

return fetchOne != null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.dadok.gaerval.domain.bookshelf.service;

public interface BookshelfLikeService {

void createBookshelfLike(Long userId, Long bookshelfId);

void deleteBookshelfLike(Long userId, Long bookshelfId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.dadok.gaerval.domain.bookshelf.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.dadok.gaerval.domain.bookshelf.entity.Bookshelf;
import com.dadok.gaerval.domain.bookshelf.entity.BookshelfLike;
import com.dadok.gaerval.domain.bookshelf.exception.AlreadyExistsBookshelfLikeException;
import com.dadok.gaerval.domain.bookshelf.repository.BookshelfLikeRepository;
import com.dadok.gaerval.domain.user.entity.User;
import com.dadok.gaerval.domain.user.service.UserService;
import com.dadok.gaerval.global.error.exception.ResourceNotfoundException;

import lombok.RequiredArgsConstructor;

@Service
@Transactional
@RequiredArgsConstructor
public class DefaultBookshelfLikeService implements BookshelfLikeService {

private final BookshelfLikeRepository bookshelfLikeRepository;

private final UserService userService;

private final BookshelfService bookshelfService;

@Override
public void createBookshelfLike(Long userId, Long bookshelfId) {
User user = userService.getById(userId);
Bookshelf bookshelf = bookshelfService.getById(bookshelfId);
if (bookshelfLikeRepository.existsLike(bookshelfId, userId)) {
throw new AlreadyExistsBookshelfLikeException(bookshelf.getId());
}
bookshelfLikeRepository.save(BookshelfLike.create(user, bookshelf));
}

@Override
public void deleteBookshelfLike(Long userId, Long bookshelfId) {
BookshelfLike bookshelfLike = bookshelfLikeRepository.findByUserIdAndBookshelfId(userId, bookshelfId)
.orElseThrow(() -> new ResourceNotfoundException(BookshelfLike.class));
bookshelfLikeRepository.deleteById(bookshelfLike.getId());
}
}
4 changes: 2 additions & 2 deletions src/main/java/com/dadok/gaerval/global/error/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public enum ErrorCode {
ALREADY_CONTAIN_BOOKSHELF_ITEM(HttpStatus.BAD_REQUEST, "BS1", "이미 책장에 포함된 아아템입니다."),
BOOKSHELF_USER_NOT_MATCHED(HttpStatus.UNAUTHORIZED, "BS2", "해당 책장에 올바르지 않은 사용자 접근입니다."),

ALREADY_EXISTS_BOOKSHELF_LIKE(HttpStatus.BAD_REQUEST, "BSL1", "해당 책장에 대한 좋아요가 이미 존재합니다. 책장 id : %s"),

// Book
BOOK_DATA_INVALID(HttpStatus.INTERNAL_SERVER_ERROR, "B1", "잘못된 도서 데이터 형식입니다."),
BOOK_API_NOT_AVAILABLE(HttpStatus.INTERNAL_SERVER_ERROR, "B2", "도서 API를 호출할 수 없습니다."),
Expand Down Expand Up @@ -73,8 +75,6 @@ public enum ErrorCode {
KAKAO_TIMEOUT(HttpStatus.REQUEST_TIMEOUT, "603", "카카오 플랫폼 내부에서 요청 처리 중 타임아웃이 발생한 경우"),
KAKAO_SERVICE_CHECK(HttpStatus.SERVICE_UNAVAILABLE, "9798", "서비스 점검중");



private final HttpStatus status;

private final String code;
Expand Down
Loading