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
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,9 @@
import com.example.cp_main_be.domain.member.notification.service.NotificationService;
import com.example.cp_main_be.domain.member.user.domain.User;
import com.example.cp_main_be.domain.member.user.domain.repository.UserRepository;
import com.example.cp_main_be.domain.mission.wishTree.WishTree;
import com.example.cp_main_be.domain.mission.wishTree.WishTreeRepository;
import com.example.cp_main_be.domain.mission.wishTree.WishTreeStage;
import com.example.cp_main_be.global.common.CustomApiException;
import com.example.cp_main_be.global.common.ErrorCode;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -73,24 +70,15 @@ public void createAvatar(Long userId, String nickname, String imageUrl, Long mas

avatarRepository.save(newAvatar);

WishTree wishTree =
wishTreeRepository
.findByUserId(userId)
// 1. 잠겨있지 않으면서(isLocked=false) 아바타가 비어있는(avatar=null) 정원을 찾습니다.
Garden emptyGarden =
gardenRepository
.findFirstByUserAndIsLockedIsFalseAndAvatarIsNullOrderBySlotNumberAsc(user)
.orElseThrow(
() -> new CustomApiException(ErrorCode.NOT_FOUND, "사용자의 소원나무를 찾을 수 없습니다."));
() -> new CustomApiException(ErrorCode.GARDEN_NOT_FOUND, "배치할 수 있는 빈 정원이 없습니다."));

WishTreeStage stage = wishTree.getStage();
Long maxGardens = stage.getMaxGardens();

List<Garden> userGardens = user.getGardens();
if (userGardens.size() < maxGardens) {
Garden newGarden =
Garden.builder().user(user).slotNumber(userGardens.size() + 1).avatar(newAvatar).build();
gardenRepository.save(newGarden);
} else {
// 이미 존재하는 'GARDEN_SLOT_MAXED_OUT' 에러 코드를 재사용하여 일관성을 유지합니다.
throw new CustomApiException(ErrorCode.GARDEN_SLOT_MAXED_OUT);
}
// 2. 해당 정원에 새로 생성한 아바타를 배치합니다.
emptyGarden.updateAvatar(newAvatar);
}

@Transactional(readOnly = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ public String processImageWithAi(MultipartFile imageFile) {
new CustomApiException(ErrorCode.AI_AVATAR_FAILED));
}))
.bodyToMono(byte[].class)
.timeout(Duration.ofSeconds(30))
.block(Duration.ofSeconds(30));
.timeout(Duration.ofSeconds(300))
.block(Duration.ofSeconds(300));

if (result == null || result.length == 0) {
log.error("AI 서버에서 유효한 이미지 바이트를 받지 못했습니다 (null/empty).");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ public Garden(User user, Integer slotNumber, GardenBackground gardenBackground,
this.avatar = avatar; // [추가]
}

public void unlock() {
this.isLocked = false;
}

public void increaseWaterCount() {
this.waterCount++;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,12 @@ public interface GardenRepository extends JpaRepository<Garden, Long> {
+ "JOIN FETCH g.gardenBackground "
+ "WHERE g.user = :user")
Optional<Garden> findByUserWithDetails(@Param("user") User user);

// 유저의 정원 중 잠겨있으면서 가장 낮은 슬롯 번호를 가진 정원 1개를 찾기
Optional<Garden> findFirstByUserAndIsLockedIsTrueOrderBySlotNumberAsc(User user);

// 유저의 해금된 정원 개수 세기
long countByUserAndIsLockedIsFalse(User user);

Optional<Garden> findFirstByUserAndIsLockedIsFalseAndAvatarIsNullOrderBySlotNumberAsc(User user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,17 @@ public ResponseEntity<ApiResponse<Void>> sunlightGarden(
@PutMapping("/slots/unlock")
public ResponseEntity<ApiResponse<Void>> unlockGarden() {
User currentUser = userService.getCurrentUser();
gardenService.unlockNewGardenSlot(currentUser.getId());
gardenService.unlockNextGarden(currentUser.getId());
return ResponseEntity.ok(ApiResponse.success(null));
}

@Operation(summary = "텃밭 슬롯 해금", description = "해금 버튼 클릭 시 호출")
@PostMapping("/unlock")
public ResponseEntity<Void> unlockGarden(@AuthenticationPrincipal User user) {
gardenService.unlockNextGarden(user.getId());
return ResponseEntity.ok().build();
}

@Operation(summary = "텃밭 배경화면 update", description = "텃밭의 배경화면을 수정한다.")
@PutMapping("/{gardenId}/background/{backgroundId}")
public ResponseEntity<ApiResponse<Void>> updateBackgroundImage(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.example.cp_main_be.domain.garden.garden.service;

import com.example.cp_main_be.domain.avatar.avatar.domain.Avatar;
import com.example.cp_main_be.domain.avatar.avatar.domain.repository.AvatarRepository;
import com.example.cp_main_be.domain.garden.garden.domain.Garden;
import com.example.cp_main_be.domain.garden.garden.domain.GardenBackground;
Expand All @@ -16,7 +15,6 @@
import com.example.cp_main_be.domain.mission.wishTree.WishTree;
import com.example.cp_main_be.domain.mission.wishTree.WishTreeRepository;
import com.example.cp_main_be.domain.mission.wishTree.WishTreeService;
import com.example.cp_main_be.domain.mission.wishTree.WishTreeStage;
import com.example.cp_main_be.global.common.CustomApiException;
import com.example.cp_main_be.global.common.ErrorCode;
import com.example.cp_main_be.global.event.WishTreeEvolvedEvent;
Expand Down Expand Up @@ -167,66 +165,27 @@ public void sunlightGarden(Long actorId, Long gardenId) {
garden.recordSunlightTime(); // 햇빛 준 시간 기록
}

@Transactional
public void unlockNewGardenSlot(Long userId) {
User user =
userRepository
.findById(userId)
.orElseThrow(() -> new CustomApiException(ErrorCode.USER_NOT_FOUND));

WishTree wishTree =
wishTreeRepository
.findByUserId(userId)
.orElseThrow(() -> new CustomApiException(ErrorCode.WISH_TREE_NOT_FOUND));

// 1. 사용자의 WishTree 포인트를 기준으로 현재 단계를 결정
// User는 WishTree를 가지고 있고, WishTree가 포인트를 관리합니다.
WishTreeStage currentStage = WishTreeStage.getStageForPoints(wishTree.getPoints());

// 2. 현재 단계에서 가질 수 있는 최대 텃밭 개수를 가져옴
long maxGardensForStage = currentStage.getMaxGardens();

// 3. 사용자가 현재 보유한 텃밭 개수 확인
int currentGardenCount = user.getGardens().size();
public void unlockNextGarden(Long userId) {
// 1. 유저와 소원나무 정보 가져오기
User user = userRepository.findById(userId).orElseThrow(/* ... */ );
WishTree wishTree = wishTreeRepository.findByUserId(user.getId()).orElseThrow(/* ... */ );

// 4. 최대 텃밭 개수에 도달했는지 확인
if (currentGardenCount >= maxGardensForStage) {
// 현재 단계에서는 더 이상 텃밭을 생성할 수 없음
// ErrorCode에 GARDEN_SLOT_LOCKED 추가가 필요할 수 있습니다.
throw new CustomApiException(
ErrorCode.GARDEN_SLOT_LOCKED, "현재 단계에서는 더 이상 텃밭을 만들 수 없습니다. 소원나무를 성장시켜주세요.");
// 2. 해금 가능한 상태인지 확인
if (!wishTree.isUnlockable()) {
throw new CustomApiException(ErrorCode.GARDEN_SLOT_LOCKED, "정원을 해금할 수 있는 포인트가 부족합니다.");
}

// TODO: 여기 뭔가 수정해야할 듯 아바타는 설정해서 가져오고 배경화면은 그냥 기본걸 씀
// 5. 새로 생성될 텃밭의 기본 배경과 아바타를 설정 (ID 1L을 기본값으로 가정)
GardenBackground defaultBackground =
gardenBackgroundRepository
.findById(1L)
.orElseThrow(
() ->
new CustomApiException(
ErrorCode.DEFAULT_RESOURCE_NOT_FOUND, "기본 텃밭 배경을 찾을 수 없습니다."));
Avatar defaultAvatar =
avatarRepository
.findById(1L)
.orElseThrow(
() ->
new CustomApiException(
ErrorCode.DEFAULT_RESOURCE_NOT_FOUND, "기본 아바타를 찾을 수 없습니다."));

// 6. 새로운 텃밭 생성
Garden newGarden =
Garden.builder()
.user(user)
.slotNumber(currentGardenCount + 1)
.gardenBackground(defaultBackground)
.avatar(defaultAvatar)
.build();
// 3. 소원나무 Stage를 다음 단계로 성장시키기
wishTree.evolveStage();

gardenRepository.save(newGarden);
// 4. 다음으로 잠겨있는 정원을 찾아 해금하기
Garden gardenToUnlock =
gardenRepository
.findFirstByUserAndIsLockedIsTrueOrderBySlotNumberAsc(user)
.orElseThrow(
() -> new CustomApiException(ErrorCode.GARDEN_NOT_FOUND, "해금할 정원을 찾을 수 없습니다."));

// User 엔티티의 gardens 리스트에도 추가
user.addGarden(newGarden);
gardenToUnlock.unlock(); // Garden 엔티티의 isLocked를 false로 변경
}

@Transactional
Expand Down Expand Up @@ -301,6 +260,6 @@ public void cleanupOldWateringLogs() {
@EventListener
@Transactional
public void handleWishTreeEvolved(WishTreeEvolvedEvent event) {
unlockNewGardenSlot(event.getUserId());
unlockNextGarden(event.getUserId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public static class GardenSummaryInfo {
private Integer gardenSlotNumber;
private AvatarInfo avatar; // 각 정원에 배치된 아바타 정보 -> 해금안되면 null
private boolean isLocked;
private boolean isUnlockable;
private boolean isOwnerWateringAble; // 본인 정원에 물주기 가능한지 여부 -> 해금안되면 null
private boolean isOwnerSunlightAble; // 본인 정원에 햇빛 주기 가능한지 여부 -> 해금안되면 null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,26 @@ public HomeResponseDto getHomeScreenData(Long userId) {
.findById(userId)
.orElseThrow(() -> new CustomApiException(ErrorCode.NOT_FOUND));

WishTree wishTreeOfUser = user.getWishTree();
// 1. UserInfo 구성
int unreadNotificationCount = notificationRepository.countByReceiverAndIsReadFalse(user);
HomeResponseDto.UserInfo userInfo =
HomeResponseDto.UserInfo.builder()
.id(user.getId())
.username(user.getNickname())
.level(user.getLevel())
.currentExp(user.getExperience())
.requiredExpForNextLevel(calculateRequiredExpForLevel(user.getLevel() + 1))
.level(wishTreeOfUser.getStage().ordinal() + 1)
.currentExp(wishTreeOfUser.getPoints())
.requiredExpForNextLevel(wishTreeOfUser.getStage().getRequiredPointsForNextStage())
.unreadNotificationCount(unreadNotificationCount)
.build();

WishTree wishTree =
wishTreeRepository
.findByUserId(userId)
.orElseThrow(() -> new CustomApiException(ErrorCode.NOT_FOUND));

long unlockedGardenCount = user.getGardens().stream().filter(g -> !g.isLocked()).count();

Map<Integer, Garden> userGardens =
user.getGardens().stream()
.collect(Collectors.toMap(Garden::getSlotNumber, garden -> garden));
Expand Down Expand Up @@ -117,15 +125,22 @@ public HomeResponseDto getHomeScreenData(Long userId) {
.gardenSlotNumber(slotNumber)
.avatar(avatarInfo)
.isLocked(false)
.isUnlockable(false)
.isOwnerWateringAble(isOwnerWateringAble)
.isOwnerSunlightAble(isOwnerSunlightAble)
.build();
} else {
boolean isUnlockable =
garden != null
&& wishTree.isUnlockable()
&& garden.getSlotNumber() == unlockedGardenCount + 1;

return HomeResponseDto.GardenSummaryInfo.builder()
.gardenId(garden != null ? garden.getId() : null)
.gardenSlotNumber(slotNumber)
.avatar(null)
.isLocked(true)
.isUnlockable(isUnlockable)
.isOwnerWateringAble(false)
.isOwnerSunlightAble(false)
.build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.example.cp_main_be.domain.member.auth.service;

import com.example.cp_main_be.domain.garden.garden.domain.Garden;
import com.example.cp_main_be.domain.garden.garden.domain.repository.GardenRepository;
import com.example.cp_main_be.domain.member.auth.domain.RefreshToken;
import com.example.cp_main_be.domain.member.auth.domain.repository.RefreshTokenRepository;
import com.example.cp_main_be.domain.member.auth.dto.request.RegistrationRequest;
Expand All @@ -13,6 +15,7 @@
import com.example.cp_main_be.global.jwt.JwtTokenProvider;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
Expand All @@ -29,6 +32,7 @@ public class AuthService {
private final RefreshTokenRepository refreshTokenRepository;
private final Logger logger = LoggerFactory.getLogger(AuthService.class);
private final WishTreeService wishTreeService;
private final GardenRepository gardenRepository;

/** 리프레시 토큰으로 액세스 토큰 재발급 + (권장) 리프레시 토큰 롤링 */
@Transactional
Expand Down Expand Up @@ -79,6 +83,17 @@ public AnonymousRegistrationResponse registerNewUser(
// 1. 사용자를 먼저 저장합니다.
User savedUser = userRepository.save(newUser);

Garden firstGarden =
Garden.builder().user(savedUser).slotNumber(1).isLocked(false).build(); // 1번은 기본 해금
Garden secondGarden =
Garden.builder().user(savedUser).slotNumber(2).isLocked(true).build(); // 2번은 잠김
Garden thirdGarden =
Garden.builder().user(savedUser).slotNumber(3).isLocked(true).build(); // 3번은 잠김
Garden fourthGarden =
Garden.builder().user(savedUser).slotNumber(4).isLocked(true).build(); // 4번은 잠김

gardenRepository.saveAll(List.of(firstGarden, secondGarden, thirdGarden, fourthGarden));

// 2. 위시트리 관련 로직을 수행합니다.
// 만약 여기서 예외가 발생하면, 위에서 저장한 newUser까지 모두 롤백됩니다.
wishTreeService.addPointsToWishTree(savedUser.getId(), 0L);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.example.cp_main_be.domain.avatar.avatar.domain.Avatar;
import com.example.cp_main_be.domain.garden.garden.domain.Garden;
import com.example.cp_main_be.domain.mission.diary.domain.Diary;
import com.example.cp_main_be.domain.mission.wishTree.WishTree;
import com.example.cp_main_be.domain.social.bookmark.domain.Bookmark;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import jakarta.persistence.*;
Expand Down Expand Up @@ -83,6 +84,9 @@ public class User implements UserDetails {

@Builder.Default private Boolean notificationEnabled = true;

@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private WishTree wishTree;

@PrePersist
protected void onCreate() {
this.createdAt = LocalDateTime.now();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,40 @@ public WishTree(User user) {
this.stage = WishTreeStage.SPROUT;
}

@Column(nullable = false)
private boolean isUnlockable = false;

/**
* 포인트 추가 및 성장 로직
*
* @param amount 추가할 포인트
* @return 성장을 했는지 여부
*/
public boolean addPoints(Long amount) {
WishTreeStage previousStage = this.stage;
this.points += amount;
WishTreeStage newStage = WishTreeStage.getStageForPoints(this.points);

if (newStage != previousStage) {
this.stage = newStage;
return true; // 성장했다!
public void addPoints(Long points) {
this.points += points;

// 이미 해금 가능 상태이거나, 다음 스테이지가 없으면 아무것도 하지 않음
if (this.isUnlockable || this.stage.getNextStage() == null) {
return;
}

// 다음 스테이지의 요구 포인트를 넘었는지 확인
WishTreeStage nextStage = this.stage.getNextStage();
if (this.points >= this.stage.getRequiredPointsForNextStage()) {
this.isUnlockable = true; // 👈 Stage를 바로 바꾸는 대신, 해금 가능 상태로 변경
}
}

public void evolveStage() {
if (!this.isUnlockable) {
// 해금 불가능한 상태에서 호출 시 예외 처리 또는 로깅
return;
}

WishTreeStage nextStage = this.stage.getNextStage();
if (nextStage != null) {
this.stage = nextStage;
this.isUnlockable = false; // 상태 플래그 초기화
}
return false; // 성장 안함
}
}
Loading
Loading