Skip to content

Commit 2cc1d12

Browse files
authored
Merge pull request #160 from prgrms-web-devcourse/bookshelf/likes
[#145] μ’‹μ•„μš” μΆ”κ°€, μ‚­μ œ api κ΅¬ν˜„
2 parents 1e9a234 + 55c9dc6 commit 2cc1d12

17 files changed

+615
-5
lines changed

β€Žsrc/docs/asciidoc/index.adoc

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,42 @@ include::{snippets}/bookshelf-controller-slice-test/find-summary-bookshelf-by-us
427427

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

430+
=== μ±…μž₯ μ’‹μ•„μš” μΆ”κ°€
431+
432+
==== Request
433+
434+
include::{snippets}/bookshelf-like-controller-slice-test/create-like/http-request.adoc[]
435+
436+
==== Path Parameter
437+
438+
include::{snippets}/bookshelf-like-controller-slice-test/create-like/path-parameters.adoc[]
439+
440+
==== Request Header
441+
442+
include::{snippets}/bookshelf-like-controller-slice-test/create-like/request-headers.adoc[]
443+
444+
==== Response
445+
446+
include::{snippets}/bookshelf-like-controller-test/create-like/http-response.adoc[]
447+
448+
=== μ±…μž₯ μ’‹μ•„μš” μ·¨μ†Œ
449+
450+
==== Request
451+
452+
include::{snippets}/bookshelf-like-controller-slice-test/delete-like/http-request.adoc[]
453+
454+
==== Path Parameter
455+
456+
include::{snippets}/bookshelf-like-controller-slice-test/delete-like/path-parameters.adoc[]
457+
458+
==== Request Header
459+
460+
include::{snippets}/bookshelf-like-controller-slice-test/delete-like/request-headers.adoc[]
461+
462+
==== Response
463+
464+
include::{snippets}/bookshelf-like-controller-test/delete-like/http-response.adoc[]
465+
430466
== Book - μ±…
431467

432468
=== μ±… 검색
@@ -473,7 +509,6 @@ include::{snippets}/book-controller-slice-test/test-find-recent-query/http-respo
473509

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

476-
477512
=== μ±… 상세 정보
478513

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

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

891-
892926
=== λͺ¨μž„ 검색
893927

894928
==== Request
929+
895930
include::{snippets}/book-group-controller-slice-test/find-all-book-groups-by-query/http-request.adoc[]
896931

897932
==== Request Header
933+
898934
include::{snippets}/book-group-controller-slice-test/find-all-book-groups-by-query/request-headers.adoc[]
899935

900936
==== Request Parameter
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.dadok.gaerval.domain.bookshelf.api;
2+
3+
import org.springframework.http.ResponseEntity;
4+
import org.springframework.security.access.prepost.PreAuthorize;
5+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
6+
import org.springframework.web.bind.annotation.DeleteMapping;
7+
import org.springframework.web.bind.annotation.PathVariable;
8+
import org.springframework.web.bind.annotation.PostMapping;
9+
import org.springframework.web.bind.annotation.RequestMapping;
10+
import org.springframework.web.bind.annotation.RestController;
11+
12+
import com.dadok.gaerval.domain.bookshelf.service.BookshelfLikeService;
13+
import com.dadok.gaerval.global.config.security.UserPrincipal;
14+
15+
import lombok.RequiredArgsConstructor;
16+
17+
@RequestMapping("/api/bookshelves/{bookshelfId}/like")
18+
@RestController
19+
@RequiredArgsConstructor
20+
public class BookshelfLikeController {
21+
22+
private final BookshelfLikeService bookshelfLikeService;
23+
24+
/***
25+
* <Pre>
26+
* μ±…μž₯ μ’‹μ•„μš” μΆ”κ°€
27+
* </Pre>
28+
* @param bookshelfId
29+
* @param userPrincipal
30+
* @return status : ok
31+
*/
32+
@PostMapping()
33+
@PreAuthorize(value = "hasAnyRole('ROLE_ADMIN', 'ROLE_USER')")
34+
public ResponseEntity<Void> createLike(@PathVariable Long bookshelfId,
35+
@AuthenticationPrincipal UserPrincipal userPrincipal) {
36+
bookshelfLikeService.createBookshelfLike(userPrincipal.getUserId(), bookshelfId);
37+
return ResponseEntity.ok().build();
38+
}
39+
40+
/***
41+
* <Pre>
42+
* μ±…μž₯ μ’‹μ•„μš” ν•΄μ œ
43+
* </Pre>
44+
* @param bookshelfId
45+
* @param userPrincipal
46+
* @return status : ok
47+
*/
48+
@DeleteMapping()
49+
@PreAuthorize(value = "hasAnyRole('ROLE_ADMIN', 'ROLE_USER')")
50+
public ResponseEntity<Void> deleteLike(@PathVariable Long bookshelfId,
51+
@AuthenticationPrincipal UserPrincipal userPrincipal) {
52+
bookshelfLikeService.deleteBookshelfLike(userPrincipal.getUserId(), bookshelfId);
53+
return ResponseEntity.ok().build();
54+
}
55+
}

β€Žsrc/main/java/com/dadok/gaerval/domain/bookshelf/entity/Bookshelf.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.dadok.gaerval.domain.bookshelf.entity;
22

33
import java.util.ArrayList;
4+
import java.util.HashSet;
45
import java.util.List;
56
import java.util.Objects;
7+
import java.util.Set;
68

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

59+
@JsonManagedReference
60+
@OneToMany(mappedBy = "bookshelf", cascade = CascadeType.PERSIST, orphanRemoval = true)
61+
private final Set<BookshelfLike> bookshelfLikes = new HashSet<>();
62+
5763
@Column(name = "job_id", nullable = true)
5864
private Long jobId;
5965

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

86+
public void addBookShelfLike(BookshelfLike bookshelfLike) {
87+
CommonValidator.validateNotnull(bookshelfLike, "bookshelfLike");
88+
bookshelfLikes.add(bookshelfLike);
89+
}
90+
8091
public void changeIsPublic(Boolean isPublic) {
8192
CommonValidator.validateNotnull(isPublic, "isPublic");
8293
this.isPublic = isPublic;
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package com.dadok.gaerval.domain.bookshelf.entity;
2+
3+
import java.util.Objects;
4+
5+
import javax.persistence.Entity;
6+
import javax.persistence.FetchType;
7+
import javax.persistence.GeneratedValue;
8+
import javax.persistence.GenerationType;
9+
import javax.persistence.Id;
10+
import javax.persistence.JoinColumn;
11+
import javax.persistence.ManyToOne;
12+
import javax.persistence.Table;
13+
import javax.persistence.UniqueConstraint;
14+
15+
import com.dadok.gaerval.domain.bookshelf.exception.BookshelfUserNotMatchedException;
16+
import com.dadok.gaerval.domain.user.entity.User;
17+
import com.dadok.gaerval.global.common.JacocoExcludeGenerated;
18+
import com.dadok.gaerval.global.common.entity.BaseTimeColumn;
19+
import com.dadok.gaerval.global.util.CommonValidator;
20+
21+
import lombok.AccessLevel;
22+
import lombok.Getter;
23+
import lombok.NoArgsConstructor;
24+
25+
@Entity(name = "bookshelf_likes")
26+
@Table(
27+
uniqueConstraints = {
28+
@UniqueConstraint(name = "bookshelf_id_user_id_unique_key",
29+
columnNames = {"bookshelf_id", "user_id"})
30+
}
31+
)
32+
@Getter
33+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
34+
public class BookshelfLike extends BaseTimeColumn {
35+
36+
@Id
37+
@GeneratedValue(strategy = GenerationType.IDENTITY)
38+
private Long id;
39+
40+
@ManyToOne(fetch = FetchType.LAZY)
41+
@JoinColumn(nullable = false, name = "user_id")
42+
private User user;
43+
44+
@ManyToOne(fetch = FetchType.LAZY)
45+
@JoinColumn(nullable = false, name = "bookshelf_id")
46+
private Bookshelf bookshelf;
47+
48+
private BookshelfLike(User user, Bookshelf bookshelf) {
49+
CommonValidator.validateNotnull(bookshelf, "bookshelf");
50+
CommonValidator.validateNotnull(user, "user");
51+
validateNotOwner(user, bookshelf);
52+
this.user = user;
53+
this.bookshelf = bookshelf;
54+
bookshelf.addBookShelfLike(this);
55+
}
56+
57+
public static BookshelfLike create(User user, Bookshelf bookshelf) {
58+
return new BookshelfLike(user, bookshelf);
59+
}
60+
61+
private void validateNotOwner(User user, Bookshelf bookshelf) {
62+
if (Objects.equals(bookshelf.getUser().getId(), user.getId())) {
63+
throw new BookshelfUserNotMatchedException();
64+
}
65+
}
66+
67+
@Override
68+
@JacocoExcludeGenerated
69+
public boolean equals(Object o) {
70+
if (this == o)
71+
return true;
72+
if (o == null || getClass() != o.getClass())
73+
return false;
74+
BookshelfLike that = (BookshelfLike)o;
75+
return user.equals(that.user) && bookshelf.equals(that.bookshelf);
76+
}
77+
78+
@Override
79+
@JacocoExcludeGenerated
80+
public int hashCode() {
81+
return Objects.hash(user, bookshelf);
82+
}
83+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.dadok.gaerval.domain.bookshelf.exception;
2+
3+
import static com.dadok.gaerval.global.error.ErrorCode.*;
4+
5+
import com.dadok.gaerval.global.error.exception.BusinessException;
6+
7+
public class AlreadyExistsBookshelfLikeException extends BusinessException {
8+
9+
public AlreadyExistsBookshelfLikeException(Long bookshelfId) {
10+
super(ALREADY_EXISTS_BOOKSHELF_LIKE, String.format(ALREADY_EXISTS_BOOKSHELF_LIKE.getMessage(), bookshelfId));
11+
}
12+
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.dadok.gaerval.domain.bookshelf.repository;
2+
3+
import java.util.Optional;
4+
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
7+
import com.dadok.gaerval.domain.bookshelf.entity.BookshelfLike;
8+
9+
public interface BookshelfLikeRepository extends JpaRepository<BookshelfLike, Long>, BookshelfLikeSupport {
10+
11+
Optional<BookshelfLike> findByUserIdAndBookshelfId(Long userId, Long bookshelfId);
12+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.dadok.gaerval.domain.bookshelf.repository;
2+
3+
public interface BookshelfLikeSupport {
4+
5+
boolean existsLike(Long bookshelfId, Long userId);
6+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.dadok.gaerval.domain.bookshelf.repository;
2+
3+
import static com.dadok.gaerval.domain.bookshelf.entity.QBookshelfLike.*;
4+
5+
import com.querydsl.jpa.impl.JPAQueryFactory;
6+
7+
import lombok.RequiredArgsConstructor;
8+
9+
@RequiredArgsConstructor
10+
public class BookshelfLikeSupportImpl implements BookshelfLikeSupport {
11+
12+
private final JPAQueryFactory query;
13+
14+
@Override
15+
public boolean existsLike(Long bookshelfId, Long userId) {
16+
Integer fetchOne = query.selectOne().from(bookshelfLike)
17+
.where(bookshelfLike.bookshelf.id.eq(bookshelfId), bookshelfLike.user.id.eq(userId))
18+
.fetchFirst();
19+
20+
return fetchOne != null;
21+
}
22+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.dadok.gaerval.domain.bookshelf.service;
2+
3+
public interface BookshelfLikeService {
4+
5+
void createBookshelfLike(Long userId, Long bookshelfId);
6+
7+
void deleteBookshelfLike(Long userId, Long bookshelfId);
8+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.dadok.gaerval.domain.bookshelf.service;
2+
3+
import org.springframework.stereotype.Service;
4+
import org.springframework.transaction.annotation.Transactional;
5+
6+
import com.dadok.gaerval.domain.bookshelf.entity.Bookshelf;
7+
import com.dadok.gaerval.domain.bookshelf.entity.BookshelfLike;
8+
import com.dadok.gaerval.domain.bookshelf.exception.AlreadyExistsBookshelfLikeException;
9+
import com.dadok.gaerval.domain.bookshelf.repository.BookshelfLikeRepository;
10+
import com.dadok.gaerval.domain.user.entity.User;
11+
import com.dadok.gaerval.domain.user.service.UserService;
12+
import com.dadok.gaerval.global.error.exception.ResourceNotfoundException;
13+
14+
import lombok.RequiredArgsConstructor;
15+
16+
@Service
17+
@Transactional
18+
@RequiredArgsConstructor
19+
public class DefaultBookshelfLikeService implements BookshelfLikeService {
20+
21+
private final BookshelfLikeRepository bookshelfLikeRepository;
22+
23+
private final UserService userService;
24+
25+
private final BookshelfService bookshelfService;
26+
27+
@Override
28+
public void createBookshelfLike(Long userId, Long bookshelfId) {
29+
User user = userService.getById(userId);
30+
Bookshelf bookshelf = bookshelfService.getById(bookshelfId);
31+
if (bookshelfLikeRepository.existsLike(bookshelfId, userId)) {
32+
throw new AlreadyExistsBookshelfLikeException(bookshelf.getId());
33+
}
34+
bookshelfLikeRepository.save(BookshelfLike.create(user, bookshelf));
35+
}
36+
37+
@Override
38+
public void deleteBookshelfLike(Long userId, Long bookshelfId) {
39+
BookshelfLike bookshelfLike = bookshelfLikeRepository.findByUserIdAndBookshelfId(userId, bookshelfId)
40+
.orElseThrow(() -> new ResourceNotfoundException(BookshelfLike.class));
41+
bookshelfLikeRepository.deleteById(bookshelfLike.getId());
42+
}
43+
}

β€Žsrc/main/java/com/dadok/gaerval/global/error/ErrorCode.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public enum ErrorCode {
3636
ALREADY_CONTAIN_BOOKSHELF_ITEM(HttpStatus.BAD_REQUEST, "BS1", "이미 μ±…μž₯에 ν¬ν•¨λœ μ•„μ•„ν…œμž…λ‹ˆλ‹€."),
3737
BOOKSHELF_USER_NOT_MATCHED(HttpStatus.UNAUTHORIZED, "BS2", "ν•΄λ‹Ή μ±…μž₯에 μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ μ‚¬μš©μž μ ‘κ·Όμž…λ‹ˆλ‹€."),
3838

39+
ALREADY_EXISTS_BOOKSHELF_LIKE(HttpStatus.BAD_REQUEST, "BSL1", "ν•΄λ‹Ή μ±…μž₯에 λŒ€ν•œ μ’‹μ•„μš”κ°€ 이미 μ‘΄μž¬ν•©λ‹ˆλ‹€. μ±…μž₯ id : %s"),
40+
3941
// Book
4042
BOOK_DATA_INVALID(HttpStatus.INTERNAL_SERVER_ERROR, "B1", "잘λͺ»λœ λ„μ„œ 데이터 ν˜•μ‹μž…λ‹ˆλ‹€."),
4143
BOOK_API_NOT_AVAILABLE(HttpStatus.INTERNAL_SERVER_ERROR, "B2", "λ„μ„œ APIλ₯Ό ν˜ΈμΆœν•  수 μ—†μŠ΅λ‹ˆλ‹€."),
@@ -73,8 +75,6 @@ public enum ErrorCode {
7375
KAKAO_TIMEOUT(HttpStatus.REQUEST_TIMEOUT, "603", "카카였 ν”Œλž«νΌ λ‚΄λΆ€μ—μ„œ μš”μ²­ 처리 쀑 νƒ€μž„μ•„μ›ƒμ΄ λ°œμƒν•œ 경우"),
7476
KAKAO_SERVICE_CHECK(HttpStatus.SERVICE_UNAVAILABLE, "9798", "μ„œλΉ„μŠ€ 점검쀑");
7577

76-
77-
7878
private final HttpStatus status;
7979

8080
private final String code;

0 commit comments

Comments
Β (0)