Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package project.flipnote.bookmark.listener;

import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import project.flipnote.bookmark.service.BookmarkService;
import project.flipnote.common.model.event.GroupLeftEvent;

@Slf4j
@RequiredArgsConstructor
@Component
public class GroupLeftCleanupBookmarkListener {

private final BookmarkService bookmarkService;

@Async
@Retryable(
maxAttempts = 3,
backoff = @Backoff(delay = 2000, multiplier = 2)
)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleGroupLeftEvent(GroupLeftEvent event) {
// TODO: 해당 이벤트 그룹 탈퇴시 퍼블리싱되게
bookmarkService.removePrivateCardSetBookmarks(event.groupId(), event.userId());
}
Comment thread
dungbik marked this conversation as resolved.

@Recover
public void recover(Exception ex, GroupLeftEvent event) {
log.error("그룹 탈퇴 후처리 - 비공개 카드셋 북마크 제거 실패: groupId={}, userId={}", event.groupId(), event.userId(), ex);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package project.flipnote.bookmark.repository;

import java.util.Optional;
import java.util.Set;

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

Page<Bookmark> findAllByTargetTypeAndUserId(BookmarkTargetType targetType, Long userId, Pageable pageable);

int deleteByTargetTypeAndUserIdAndTargetIdIn(BookmarkTargetType targetType, Long userId, Set<Long> targetIds);
Comment thread
dungbik marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public class BookmarkPolicyService {
private final BookmarkRepository bookmarkRepository;
private final BookmarkTargetFetchService<BookmarkTargetResponse> bookmarkTargetFetchService;

public void validateTargetExists(BookmarkTargetType targetType, Long targetId) {
if (!bookmarkTargetFetchService.existsByTypeAndId(targetType, targetId)) {
public void validateTargetViewable(BookmarkTargetType targetType, Long targetId, Long userId) {
if (!bookmarkTargetFetchService.isTargetViewable(targetType, targetId, userId)) {
throw new BizException(BookmarkErrorCode.BOOKMARK_TARGET_NOT_FOUND);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import project.flipnote.bookmark.model.BookmarkSearchRequest;
import project.flipnote.bookmark.model.BookmarkTargetResponse;
import project.flipnote.bookmark.repository.BookmarkRepository;
import project.flipnote.cardset.service.CardSetService;
import project.flipnote.common.exception.BizException;
import project.flipnote.common.model.response.IdResponse;
import project.flipnote.common.model.response.PagingResponse;
Expand All @@ -30,6 +31,7 @@ public class BookmarkService {
private final BookmarkPolicyService bookmarkPolicyService;
private final BookmarkRepository bookmarkRepository;
private final BookmarkTargetFetchService<BookmarkTargetResponse> bookmarkTargetFetchService;
private final CardSetService cardSetService;

/**
* 즐겨찾기 추가
Expand All @@ -43,7 +45,7 @@ public class BookmarkService {
@Transactional
public IdResponse addBookmark(Long userId, BookmarkTargetType targetType, Long targetId) {
bookmarkPolicyService.validateBookmarkNotExists(targetType, userId, targetId);
bookmarkPolicyService.validateTargetExists(targetType, targetId);
bookmarkPolicyService.validateTargetViewable(targetType, targetId, userId);

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

Map<Long, BookmarkTargetResponse> targetMap
= bookmarkTargetFetchService.fetchByTypeAndIds(targetType, targetIds);
= bookmarkTargetFetchService.fetchByTypeAndIds(targetType, targetIds, userId);
Page<BookmarkResponse<BookmarkTargetResponse>> content
= bookmarkPage.map(bookmark ->
new BookmarkResponse<>(
Expand All @@ -111,4 +113,21 @@ public PagingResponse<BookmarkResponse<BookmarkTargetResponse>> getBookmarks(

return PagingResponse.from(content);
}

/**
* 해당 그룹의 비공개 카드셋 즐겨찾기 제거
*
* @param groupId 즐겨찾기를 제거할 카드셋이 속한 그룹 ID
* @param userId 즐겨찾기를 제거할 사용자 ID
* @author 윤정환
*/
@Transactional
public void removePrivateCardSetBookmarks(Long groupId, Long userId) {
Set<Long> privateCardSetIds = cardSetService.findPrivateCardSetIds(groupId);
if (privateCardSetIds == null || privateCardSetIds.isEmpty()) {
return;
}

bookmarkRepository.deleteByTargetTypeAndUserIdAndTargetIdIn(BookmarkTargetType.CARD_SET, userId, privateCardSetIds);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,20 @@ public void init() {
.collect(Collectors.toMap(BookmarkTargetFetcher::getTargetType, Function.identity()));
}

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

return targetFetcher.existsById(targetId);
return targetFetcher.isTargetViewable(targetId, userId);
}

public Map<Long, T> fetchByTypeAndIds(
BookmarkTargetType targetType,
Set<Long> targetIds
Set<Long> targetIds,
Long userId
) {
BookmarkTargetFetcher<T> targetFetcher = getFetcher(targetType);

return targetFetcher.fetchByIds(targetIds);
return targetFetcher.fetchByIds(targetIds, userId);
}

private BookmarkTargetFetcher<T> getFetcher(BookmarkTargetType targetType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ public BookmarkTargetType getTargetType() {
}

@Override
public boolean existsById(Long targetId) {
return cardSetService.existsById(targetId);
public boolean isTargetViewable(Long targetId, Long userId) {
return cardSetService.isCardSetViewable(targetId, userId);
}

@Override
public Map<Long, CardSetBookmarkResponse> fetchByIds(Set<Long> ids) {
return cardSetService.getCardSetsByIds(ids).stream()
public Map<Long, CardSetBookmarkResponse> fetchByIds(Set<Long> targetIds, Long userId) {
return cardSetService.findViewableCardSetsByIds(targetIds, userId).stream()
.map(CardSetBookmarkResponse::from)
.collect(Collectors.toMap(CardSetBookmarkResponse::getId, Function.identity()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
public interface BookmarkTargetFetcher<T extends BookmarkTargetResponse> {
BookmarkTargetType getTargetType();

boolean existsById(Long targetId);
boolean isTargetViewable(Long targetId, Long userId);

Map<Long, T> fetchByIds(Set<Long> ids);
Map<Long, T> fetchByIds(Set<Long> targetIds, Long userId);
}
Comment thread
dungbik marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package project.flipnote.cardset.repository;

import java.util.Optional;
import java.util.Set;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand Down Expand Up @@ -29,4 +30,10 @@ Page<CardSet> findByNameContainingAndCategory(

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

@Query("""
SELECT c.id FROM CardSet c
WHERE c.group.id = :groupId
AND c.publicVisible = false
""")
Set<Long> findPrivateIdsByGroupId(@Param("groupId") Long groupId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,30 @@ public void validateCardSetEditable(Long userId, Long cardSetId) {
*
* @param cardSet 조회 대상 카드셋 엔티티
* @param userId 조회 권한을 검증할 회원의 ID
* @param groupId 카드셋이 속한 그룹의 ID
* @author 윤정환
*/
public void validateCardSetViewable(CardSet cardSet, Long userId, Long groupId) {
if (!cardSet.getPublicVisible() && !groupService.existsMember(groupId, userId)) {
public void validateCardSetViewable(CardSet cardSet, Long userId) {
if (!isCardSetViewable(cardSet, userId)) {
throw new BizException(CardSetErrorCode.CARD_SET_PRIVATE);
}
}

/**
* 특정 회원이 해당 카드셋을 조회할 수 있는 권한이 있는지 확인
*
* @param cardSet 조회 대상 카드셋 엔티티
* @param userId 조회 권한을 검증할 회원의 ID
* @return 카드셋 조회 가능 여부
* @author 윤정환
*/
public boolean isCardSetViewable(CardSet cardSet, Long userId) {
if (cardSet == null || userId == null) {
return false;
}
if (cardSet.getGroup() == null || cardSet.getGroup().getId() == null) {
return false;
}

return cardSet.getPublicVisible() || groupService.existsMember(cardSet.getGroup().getId(), userId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ public PagingResponse<CardSetSummaryResponse> getCardSets(CardSetSearchRequest r
public CardSetDetailResponse getCardSet(Long userId, Long groupId, Long cardSetId) {
CardSet cardSet = cardSetPolicyService.findByIdAndGroupIdOrThrow(groupId, cardSetId);

cardSetPolicyService.validateCardSetViewable(cardSet, userId, groupId);
cardSetPolicyService.validateCardSetViewable(cardSet, userId);

return CardSetDetailResponse.from(cardSet);
}
Expand Down Expand Up @@ -219,4 +219,46 @@ public List<CardSetSummaryResponse> getCardSetsByIds(Set<Long> targetIds) {
.map(CardSetSummaryResponse::from)
.toList();
}

/**
* 사용자가 특정 카드셋에 접근할 수 있는지 여부를 확인
*
* @param cardSetId 확인할 카드셋의 ID
* @param userId 접근 권한을 확인할 사용자의 ID
* @return 접근 가능 여부
* @author 윤정환
*/
public boolean isCardSetViewable(Long cardSetId, Long userId) {
return cardSetRepository.findById(cardSetId)
.map(cardSet -> cardSetPolicyService.isCardSetViewable(cardSet, userId))
.orElse(false);
}

/**
* 카드셋 ID 목록에 해당하는 카드셋 목록 조회
*
* @param targetIds 조회할 카드셋 ID 목록
* @param userId 카드셋 목록을 조회하는 회원 ID
* @return 조회된 카드셋 목록
* @author 윤정환
*/
@Transactional
public List<CardSetSummaryResponse> findViewableCardSetsByIds(Set<Long> targetIds, Long userId) {
// TODO: MSA로 전환시 전용 DTO로 변경 필요
return cardSetRepository.findAllById(targetIds).stream()
.filter(cardSet -> cardSetPolicyService.isCardSetViewable(cardSet, userId))
.map(CardSetSummaryResponse::from)
.toList();
}

/**
* 해당 그룹의 비공개인 카드셋의 ID들을 조회
*
* @param groupId 조회할 그룹의 ID
* @return 그룹에 속한 비공개 카드셋 ID의 집합
* @author 윤정환
*/
public Set<Long> findPrivateCardSetIds(Long groupId) {
return cardSetRepository.findPrivateIdsByGroupId(groupId);
}
}
9 changes: 9 additions & 0 deletions src/main/java/project/flipnote/common/config/RetryConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package project.flipnote.common.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;

@EnableRetry
@Configuration
public class RetryConfig {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package project.flipnote.common.model.event;

public record GroupLeftEvent(
Long groupId,
Long userId
) {
}
Loading