Skip to content

Commit dd5cc0e

Browse files
authored
feat(): 인기 컬렉션 테이블 구현 (#189)
* feat: 인기 컬렉션 테이블 및 엔티티 추가 * feat: 인기 컬렉션 테이블에서 조회 수정 * feat: 커뮤니티 응답에 인기 새록인지 아닌지 추가 * feat: 좋아요가 기준치를 넘는 시점에 인기 컬렉션 테이블에 저장 로직 추가 * test: 테스트 수정
1 parent af48b63 commit dd5cc0e

12 files changed

Lines changed: 163 additions & 25 deletions

File tree

src/main/java/org/devkor/apu/saerok_server/domain/collection/application/CollectionLikeCommandService.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollectionLike;
77
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionLikeRepository;
88
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionRepository;
9+
import org.devkor.apu.saerok_server.domain.community.core.entity.PopularCollection;
10+
import org.devkor.apu.saerok_server.domain.community.core.repository.PopularCollectionRepository;
911
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.ActionKind;
1012
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Actor;
1113
import org.devkor.apu.saerok_server.domain.notification.application.facade.NotifyActionDsl;
@@ -22,9 +24,12 @@
2224
@RequiredArgsConstructor
2325
public class CollectionLikeCommandService {
2426

27+
private static final int POPULAR_COLLECTION_THRESHOLD = 3;
28+
2529
private final CollectionLikeRepository collectionLikeRepository;
2630
private final CollectionRepository collectionRepository;
2731
private final UserRepository userRepository;
32+
private final PopularCollectionRepository popularCollectionRepository;
2833
private final NotifyActionDsl notifyAction;
2934

3035
/**
@@ -51,6 +56,16 @@ public LikeStatusResponse toggleLikeResponse(Long userId, Long collectionId) {
5156
UserBirdCollectionLike like = new UserBirdCollectionLike(user, collection);
5257
collectionLikeRepository.save(like);
5358

59+
// 좋아요 수 확인 후 인기 컬렉션 등록
60+
long likeCount = collectionLikeRepository.countByCollectionId(collectionId);
61+
if (likeCount == POPULAR_COLLECTION_THRESHOLD) {
62+
boolean alreadyPopular = popularCollectionRepository.existsByCollectionId(collectionId);
63+
if (!alreadyPopular) {
64+
PopularCollection popularCollection = new PopularCollection(collection);
65+
popularCollectionRepository.save(popularCollection);
66+
}
67+
}
68+
5469
// 자신의 컬렉션이 아닌 경우에만 푸시 알림 발송
5570
if (!collection.getUser().getId().equals(userId)) {
5671
notifyAction

src/main/java/org/devkor/apu/saerok_server/domain/community/api/dto/common/CommunityCollectionInfo.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ public record CommunityCollectionInfo(
3939
@Schema(description = "내가 좋아요 눌렀는지 여부", example = "true")
4040
Boolean isLiked,
4141

42+
@Schema(description = "인기 컬렉션 여부", example = "true")
43+
Boolean isPopular,
44+
4245
@Schema(description = "동정 돕기에 참여한 유저 수 (동정 요청 컬렉션인 경우에만)", example = "5", nullable = true)
4346
Long suggestionUserCount,
4447

src/main/java/org/devkor/apu/saerok_server/domain/community/application/CommunityDataAssembler.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionCommentRepository;
77
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionLikeRepository;
88
import org.devkor.apu.saerok_server.domain.collection.core.repository.BirdIdSuggestionRepository;
9+
import org.devkor.apu.saerok_server.domain.community.core.repository.PopularCollectionRepository;
910
import org.devkor.apu.saerok_server.domain.community.api.dto.common.CommunityCollectionInfo;
1011
import org.devkor.apu.saerok_server.domain.community.api.dto.common.CommunityUserInfo;
1112
import org.devkor.apu.saerok_server.domain.community.mapper.CommunityWebMapper;
@@ -24,6 +25,7 @@ public class CommunityDataAssembler {
2425
private final UserProfileImageUrlService userProfileImageUrlService;
2526
private final CollectionLikeRepository collectionLikeRepository;
2627
private final CollectionCommentRepository collectionCommentRepository;
28+
private final PopularCollectionRepository popularCollectionRepository;
2729
private final CommunityWebMapper communityWebMapper;
2830
private final BirdIdSuggestionRepository birdIdSuggestionRepository;
2931

@@ -34,6 +36,11 @@ public List<CommunityCollectionInfo> toCollectionInfos(List<UserBirdCollection>
3436

3537
Map<Long, String> imageUrls = collectionImageUrlService.getPrimaryImageUrlsFor(collections);
3638

39+
List<Long> collectionIds = collections.stream()
40+
.map(UserBirdCollection::getId)
41+
.toList();
42+
Map<Long, Boolean> popularStatusMap = popularCollectionRepository.existsByCollectionIds(collectionIds);
43+
3744
List<Long> pendingCollectionIds = collections.stream()
3845
.filter(c -> c.getBird() == null)
3946
.map(UserBirdCollection::getId)
@@ -49,13 +56,14 @@ public List<CommunityCollectionInfo> toCollectionInfos(List<UserBirdCollection>
4956
long likeCount = collectionLikeRepository.countByCollectionId(collection.getId());
5057
long commentCount = collectionCommentRepository.countByCollectionId(collection.getId());
5158
boolean isLiked = userId != null && collectionLikeRepository.existsByUserIdAndCollectionId(userId, collection.getId());
59+
boolean isPopular = popularStatusMap.getOrDefault(collection.getId(), false);
5260

5361
Long suggestionUserCount = collection.getBird() == null
5462
? suggestionUserCounts.getOrDefault(collection.getId(), 0L)
5563
: null;
5664

5765
return communityWebMapper.toCommunityCollectionInfo(
58-
collection, imageUrl, userProfileImageUrl, likeCount, commentCount, isLiked, suggestionUserCount
66+
collection, imageUrl, userProfileImageUrl, likeCount, commentCount, isLiked, isPopular, suggestionUserCount
5967
);
6068
})
6169
.toList();

src/main/java/org/devkor/apu/saerok_server/domain/community/application/CommunityQueryService.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@
1919
@RequiredArgsConstructor
2020
public class CommunityQueryService {
2121

22-
private static final int POPULAR_MIN_LIKES = 3;
23-
2422
private final CommunityRepository communityRepository;
2523
private final CommunityDataAssembler dataAssembler;
2624

@@ -29,7 +27,7 @@ public GetCommunityMainResponse getCommunityMain(Long userId) {
2927
CommunityQueryCommand mainCommand = new CommunityQueryCommand(1, 3, null);
3028

3129
List<UserBirdCollection> recentCollections = communityRepository.findRecentPublicCollections(mainCommand);
32-
List<UserBirdCollection> popularCollections = communityRepository.findPopularCollections(mainCommand, POPULAR_MIN_LIKES);
30+
List<UserBirdCollection> popularCollections = communityRepository.findPopularCollections(mainCommand);
3331
List<UserBirdCollection> pendingCollections = communityRepository.findPendingBirdIdCollections(mainCommand);
3432

3533
return new GetCommunityMainResponse(
@@ -45,7 +43,7 @@ public GetCommunityCollectionsResponse getRecentCollections(Long userId, Communi
4543
}
4644

4745
public GetCommunityCollectionsResponse getPopularCollections(Long userId, CommunityQueryCommand command) {
48-
List<UserBirdCollection> collections = communityRepository.findPopularCollections(command, POPULAR_MIN_LIKES);
46+
List<UserBirdCollection> collections = communityRepository.findPopularCollections(command);
4947
return new GetCommunityCollectionsResponse(dataAssembler.toCollectionInfos(collections, userId));
5048
}
5149

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.devkor.apu.saerok_server.domain.community.core.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.AccessLevel;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollection;
8+
import org.devkor.apu.saerok_server.global.shared.entity.CreatedAtOnly;
9+
10+
@Entity
11+
@Table(
12+
uniqueConstraints = @UniqueConstraint(columnNames = {"user_bird_collection_id"})
13+
)
14+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
15+
@Getter
16+
public class PopularCollection extends CreatedAtOnly {
17+
18+
@Id
19+
@GeneratedValue(strategy = GenerationType.SEQUENCE)
20+
private Long id;
21+
22+
@ManyToOne(fetch = FetchType.LAZY)
23+
@JoinColumn(name = "user_bird_collection_id", nullable = false)
24+
private UserBirdCollection collection;
25+
26+
public PopularCollection(UserBirdCollection collection) {
27+
this.collection = collection;
28+
}
29+
}

src/main/java/org/devkor/apu/saerok_server/domain/community/core/repository/CommunityRepository.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,16 @@ public List<UserBirdCollection> findRecentPublicCollections(CommunityQueryComman
3333
}
3434

3535
// 인기 있는 컬렉션들을 조회 (좋아요 수가 minLikes 이상인 것들 최신순)
36-
public List<UserBirdCollection> findPopularCollections(CommunityQueryCommand command, int minLikes) {
36+
public List<UserBirdCollection> findPopularCollections(CommunityQueryCommand command) {
3737
Query query = em.createQuery("""
3838
SELECT c FROM UserBirdCollection c
3939
JOIN FETCH c.user u
4040
LEFT JOIN FETCH c.bird b
41-
WHERE c.accessLevel = :public AND (SELECT COUNT(l) FROM UserBirdCollectionLike l WHERE l.collection.id = c.id) >= :minLikes
42-
ORDER BY c.createdAt DESC
41+
JOIN PopularCollection pc ON pc.collection.id = c.id
42+
WHERE c.accessLevel = :public
43+
ORDER BY pc.createdAt DESC
4344
""", UserBirdCollection.class)
44-
.setParameter("public", AccessLevelType.PUBLIC)
45-
.setParameter("minLikes", minLikes);
45+
.setParameter("public", AccessLevelType.PUBLIC);
4646

4747
applyPagination(query, command);
4848
return query.getResultList();
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package org.devkor.apu.saerok_server.domain.community.core.repository;
2+
3+
import jakarta.persistence.EntityManager;
4+
import lombok.RequiredArgsConstructor;
5+
import org.devkor.apu.saerok_server.domain.community.core.entity.PopularCollection;
6+
import org.springframework.stereotype.Repository;
7+
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.stream.Collectors;
11+
12+
@Repository
13+
@RequiredArgsConstructor
14+
public class PopularCollectionRepository {
15+
16+
private final EntityManager em;
17+
18+
public void save(PopularCollection popularCollection) {
19+
em.persist(popularCollection);
20+
}
21+
22+
public boolean existsByCollectionId(Long collectionId) {
23+
return !em.createQuery(
24+
"SELECT 1 FROM PopularCollection pc " +
25+
"WHERE pc.collection.id = :collectionId",
26+
Integer.class)
27+
.setParameter("collectionId", collectionId)
28+
.setMaxResults(1)
29+
.getResultList()
30+
.isEmpty();
31+
}
32+
33+
public Map<Long, Boolean> existsByCollectionIds(List<Long> collectionIds) {
34+
if (collectionIds.isEmpty()) {
35+
return Map.of();
36+
}
37+
38+
List<Long> popularCollectionIds = em.createQuery(
39+
"SELECT pc.collection.id FROM PopularCollection pc " +
40+
"WHERE pc.collection.id IN :collectionIds",
41+
Long.class)
42+
.setParameter("collectionIds", collectionIds)
43+
.getResultList();
44+
45+
return collectionIds.stream()
46+
.collect(Collectors.toMap(
47+
id -> id,
48+
popularCollectionIds::contains
49+
));
50+
}
51+
}

src/main/java/org/devkor/apu/saerok_server/domain/community/mapper/CommunityWebMapper.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public interface CommunityWebMapper {
2222
@Mapping(target = "likeCount", source = "likeCount")
2323
@Mapping(target = "commentCount", source = "commentCount")
2424
@Mapping(target = "isLiked", source = "isLiked")
25+
@Mapping(target = "isPopular", source = "isPopular")
2526
@Mapping(target = "suggestionUserCount", source = "suggestionUserCount")
2627
@Mapping(target = "bird", expression = "java(mapBirdInfo(collection))")
2728
@Mapping(target = "user", expression = "java(mapUserInfo(collection, userProfileImageUrl))")
@@ -32,6 +33,7 @@ CommunityCollectionInfo toCommunityCollectionInfo(
3233
Long likeCount,
3334
Long commentCount,
3435
Boolean isLiked,
36+
Boolean isPopular,
3537
Long suggestionUserCount
3638
);
3739

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
CREATE SEQUENCE popular_collection_seq START WITH 1 INCREMENT BY 50;
2+
3+
-- 테이블 생성
4+
CREATE TABLE popular_collection (
5+
id BIGINT NOT NULL PRIMARY KEY,
6+
user_bird_collection_id BIGINT NOT NULL,
7+
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
8+
9+
CONSTRAINT uq_popular_collection_user_bird_collection UNIQUE (user_bird_collection_id)
10+
);
11+
12+
-- 외래키 제약조건 추가
13+
ALTER TABLE popular_collection
14+
ADD CONSTRAINT fk_popular_collection_user_bird_collection FOREIGN KEY (user_bird_collection_id) REFERENCES user_bird_collection(id) ON DELETE CASCADE;
15+
16+
-- 인덱스 생성
17+
CREATE INDEX idx_popular_collection_created_at ON popular_collection(created_at);

src/test/java/org/devkor/apu/saerok_server/domain/collection/application/CollectionLikeCommandServiceTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.devkor.apu.saerok_server.domain.collection.core.entity.UserBirdCollectionLike;
66
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionLikeRepository;
77
import org.devkor.apu.saerok_server.domain.collection.core.repository.CollectionRepository;
8+
import org.devkor.apu.saerok_server.domain.community.core.repository.PopularCollectionRepository;
89
import org.devkor.apu.saerok_server.domain.notification.application.facade.NotificationPublisher;
910
import org.devkor.apu.saerok_server.domain.notification.application.facade.NotifyActionDsl;
1011
import org.devkor.apu.saerok_server.domain.notification.application.model.dsl.Target;
@@ -39,6 +40,7 @@ class CollectionLikeCommandServiceTest {
3940
@Mock CollectionLikeRepository collectionLikeRepository;
4041
@Mock CollectionRepository collectionRepository;
4142
@Mock UserRepository userRepository;
43+
@Mock PopularCollectionRepository popularCollectionRepository;
4244
@Mock NotificationPublisher publisher;
4345

4446
@BeforeEach
@@ -55,7 +57,7 @@ void setUp() {
5557
}
5658
);
5759
collectionLikeCommandService = new CollectionLikeCommandService(
58-
collectionLikeRepository, collectionRepository, userRepository, notifyActionDsl
60+
collectionLikeRepository, collectionRepository, userRepository, popularCollectionRepository, notifyActionDsl
5961
);
6062
}
6163

@@ -76,6 +78,7 @@ void toggleLike_addLike_success() {
7678
given(userRepository.findById(userId)).willReturn(Optional.of(user));
7779
given(collectionRepository.findById(collectionId)).willReturn(Optional.of(collection));
7880
given(collectionLikeRepository.existsByUserIdAndCollectionId(userId, collectionId)).willReturn(false);
81+
given(collectionLikeRepository.countByCollectionId(collectionId)).willReturn(5L);
7982

8083
LikeStatusResponse response = collectionLikeCommandService.toggleLikeResponse(userId, collectionId);
8184

0 commit comments

Comments
 (0)