Skip to content

Commit 9a4bc0c

Browse files
authored
Merge pull request #43 from FlipNoteTeam/feat/bookmark
Feat: [FN-144][FN-160][FN-161] 즐겨찾기 기능
2 parents fea4430 + 1ff5322 commit 9a4bc0c

12 files changed

Lines changed: 162 additions & 18 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package project.flipnote.bookmark.listener;
2+
3+
import org.springframework.retry.annotation.Backoff;
4+
import org.springframework.retry.annotation.Recover;
5+
import org.springframework.retry.annotation.Retryable;
6+
import org.springframework.scheduling.annotation.Async;
7+
import org.springframework.stereotype.Component;
8+
import org.springframework.transaction.event.TransactionPhase;
9+
import org.springframework.transaction.event.TransactionalEventListener;
10+
11+
import lombok.RequiredArgsConstructor;
12+
import lombok.extern.slf4j.Slf4j;
13+
import project.flipnote.bookmark.service.BookmarkService;
14+
import project.flipnote.common.model.event.GroupLeftEvent;
15+
16+
@Slf4j
17+
@RequiredArgsConstructor
18+
@Component
19+
public class GroupLeftCleanupBookmarkListener {
20+
21+
private final BookmarkService bookmarkService;
22+
23+
@Async
24+
@Retryable(
25+
maxAttempts = 3,
26+
backoff = @Backoff(delay = 2000, multiplier = 2)
27+
)
28+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
29+
public void handleGroupLeftEvent(GroupLeftEvent event) {
30+
// TODO: 해당 이벤트 그룹 탈퇴시 퍼블리싱되게
31+
bookmarkService.removePrivateCardSetBookmarks(event.groupId(), event.userId());
32+
}
33+
34+
@Recover
35+
public void recover(Exception ex, GroupLeftEvent event) {
36+
log.error("그룹 탈퇴 후처리 - 비공개 카드셋 북마크 제거 실패: groupId={}, userId={}", event.groupId(), event.userId(), ex);
37+
}
38+
}

src/main/java/project/flipnote/bookmark/repository/BookmarkRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package project.flipnote.bookmark.repository;
22

33
import java.util.Optional;
4+
import java.util.Set;
45

56
import org.springframework.data.domain.Page;
67
import org.springframework.data.domain.Pageable;
@@ -15,4 +16,6 @@ public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {
1516
Optional<Bookmark> findByTargetTypeAndUserIdAndTargetId(BookmarkTargetType targetType, Long userId, Long targetId);
1617

1718
Page<Bookmark> findAllByTargetTypeAndUserId(BookmarkTargetType targetType, Long userId, Pageable pageable);
19+
20+
int deleteByTargetTypeAndUserIdAndTargetIdIn(BookmarkTargetType targetType, Long userId, Set<Long> targetIds);
1821
}

src/main/java/project/flipnote/bookmark/service/BookmarkPolicyService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ public class BookmarkPolicyService {
1616
private final BookmarkRepository bookmarkRepository;
1717
private final BookmarkTargetFetchService<BookmarkTargetResponse> bookmarkTargetFetchService;
1818

19-
public void validateTargetExists(BookmarkTargetType targetType, Long targetId) {
20-
if (!bookmarkTargetFetchService.existsByTypeAndId(targetType, targetId)) {
19+
public void validateTargetViewable(BookmarkTargetType targetType, Long targetId, Long userId) {
20+
if (!bookmarkTargetFetchService.isTargetViewable(targetType, targetId, userId)) {
2121
throw new BizException(BookmarkErrorCode.BOOKMARK_TARGET_NOT_FOUND);
2222
}
2323
}

src/main/java/project/flipnote/bookmark/service/BookmarkService.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import project.flipnote.bookmark.model.BookmarkSearchRequest;
1919
import project.flipnote.bookmark.model.BookmarkTargetResponse;
2020
import project.flipnote.bookmark.repository.BookmarkRepository;
21+
import project.flipnote.cardset.service.CardSetService;
2122
import project.flipnote.common.exception.BizException;
2223
import project.flipnote.common.model.response.IdResponse;
2324
import project.flipnote.common.model.response.PagingResponse;
@@ -30,6 +31,7 @@ public class BookmarkService {
3031
private final BookmarkPolicyService bookmarkPolicyService;
3132
private final BookmarkRepository bookmarkRepository;
3233
private final BookmarkTargetFetchService<BookmarkTargetResponse> bookmarkTargetFetchService;
34+
private final CardSetService cardSetService;
3335

3436
/**
3537
* 즐겨찾기 추가
@@ -43,7 +45,7 @@ public class BookmarkService {
4345
@Transactional
4446
public IdResponse addBookmark(Long userId, BookmarkTargetType targetType, Long targetId) {
4547
bookmarkPolicyService.validateBookmarkNotExists(targetType, userId, targetId);
46-
bookmarkPolicyService.validateTargetExists(targetType, targetId);
48+
bookmarkPolicyService.validateTargetViewable(targetType, targetId, userId);
4749

4850
Bookmark bookmark = Bookmark.builder()
4951
.targetType(targetType)
@@ -100,7 +102,7 @@ public PagingResponse<BookmarkResponse<BookmarkTargetResponse>> getBookmarks(
100102
Set<Long> targetIds = likedAtMap.keySet();
101103

102104
Map<Long, BookmarkTargetResponse> targetMap
103-
= bookmarkTargetFetchService.fetchByTypeAndIds(targetType, targetIds);
105+
= bookmarkTargetFetchService.fetchByTypeAndIds(targetType, targetIds, userId);
104106
Page<BookmarkResponse<BookmarkTargetResponse>> content
105107
= bookmarkPage.map(bookmark ->
106108
new BookmarkResponse<>(
@@ -111,4 +113,21 @@ public PagingResponse<BookmarkResponse<BookmarkTargetResponse>> getBookmarks(
111113

112114
return PagingResponse.from(content);
113115
}
116+
117+
/**
118+
* 해당 그룹의 비공개 카드셋 즐겨찾기 제거
119+
*
120+
* @param groupId 즐겨찾기를 제거할 카드셋이 속한 그룹 ID
121+
* @param userId 즐겨찾기를 제거할 사용자 ID
122+
* @author 윤정환
123+
*/
124+
@Transactional
125+
public void removePrivateCardSetBookmarks(Long groupId, Long userId) {
126+
Set<Long> privateCardSetIds = cardSetService.findPrivateCardSetIds(groupId);
127+
if (privateCardSetIds == null || privateCardSetIds.isEmpty()) {
128+
return;
129+
}
130+
131+
bookmarkRepository.deleteByTargetTypeAndUserIdAndTargetIdIn(BookmarkTargetType.CARD_SET, userId, privateCardSetIds);
132+
}
114133
}

src/main/java/project/flipnote/bookmark/service/BookmarkTargetFetchService.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,20 @@ public void init() {
3030
.collect(Collectors.toMap(BookmarkTargetFetcher::getTargetType, Function.identity()));
3131
}
3232

33-
public boolean existsByTypeAndId(BookmarkTargetType targetType, Long targetId) {
33+
public boolean isTargetViewable(BookmarkTargetType targetType, Long targetId, Long userId) {
3434
BookmarkTargetFetcher<T> targetFetcher = getFetcher(targetType);
3535

36-
return targetFetcher.existsById(targetId);
36+
return targetFetcher.isTargetViewable(targetId, userId);
3737
}
3838

3939
public Map<Long, T> fetchByTypeAndIds(
4040
BookmarkTargetType targetType,
41-
Set<Long> targetIds
41+
Set<Long> targetIds,
42+
Long userId
4243
) {
4344
BookmarkTargetFetcher<T> targetFetcher = getFetcher(targetType);
4445

45-
return targetFetcher.fetchByIds(targetIds);
46+
return targetFetcher.fetchByIds(targetIds, userId);
4647
}
4748

4849
private BookmarkTargetFetcher<T> getFetcher(BookmarkTargetType targetType) {

src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkCardSetFetcher.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ public BookmarkTargetType getTargetType() {
2424
}
2525

2626
@Override
27-
public boolean existsById(Long targetId) {
28-
return cardSetService.existsById(targetId);
27+
public boolean isTargetViewable(Long targetId, Long userId) {
28+
return cardSetService.isCardSetViewable(targetId, userId);
2929
}
3030

3131
@Override
32-
public Map<Long, CardSetBookmarkResponse> fetchByIds(Set<Long> ids) {
33-
return cardSetService.getCardSetsByIds(ids).stream()
32+
public Map<Long, CardSetBookmarkResponse> fetchByIds(Set<Long> targetIds, Long userId) {
33+
return cardSetService.findViewableCardSetsByIds(targetIds, userId).stream()
3434
.map(CardSetBookmarkResponse::from)
3535
.collect(Collectors.toMap(CardSetBookmarkResponse::getId, Function.identity()));
3636
}

src/main/java/project/flipnote/bookmark/service/fetcher/BookmarkTargetFetcher.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
public interface BookmarkTargetFetcher<T extends BookmarkTargetResponse> {
1010
BookmarkTargetType getTargetType();
1111

12-
boolean existsById(Long targetId);
12+
boolean isTargetViewable(Long targetId, Long userId);
1313

14-
Map<Long, T> fetchByIds(Set<Long> ids);
14+
Map<Long, T> fetchByIds(Set<Long> targetIds, Long userId);
1515
}

src/main/java/project/flipnote/cardset/repository/CardSetRepository.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package project.flipnote.cardset.repository;
22

33
import java.util.Optional;
4+
import java.util.Set;
45

56
import org.springframework.data.domain.Page;
67
import org.springframework.data.domain.Pageable;
@@ -29,4 +30,10 @@ Page<CardSet> findByNameContainingAndCategory(
2930

3031
Optional<CardSet> findByIdAndGroup_Id(Long id, Long groupId);
3132

33+
@Query("""
34+
SELECT c.id FROM CardSet c
35+
WHERE c.group.id = :groupId
36+
AND c.publicVisible = false
37+
""")
38+
Set<Long> findPrivateIdsByGroupId(@Param("groupId") Long groupId);
3239
}

src/main/java/project/flipnote/cardset/service/CardSetPolicyService.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,30 @@ public void validateCardSetEditable(Long userId, Long cardSetId) {
4949
*
5050
* @param cardSet 조회 대상 카드셋 엔티티
5151
* @param userId 조회 권한을 검증할 회원의 ID
52-
* @param groupId 카드셋이 속한 그룹의 ID
5352
* @author 윤정환
5453
*/
55-
public void validateCardSetViewable(CardSet cardSet, Long userId, Long groupId) {
56-
if (!cardSet.getPublicVisible() && !groupService.existsMember(groupId, userId)) {
54+
public void validateCardSetViewable(CardSet cardSet, Long userId) {
55+
if (!isCardSetViewable(cardSet, userId)) {
5756
throw new BizException(CardSetErrorCode.CARD_SET_PRIVATE);
5857
}
5958
}
59+
60+
/**
61+
* 특정 회원이 해당 카드셋을 조회할 수 있는 권한이 있는지 확인
62+
*
63+
* @param cardSet 조회 대상 카드셋 엔티티
64+
* @param userId 조회 권한을 검증할 회원의 ID
65+
* @return 카드셋 조회 가능 여부
66+
* @author 윤정환
67+
*/
68+
public boolean isCardSetViewable(CardSet cardSet, Long userId) {
69+
if (cardSet == null || userId == null) {
70+
return false;
71+
}
72+
if (cardSet.getGroup() == null || cardSet.getGroup().getId() == null) {
73+
return false;
74+
}
75+
76+
return cardSet.getPublicVisible() || groupService.existsMember(cardSet.getGroup().getId(), userId);
77+
}
6078
}

src/main/java/project/flipnote/cardset/service/CardSetService.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ public PagingResponse<CardSetSummaryResponse> getCardSets(CardSetSearchRequest r
143143
public CardSetDetailResponse getCardSet(Long userId, Long groupId, Long cardSetId) {
144144
CardSet cardSet = cardSetPolicyService.findByIdAndGroupIdOrThrow(groupId, cardSetId);
145145

146-
cardSetPolicyService.validateCardSetViewable(cardSet, userId, groupId);
146+
cardSetPolicyService.validateCardSetViewable(cardSet, userId);
147147

148148
return CardSetDetailResponse.from(cardSet);
149149
}
@@ -219,4 +219,46 @@ public List<CardSetSummaryResponse> getCardSetsByIds(Set<Long> targetIds) {
219219
.map(CardSetSummaryResponse::from)
220220
.toList();
221221
}
222+
223+
/**
224+
* 사용자가 특정 카드셋에 접근할 수 있는지 여부를 확인
225+
*
226+
* @param cardSetId 확인할 카드셋의 ID
227+
* @param userId 접근 권한을 확인할 사용자의 ID
228+
* @return 접근 가능 여부
229+
* @author 윤정환
230+
*/
231+
public boolean isCardSetViewable(Long cardSetId, Long userId) {
232+
return cardSetRepository.findById(cardSetId)
233+
.map(cardSet -> cardSetPolicyService.isCardSetViewable(cardSet, userId))
234+
.orElse(false);
235+
}
236+
237+
/**
238+
* 카드셋 ID 목록에 해당하는 카드셋 목록 조회
239+
*
240+
* @param targetIds 조회할 카드셋 ID 목록
241+
* @param userId 카드셋 목록을 조회하는 회원 ID
242+
* @return 조회된 카드셋 목록
243+
* @author 윤정환
244+
*/
245+
@Transactional
246+
public List<CardSetSummaryResponse> findViewableCardSetsByIds(Set<Long> targetIds, Long userId) {
247+
// TODO: MSA로 전환시 전용 DTO로 변경 필요
248+
return cardSetRepository.findAllById(targetIds).stream()
249+
.filter(cardSet -> cardSetPolicyService.isCardSetViewable(cardSet, userId))
250+
.map(CardSetSummaryResponse::from)
251+
.toList();
252+
}
253+
254+
/**
255+
* 해당 그룹의 비공개인 카드셋의 ID들을 조회
256+
*
257+
* @param groupId 조회할 그룹의 ID
258+
* @return 그룹에 속한 비공개 카드셋 ID의 집합
259+
* @author 윤정환
260+
*/
261+
public Set<Long> findPrivateCardSetIds(Long groupId) {
262+
return cardSetRepository.findPrivateIdsByGroupId(groupId);
263+
}
222264
}

0 commit comments

Comments
 (0)