Skip to content
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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

총 찜 개수를 스트림을 사용해서 계산하는 것도 좋지만, 게시글이 많아졌을 때를 고려해서 쿼리문에서 sum을 이용해서 하는 것도 좋을 것 같습니다...!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 사항 좋은 포인트 같습니다! 검토 후 반영해보겠습니다!


// 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));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

프로필을 조회할 때 값을 넘겨주는 부분에 있어서는 문제가 없을 것 같은데, 새로운 프로필이 해금 되었을 때 DB에 중복 저장이 될 것 같아요. 예를 들어서 현재 레벨이 2인데 3으로 레벨이 올라가면 DB에는 (userId, 2)가 있는 상태에서 (userId, 3)이 추가로 저장되는 것 같습니다. 혹시 제가 생각하는 로직이 맞다면 기존 레벨은 지우는 것은 어떨까요?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분 충분히 이해했습니다!
맞습니다! 현재 로직은 DB에 해금에 대한 내역이 히스토리 형식으로 저장됩니다. 추후 사용자에게 해금에 대해 알림을 보내는 것을 고려하여 이렇게 구현했습니다! 이 부분에 대해서는 일요일 회의에서 한번 논의해봐도 좋을 것 같습니다!

}
// 이미 잠금해제된 경우 (한번 해제되면 영구 해제)
else if (wasUnlocked) {
result.add(ProfileImageResponseDTO.of(profileImage, true));
}
// 잠금 상태
else {
result.add(ProfileImageResponseDTO.of(profileImage, false));
}
}

return ProfileImageListResponseDTO.of(result);
}
}
Loading