Skip to content

Commit d51c9bd

Browse files
authored
Merge pull request #39 from FlipNoteTeam/feat/cardset-like
Feat: [FN-145][FN-150][FN-151] 좋아요 기능
2 parents a06d0ea + 3a4f571 commit d51c9bd

28 files changed

Lines changed: 775 additions & 33 deletions

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ dependencies {
4747
annotationProcessor 'org.projectlombok:lombok'
4848
testImplementation 'org.springframework.boot:spring-boot-starter-test'
4949
testImplementation 'org.springframework.security:spring-security-test'
50+
testImplementation 'org.mockito:mockito-inline:5.2.0'
5051
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
5152
testRuntimeOnly 'com.h2database:h2'
5253
implementation platform('software.amazon.awssdk:bom:2.20.56')
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package project.flipnote.cardset.entity;
2+
3+
import jakarta.persistence.Column;
4+
import jakarta.persistence.Entity;
5+
import jakarta.persistence.Id;
6+
import jakarta.persistence.Table;
7+
import lombok.AccessLevel;
8+
import lombok.Builder;
9+
import lombok.Getter;
10+
import lombok.NoArgsConstructor;
11+
12+
@Getter
13+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
14+
@Table(name = "card_set_metadata")
15+
@Entity
16+
public class CardSetMetadata {
17+
18+
@Id
19+
private Long id;
20+
21+
@Column(nullable = false)
22+
private int likeCount;
23+
24+
@Builder
25+
public CardSetMetadata(Long id) {
26+
this.id = id;
27+
}
28+
}
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.entity.LikeType;
16+
import project.flipnote.common.model.event.LikeEvent;
17+
18+
@Slf4j
19+
@RequiredArgsConstructor
20+
@Component
21+
public class CardSetLikeEventHandler {
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 handleLikeEvent(LikeEvent event) {
33+
if (event.likeType() != LikeType.CARD_SET) {
34+
return;
35+
}
36+
37+
cardSetService.incrementLikeCount(event.targetId());
38+
}
39+
40+
@Recover
41+
public void recover(Exception ex, LikeEvent event) {
42+
log.error(
43+
"좋아요 수 반영 처리 중 예외 발생 : likeType={}, targetId={}, userId={}",
44+
event.likeType(), 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.entity.LikeType;
16+
import project.flipnote.common.model.event.UnlikeEvent;
17+
18+
@Slf4j
19+
@RequiredArgsConstructor
20+
@Component
21+
public class CardSetUnlikeEventHandler {
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 handleUnlikeEvent(UnlikeEvent event) {
33+
if (event.likeType() != LikeType.CARD_SET) {
34+
return;
35+
}
36+
37+
cardSetService.decrementLikeCount(event.targetId());
38+
}
39+
40+
@Recover
41+
public void recover(Exception ex, UnlikeEvent event) {
42+
log.error(
43+
"좋아요 취소 수 반영 처리 중 예외 발생 : likeType={}, targetId={}, userId={}",
44+
event.likeType(), event.targetId(), event.userId(), ex
45+
);
46+
}
47+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package project.flipnote.cardset.repository;
2+
3+
import org.springframework.data.jpa.repository.JpaRepository;
4+
import org.springframework.data.jpa.repository.Modifying;
5+
import org.springframework.data.jpa.repository.Query;
6+
import org.springframework.data.repository.query.Param;
7+
8+
import project.flipnote.cardset.entity.CardSetMetadata;
9+
10+
public interface CardSetMetadataRepository extends JpaRepository<CardSetMetadata, Long> {
11+
12+
@Modifying(clearAutomatically = true, flushAutomatically = true)
13+
@Query("UPDATE CardSetMetadata m SET m.likeCount = m.likeCount + 1 WHERE m.id = :cardSetId")
14+
int incrementLikeCount(@Param("cardSetId") Long cardSetId);
15+
16+
@Modifying(clearAutomatically = true, flushAutomatically = true)
17+
@Query("""
18+
UPDATE CardSetMetadata m
19+
SET m.likeCount = CASE WHEN m.likeCount > 0 THEN m.likeCount - 1 ELSE 0 END
20+
WHERE m.id = :cardSetId
21+
""")
22+
int decrementLikeCount(@Param("cardSetId") Long cardSetId);
23+
}

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

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

3+
import java.util.List;
4+
35
import org.springframework.data.domain.Page;
46
import org.springframework.stereotype.Service;
57
import org.springframework.transaction.annotation.Transactional;
@@ -8,6 +10,7 @@
810
import lombok.extern.slf4j.Slf4j;
911
import project.flipnote.cardset.entity.CardSet;
1012
import project.flipnote.cardset.entity.CardSetManager;
13+
import project.flipnote.cardset.entity.CardSetMetadata;
1114
import project.flipnote.cardset.exception.CardSetErrorCode;
1215
import project.flipnote.cardset.model.CardSetDetailResponse;
1316
import project.flipnote.cardset.model.CardSetSearchRequest;
@@ -17,17 +20,16 @@
1720
import project.flipnote.cardset.model.CreateCardSetRequest;
1821
import project.flipnote.cardset.model.CreateCardSetResponse;
1922
import project.flipnote.cardset.repository.CardSetManagerRepository;
23+
import project.flipnote.cardset.repository.CardSetMetadataRepository;
2024
import project.flipnote.cardset.repository.CardSetRepository;
2125
import project.flipnote.common.exception.BizException;
2226
import project.flipnote.common.model.response.PagingResponse;
2327
import project.flipnote.common.security.dto.AuthPrinciple;
2428
import project.flipnote.group.entity.Category;
2529
import project.flipnote.group.entity.Group;
26-
import project.flipnote.group.entity.GroupPermissionStatus;
2730
import project.flipnote.group.exception.GroupErrorCode;
2831
import project.flipnote.group.repository.GroupMemberRepository;
2932
import project.flipnote.group.repository.GroupRepository;
30-
import project.flipnote.group.service.GroupService;
3133
import project.flipnote.user.entity.UserProfile;
3234
import project.flipnote.user.entity.UserStatus;
3335
import project.flipnote.user.exception.UserErrorCode;
@@ -44,8 +46,8 @@ public class CardSetService {
4446
private final GroupRepository groupRepository;
4547
private final GroupMemberRepository groupMemberRepository;
4648
private final CardSetManagerRepository cardSetManagerRepository;
47-
private final GroupService groupService;
48-
private final CardSetPolicyService cardSetPolicyService;
49+
private final CardSetPolicyService cardSetPolicyService;
50+
private final CardSetMetadataRepository cardSetMetadataRepository;
4951

5052
private UserProfile validateUser(Long userId) {
5153
return userProfileRepository.findByIdAndStatus(userId, UserStatus.ACTIVE).orElseThrow(
@@ -93,6 +95,11 @@ public CreateCardSetResponse createCardSet(Long groupId, AuthPrinciple authPrinc
9395

9496
cardSetRepository.save(cardSet);
9597

98+
CardSetMetadata metadata = CardSetMetadata.builder()
99+
.id(cardSet.getId())
100+
.build();
101+
cardSetMetadataRepository.save(metadata);
102+
96103
//카드셋 매니저도 저장
97104
CardSetManager cardSetManager = CardSetManager.builder()
98105
.user(user)
@@ -114,11 +121,11 @@ public CreateCardSetResponse createCardSet(Long groupId, AuthPrinciple authPrinc
114121
public PagingResponse<CardSetSummaryResponse> getCardSets(CardSetSearchRequest req) {
115122

116123
// TODO: Projection 및 카운트 쿼리 튜닝 필요, 좋아요 수 및 즐겨찾기 수 등 다양한 정렬 조건 추가 필요
117-
Page<CardSet> CardSetPage = cardSetRepository.findByNameContainingAndCategory(
124+
Page<CardSet> cardSetPage = cardSetRepository.findByNameContainingAndCategory(
118125
req.getKeyword(), Category.from(req.getCategory()), req.getPageRequest()
119126
);
120127

121-
Page<CardSetSummaryResponse> res = CardSetPage.map(CardSetSummaryResponse::from);
128+
Page<CardSetSummaryResponse> res = cardSetPage.map(CardSetSummaryResponse::from);
122129

123130
return PagingResponse.from(res);
124131
}
@@ -163,4 +170,52 @@ public CardSetDetailResponse updateCardSet(Long userId, Long groupId, Long cardS
163170

164171
return CardSetDetailResponse.from(cardSet);
165172
}
173+
174+
/**
175+
* 카드셋 존재 여부 확인
176+
*
177+
* @param cardSetId 존재하는지 확인할 카드셋 ID
178+
* @return 카드셋 존재 여부
179+
* @author 윤정환
180+
*/
181+
public boolean existsById(Long cardSetId) {
182+
return cardSetRepository.existsById(cardSetId);
183+
}
184+
185+
/**
186+
* 카드셋 좋아요 수를 1 증가
187+
*
188+
* @param cardSetId 좋아요 수를 증가시킬 카드셋 ID
189+
* @author 윤정환
190+
*/
191+
@Transactional
192+
public void incrementLikeCount(Long cardSetId) {
193+
cardSetMetadataRepository.incrementLikeCount(cardSetId);
194+
}
195+
196+
/**
197+
* 카드셋 좋아요 수를 1 감소
198+
*
199+
* @param cardSetId 좋아요 수를 감소시킬 카드셋 ID
200+
* @author 윤정환
201+
*/
202+
@Transactional
203+
public void decrementLikeCount(Long cardSetId) {
204+
cardSetMetadataRepository.decrementLikeCount(cardSetId);
205+
}
206+
207+
/**
208+
* 카드셋 ID 목록에 해당하는 카드셋 목록 조회
209+
*
210+
* @param targetIds 조회할 카드셋 ID 목록
211+
* @return 조회된 카드셋 목록
212+
* @author 윤정환
213+
*/
214+
@Transactional
215+
public List<CardSetSummaryResponse> getCardSetsByIds(List<Long> targetIds) {
216+
// TODO: MSA로 전환시 전용 DTO로 변경 필요
217+
return cardSetRepository.findAllById(targetIds).stream()
218+
.map(CardSetSummaryResponse::from)
219+
.toList();
220+
}
166221
}

src/main/java/project/flipnote/common/config/S3Config.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.springframework.beans.factory.annotation.Value;
44
import org.springframework.context.annotation.Bean;
55
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.context.annotation.Profile;
67

78
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
89
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
@@ -24,6 +25,7 @@ public class S3Config {
2425
/*
2526
리전과 자격 증명한 객체 생성
2627
*/
28+
@Profile("!test")
2729
@Bean
2830
public S3Client s3Client() {
2931
return S3Client.builder()
@@ -36,6 +38,7 @@ public S3Client s3Client() {
3638
.build();
3739
}
3840

41+
@Profile("!test")
3942
@Bean
4043
public S3Presigner s3Presigner() {
4144
return S3Presigner.builder()
@@ -47,5 +50,4 @@ public S3Presigner s3Presigner() {
4750
)
4851
.build();
4952
}
50-
5153
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package project.flipnote.common.entity;
2+
3+
public enum LikeType {
4+
CARD_SET
5+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package project.flipnote.common.model.event;
2+
3+
import project.flipnote.common.entity.LikeType;
4+
5+
public record LikeEvent(
6+
LikeType likeType,
7+
Long targetId,
8+
Long userId
9+
) {
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package project.flipnote.common.model.event;
2+
3+
import project.flipnote.common.entity.LikeType;
4+
5+
public record UnlikeEvent(
6+
LikeType likeType,
7+
Long targetId,
8+
Long userId
9+
) {
10+
}

0 commit comments

Comments
 (0)