-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 프로필 해금로직 변경 #244
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
[FEAT] 프로필 해금로직 변경 #244
Changes from all commits
02a1cfa
fb78afc
64be03f
3a74081
ca03b13
8d12540
e9df3e3
8213f3c
cfd5e06
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package com.spoony.spoony_server.adapter.out.persistence.user; | ||
|
|
||
| import com.spoony.spoony_server.adapter.out.persistence.user.db.UnlockedProfileImageEntity; | ||
| import com.spoony.spoony_server.adapter.out.persistence.user.db.UnlockedProfileImageRepository; | ||
| import com.spoony.spoony_server.adapter.out.persistence.user.db.UserEntity; | ||
| import com.spoony.spoony_server.adapter.out.persistence.user.db.UserRepository; | ||
| import com.spoony.spoony_server.application.port.out.user.UnlockedProfileImagePort; | ||
| import com.spoony.spoony_server.global.exception.BusinessException; | ||
| import com.spoony.spoony_server.global.message.business.UserErrorMessage; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.stereotype.Repository; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
|
|
||
| @Slf4j | ||
| @Repository | ||
| @RequiredArgsConstructor | ||
| public class UnlockedProfileImagePersistenceAdapter implements UnlockedProfileImagePort { | ||
|
|
||
| private final UnlockedProfileImageRepository unlockedProfileImageRepository; | ||
| private final UserRepository userRepository; | ||
|
|
||
| @Override | ||
| public Set<Integer> findUnlockedLevelsByUserId(Long userId) { | ||
| List<Integer> levels = unlockedProfileImageRepository.findProfileLevelsByUserId(userId); | ||
| return new HashSet<>(levels); | ||
| } | ||
|
|
||
| @Override | ||
| @Transactional | ||
| public void saveUnlockedLevel(Long userId, Integer profileLevel) { | ||
| // 중복 저장 방지 | ||
| if (isLevelUnlocked(userId, profileLevel)) { | ||
| log.debug("Profile level {} already unlocked for user {}", profileLevel, userId); | ||
| return; | ||
| } | ||
|
|
||
| UserEntity user = userRepository.findById(userId) | ||
| .orElseThrow(() -> new BusinessException(UserErrorMessage.USER_NOT_FOUND)); | ||
|
|
||
| UnlockedProfileImageEntity entity = UnlockedProfileImageEntity.builder() | ||
| .user(user) | ||
| .profileLevel(profileLevel) | ||
| .build(); | ||
|
|
||
| unlockedProfileImageRepository.save(entity); | ||
| log.info("Profile level {} unlocked for user {}", profileLevel, userId); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isLevelUnlocked(Long userId, Integer profileLevel) { | ||
| return unlockedProfileImageRepository | ||
| .existsByUser_UserIdAndProfileLevel(userId, profileLevel); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package com.spoony.spoony_server.adapter.out.persistence.user.db; | ||
|
|
||
| import jakarta.persistence.*; | ||
| import lombok.AccessLevel; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import org.springframework.data.annotation.CreatedDate; | ||
| import org.springframework.data.jpa.domain.support.AuditingEntityListener; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
| @EntityListeners(AuditingEntityListener.class) | ||
| @Table( | ||
| name = "unlocked_profile_image", | ||
| uniqueConstraints = @UniqueConstraint( | ||
| name = "uk_user_profile_level", | ||
| columnNames = {"user_id", "profile_level"} | ||
| ) | ||
| ) | ||
| public class UnlockedProfileImageEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "user_id", nullable = false) | ||
| private UserEntity user; | ||
|
|
||
| @Column(name = "profile_level", nullable = false) | ||
| private Integer profileLevel; | ||
|
|
||
| @CreatedDate | ||
| @Column(name = "unlocked_at", nullable = false, updatable = false) | ||
| private LocalDateTime unlockedAt; | ||
|
|
||
| @Builder | ||
| public UnlockedProfileImageEntity(UserEntity user, Integer profileLevel) { | ||
| this.user = user; | ||
| this.profileLevel = profileLevel; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package com.spoony.spoony_server.adapter.out.persistence.user.db; | ||
|
|
||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import org.springframework.data.repository.query.Param; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| @Repository | ||
| public interface UnlockedProfileImageRepository extends JpaRepository<UnlockedProfileImageEntity, Long> { | ||
|
|
||
| /** | ||
| * 사용자의 모든 잠금해제된 프로필 조회 | ||
| */ | ||
| List<UnlockedProfileImageEntity> findByUser_UserId(Long userId); | ||
|
|
||
| /** | ||
| * 특정 레벨이 잠금해제되었는지 확인 | ||
| */ | ||
| boolean existsByUser_UserIdAndProfileLevel(Long userId, Integer profileLevel); | ||
|
|
||
| /** | ||
| * 사용자의 잠금해제된 프로필 레벨 목록 조회 (레벨 번호만) | ||
| */ | ||
| @Query("SELECT u.profileLevel FROM UnlockedProfileImageEntity u WHERE u.user.userId = :userId") | ||
| List<Integer> findProfileLevelsByUserId(@Param("userId") Long userId); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.spoony.spoony_server.adapter.out.persistence.user.mapper; | ||
|
|
||
| import com.spoony.spoony_server.adapter.out.persistence.user.db.UnlockedProfileImageEntity; | ||
| import com.spoony.spoony_server.domain.user.UnlockedProfileImage; | ||
|
|
||
| public class UnlockedProfileImageMapper { | ||
|
|
||
| public static UnlockedProfileImage toDomain(UnlockedProfileImageEntity entity) { | ||
| if (entity == null) { | ||
| return null; | ||
| } | ||
|
|
||
| return new UnlockedProfileImage( | ||
| entity.getId(), | ||
| UserMapper.toDomain(entity.getUser()), | ||
| entity.getProfileLevel(), | ||
| entity.getUnlockedAt() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| package com.spoony.spoony_server.application.port.out.user; | ||
|
|
||
| import java.util.Set; | ||
|
|
||
| public interface UnlockedProfileImagePort { | ||
|
|
||
| /** | ||
| * 사용자의 잠금해제된 프로필 레벨 조회 | ||
| * @param userId 사용자 ID | ||
| * @return 잠금해제된 레벨 Set | ||
| */ | ||
| Set<Integer> findUnlockedLevelsByUserId(Long userId); | ||
|
|
||
| /** | ||
| * 새로운 프로필 레벨 잠금해제 저장 | ||
| * @param userId 사용자 ID | ||
| * @param profileLevel 프로필 레벨 | ||
| */ | ||
| void saveUnlockedLevel(Long userId, Integer profileLevel); | ||
|
|
||
| /** | ||
| * 특정 레벨이 잠금해제되었는지 확인 | ||
| * @param userId 사용자 ID | ||
| * @param profileLevel 프로필 레벨 | ||
| * @return 잠금해제 여부 | ||
| */ | ||
| boolean isLevelUnlocked(Long userId, Integer profileLevel); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,42 +1,68 @@ | ||
| package com.spoony.spoony_server.application.service.user; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
|
|
||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import com.spoony.spoony_server.adapter.dto.user.response.ProfileImageListResponseDTO; | ||
| import com.spoony.spoony_server.adapter.dto.user.response.ProfileImageResponseDTO; | ||
| import com.spoony.spoony_server.application.port.command.user.UserGetCommand; | ||
| import com.spoony.spoony_server.application.port.in.user.ProfileImageGetUseCase; | ||
| import com.spoony.spoony_server.application.port.out.post.PostPort; | ||
| import com.spoony.spoony_server.application.port.out.user.UnlockedProfileImagePort; | ||
| import com.spoony.spoony_server.domain.post.Post; | ||
| import com.spoony.spoony_server.domain.user.ProfileImage; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Service; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class ProfileImageService implements ProfileImageGetUseCase { | ||
| private final PostPort postPort; | ||
|
|
||
| @Override | ||
| public ProfileImageListResponseDTO getAvailableProfileImages(UserGetCommand command) { | ||
| List<Post> postList = postPort.findPostsByUserId(command.getUserId()); | ||
|
|
||
| long totalZzimCount = postList | ||
| .stream() | ||
| .mapToLong(Post::getZzimCount) | ||
| .sum(); | ||
|
|
||
| List<ProfileImageResponseDTO> unlockedImages = new ArrayList<>(); | ||
|
|
||
| for (ProfileImage profileImage : ProfileImage.values()){ | ||
| boolean isUnlocked = totalZzimCount >= profileImage.getRequiredZzimCount(); | ||
| if (isUnlocked){ | ||
| unlockedImages.add(ProfileImageResponseDTO.of(profileImage,true)); | ||
| } else{ | ||
| unlockedImages.add(ProfileImageResponseDTO.of(profileImage,false)); | ||
| } | ||
| } | ||
| return ProfileImageListResponseDTO.of(unlockedImages); | ||
| } | ||
|
|
||
| private final PostPort postPort; | ||
| private final UnlockedProfileImagePort unlockedProfileImagePort; | ||
|
|
||
| @Override | ||
| @Transactional | ||
| public ProfileImageListResponseDTO getAvailableProfileImages(UserGetCommand command) { | ||
| Long userId = command.getUserId(); | ||
|
|
||
| // 1. 현재 총 찜 개수 계산 | ||
| List<Post> postList = postPort.findPostsByUserId(userId); | ||
| long totalZzimCount = postList.stream() | ||
| .mapToLong(Post::getZzimCount) | ||
| .sum(); | ||
|
|
||
| // 2. 이미 잠금해제된 레벨 조회 | ||
| Set<Integer> unlockedLevels = unlockedProfileImagePort | ||
| .findUnlockedLevelsByUserId(userId); | ||
|
|
||
| List<ProfileImageResponseDTO> result = new ArrayList<>(); | ||
|
|
||
| // 3. 각 레벨별로 확인 및 새로운 잠금해제 처리 | ||
| for (ProfileImage profileImage : ProfileImage.values()) { | ||
| int level = profileImage.getImageLevel(); | ||
| boolean wasUnlocked = unlockedLevels.contains(level); | ||
| boolean canUnlock = totalZzimCount >= profileImage.getRequiredZzimCount(); | ||
|
|
||
| // 새로 잠금해제 조건 달성 시 저장 | ||
| if (!wasUnlocked && canUnlock) { | ||
| unlockedProfileImagePort.saveUnlockedLevel(userId, level); | ||
| result.add(ProfileImageResponseDTO.of(profileImage, true)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 프로필을 조회할 때 값을 넘겨주는 부분에 있어서는 문제가 없을 것 같은데, 새로운 프로필이 해금 되었을 때 DB에 중복 저장이 될 것 같아요. 예를 들어서 현재 레벨이 2인데 3으로 레벨이 올라가면 DB에는 (userId, 2)가 있는 상태에서 (userId, 3)이 추가로 저장되는 것 같습니다. 혹시 제가 생각하는 로직이 맞다면 기존 레벨은 지우는 것은 어떨까요?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 부분 충분히 이해했습니다! |
||
| } | ||
| // 이미 잠금해제된 경우 (한번 해제되면 영구 해제) | ||
| else if (wasUnlocked) { | ||
| result.add(ProfileImageResponseDTO.of(profileImage, true)); | ||
| } | ||
| // 잠금 상태 | ||
| else { | ||
| result.add(ProfileImageResponseDTO.of(profileImage, false)); | ||
| } | ||
| } | ||
|
|
||
| return ProfileImageListResponseDTO.of(result); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
총 찜 개수를 스트림을 사용해서 계산하는 것도 좋지만, 게시글이 많아졌을 때를 고려해서 쿼리문에서 sum을 이용해서 하는 것도 좋을 것 같습니다...!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 사항 좋은 포인트 같습니다! 검토 후 반영해보겠습니다!