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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.mockito:mockito-inline:5.2.0'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testRuntimeOnly 'com.h2database:h2'
implementation platform('software.amazon.awssdk:bom:2.20.56')
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/project/flipnote/cardset/entity/CardSetMetadata.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package project.flipnote.cardset.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "card_set_metadata")
@Entity
public class CardSetMetadata {

@Id
private Long id;

@Column(nullable = false)
private int likeCount;

@Builder
public CardSetMetadata(Long id) {
this.id = id;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package project.flipnote.cardset.listener;

import org.springframework.dao.DataAccessException;
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.cardset.service.CardSetService;
import project.flipnote.common.entity.LikeType;
import project.flipnote.common.model.event.LikeEvent;

@Slf4j
@RequiredArgsConstructor
@Component
public class CardSetLikeEventHandler {

private final CardSetService cardSetService;

@Async
@Retryable(
maxAttempts = 3,
retryFor = DataAccessException.class,
backoff = @Backoff(delay = 2000, multiplier = 2)
)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleLikeEvent(LikeEvent event) {
if (event.likeType() != LikeType.CARD_SET) {
return;
}

cardSetService.incrementLikeCount(event.targetId());
}

@Recover
public void recover(Exception ex, LikeEvent event) {
log.error(
"좋아요 수 반영 처리 중 예외 발생 : likeType={}, targetId={}, userId={}",
event.likeType(), event.targetId(), event.userId(), ex
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package project.flipnote.cardset.listener;

import org.springframework.dao.DataAccessException;
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.cardset.service.CardSetService;
import project.flipnote.common.entity.LikeType;
import project.flipnote.common.model.event.UnlikeEvent;

@Slf4j
@RequiredArgsConstructor
@Component
public class CardSetUnlikeEventHandler {

private final CardSetService cardSetService;

@Async
@Retryable(
maxAttempts = 3,
retryFor = DataAccessException.class,
backoff = @Backoff(delay = 2000, multiplier = 2)
)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleUnlikeEvent(UnlikeEvent event) {
if (event.likeType() != LikeType.CARD_SET) {
return;
}

cardSetService.decrementLikeCount(event.targetId());
}

@Recover
public void recover(Exception ex, UnlikeEvent event) {
log.error(
"좋아요 취소 수 반영 처리 중 예외 발생 : likeType={}, targetId={}, userId={}",
event.likeType(), event.targetId(), event.userId(), ex
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package project.flipnote.cardset.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import project.flipnote.cardset.entity.CardSetMetadata;

public interface CardSetMetadataRepository extends JpaRepository<CardSetMetadata, Long> {

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE CardSetMetadata m SET m.likeCount = m.likeCount + 1 WHERE m.id = :cardSetId")
int incrementLikeCount(@Param("cardSetId") Long cardSetId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("""
UPDATE CardSetMetadata m
SET m.likeCount = CASE WHEN m.likeCount > 0 THEN m.likeCount - 1 ELSE 0 END
WHERE m.id = :cardSetId
""")
int decrementLikeCount(@Param("cardSetId") Long cardSetId);
}
67 changes: 61 additions & 6 deletions src/main/java/project/flipnote/cardset/service/CardSetService.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package project.flipnote.cardset.service;

import java.util.List;

import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -8,6 +10,7 @@
import lombok.extern.slf4j.Slf4j;
import project.flipnote.cardset.entity.CardSet;
import project.flipnote.cardset.entity.CardSetManager;
import project.flipnote.cardset.entity.CardSetMetadata;
import project.flipnote.cardset.exception.CardSetErrorCode;
import project.flipnote.cardset.model.CardSetDetailResponse;
import project.flipnote.cardset.model.CardSetSearchRequest;
Expand All @@ -17,17 +20,16 @@
import project.flipnote.cardset.model.CreateCardSetRequest;
import project.flipnote.cardset.model.CreateCardSetResponse;
import project.flipnote.cardset.repository.CardSetManagerRepository;
import project.flipnote.cardset.repository.CardSetMetadataRepository;
import project.flipnote.cardset.repository.CardSetRepository;
import project.flipnote.common.exception.BizException;
import project.flipnote.common.model.response.PagingResponse;
import project.flipnote.common.security.dto.AuthPrinciple;
import project.flipnote.group.entity.Category;
import project.flipnote.group.entity.Group;
import project.flipnote.group.entity.GroupPermissionStatus;
import project.flipnote.group.exception.GroupErrorCode;
import project.flipnote.group.repository.GroupMemberRepository;
import project.flipnote.group.repository.GroupRepository;
import project.flipnote.group.service.GroupService;
import project.flipnote.user.entity.UserProfile;
import project.flipnote.user.entity.UserStatus;
import project.flipnote.user.exception.UserErrorCode;
Expand All @@ -44,8 +46,8 @@ public class CardSetService {
private final GroupRepository groupRepository;
private final GroupMemberRepository groupMemberRepository;
private final CardSetManagerRepository cardSetManagerRepository;
private final GroupService groupService;
private final CardSetPolicyService cardSetPolicyService;
private final CardSetPolicyService cardSetPolicyService;
private final CardSetMetadataRepository cardSetMetadataRepository;

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

cardSetRepository.save(cardSet);

CardSetMetadata metadata = CardSetMetadata.builder()
.id(cardSet.getId())
.build();
cardSetMetadataRepository.save(metadata);

//카드셋 매니저도 저장
CardSetManager cardSetManager = CardSetManager.builder()
.user(user)
Expand All @@ -114,11 +121,11 @@ public CreateCardSetResponse createCardSet(Long groupId, AuthPrinciple authPrinc
public PagingResponse<CardSetSummaryResponse> getCardSets(CardSetSearchRequest req) {

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

Page<CardSetSummaryResponse> res = CardSetPage.map(CardSetSummaryResponse::from);
Page<CardSetSummaryResponse> res = cardSetPage.map(CardSetSummaryResponse::from);

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

return CardSetDetailResponse.from(cardSet);
}

/**
* 카드셋 존재 여부 확인
*
* @param cardSetId 존재하는지 확인할 카드셋 ID
* @return 카드셋 존재 여부
* @author 윤정환
*/
public boolean existsById(Long cardSetId) {
return cardSetRepository.existsById(cardSetId);
}

/**
* 카드셋 좋아요 수를 1 증가
*
* @param cardSetId 좋아요 수를 증가시킬 카드셋 ID
* @author 윤정환
*/
@Transactional
public void incrementLikeCount(Long cardSetId) {
cardSetMetadataRepository.incrementLikeCount(cardSetId);
}

/**
* 카드셋 좋아요 수를 1 감소
*
* @param cardSetId 좋아요 수를 감소시킬 카드셋 ID
* @author 윤정환
*/
@Transactional
public void decrementLikeCount(Long cardSetId) {
cardSetMetadataRepository.decrementLikeCount(cardSetId);
}

/**
* 카드셋 ID 목록에 해당하는 카드셋 목록 조회
*
* @param targetIds 조회할 카드셋 ID 목록
* @return 조회된 카드셋 목록
* @author 윤정환
*/
@Transactional
public List<CardSetSummaryResponse> getCardSetsByIds(List<Long> targetIds) {
// TODO: MSA로 전환시 전용 DTO로 변경 필요
return cardSetRepository.findAllById(targetIds).stream()
.map(CardSetSummaryResponse::from)
.toList();
}
}
4 changes: 3 additions & 1 deletion src/main/java/project/flipnote/common/config/S3Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
Expand All @@ -24,6 +25,7 @@ public class S3Config {
/*
리전과 자격 증명한 객체 생성
*/
@Profile("!test")
@Bean
public S3Client s3Client() {
return S3Client.builder()
Expand All @@ -36,6 +38,7 @@ public S3Client s3Client() {
.build();
}

@Profile("!test")
@Bean
public S3Presigner s3Presigner() {
return S3Presigner.builder()
Expand All @@ -47,5 +50,4 @@ public S3Presigner s3Presigner() {
)
.build();
}

}
5 changes: 5 additions & 0 deletions src/main/java/project/flipnote/common/entity/LikeType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package project.flipnote.common.entity;

public enum LikeType {
CARD_SET
}
10 changes: 10 additions & 0 deletions src/main/java/project/flipnote/common/model/event/LikeEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package project.flipnote.common.model.event;

import project.flipnote.common.entity.LikeType;

public record LikeEvent(
LikeType likeType,
Long targetId,
Long userId
) {
}
10 changes: 10 additions & 0 deletions src/main/java/project/flipnote/common/model/event/UnlikeEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package project.flipnote.common.model.event;

import project.flipnote.common.entity.LikeType;

public record UnlikeEvent(
LikeType likeType,
Long targetId,
Long userId
) {
}
Loading
Loading