diff --git a/backend/src/docs/asciidoc/bookmark.adoc b/backend/src/docs/asciidoc/bookmark.adoc new file mode 100644 index 00000000..d58e444e --- /dev/null +++ b/backend/src/docs/asciidoc/bookmark.adoc @@ -0,0 +1,15 @@ +== 즐겨찾기 + +=== 토픽을 유저의 즐겨찾기에 추가 + +operation::bookmark-controller-test/add-topic-in-bookmark[snippets='http-request,http-response'] + +=== 유저의 토픽 즐겨찾기 목록 조회 + +operation::bookmark-controller-test/find-topics-in-bookmark[snippets='http-request,http-response'] + +=== 유저의 토픽 즐겨찾기 단일 삭제 +operation::bookmark-controller-test/delete-topic-in-bookmark[snippets='http-request,http-response'] + +=== 유저의 토픽 즐겨찾기 전체 삭제 +operation::bookmark-controller-test/delete-all-topics-in-bookmark[snippets='http-request,http-response'] diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index b6cbd853..9a43994e 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -13,3 +13,4 @@ include::pin.adoc[] include::member.adoc[] include::permission.adoc[] include::oauth.adoc[] +include::bookmark.adoc[] diff --git a/backend/src/docs/asciidoc/member.adoc b/backend/src/docs/asciidoc/member.adoc index fc40578c..93c56cf2 100644 --- a/backend/src/docs/asciidoc/member.adoc +++ b/backend/src/docs/asciidoc/member.adoc @@ -14,4 +14,4 @@ operation::member-controller-test/find-pins-by-member[snippets='http-request,htt === 유저가 만든 토픽 조회 -operation::member-controller-test/find-topics-by-member[snippets='http-request,http-response'] +operation::member-controller-test/find-topics-by-member[snippets='http-request,http-response'] \ No newline at end of file diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java index 0ec5eb0a..af4565f1 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/infrastructure/JwtTokenProvider.java @@ -1,6 +1,10 @@ package com.mapbefine.mapbefine.auth.infrastructure; -import io.jsonwebtoken.*; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandService.java new file mode 100644 index 00000000..81eae7b1 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandService.java @@ -0,0 +1,92 @@ +package com.mapbefine.mapbefine.bookmark.application; + +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.bookmark.domain.Bookmark; +import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import java.util.NoSuchElementException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class BookmarkCommandService { + + private final BookmarkRepository bookmarkRepository; + + private final MemberRepository memberRepository; + + private final TopicRepository topicRepository; + + public BookmarkCommandService( + BookmarkRepository bookmarkRepository, + MemberRepository memberRepository, + TopicRepository topicRepository + ) { + this.bookmarkRepository = bookmarkRepository; + this.memberRepository = memberRepository; + this.topicRepository = topicRepository; + } + + public Long addTopicInBookmark(AuthMember authMember, Long topicId) { + validateBookmarkDuplication(authMember, topicId); + Topic topic = getTopicById(topicId); + validateBookmarkingPermission(authMember, topic); + Member member = getMemberById(authMember); + + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(topic, member); + bookmarkRepository.save(bookmark); + + return bookmark.getId(); + } + + private Topic getTopicById(Long topicId) { + return topicRepository.findById(topicId) + .orElseThrow(() -> new NoSuchElementException("존재하지 않는 토픽입니다.")); + } + + private void validateBookmarkDuplication(AuthMember authMember, Long topicId) { + if (isExistBookmark(authMember, topicId)) { + throw new IllegalArgumentException("이미 즐겨찾기로 등록된 토픽입니다."); + } + } + + private boolean isExistBookmark(AuthMember authMember, Long topicId) { + return bookmarkRepository.existsByMemberIdAndTopicId(authMember.getMemberId(), topicId); + } + + private void validateBookmarkingPermission(AuthMember authMember, Topic topic) { + if (authMember.canRead(topic)) { + return; + } + + throw new IllegalArgumentException("토픽에 대한 권한이 없어서 즐겨찾기에 추가할 수 없습니다."); + } + + private Member getMemberById(AuthMember authMember) { + return memberRepository.findById(authMember.getMemberId()) + .orElseThrow(() -> new NoSuchElementException("존재하지 않는 멤버입니다.")); + } + + public void deleteTopicInBookmark(AuthMember authMember, Long topicId) { + validateBookmarkDeletingPermission(authMember, topicId); + + bookmarkRepository.deleteByMemberIdAndTopicId(authMember.getMemberId(), topicId); + } + + private void validateBookmarkDeletingPermission(AuthMember authMember, Long topicId) { + if (isExistBookmark(authMember, topicId)) { + return; + } + + throw new IllegalArgumentException("즐겨찾기 삭제에 대한 권한이 없습니다."); + } + + public void deleteAllBookmarks(AuthMember authMember) { + bookmarkRepository.deleteAllByMemberId(authMember.getMemberId()); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/bookmark/application/BookmarkQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/application/BookmarkQueryService.java new file mode 100644 index 00000000..c2d82b36 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/application/BookmarkQueryService.java @@ -0,0 +1,37 @@ +package com.mapbefine.mapbefine.bookmark.application; + +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.bookmark.domain.Bookmark; +import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; +import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; +import java.util.List; +import java.util.Objects; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class BookmarkQueryService { + + private final BookmarkRepository bookmarkRepository; + + public BookmarkQueryService(BookmarkRepository bookmarkRepository) { + this.bookmarkRepository = bookmarkRepository; + } + + public List findAllTopicsInBookmark(AuthMember authMember) { + validateNonExistsMember(authMember); + List bookmarks = bookmarkRepository.findAllByMemberId(authMember.getMemberId()); + + return bookmarks.stream() + .map(bookmark -> TopicResponse.from(bookmark.getTopic(), Boolean.TRUE)) + .toList(); + } + + public void validateNonExistsMember(AuthMember authMember) { + if (Objects.isNull(authMember.getMemberId())) { + throw new IllegalArgumentException("존재하지 않는 유저입니다."); + } + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/bookmark/domain/Bookmark.java b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/domain/Bookmark.java new file mode 100644 index 00000000..5b463769 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/domain/Bookmark.java @@ -0,0 +1,47 @@ +package com.mapbefine.mapbefine.bookmark.domain; + +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.topic.domain.Topic; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Bookmark { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "topic_id", nullable = false) + private Topic topic; + + @ManyToOne + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + private Bookmark(Topic topic, Member member) { + this.topic = topic; + this.member = member; + } + + // TODO: 2023/08/11 필요한 검증이 무엇이 있을까.. 현재로썬 외부에서 검증하는 방법 밖에 ? + public static Bookmark createWithAssociatedTopicAndMember(Topic topic, Member member) { + Bookmark bookmark = new Bookmark(topic, member); + + topic.addBookmark(bookmark); + member.addBookmark(bookmark); + + return bookmark; + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/bookmark/domain/BookmarkRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/domain/BookmarkRepository.java new file mode 100644 index 00000000..e7773b86 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/domain/BookmarkRepository.java @@ -0,0 +1,15 @@ +package com.mapbefine.mapbefine.bookmark.domain; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BookmarkRepository extends JpaRepository { + + List findAllByMemberId(Long memberId); + + void deleteAllByMemberId(Long memberId); + + boolean existsByMemberIdAndTopicId(Long memberId, Long topicId); + + void deleteByMemberIdAndTopicId(Long memberId, Long topicId); +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkController.java b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkController.java new file mode 100644 index 00000000..23273588 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkController.java @@ -0,0 +1,64 @@ +package com.mapbefine.mapbefine.bookmark.presentation; + +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.bookmark.application.BookmarkCommandService; +import com.mapbefine.mapbefine.bookmark.application.BookmarkQueryService; +import com.mapbefine.mapbefine.common.interceptor.LoginRequired; +import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; +import java.net.URI; +import java.util.List; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/bookmarks") +public class BookmarkController { + + private final BookmarkCommandService bookmarkCommandService; + + private final BookmarkQueryService bookmarkQueryService; + + public BookmarkController( + BookmarkCommandService bookmarkCommandService, + BookmarkQueryService bookmarkQueryService + ) { + this.bookmarkCommandService = bookmarkCommandService; + this.bookmarkQueryService = bookmarkQueryService; + } + + @LoginRequired + @PostMapping + public ResponseEntity addTopicInBookmark( + AuthMember authMember, + @RequestParam Long topicId + ) { + Long bookmarkId = bookmarkCommandService.addTopicInBookmark(authMember, topicId); + + return ResponseEntity.created(URI.create("/bookmarks/" + bookmarkId)).build(); + } + + @LoginRequired + @GetMapping + public ResponseEntity> findAllTopicsInBookmark(AuthMember authMember) { + List responses = bookmarkQueryService.findAllTopicsInBookmark(authMember); + + return ResponseEntity.ok(responses); + } + + @LoginRequired + @DeleteMapping + public ResponseEntity deleteTopicInBookmark( + AuthMember authMember, + @RequestParam Long topicId + ) { + bookmarkCommandService.deleteTopicInBookmark(authMember, topicId); + + return ResponseEntity.noContent().build(); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/location/application/LocationQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/location/application/LocationQueryService.java index 1cb9a4fb..a5544958 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/location/application/LocationQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/location/application/LocationQueryService.java @@ -4,6 +4,8 @@ import static java.util.stream.Collectors.groupingBy; import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.bookmark.domain.Bookmark; +import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; import com.mapbefine.mapbefine.location.domain.Coordinate; import com.mapbefine.mapbefine.location.domain.Location; import com.mapbefine.mapbefine.location.domain.LocationRepository; @@ -24,9 +26,14 @@ public class LocationQueryService { private static final double NEAR_DISTANCE_METERS = 3000; private final LocationRepository locationRepository; + private final BookmarkRepository bookmarkRepository; - public LocationQueryService(LocationRepository locationRepository) { + public LocationQueryService( + LocationRepository locationRepository, + BookmarkRepository bookmarkRepository + ) { this.locationRepository = locationRepository; + this.bookmarkRepository = bookmarkRepository; } public List findNearbyTopicsSortedByPinCount( @@ -52,12 +59,31 @@ private Map countPinsByTopicInLocations(List locations) { .collect(groupingBy(Pin::getTopic, counting())); } - private List sortTopicsByPinCounts(Map topicCounts, AuthMember member) { + private List sortTopicsByPinCounts( + Map topicCounts, + AuthMember member + ) { + List bookmarkedTopics = getBookmarkedTopics(member); + return topicCounts.entrySet().stream() .filter(entry -> member.canRead(entry.getKey())) .sorted(Collections.reverseOrder(Map.Entry.comparingByValue())) - .map(entry -> TopicResponse.from(entry.getKey())) + .map(entry -> { + Topic topic = entry.getKey(); + + return TopicResponse.from(topic, isInBookmark(bookmarkedTopics, topic)); + }) .toList(); } + private List getBookmarkedTopics(AuthMember member) { + return bookmarkRepository.findAllByMemberId(member.getMemberId()) + .stream() + .map(Bookmark::getTopic) + .toList(); + } + + private boolean isInBookmark(List bookmarkedTopics, Topic topic) { + return bookmarkedTopics.contains(topic); + } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java index 6614e6dd..dbb9b1f3 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java @@ -8,8 +8,8 @@ import com.mapbefine.mapbefine.pin.domain.Pin; import com.mapbefine.mapbefine.pin.domain.PinRepository; import com.mapbefine.mapbefine.pin.dto.response.PinResponse; -import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import com.mapbefine.mapbefine.topic.domain.TopicWithBookmarkStatus; import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; import java.util.List; import java.util.NoSuchElementException; @@ -52,17 +52,21 @@ public List findAll() { // TODO: 2023/08/14 해당 메서드는 TopicQueryService로 옮기기 public List findTopicsByMember(AuthMember authMember) { - validateNonExistsMember(authMember.getMemberId()); - List topicsByCreator = topicRepository.findByCreatorId(authMember.getMemberId()); + validateNonExistsMember(authMember); + List topicsWithBookmarkStatus = + topicRepository.findAllWithBookmarkStatusByMemberId(authMember.getMemberId()); - return topicsByCreator.stream() - .map(TopicResponse::from) + return topicsWithBookmarkStatus.stream() + .map(topicWithBookmarkStatus -> TopicResponse.from( + topicWithBookmarkStatus.getTopic(), + topicWithBookmarkStatus.getIsBookmarked() + )) .toList(); } // TODO: 2023/08/14 해당 메서드는 PinQueryService로 옮기기 public List findPinsByMember(AuthMember authMember) { - validateNonExistsMember(authMember.getMemberId()); + validateNonExistsMember(authMember); List pinsByCreator = pinRepository.findByCreatorId(authMember.getMemberId()); return pinsByCreator.stream() @@ -70,8 +74,8 @@ public List findPinsByMember(AuthMember authMember) { .toList(); } - private void validateNonExistsMember(Long memberId) { - if (Objects.isNull(memberId)) { + private void validateNonExistsMember(AuthMember authMember) { + if (Objects.isNull(authMember.getMemberId())) { throw new IllegalArgumentException("존재하지 않는 유저입니다."); } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java index 8a67118d..b737abbc 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java @@ -3,6 +3,7 @@ import static java.util.UUID.randomUUID; import static lombok.AccessLevel.PROTECTED; +import com.mapbefine.mapbefine.bookmark.domain.Bookmark; import com.mapbefine.mapbefine.common.entity.BaseTimeEntity; import com.mapbefine.mapbefine.permission.domain.Permission; import com.mapbefine.mapbefine.pin.domain.Pin; @@ -45,6 +46,9 @@ public class Member extends BaseTimeEntity { @OneToMany(mappedBy = "member") private List topicsWithPermissions = new ArrayList<>(); + @OneToMany(mappedBy = "member") + private List bookmarks = new ArrayList<>(); + private Member(MemberInfo memberInfo, OauthId oauthId) { this.memberInfo = memberInfo; this.oauthId = oauthId; @@ -110,6 +114,10 @@ public void addMemberTopicPermission(Permission permission) { topicsWithPermissions.add(permission); } + public void addBookmark(Bookmark bookmark) { + bookmarks.add(bookmark); + } + public String getRoleKey() { return memberInfo.getRole().getKey(); } @@ -117,6 +125,7 @@ public String getRoleKey() { public boolean isAdmin() { return memberInfo.getRole() == Role.ADMIN; } + public boolean isUser() { return memberInfo.getRole() == Role.USER; } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java index 367f47d8..00bf671a 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java @@ -3,9 +3,11 @@ import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import com.mapbefine.mapbefine.topic.domain.TopicWithBookmarkStatus; import com.mapbefine.mapbefine.topic.dto.response.TopicDetailResponse; import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; import java.util.List; +import java.util.Objects; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -20,23 +22,47 @@ public TopicQueryService(final TopicRepository topicRepository) { } public List findAllReadable(AuthMember member) { - return topicRepository.findAll().stream() + if (Objects.isNull(member.getMemberId())) { + return findAllWithoutBookmarkStatus(member); + } + + return findAllWithBookmarkStatus(member); + } + + private List findAllWithoutBookmarkStatus(AuthMember member) { + return topicRepository.findAll() + .stream() .filter(member::canRead) - .map(TopicResponse::from) + .map(topic -> TopicResponse.from(topic, Boolean.FALSE)) .toList(); } - public TopicDetailResponse findDetailById(AuthMember member, Long id) { - Topic topic = findTopic(id); + private List findAllWithBookmarkStatus(AuthMember member) { + return topicRepository.findAllWithBookmarkStatusByMemberId(member.getMemberId()) + .stream() + .filter(topicWithBookmark -> member.canRead(topicWithBookmark.getTopic())) + .map(topicWithBookmark -> TopicResponse.from( + topicWithBookmark.getTopic(), + topicWithBookmark.getIsBookmarked() + )) + .toList(); + } - validateReadableTopic(member, topic); + public TopicDetailResponse findDetailById(AuthMember member, Long topicId) { + if (Objects.isNull(member.getMemberId())) { + return findWithoutBookmarkStatus(member, topicId); + } - return TopicDetailResponse.from(topic); + return findWithBookmarkStatus(member, topicId); } - private Topic findTopic(Long id) { - return topicRepository.findById(id) + private TopicDetailResponse findWithoutBookmarkStatus(AuthMember member, Long topicId) { + Topic topic = topicRepository.findById(topicId) .orElseThrow(() -> new IllegalArgumentException("해당하는 Topic이 존재하지 않습니다.")); + + validateReadableTopic(member, topic); + + return TopicDetailResponse.from(topic, Boolean.FALSE); } private void validateReadableTopic(AuthMember member, Topic topic) { @@ -45,17 +71,38 @@ private void validateReadableTopic(AuthMember member, Topic topic) { } throw new IllegalArgumentException("조회권한이 없는 Topic 입니다."); + } + + private TopicDetailResponse findWithBookmarkStatus(AuthMember member, Long topicId) { + TopicWithBookmarkStatus topicWithBookmarkStatus = + topicRepository.findWithBookmarkStatusByIdAndMemberId(topicId, member.getMemberId()) + .orElseThrow(() -> new IllegalArgumentException("해당하는 Topic이 존재하지 않습니다.")); + validateReadableTopic(member, topicWithBookmarkStatus.getTopic()); + + return TopicDetailResponse.from( + topicWithBookmarkStatus.getTopic(), + topicWithBookmarkStatus.getIsBookmarked() + ); + } + + public List findDetailsByIds(AuthMember member, List topicIds) { + if (Objects.isNull(member.getMemberId())) { + return findDetailsWithoutBookmarkStatus(member, topicIds); + } + + return findDetailsWithBookmarkStatus(member, topicIds); } - public List findDetailsByIds(AuthMember member, List ids) { - List topics = topicRepository.findByIdIn(ids); + private List findDetailsWithoutBookmarkStatus(AuthMember member, + List topicIds) { + List topics = topicRepository.findByIdIn(topicIds); - validateTopicsCount(ids, topics); + validateTopicsCount(topicIds, topics); validateReadableTopics(member, topics); return topics.stream() - .map(TopicDetailResponse::from) + .map(topic -> TopicDetailResponse.from(topic, Boolean.FALSE)) .toList(); } @@ -75,4 +122,29 @@ private void validateReadableTopics(AuthMember member, List topics) { } } + private List findDetailsWithBookmarkStatus( + AuthMember member, + List topicIds + ) { + List topicsWithBookmarkStatus = + topicRepository.findWithBookmarkStatusByIdsAndMemberId( + topicIds, + member.getMemberId() + ); + + List topics = topicsWithBookmarkStatus.stream() + .map(TopicWithBookmarkStatus::getTopic) + .toList(); + + validateTopicsCount(topicIds, topics); + validateReadableTopics(member, topics); + + return topicsWithBookmarkStatus.stream() + .map(topicWithBookmarkStatus -> TopicDetailResponse.from( + topicWithBookmarkStatus.getTopic(), + topicWithBookmarkStatus.getIsBookmarked() + )) + .toList(); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java index ba6f9ec3..a41c807e 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java @@ -2,6 +2,7 @@ import static lombok.AccessLevel.PROTECTED; +import com.mapbefine.mapbefine.bookmark.domain.Bookmark; import com.mapbefine.mapbefine.common.entity.BaseTimeEntity; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.permission.domain.Permission; @@ -47,6 +48,9 @@ public class Topic extends BaseTimeEntity { @OneToMany(mappedBy = "topic", cascade = CascadeType.PERSIST) private List pins = new ArrayList<>(); + @OneToMany(mappedBy = "topic") + private List bookmarks = new ArrayList<>(); + @Column(nullable = false) @ColumnDefault(value = "false") private boolean isDeleted = false; @@ -98,8 +102,16 @@ public void addPin(Pin pin) { pins.add(pin); } + public void addBookmark(Bookmark bookmark) { + bookmarks.add(bookmark); + } + public void addMemberTopicPermission(Permission permission) { permissions.add(permission); } + public int countBookmarks() { + return bookmarks.size(); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java index 1ef9fb6f..bd6a0dd7 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.topic.domain; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -20,4 +21,36 @@ public interface TopicRepository extends JpaRepository { List findByCreatorId(Long creatorId); + @Query("select new com.mapbefine.mapbefine.topic.domain.TopicWithBookmarkStatus(" + + "t, case when b.id is not null then true else false end" + + ") " + + "from Topic t " + + "left join t.bookmarks b on b.member.id = :memberId") + List findAllWithBookmarkStatusByMemberId( + @Param("memberId") Long memberId); + + + @Query("select new com.mapbefine.mapbefine.topic.domain.TopicWithBookmarkStatus(" + + "t, case when b.id is not null then true else false end" + + ") " + + "from Topic t " + + "left join t.bookmarks b on t.id = :topicId AND b.member.id = :memberId " + + "where t.id = :topicId") + Optional findWithBookmarkStatusByIdAndMemberId( + @Param("topicId") Long topicId, + @Param("memberId") Long memberId + ); + + @Query("select new com.mapbefine.mapbefine.topic.domain.TopicWithBookmarkStatus(" + + "t, case when b.id is not null then true else false end" + + " )" + + "from Topic t " + + "left join t.bookmarks b on b.member.id = :memberId " + + "where t.id in :topicIds" + ) + List findWithBookmarkStatusByIdsAndMemberId( + @Param("topicIds") List topicIds, + @Param("memberId") Long memberId + ); + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicWithBookmarkStatus.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicWithBookmarkStatus.java new file mode 100644 index 00000000..1408a8b9 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicWithBookmarkStatus.java @@ -0,0 +1,16 @@ +package com.mapbefine.mapbefine.topic.domain; + +import lombok.Getter; + +@Getter +public class TopicWithBookmarkStatus { + + private final Topic topic; + private final Boolean isBookmarked; + + public TopicWithBookmarkStatus(Topic topic, Boolean isBookmarked) { + this.topic = topic; + this.isBookmarked = isBookmarked; + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicDetailResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicDetailResponse.java index 30725a95..c2cbe944 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicDetailResponse.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicDetailResponse.java @@ -13,9 +13,12 @@ public record TopicDetailResponse( String image, Integer pinCount, LocalDateTime updatedAt, - List pins + List pins, + Boolean isBookmarked, + Integer bookmarkCount ) { - public static TopicDetailResponse from(Topic topic) { + + public static TopicDetailResponse from(Topic topic, Boolean isBookmarked) { List pinResponses = topic.getPins().stream() .map(PinResponse::from) .toList(); @@ -29,7 +32,10 @@ public static TopicDetailResponse from(Topic topic) { topicInfo.getImageUrl(), topic.countPins(), topic.getUpdatedAt(), - pinResponses + pinResponses, + isBookmarked, + topic.countBookmarks() ); } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicResponse.java index 8efdf843..9f30f108 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicResponse.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/dto/response/TopicResponse.java @@ -9,10 +9,12 @@ public record TopicResponse( String name, String image, Integer pinCount, + Integer bookmarkCount, + Boolean isBookmarked, LocalDateTime updatedAt ) { - public static TopicResponse from(Topic topic) { + public static TopicResponse from(Topic topic, Boolean isBookmarked) { TopicInfo topicInfo = topic.getTopicInfo(); return new TopicResponse( @@ -20,7 +22,10 @@ public static TopicResponse from(Topic topic) { topicInfo.getName(), topicInfo.getImageUrl(), topic.countPins(), + topic.countBookmarks(), + isBookmarked, topic.getUpdatedAt() ); } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandServiceTest.java new file mode 100644 index 00000000..71a35722 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkCommandServiceTest.java @@ -0,0 +1,198 @@ +package com.mapbefine.mapbefine.bookmark.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.bookmark.domain.Bookmark; +import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; +import com.mapbefine.mapbefine.common.annotation.ServiceTest; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@ServiceTest +class BookmarkCommandServiceTest { + + @Autowired + private BookmarkCommandService bookmarkCommandService; + + @Autowired + private BookmarkRepository bookmarkRepository; + + @Autowired + private TopicRepository topicRepository; + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("다른 유저의 토픽을 즐겨찾기에 추가할 수 있다.") + public void addTopicInBookmark_Success() { + //given + Member creator = MemberFixture.create( + "member", + "member@naver.com", + Role.USER + ); + Topic topic = TopicFixture.createPublicAndAllMembersTopic(creator); + + memberRepository.save(creator); + topicRepository.save(topic); + + //when + Member otherMember = MemberFixture.create( + "otherMember", + "otherMember@naver.com", + Role.USER + ); + memberRepository.save(otherMember); + Long bookmarkId = bookmarkCommandService.addTopicInBookmark( + MemberFixture.createUser(otherMember), + topic.getId() + ); + + //then + Bookmark bookmark = bookmarkRepository.findById(bookmarkId).get(); + + assertThat(bookmark.getTopic().getId()).isEqualTo(topic.getId()); + assertThat(bookmark.getMember().getId()).isEqualTo(otherMember.getId()); + } + + @Test + @DisplayName("권한이 없는 다른 유저의 토픽을 즐겨찾기에 추가할 수 없다.") + public void addTopicInBookmark_Fail1() { + //given + Member creator = MemberFixture.create( + "member", + "member@naver.com", + Role.USER + ); + Topic topic = TopicFixture.createPrivateAndGroupOnlyTopic(creator); + + memberRepository.save(creator); + topicRepository.save(topic); + + //when + Member otherMember = MemberFixture.create( + "otherMember", + "otherMember@naver.com", + Role.USER + ); + memberRepository.save(otherMember); + + //then + assertThatThrownBy(() -> bookmarkCommandService.addTopicInBookmark( + MemberFixture.createUser(otherMember), + topic.getId() + )).isInstanceOf(IllegalArgumentException.class) + .hasMessage("토픽에 대한 권한이 없어서 즐겨찾기에 추가할 수 없습니다."); + } + + @Test + @DisplayName("즐겨찾기 목록에 있는 토픽을 삭제할 수 있다.") + public void deleteTopicInBookmark_Success() { + //given + Member creator = MemberFixture.create( + "member", + "member@naver.com", + Role.USER + ); + Topic topic = TopicFixture.createPrivateAndGroupOnlyTopic(creator); + + memberRepository.save(creator); + topicRepository.save(topic); + + Member otherMember = MemberFixture.create( + "otherMember", + "otherMember@naver.com", + Role.USER + ); + memberRepository.save(otherMember); + + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(topic, otherMember); + bookmarkRepository.save(bookmark); + + //when + AuthMember user = MemberFixture.createUser(otherMember); + assertThat(bookmarkRepository.existsById(bookmark.getId())).isTrue(); + + bookmarkCommandService.deleteTopicInBookmark(user, topic.getId()); + + //then + assertThat(bookmarkRepository.existsById(bookmark.getId())).isFalse(); + } + + @Test + @DisplayName("즐겨찾기 목록에 있는 권한이 없는 토픽은 삭제할 수 없다.") + public void deleteTopicInBookmark_Fail() { + //given + Member creator = MemberFixture.create( + "member", + "member@naver.com", + Role.USER + ); + Topic topic = TopicFixture.createPrivateAndGroupOnlyTopic(creator); + + memberRepository.save(creator); + topicRepository.save(topic); + + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(topic, creator); + bookmarkRepository.save(bookmark); + + Member otherMember = MemberFixture.create( + "otherMember", + "otherMember@naver.com", + Role.USER + ); + memberRepository.save(otherMember); + + //when then + AuthMember otherUser = MemberFixture.createUser(otherMember); + + assertThatThrownBy( + () -> bookmarkCommandService.deleteTopicInBookmark(otherUser, topic.getId())) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("즐겨찾기 삭제에 대한 권한이 없습니다."); + } + + @Test + @DisplayName("즐겨찾기 목록에 있는 모든 토픽을 삭제할 수 있다") + public void deleteAllBookmarks_Success() { + //given + Member creator = MemberFixture.create( + "member", + "member@naver.com", + Role.USER + ); + Topic topic1 = TopicFixture.createPrivateAndGroupOnlyTopic(creator); + Topic topic2 = TopicFixture.createPrivateAndGroupOnlyTopic(creator); + + memberRepository.save(creator); + topicRepository.save(topic1); + topicRepository.save(topic2); + + Bookmark bookmark1 = Bookmark.createWithAssociatedTopicAndMember(topic1, creator); + Bookmark bookmark2 = Bookmark.createWithAssociatedTopicAndMember(topic1, creator); + + bookmarkRepository.save(bookmark1); + bookmarkRepository.save(bookmark2); + + //when + assertThat(bookmarkRepository.findAllByMemberId(creator.getId())).hasSize(2); + + AuthMember user = MemberFixture.createUser(creator); + bookmarkCommandService.deleteAllBookmarks(user); + + //then + assertThat(bookmarkRepository.findAllByMemberId(creator.getId())).hasSize(0); + } + +} \ No newline at end of file diff --git a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkQueryServiceTest.java new file mode 100644 index 00000000..888e9a8d --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/application/BookmarkQueryServiceTest.java @@ -0,0 +1,70 @@ +package com.mapbefine.mapbefine.bookmark.application; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.mapbefine.mapbefine.bookmark.domain.Bookmark; +import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; +import com.mapbefine.mapbefine.common.annotation.ServiceTest; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@ServiceTest +class BookmarkQueryServiceTest { + + @Autowired + private BookmarkQueryService bookmarkQueryService; + + @Autowired + private BookmarkRepository bookmarkRepository; + + @Autowired + private TopicRepository topicRepository; + + @Autowired + private MemberRepository memberRepository; + + @Test + @DisplayName("즐겨찾기 목록에 추가 된 토픽을 조회할 수 있다") + public void findAllTopicsInBookmark_success() { + //given + Member creator = MemberFixture.create("creator", "member@naver.com", Role.USER); + memberRepository.save(creator); + + Topic topic1 = topicRepository.save(TopicFixture.createPublicAndAllMembersTopic(creator)); + Topic topic2 = topicRepository.save(TopicFixture.createPublicAndAllMembersTopic(creator)); + topicRepository.save(topic1); + topicRepository.save(topic2); + + //when + Member otherMember = + MemberFixture.create("otherMember", "otherMember@naver.com", Role.USER); + memberRepository.save(otherMember); + Bookmark bookmark1 = + Bookmark.createWithAssociatedTopicAndMember(topic1, otherMember); + Bookmark bookmark2 = + Bookmark.createWithAssociatedTopicAndMember(topic2, otherMember); + + bookmarkRepository.save(bookmark1); + bookmarkRepository.save(bookmark2); + + //then + List topicsInBookmark = bookmarkQueryService.findAllTopicsInBookmark( + MemberFixture.createUser(otherMember) + ); + + assertThat(topicsInBookmark).hasSize(2); + assertThat(topicsInBookmark).extractingResultOf("id") + .containsExactlyInAnyOrder(topic1.getId(), topic2.getId()); + } + +} \ No newline at end of file diff --git a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkControllerTest.java new file mode 100644 index 00000000..c343a3af --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkControllerTest.java @@ -0,0 +1,90 @@ +package com.mapbefine.mapbefine.bookmark.presentation; + +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; + +import com.mapbefine.mapbefine.bookmark.application.BookmarkCommandService; +import com.mapbefine.mapbefine.bookmark.application.BookmarkQueryService; +import com.mapbefine.mapbefine.common.RestDocsIntegration; +import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; +import java.time.LocalDateTime; +import java.util.List; +import org.apache.tomcat.util.codec.binary.Base64; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +class BookmarkControllerTest extends RestDocsIntegration { + + @MockBean + private BookmarkCommandService bookmarkCommandService; + + @MockBean + private BookmarkQueryService bookmarkQueryService; + + @Test + @DisplayName("토픽을 유저의 즐겨찾기에 추가") + public void addTopicInBookmark() throws Exception { + String authHeader = Base64.encodeBase64String("Basic member@naver.com".getBytes()); + + given(bookmarkCommandService.addTopicInBookmark(any(), any())).willReturn(1L); + + mockMvc.perform( + MockMvcRequestBuilders.post("/bookmarks") + .header(AUTHORIZATION, authHeader) + .param("topicId", String.valueOf(1L)) + ).andDo(restDocs.document()); + } + + @Test + @DisplayName("유저의 토픽 즐겨찾기 목록 조회") + public void findTopicsInBookmark() throws Exception { + String authHeader = Base64.encodeBase64String("Basic member@naver.com".getBytes()); + + List response = List.of( + new TopicResponse( + 1L, + "준팍의 또 토픽", + "https://map-befine-official.github.io/favicon.png", + 3, + 100, + Boolean.TRUE, + LocalDateTime.now() + ), + new TopicResponse( + 2L, + "준팍의 두번째 토픽", + "https://map-befine-official.github.io/favicon.png", + 5, + 150, + Boolean.TRUE, + LocalDateTime.now() + ) + ); + + given(bookmarkQueryService.findAllTopicsInBookmark(any())).willReturn(response); + + mockMvc.perform( + MockMvcRequestBuilders.get("/bookmarks") + .header(AUTHORIZATION, authHeader) + ).andDo(restDocs.document()); + } + + @Test + @DisplayName("유저의 토픽 즐겨찾기 목록 삭제") + public void deleteTopicInBookmark() throws Exception { + String authHeader = Base64.encodeBase64String("Basic member@naver.com".getBytes()); + + doNothing().when(bookmarkCommandService).deleteTopicInBookmark(any(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.delete("/bookmarks") + .param("topicId", String.valueOf(1L)) + .header(AUTHORIZATION, authHeader) + ).andDo(restDocs.document()); + } + +} \ No newline at end of file diff --git a/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkIntegrationTest.java new file mode 100644 index 00000000..ec34b7ad --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/bookmark/presentation/BookmarkIntegrationTest.java @@ -0,0 +1,139 @@ +package com.mapbefine.mapbefine.bookmark.presentation; + +import static io.restassured.RestAssured.given; +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.assertj.core.api.Assertions.assertThat; + +import com.mapbefine.mapbefine.bookmark.domain.Bookmark; +import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; +import com.mapbefine.mapbefine.common.IntegrationTest; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; +import io.restassured.common.mapper.TypeRef; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +public class BookmarkIntegrationTest extends IntegrationTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TopicRepository topicRepository; + + @Autowired + private BookmarkRepository bookmarkRepository; + + @Test + @DisplayName("유저가 토픽을 즐겨찾기 목록에 추가하면, 201을 반환한다.") + void addTopicInBookmark_Success() { + //given + Member creator = MemberFixture.create("member", "member@naver.com", Role.USER); + memberRepository.save(creator); + + Topic topic = TopicFixture.createByName("topic1", creator); + topicRepository.save(topic); + + Member otherUser = + MemberFixture.create("otherUser", "otherUse@naver.com", Role.USER); + memberRepository.save(otherUser); + + String otherUserAuthHeader = testAuthHeaderProvider.createAuthHeader(otherUser); + + //when + ExtractableResponse response = given().log().all() + .header(AUTHORIZATION, otherUserAuthHeader) + .param("topicId", topic.getId()) + .when().post("/bookmarks") + .then().log().all() + .extract(); + + //then + assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + assertThat(response.header("Location")).startsWith("/bookmarks/") + .isNotNull(); + } + + @Test + @DisplayName("유저의 즐겨찾기 토픽 목록을 조회하면, 200을 반환한다.") + void findTopicsInBookmarks_Success() { + //given + Member creator = MemberFixture.create("member", "member@naver.com", Role.USER); + memberRepository.save(creator); + + Topic topic1 = TopicFixture.createByName("topic1", creator); + Topic topic2 = TopicFixture.createByName("topic1", creator); + topicRepository.save(topic1); + topicRepository.save(topic2); + + Member otherUser = + MemberFixture.create("otherUser", "otherUse@naver.com", Role.USER); + memberRepository.save(otherUser); + + Bookmark bookmark1 = Bookmark.createWithAssociatedTopicAndMember(topic1, otherUser); + Bookmark bookmark2 = Bookmark.createWithAssociatedTopicAndMember(topic2, otherUser); + + bookmarkRepository.save(bookmark1); + bookmarkRepository.save(bookmark2); + + String otherUserAuthHeader = testAuthHeaderProvider.createAuthHeader(otherUser); + + //when + List response = given().log().all() + .header(AUTHORIZATION, otherUserAuthHeader) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/bookmarks") + .then().log().all() + .statusCode(HttpStatus.OK.value()) + .extract() + .as(new TypeRef<>() { + }); + + //then + assertThat(response).hasSize(2) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(LocalDateTime.class) + .isEqualTo(List.of( + TopicResponse.from(topic1, Boolean.TRUE), + TopicResponse.from(topic2, Boolean.TRUE)) + ); + } + + @Test + @DisplayName("유저의 즐겨찾기 토픽을 삭제하면, 204를 반환한다.") + void deleteTopicInBookmark_Success() { + //given + Member creator = MemberFixture.create("member", "member@naver.com", Role.USER); + memberRepository.save(creator); + + Topic topic = TopicFixture.createByName("topic1", creator); + topicRepository.save(topic); + + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(topic, creator); + bookmarkRepository.save(bookmark); + + String creatorAuthHeader = testAuthHeaderProvider.createAuthHeader(creator); + + //when then + given().log().all() + .header(AUTHORIZATION, creatorAuthHeader) + .param("topicId", topic.getId()) + .when().delete("/bookmarks") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/common/IntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/common/IntegrationTest.java index e20e5caa..9a91e94a 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/common/IntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/common/IntegrationTest.java @@ -1,7 +1,7 @@ package com.mapbefine.mapbefine.common; import com.mapbefine.mapbefine.DatabaseCleanup; -import io.restassured.*; +import io.restassured.RestAssured; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/common/config/MockBeansConfig.java b/backend/src/test/java/com/mapbefine/mapbefine/common/config/MockBeansConfig.java index d930797c..3917d42e 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/common/config/MockBeansConfig.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/common/config/MockBeansConfig.java @@ -1,7 +1,5 @@ package com.mapbefine.mapbefine.common.config; -import com.mapbefine.mapbefine.oauth.application.OauthService; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Configuration; @Configuration diff --git a/backend/src/test/java/com/mapbefine/mapbefine/location/application/LocationQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/location/application/LocationQueryServiceTest.java index 20e93645..fad5274f 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/location/application/LocationQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/location/application/LocationQueryServiceTest.java @@ -86,7 +86,7 @@ void findNearbyTopicsSortedByPinCount() { // then List expected = topics.stream() .sorted(Collections.reverseOrder(Comparator.comparingInt(Topic::countPins))) - .map(TopicResponse::from) + .map(topic -> TopicResponse.from(topic,Boolean.FALSE)) .collect(Collectors.toList()); assertThat(currentTopics).isEqualTo(expected); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/location/presentation/LocationControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/location/presentation/LocationControllerTest.java index cf65842d..7c59a22a 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/location/presentation/LocationControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/location/presentation/LocationControllerTest.java @@ -43,12 +43,16 @@ void setUp() { "준팍의 또 토픽", "https://map-befine-official.github.io/favicon.png", 5, + 0, + Boolean.FALSE, LocalDateTime.now() ), new TopicResponse( 2L, "준팍의 두번째 토픽", "https://map-befine-official.github.io/favicon.png", 3, + 0, + Boolean.FALSE, LocalDateTime.now() ) ); @@ -62,7 +66,8 @@ void findNearbyTopicsSortedByPinCount() throws Exception { double longitude = 127; //when - given(locationQueryService.findNearbyTopicsSortedByPinCount(any(), anyDouble(), anyDouble())) + given(locationQueryService.findNearbyTopicsSortedByPinCount(any(), anyDouble(), + anyDouble())) .willReturn(responses); //then diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberQueryServiceTest.java index db54086c..098ba11e 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/application/MemberQueryServiceTest.java @@ -155,8 +155,11 @@ void findTopicsByMember() { List response = memberQueryService.findTopicsByMember(authCreator); // then + List expected = List.of(TopicResponse.from(topic1, Boolean.FALSE), + TopicResponse.from(topic2, Boolean.FALSE)); + assertThat(response).usingRecursiveComparison() - .isEqualTo(List.of(TopicResponse.from(topic1), TopicResponse.from(topic2))); + .isEqualTo(expected); } @Test diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/presentation/MemberControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/presentation/MemberControllerTest.java index f92455c1..f703bcb1 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/presentation/MemberControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/presentation/MemberControllerTest.java @@ -116,12 +116,16 @@ void findTopicsByMember() throws Exception { "준팍의 또 토픽", "https://map-befine-official.github.io/favicon.png", 3, + 0, + Boolean.FALSE, LocalDateTime.now() ), new TopicResponse( 2L, "준팍의 두번째 토픽", "https://map-befine-official.github.io/favicon.png", 5, + 0, + Boolean.FALSE, LocalDateTime.now() ) ); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/presentation/MemberIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/presentation/MemberIntegrationTest.java index edd2fdb3..c49c2d7b 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/presentation/MemberIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/presentation/MemberIntegrationTest.java @@ -1,7 +1,7 @@ package com.mapbefine.mapbefine.member.presentation; import static com.mapbefine.mapbefine.oauth.domain.OauthServerType.KAKAO; -import static io.restassured.RestAssured.*; +import static io.restassured.RestAssured.given; import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.assertj.core.api.Assertions.assertThat; @@ -24,8 +24,9 @@ import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; -import io.restassured.common.mapper.*; -import io.restassured.response.*; +import io.restassured.common.mapper.TypeRef; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -176,10 +177,13 @@ void findTopicsByMember() { }); // then + List expected = List.of(TopicResponse.from(topic1, Boolean.FALSE), + TopicResponse.from(topic2, Boolean.FALSE)); + assertThat(topicResponses).hasSize(2) .usingRecursiveComparison() .ignoringFieldsOfTypes(LocalDateTime.class) - .isEqualTo(List.of(TopicResponse.from(topic1), TopicResponse.from(topic2))); + .isEqualTo(expected); } } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/permission/PermissionIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/permission/PermissionIntegrationTest.java index 12e82ae9..9149ecdf 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/permission/PermissionIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/permission/PermissionIntegrationTest.java @@ -1,7 +1,7 @@ package com.mapbefine.mapbefine.permission; import static com.mapbefine.mapbefine.oauth.domain.OauthServerType.KAKAO; -import static io.restassured.RestAssured.*; +import static io.restassured.RestAssured.given; import static org.apache.http.HttpHeaders.AUTHORIZATION; import static org.assertj.core.api.Assertions.assertThat; @@ -21,8 +21,9 @@ import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import io.restassured.common.mapper.*; -import io.restassured.response.*; +import io.restassured.common.mapper.TypeRef; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; import java.time.LocalDateTime; import java.util.List; import org.junit.jupiter.api.BeforeEach; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionCommandServiceTest.java index 4d735276..ee1f2c0d 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionCommandServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/permission/application/PermissionCommandServiceTest.java @@ -43,8 +43,10 @@ class PermissionCommandServiceTest { @DisplayName("Admin 이 권한을 주는 경우 정상적으로 권한이 주어진다.") void saveMemberTopicPermissionByAdmin() { // given - Member admin = memberRepository.save(MemberFixture.create("member", "member@naver.com", Role.ADMIN)); - Member member = memberRepository.save(MemberFixture.create("members", "members@naver.com", Role.USER)); + Member admin = memberRepository.save( + MemberFixture.create("member", "member@naver.com", Role.ADMIN)); + Member member = memberRepository.save( + MemberFixture.create("members", "members@naver.com", Role.USER)); Topic topic = topicRepository.save(TopicFixture.createByName("topic", admin)); AuthMember authAdmin = new Admin(admin.getId()); PermissionRequest request = new PermissionRequest( @@ -68,8 +70,10 @@ void saveMemberTopicPermissionByAdmin() { @DisplayName("Topic 의 Creator 이 권한을 주는 경우 정상적으로 권한이 주어진다.") void saveMemberTopicPermissionByCreator() { // given - Member creator = memberRepository.save(MemberFixture.create("member", "member@naver.com", Role.USER)); - Member member = memberRepository.save(MemberFixture.create("members", "members@naver.com", Role.USER)); + Member creator = memberRepository.save( + MemberFixture.create("member", "member@naver.com", Role.USER)); + Member member = memberRepository.save( + MemberFixture.create("members", "members@naver.com", Role.USER)); Topic topic = topicRepository.save(TopicFixture.createByName("topic", creator)); AuthMember authCreator = new User( creator.getId(), @@ -97,9 +101,12 @@ void saveMemberTopicPermissionByCreator() { @DisplayName("Creator 가 아닌 유저가 권한을 주려는 경우 예외가 발생한다.") void saveMemberTopicPermissionByUser() { // given - Member creator = memberRepository.save(MemberFixture.create("member", "member@naver.com", Role.USER)); - Member notCreator = memberRepository.save(MemberFixture.create("members", "members@naver.com", Role.USER)); - Member member = memberRepository.save(MemberFixture.create("memberss", "memberss@naver.com", Role.USER)); + Member creator = memberRepository.save( + MemberFixture.create("member", "member@naver.com", Role.USER)); + Member notCreator = memberRepository.save( + MemberFixture.create("members", "members@naver.com", Role.USER)); + Member member = memberRepository.save( + MemberFixture.create("memberss", "memberss@naver.com", Role.USER)); Topic topic = topicRepository.save(TopicFixture.createByName("topic", creator)); AuthMember authNotCreator = new User( notCreator.getId(), @@ -120,8 +127,10 @@ void saveMemberTopicPermissionByUser() { @DisplayName("Guest 가 유저에게 권한을 주려는 경우 예외가 발생한다.") void saveMemberTopicPermissionByGuest() { // given - Member creator = memberRepository.save(MemberFixture.create("member", "member@naver.com", Role.USER)); - Member member = memberRepository.save(MemberFixture.create("memberss", "memberss@naver.com", Role.USER)); + Member creator = memberRepository.save( + MemberFixture.create("member", "member@naver.com", Role.USER)); + Member member = memberRepository.save( + MemberFixture.create("memberss", "memberss@naver.com", Role.USER)); Topic topic = topicRepository.save(TopicFixture.createByName("topic", creator)); AuthMember guest = new Guest(); PermissionRequest request = new PermissionRequest( @@ -138,7 +147,8 @@ void saveMemberTopicPermissionByGuest() { @DisplayName("본인에게 권한을 주려하는 경우 예외가 발생한다.") void saveMemberTopicPermissionByCreator_whenSelf_thenFail() { // given - Member creator = memberRepository.save(MemberFixture.create("member", "member@naver.com", Role.USER)); + Member creator = memberRepository.save( + MemberFixture.create("member", "member@naver.com", Role.USER)); Topic topic = topicRepository.save(TopicFixture.createByName("topic", creator)); AuthMember authCreator = new User( creator.getId(), @@ -196,8 +206,10 @@ void saveMemberTopicPermissionByCreator_whenDuplicate_thenFail() { @DisplayName("Admin 이 권한을 삭제하는 경우 정상적으로 삭제가 이루어진다.") void deleteMemberTopicPermissionByAdmin() { // given - Member admin = memberRepository.save(MemberFixture.create("member", "member@naver.com", Role.ADMIN)); - Member member = memberRepository.save(MemberFixture.create("members", "members@naver.com", Role.USER)); + Member admin = memberRepository.save( + MemberFixture.create("member", "member@naver.com", Role.ADMIN)); + Member member = memberRepository.save( + MemberFixture.create("members", "members@naver.com", Role.USER)); Topic topic = topicRepository.save(TopicFixture.createByName("topic", admin)); AuthMember authAdmin = new Admin(admin.getId()); @@ -215,8 +227,10 @@ void deleteMemberTopicPermissionByAdmin() { @DisplayName("creator 이 권한을 삭제하는 경우 정상적으로 삭제가 이루어진다.") void deleteMemberTopicPermissionByCreator() { // given - Member creator = memberRepository.save(MemberFixture.create("member", "member@naver.com", Role.USER)); - Member member = memberRepository.save(MemberFixture.create("members", "members@naver.com", Role.USER)); + Member creator = memberRepository.save( + MemberFixture.create("member", "member@naver.com", Role.USER)); + Member member = memberRepository.save( + MemberFixture.create("members", "members@naver.com", Role.USER)); Topic topic = topicRepository.save(TopicFixture.createByName("topic", creator)); AuthMember authCreator = new User( creator.getId(), @@ -238,9 +252,12 @@ void deleteMemberTopicPermissionByCreator() { @DisplayName("creator 가 아닌 유저가 권한을 삭제하는 경우 예외가 발생한다.") void deleteMemberTopicPermissionByUser() { // given - Member creator = memberRepository.save(MemberFixture.create("member", "member@naver.com", Role.USER)); - Member nonCreator = memberRepository.save(MemberFixture.create("memberss", "memberss@naver.com", Role.USER)); - Member member = memberRepository.save(MemberFixture.create("members", "members@naver.com", Role.USER)); + Member creator = memberRepository.save( + MemberFixture.create("member", "member@naver.com", Role.USER)); + Member nonCreator = memberRepository.save( + MemberFixture.create("memberss", "memberss@naver.com", Role.USER)); + Member member = memberRepository.save( + MemberFixture.create("members", "members@naver.com", Role.USER)); Topic topic = topicRepository.save(TopicFixture.createByName("topic", creator)); AuthMember authNonCreator = new User( nonCreator.getId(), @@ -254,8 +271,10 @@ void deleteMemberTopicPermissionByUser() { Long savedId = permissionRepository.save(permission).getId(); // then - assertThatThrownBy(() -> permissionCommandService.deleteMemberTopicPermission(authNonCreator, savedId)) - .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> permissionCommandService.deleteMemberTopicPermission( + authNonCreator, + savedId + )).isInstanceOf(IllegalArgumentException.class); } @Test @@ -276,8 +295,10 @@ void deleteMemberTopicPermissionByCreator_whenNoneExistsPermission_thenFail() { ); // when then - assertThatThrownBy(() -> permissionCommandService.deleteMemberTopicPermission(authCreator, Long.MAX_VALUE)) - .isInstanceOf(NoSuchElementException.class); + assertThatThrownBy(() -> permissionCommandService.deleteMemberTopicPermission( + authCreator, + Long.MAX_VALUE + )).isInstanceOf(NoSuchElementException.class); } private List getTopicsWithPermission(Member member) { diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java index 17838360..a46598fe 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/PinIntegrationTest.java @@ -17,8 +17,9 @@ import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; -import io.restassured.*; -import io.restassured.response.*; +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java index 090dd9ef..03e27b88 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/TopicIntegrationTest.java @@ -22,8 +22,9 @@ import com.mapbefine.mapbefine.topic.dto.request.TopicMergeRequest; import com.mapbefine.mapbefine.topic.dto.request.TopicUpdateRequest; import com.mapbefine.mapbefine.topic.dto.response.TopicDetailResponse; -import io.restassured.*; -import io.restassured.response.*; +import io.restassured.RestAssured; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; import java.util.Collections; import java.util.List; import org.junit.jupiter.api.BeforeEach; diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java index 0dbbf7e8..cf6a0f23 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/application/TopicQueryServiceTest.java @@ -5,6 +5,8 @@ import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.domain.member.Guest; +import com.mapbefine.mapbefine.bookmark.domain.Bookmark; +import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; import com.mapbefine.mapbefine.common.annotation.ServiceTest; import com.mapbefine.mapbefine.member.MemberFixture; import com.mapbefine.mapbefine.member.domain.Member; @@ -33,6 +35,9 @@ class TopicQueryServiceTest { @Autowired private TopicRepository topicRepository; + @Autowired + private BookmarkRepository bookmarkRepository; + private Member member; @BeforeEach @@ -214,4 +219,135 @@ void findDetailByIds_Fail2() { .hasMessage("존재하지 않는 토픽이 존재합니다"); } + @Test + @DisplayName("모든 토픽을 조회할 때, 즐겨찾기 여부를 함께 반환한다.") + public void findAllReadableWithBookmark_Success() { + //given + Topic topic1 = TopicFixture.createPublicAndAllMembersTopic(member); + Topic topic2 = TopicFixture.createPublicAndAllMembersTopic(member); + topicRepository.save(topic1); + topicRepository.save(topic2); + + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(topic1, member); + bookmarkRepository.save(bookmark); + + //when //then + AuthMember user = MemberFixture.createUser(member); + List topics = topicQueryService.findAllReadable(user); + + assertThat(topics).hasSize(2); + assertThat(topics).extractingResultOf("id") + .containsExactlyInAnyOrder(topic1.getId(), topic2.getId()); + assertThat(topics).extractingResultOf("isBookmarked") + .containsExactlyInAnyOrder(Boolean.FALSE, Boolean.TRUE); + } + + @Test + @DisplayName("모든 토픽을 조회할 때, 로그인 유저가 아니면 즐겨찾기 여부가 항상 False다") + public void findAllReadableWithoutBookmark_Success() { + //given + Topic topic1 = TopicFixture.createPublicAndAllMembersTopic(member); + Topic topic2 = TopicFixture.createPublicAndAllMembersTopic(member); + topicRepository.save(topic1); + topicRepository.save(topic2); + + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(topic1, member); + bookmarkRepository.save(bookmark); + + //when //then + AuthMember guest = new Guest(); + List topics = topicQueryService.findAllReadable(guest); + + assertThat(topics).hasSize(2); + assertThat(topics).extractingResultOf("id") + .containsExactlyInAnyOrder(topic1.getId(), topic2.getId()); + assertThat(topics).extractingResultOf("isBookmarked") + .containsExactlyInAnyOrder(Boolean.FALSE, Boolean.FALSE); + } + + @Test + @DisplayName("토픽 상세조회시, 즐겨찾기 여부를 함께 반환한다.") + public void findWithBookmarkStatus_Success() { + //given + Topic topic = TopicFixture.createPublicAndAllMembersTopic(member); + topicRepository.save(topic); + + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(topic, member); + bookmarkRepository.save(bookmark); + + //when then + AuthMember user = MemberFixture.createUser(member); + TopicDetailResponse topicDetail = topicQueryService.findDetailById(user, topic.getId()); + + assertThat(topicDetail.id()).isEqualTo(topic.getId()); + assertThat(topicDetail.isBookmarked()).isEqualTo(Boolean.TRUE); + + } + + @Test + @DisplayName("토픽 상세조회시, 로그인 유저가 아니라면 즐겨찾기 여부가 항상 False다.") + public void findWithoutBookmarkStatus_Success() { + //given + Topic topic = TopicFixture.createPublicAndAllMembersTopic(member); + topicRepository.save(topic); + + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(topic, member); + bookmarkRepository.save(bookmark); + + //when then + AuthMember guest = new Guest(); + TopicDetailResponse topicDetail = topicQueryService.findDetailById(guest, topic.getId()); + + assertThat(topicDetail.id()).isEqualTo(topic.getId()); + assertThat(topicDetail.isBookmarked()).isEqualTo(Boolean.FALSE); + } + + @Test + @DisplayName("여러 토픽 조회시, 즐겨 찾기 여부를 함께 반환한다.") + public void findDetailsWithBookmarkStatus_Success() { + //given + Topic topic1 = TopicFixture.createPublicAndAllMembersTopic(member); + Topic topic2 = TopicFixture.createPublicAndAllMembersTopic(member); + topicRepository.save(topic1); + topicRepository.save(topic2); + + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(topic1, member); + bookmarkRepository.save(bookmark); + + //when //then + AuthMember user = MemberFixture.createUser(member); + List topicDetails = + topicQueryService.findDetailsByIds(user, List.of(topic1.getId(), topic2.getId())); + + assertThat(topicDetails).hasSize(2); + assertThat(topicDetails).extractingResultOf("id") + .containsExactlyInAnyOrder(topic1.getId(), topic2.getId()); + assertThat(topicDetails).extractingResultOf("isBookmarked") + .containsExactlyInAnyOrder(Boolean.FALSE, Boolean.TRUE); + } + + @Test + @DisplayName("여러 토픽 조회시, 로그인 유저가 아니라면 즐겨 찾기 여부가 항상 False다.") + public void findDetailsWithoutBookmarkStatus_Success() { + //given + Topic topic1 = TopicFixture.createPublicAndAllMembersTopic(member); + Topic topic2 = TopicFixture.createPublicAndAllMembersTopic(member); + topicRepository.save(topic1); + topicRepository.save(topic2); + + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(topic1, member); + bookmarkRepository.save(bookmark); + + //when //then + AuthMember guest = new Guest(); + List topicDetails = + topicQueryService.findDetailsByIds(guest, List.of(topic1.getId(), topic2.getId())); + + assertThat(topicDetails).hasSize(2); + assertThat(topicDetails).extractingResultOf("id") + .containsExactlyInAnyOrder(topic1.getId(), topic2.getId()); + assertThat(topicDetails).extractingResultOf("isBookmarked") + .containsExactlyInAnyOrder(Boolean.FALSE, Boolean.FALSE); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java index 3280df30..f6eb2206 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java @@ -2,10 +2,14 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.mapbefine.mapbefine.bookmark.domain.Bookmark; +import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; import com.mapbefine.mapbefine.member.MemberFixture; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.topic.TopicFixture; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -21,6 +25,9 @@ class TopicRepositoryTest { @Autowired private MemberRepository memberRepository; + @Autowired + private BookmarkRepository bookmarkRepository; + private Member member; @BeforeEach @@ -54,4 +61,176 @@ void deleteById_Success() { assertThat(deletedTopic.isDeleted()).isTrue(); } -} + @Test + @DisplayName("특정 토픽의 즐겨찾기 여부를 함께 반환한다.") + public void findWithBookmarkStatusByIdAndMemberId_Success1() { + //given + Topic topicNotBookmarked = TopicFixture.createPublicAndAllMembersTopic(member); + Topic bookmarkedTopic = TopicFixture.createPublicAndAllMembersTopic(member); + topicRepository.save(topicNotBookmarked); + topicRepository.save(bookmarkedTopic); + + //when + TopicWithBookmarkStatus beforeBookmarking = + topicRepository.findWithBookmarkStatusByIdAndMemberId( + bookmarkedTopic.getId(), + member.getId() + ).get(); + assertThat(beforeBookmarking.getIsBookmarked()).isFalse(); + + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(bookmarkedTopic, member); + bookmarkRepository.save(bookmark); + + //then + TopicWithBookmarkStatus afterBookmarking = + topicRepository.findWithBookmarkStatusByIdAndMemberId( + bookmarkedTopic.getId(), + member.getId()).get(); + + assertThat(afterBookmarking.getIsBookmarked()).isTrue(); + } + + @Test + @DisplayName("다른 유저와 상관 없이, 특정 토픽의 즐겨찾기 여부를 함께 반환한다.") + public void findWithBookmarkStatusByIdAndMemberId_Success2() { + //given + Member otherMember = + MemberFixture.create("otherMember", "otherMember@naver.com", Role.USER); + memberRepository.save(otherMember); + + Topic otherTopic = TopicFixture.createPublicAndAllMembersTopic(member); + Topic bookmarkedTopic = TopicFixture.createPublicAndAllMembersTopic(member); + topicRepository.save(otherTopic); + topicRepository.save(bookmarkedTopic); + + Bookmark otherBookmark = + Bookmark.createWithAssociatedTopicAndMember(otherTopic, otherMember); + bookmarkRepository.save(otherBookmark); + + //when + TopicWithBookmarkStatus beforeBookmarking = + topicRepository.findWithBookmarkStatusByIdAndMemberId( + bookmarkedTopic.getId(), + member.getId() + ).get(); + assertThat(beforeBookmarking.getIsBookmarked()).isFalse(); + + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(bookmarkedTopic, member); + bookmarkRepository.save(bookmark); + + //then + TopicWithBookmarkStatus afterBookmarking = + topicRepository.findWithBookmarkStatusByIdAndMemberId( + bookmarkedTopic.getId(), + member.getId()).get(); + + assertThat(afterBookmarking.getIsBookmarked()).isTrue(); + } + + @Test + @DisplayName("각각의 토픽에 대한 즐겨찾기 여부를 함께 반환한다.") + public void findAllWithBookmarkStatusByMemberId_Success1() { + //given + Topic topicNotBookmarked = TopicFixture.createPublicAndAllMembersTopic(member); + Topic bookmarkedTopic = TopicFixture.createPublicAndAllMembersTopic(member); + + topicRepository.save(topicNotBookmarked); + topicRepository.save(bookmarkedTopic); + + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(bookmarkedTopic, member); + bookmarkRepository.save(bookmark); + + //when then + List topicsWithBookmarkStatus = + topicRepository.findAllWithBookmarkStatusByMemberId(member.getId()); + + assertThat(topicsWithBookmarkStatus).hasSize(2); + assertThat(topicsWithBookmarkStatus).extractingResultOf("getIsBookmarked") + .containsExactlyInAnyOrder(Boolean.FALSE, Boolean.TRUE); + } + + @Test + @DisplayName("다른 유저와 상관없이, 해당 유저에 대한 각각의 토픽의 즐겨찾기 여부를 반환한다.") + public void findAllWithBookmarkStatusByMemberId_Success2() { + //given + Member otherMember = + MemberFixture.create("otherMember", "otherMember@naver.com", Role.USER); + memberRepository.save(otherMember); + + Topic otherTopic = TopicFixture.createPublicAndAllMembersTopic(member); + Topic bookmarkedTopic = TopicFixture.createPublicAndAllMembersTopic(member); + topicRepository.save(otherTopic); + topicRepository.save(bookmarkedTopic); + + Bookmark otherBookmark = + Bookmark.createWithAssociatedTopicAndMember(otherTopic, otherMember); + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(bookmarkedTopic, member); + bookmarkRepository.save(otherBookmark); + bookmarkRepository.save(bookmark); + + //when then + List topicsWithBookmarkStatus = + topicRepository.findAllWithBookmarkStatusByMemberId(member.getId()); + + assertThat(topicsWithBookmarkStatus).hasSize(2); + assertThat(topicsWithBookmarkStatus).extractingResultOf("getIsBookmarked") + .containsExactlyInAnyOrder(Boolean.FALSE, Boolean.TRUE); + } + + @Test + @DisplayName("여러 토픽 조회시, 해당 유저에 대한 각각의 토픽의 즐겨찾기 여부를 반환한다.") + public void findWithBookmarkStatusByIds_Success1() { + //given + Topic topicNotBookmarked = TopicFixture.createPublicAndAllMembersTopic(member); + Topic bookmarkedTopic = TopicFixture.createPublicAndAllMembersTopic(member); + + topicRepository.save(topicNotBookmarked); + topicRepository.save(bookmarkedTopic); + + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(bookmarkedTopic, member); + bookmarkRepository.save(bookmark); + + //when then + List topicsWithBookmarkStatus = + topicRepository.findWithBookmarkStatusByIdsAndMemberId( + List.of(topicNotBookmarked.getId(), bookmarkedTopic.getId()), + member.getId() + ); + + assertThat(topicsWithBookmarkStatus).hasSize(2); + assertThat(topicsWithBookmarkStatus).extractingResultOf("getIsBookmarked") + .containsExactlyInAnyOrder(Boolean.FALSE, Boolean.TRUE); + } + + @Test + @DisplayName("여러 토픽 조회시, 다른 유저와 상관없이 해당 유저의 각각에 대한 토픽의 즐겨찾기 여부를 반환한다.") + public void findWithBookmarkStatusByIds_Success2() { + //given + Member otherMember = + MemberFixture.create("otherMember", "otherMember@naver.com", Role.USER); + memberRepository.save(otherMember); + + Topic otherTopic = TopicFixture.createPublicAndAllMembersTopic(member); + Topic bookmarkedTopic = TopicFixture.createPublicAndAllMembersTopic(member); + topicRepository.save(otherTopic); + topicRepository.save(bookmarkedTopic); + + Bookmark otherBookmark = + Bookmark.createWithAssociatedTopicAndMember(otherTopic, otherMember); + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(bookmarkedTopic, member); + bookmarkRepository.save(otherBookmark); + bookmarkRepository.save(bookmark); + + //when then + List topicsWithBookmarkStatus = + topicRepository.findWithBookmarkStatusByIdsAndMemberId( + List.of(otherTopic.getId(), bookmarkedTopic.getId()), + member.getId() + ); + + assertThat(topicsWithBookmarkStatus).hasSize(2); + assertThat(topicsWithBookmarkStatus).extractingResultOf("getIsBookmarked") + .containsExactlyInAnyOrder(Boolean.FALSE, Boolean.TRUE); + } + +} \ No newline at end of file diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java index 0af3b59a..80097fc0 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/presentation/TopicControllerTest.java @@ -129,19 +129,24 @@ void findAll() throws Exception { String authHeader = Base64.encodeBase64String( String.format(BASIC_FORMAT, member.getMemberInfo().getEmail()).getBytes() ); - List responses = List.of(new TopicResponse( - 1L, - "준팍의 또 토픽", - "https://map-befine-official.github.io/favicon.png", - 3, - LocalDateTime.now() - ), new TopicResponse( - 2L, - "준팍의 두번째 토픽", - "https://map-befine-official.github.io/favicon.png", - 5, - LocalDateTime.now() - )); + List responses = List.of( + new TopicResponse( + 1L, + "준팍의 또 토픽", + "https://map-befine-official.github.io/favicon.png", + 3, + 0, + Boolean.FALSE, + LocalDateTime.now() + ), new TopicResponse( + 2L, + "준팍의 두번째 토픽", + "https://map-befine-official.github.io/favicon.png", + 5, + 0, + Boolean.FALSE, + LocalDateTime.now() + )); given(topicQueryService.findAllReadable(any())).willReturn(responses); mockMvc.perform( @@ -179,7 +184,9 @@ void findById() throws Exception { 37, 127 ) - ) + ), + Boolean.FALSE, + 0 ); given(topicQueryService.findDetailById(any(), any())).willReturn(topicDetailResponse);