Skip to content

Commit df5978f

Browse files
authored
Merge pull request #47 from FlipNoteTeam/feat/cardset-search-sort
Feat: [FN-151] 카드셋 목록 조회시 즐겨찾기 수 정렬 기능
2 parents 9474f4b + d45e4b3 commit df5978f

15 files changed

Lines changed: 284 additions & 8 deletions
Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
package project.flipnote.bookmark.entity;
22

3+
import lombok.extern.slf4j.Slf4j;
4+
import project.flipnote.common.model.event.BookmarkEventTargetType;
5+
6+
@Slf4j
37
public enum BookmarkTargetType {
4-
CARD_SET
8+
CARD_SET;
9+
10+
public BookmarkEventTargetType toEventType() {
11+
try {
12+
return BookmarkEventTargetType.valueOf(this.name());
13+
} catch (IllegalArgumentException e) {
14+
log.error("Failed to map BookmarkTargetType '{}' to BookmarkEventTargetType", this.name(), e);
15+
throw new IllegalStateException(
16+
"Invalid mapping from BookmarkTargetType to BookmarkEventTargetType: " + this.name(),
17+
e
18+
);
19+
}
20+
}
521
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package project.flipnote.bookmark.repository;
22

3+
import java.util.List;
34
import java.util.Optional;
45
import java.util.Set;
56

@@ -17,5 +18,7 @@ public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {
1718

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

20-
int deleteByTargetTypeAndUserIdAndTargetIdIn(BookmarkTargetType targetType, Long userId, Set<Long> targetIds);
21+
List<Bookmark> findAllByTargetTypeAndUserIdAndTargetIdIn(
22+
BookmarkTargetType targetType, Long userId, Set<Long> targetIds
23+
);
2124
}

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package project.flipnote.bookmark.service;
22

33
import java.time.LocalDateTime;
4+
import java.util.List;
45
import java.util.Map;
56
import java.util.Set;
67
import java.util.stream.Collectors;
78

9+
import org.springframework.context.ApplicationEventPublisher;
810
import org.springframework.dao.DataIntegrityViolationException;
911
import org.springframework.data.domain.Page;
1012
import org.springframework.stereotype.Service;
@@ -20,6 +22,9 @@
2022
import project.flipnote.bookmark.repository.BookmarkRepository;
2123
import project.flipnote.cardset.service.CardSetService;
2224
import project.flipnote.common.exception.BizException;
25+
import project.flipnote.common.model.event.BookmarkAddedEvent;
26+
import project.flipnote.common.model.event.BookmarkRemovedEvent;
27+
import project.flipnote.common.model.event.BulkBookmarkRemovedEvent;
2328
import project.flipnote.common.model.response.IdResponse;
2429
import project.flipnote.common.model.response.PagingResponse;
2530

@@ -32,6 +37,7 @@ public class BookmarkService {
3237
private final BookmarkRepository bookmarkRepository;
3338
private final BookmarkTargetFetchService<BookmarkTargetResponse> bookmarkTargetFetchService;
3439
private final CardSetService cardSetService;
40+
private final ApplicationEventPublisher eventPublisher;
3541

3642
/**
3743
* 즐겨찾기 추가
@@ -59,6 +65,8 @@ public IdResponse addBookmark(Long userId, BookmarkTargetType targetType, Long t
5965
throw new BizException(BookmarkErrorCode.BOOKMARK_ALREADY_EXISTS);
6066
}
6167

68+
eventPublisher.publishEvent(new BookmarkAddedEvent(targetType.toEventType(), targetId, userId));
69+
6270
return IdResponse.from(bookmark.getId());
6371
}
6472

@@ -78,6 +86,8 @@ public IdResponse deleteBookmark(Long userId, BookmarkTargetType targetType, Lon
7886

7987
bookmarkRepository.delete(bookmark);
8088

89+
eventPublisher.publishEvent(new BookmarkRemovedEvent(targetType.toEventType(), targetId, userId));
90+
8191
return IdResponse.from(bookmark.getId());
8292
}
8393

@@ -128,6 +138,18 @@ public void removePrivateCardSetBookmarks(Long groupId, Long userId) {
128138
return;
129139
}
130140

131-
bookmarkRepository.deleteByTargetTypeAndUserIdAndTargetIdIn(BookmarkTargetType.CARD_SET, userId, privateCardSetIds);
141+
BookmarkTargetType targetType = BookmarkTargetType.CARD_SET;
142+
List<Bookmark> bookmarks
143+
= bookmarkRepository.findAllByTargetTypeAndUserIdAndTargetIdIn(targetType, userId, privateCardSetIds);
144+
if (bookmarks.isEmpty()) {
145+
return;
146+
}
147+
148+
bookmarkRepository.deleteAll(bookmarks);
149+
150+
List<Long> targetIds = bookmarks.stream()
151+
.map(Bookmark::getTargetId)
152+
.toList();
153+
eventPublisher.publishEvent(new BulkBookmarkRemovedEvent(targetType.toEventType(), targetIds, userId));
132154
}
133155
}

src/main/java/project/flipnote/cardset/entity/CardSetMetadata.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,13 @@ public class CardSetMetadata {
2121
@Column(nullable = false)
2222
private int likeCount;
2323

24+
@Column(nullable = false)
25+
private int bookmarkCount;
26+
2427
@Builder
2528
public CardSetMetadata(Long id) {
2629
this.id = id;
30+
this.likeCount = 0;
31+
this.bookmarkCount = 0;
2732
}
2833
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package project.flipnote.cardset.listener;
2+
3+
import org.springframework.dao.DataAccessException;
4+
import org.springframework.retry.annotation.Backoff;
5+
import org.springframework.retry.annotation.Recover;
6+
import org.springframework.retry.annotation.Retryable;
7+
import org.springframework.scheduling.annotation.Async;
8+
import org.springframework.stereotype.Component;
9+
import org.springframework.transaction.event.TransactionPhase;
10+
import org.springframework.transaction.event.TransactionalEventListener;
11+
12+
import lombok.RequiredArgsConstructor;
13+
import lombok.extern.slf4j.Slf4j;
14+
import project.flipnote.cardset.service.CardSetService;
15+
import project.flipnote.common.model.event.BookmarkEventTargetType;
16+
import project.flipnote.common.model.event.BulkBookmarkRemovedEvent;
17+
18+
@Slf4j
19+
@RequiredArgsConstructor
20+
@Component
21+
public class BulkCardSetBookmarkRemovedEventHandler {
22+
23+
private final CardSetService cardSetService;
24+
25+
@Async
26+
@Retryable(
27+
maxAttempts = 3,
28+
retryFor = DataAccessException.class,
29+
backoff = @Backoff(delay = 2000, multiplier = 2)
30+
)
31+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
32+
public void handleBookmarkRemovedEvent(BulkBookmarkRemovedEvent event) {
33+
if (event.targetType() != BookmarkEventTargetType.CARD_SET) {
34+
return;
35+
}
36+
37+
cardSetService.decrementBookmarkCount(event.targetIds());
38+
}
39+
40+
@Recover
41+
public void recover(Exception ex, BulkBookmarkRemovedEvent event) {
42+
log.error(
43+
"카드셋 즐겨찾기 벌크 삭제 처리 중 예외 발생 : targetType={}, userId={}, targetIds={}",
44+
event.targetType(), event.userId(), event.targetIds(), ex
45+
);
46+
}
47+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package project.flipnote.cardset.listener;
2+
3+
import org.springframework.dao.DataAccessException;
4+
import org.springframework.retry.annotation.Backoff;
5+
import org.springframework.retry.annotation.Recover;
6+
import org.springframework.retry.annotation.Retryable;
7+
import org.springframework.scheduling.annotation.Async;
8+
import org.springframework.stereotype.Component;
9+
import org.springframework.transaction.event.TransactionPhase;
10+
import org.springframework.transaction.event.TransactionalEventListener;
11+
12+
import lombok.RequiredArgsConstructor;
13+
import lombok.extern.slf4j.Slf4j;
14+
import project.flipnote.cardset.service.CardSetService;
15+
import project.flipnote.common.model.event.BookmarkAddedEvent;
16+
import project.flipnote.common.model.event.BookmarkEventTargetType;
17+
18+
@Slf4j
19+
@RequiredArgsConstructor
20+
@Component
21+
public class CardSetBookmarkAddedEventHandler {
22+
23+
private final CardSetService cardSetService;
24+
25+
@Async
26+
@Retryable(
27+
maxAttempts = 3,
28+
retryFor = DataAccessException.class,
29+
backoff = @Backoff(delay = 2000, multiplier = 2)
30+
)
31+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
32+
public void handleBookmarkAddedEvent(BookmarkAddedEvent event) {
33+
if (event.targetType() != BookmarkEventTargetType.CARD_SET) {
34+
return;
35+
}
36+
37+
cardSetService.incrementBookmarkCount(event.targetId());
38+
}
39+
40+
@Recover
41+
public void recover(Exception ex, BookmarkAddedEvent event) {
42+
log.error(
43+
"카드셋 즐겨찾기 추가 처리 중 예외 발생 : targetType={}, targetId={}, userId={}",
44+
event.targetType(), event.targetId(), event.userId(), ex
45+
);
46+
}
47+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package project.flipnote.cardset.listener;
2+
3+
import org.springframework.dao.DataAccessException;
4+
import org.springframework.retry.annotation.Backoff;
5+
import org.springframework.retry.annotation.Recover;
6+
import org.springframework.retry.annotation.Retryable;
7+
import org.springframework.scheduling.annotation.Async;
8+
import org.springframework.stereotype.Component;
9+
import org.springframework.transaction.event.TransactionPhase;
10+
import org.springframework.transaction.event.TransactionalEventListener;
11+
12+
import lombok.RequiredArgsConstructor;
13+
import lombok.extern.slf4j.Slf4j;
14+
import project.flipnote.cardset.service.CardSetService;
15+
import project.flipnote.common.model.event.BookmarkEventTargetType;
16+
import project.flipnote.common.model.event.BookmarkRemovedEvent;
17+
18+
@Slf4j
19+
@RequiredArgsConstructor
20+
@Component
21+
public class CardSetBookmarkRemovedEventHandler {
22+
23+
private final CardSetService cardSetService;
24+
25+
@Async
26+
@Retryable(
27+
maxAttempts = 3,
28+
retryFor = DataAccessException.class,
29+
backoff = @Backoff(delay = 2000, multiplier = 2)
30+
)
31+
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
32+
public void handleBookmarkRemovedEvent(BookmarkRemovedEvent event) {
33+
if (event.targetType() != BookmarkEventTargetType.CARD_SET) {
34+
return;
35+
}
36+
37+
cardSetService.decrementBookmarkCount(event.targetId());
38+
}
39+
40+
@Recover
41+
public void recover(Exception ex, BookmarkRemovedEvent event) {
42+
log.error(
43+
"카드셋 즐겨찾기 삭제 처리 중 예외 발생 : targetType={}, targetId={}, userId={}",
44+
event.targetType(), event.targetId(), event.userId(), ex
45+
);
46+
}
47+
}

src/main/java/project/flipnote/cardset/model/CardSetSortField.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import java.util.stream.Collectors;
66

77
public enum CardSetSortField {
8-
ID, LIKE;
8+
ID, LIKE, BOOKMARK;
99

1010
public static Set<String> getFieldNames() {
1111
return Arrays.stream(values())

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

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

3+
import java.util.List;
4+
35
import org.springframework.data.jpa.repository.JpaRepository;
46
import org.springframework.data.jpa.repository.Modifying;
57
import org.springframework.data.jpa.repository.Query;
@@ -20,4 +22,24 @@ public interface CardSetMetadataRepository extends JpaRepository<CardSetMetadata
2022
WHERE m.id = :cardSetId
2123
""")
2224
int decrementLikeCount(@Param("cardSetId") Long cardSetId);
25+
26+
@Modifying(clearAutomatically = true, flushAutomatically = true)
27+
@Query("UPDATE CardSetMetadata m SET m.bookmarkCount = m.bookmarkCount + 1 WHERE m.id = :cardSetId")
28+
int incrementBookmarkCount(@Param("cardSetId") Long cardSetId);
29+
30+
@Modifying(clearAutomatically = true, flushAutomatically = true)
31+
@Query("""
32+
UPDATE CardSetMetadata m
33+
SET m.bookmarkCount = CASE WHEN m.bookmarkCount > 0 THEN m.bookmarkCount - 1 ELSE 0 END
34+
WHERE m.id = :cardSetId
35+
""")
36+
int decrementBookmarkCount(@Param("cardSetId") Long cardSetId);
37+
38+
@Modifying(clearAutomatically = true, flushAutomatically = true)
39+
@Query("""
40+
UPDATE CardSetMetadata m
41+
SET m.bookmarkCount = CASE WHEN m.bookmarkCount > 0 THEN m.bookmarkCount - 1 ELSE 0 END
42+
WHERE m.id IN :cardSetIds
43+
""")
44+
int decrementBookmarkCount(@Param("cardSetIds") List<Long> cardSetIds);
2345
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ public Page<CardSet> searchByNameContainingAndCategory(
5858
if (sortField == CardSetSortField.LIKE) {
5959
orders.add(toOrderSpecifier(cardSetMetadata.likeCount, order));
6060
useMetadata = true;
61+
} else if (sortField == CardSetSortField.BOOKMARK) {
62+
orders.add(toOrderSpecifier(cardSetMetadata.bookmarkCount, order));
63+
useMetadata = true;
6164
} else {
6265
orders.add(toOrderSpecifier(cardSet.id, order));
6366
hasIdSort = true;
@@ -74,7 +77,7 @@ public Page<CardSet> searchByNameContainingAndCategory(
7477
.where(buildCardSetSearchFilterConditions(name, category));
7578

7679
if (useMetadata) {
77-
selectQuery.join(cardSetMetadata).on(cardSet.id.eq(cardSetMetadata.id));
80+
selectQuery.leftJoin(cardSetMetadata).on(cardSet.id.eq(cardSetMetadata.id));
7881
}
7982

8083
List<CardSet> content = selectQuery

0 commit comments

Comments
 (0)