Skip to content

Commit 0e7eab9

Browse files
Merge pull request #97 from focus-to-level-up/hotfix/song
refactor: Fix all apis bug
2 parents 1b55b2c + 6b0bc5d commit 0e7eab9

12 files changed

Lines changed: 201 additions & 42 deletions

File tree

src/main/java/com/studioedge/focus_to_levelup_server/domain/focus/controller/DailyGoalController.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ public ResponseEntity<CommonResponse<GetDailyGoalResponse>> getDailyGoal(
5454
@AuthenticationPrincipal Member member,
5555
@Parameter(description = "날짜")
5656
@RequestParam(defaultValue = "2025-12-18") LocalDate date
57-
5857
) {
5958
return HttpResponseUtil.ok(dailyGoalService.getTodayDailyGoal(member, date));
6059
}
@@ -122,4 +121,36 @@ public ResponseEntity<CommonResponse<Void>> receiveDailyGoal(
122121
dailyGoalService.receiveDailyGoal(member, dailyGoalId, request);
123122
return HttpResponseUtil.ok(null);
124123
}
124+
125+
/**
126+
* 목표 보상 수령
127+
* */
128+
@PostMapping("/v2/daily-goal/{dailyGoalId}")
129+
@Operation(summary = "일일 목표 보상 수령", description = """
130+
### 기능
131+
- `dailyGoalId`에 해당하는 목표의 달성/실패 여부를 정산하고, '추가 보상'을 수령합니다.
132+
- (목표 달성 시 f(n) 배율 적용, 실패 시 패널티 적용)
133+
"""
134+
)
135+
@ApiResponses({
136+
@ApiResponse(
137+
responseCode = "200",
138+
description = "보상 수령 성공"
139+
),
140+
@ApiResponse(
141+
responseCode = "404",
142+
description = "해당 일일 목표/유저를 찾을 수 없습니다."
143+
),
144+
@ApiResponse(
145+
responseCode = "409",
146+
description = "이미 보상을 수령한 목표입니다."
147+
)
148+
})
149+
public ResponseEntity<CommonResponse<Void>> receiveDailyGoalV2(
150+
@AuthenticationPrincipal Member member,
151+
@PathVariable Long dailyGoalId
152+
) {
153+
dailyGoalService.receiveDailyGoalV2(member, dailyGoalId);
154+
return HttpResponseUtil.ok(null);
155+
}
125156
}

src/main/java/com/studioedge/focus_to_levelup_server/domain/focus/dao/SubjectRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public interface SubjectRepository extends JpaRepository<Subject, Long> {
1212

1313
Optional<Subject> findByIdAndDeleteAtIsNull(Long id);
1414

15-
Optional<Subject> findByMemberAndName(Member member, String name);
15+
List<Subject> findAllByMemberAndName(Member member, String name);
1616

1717
List<Subject> findAllByMemberAndDeleteAtIsNull(Member member);
1818

src/main/java/com/studioedge/focus_to_levelup_server/domain/focus/service/DailyGoalService.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class DailyGoalService {
2929
private final DailyGoalRepository dailyGoalRepository;
3030
private final MemberCharacterRepository memberCharacterRepository;
3131
private final ItemAchievementService itemAchievementService;
32+
3233
/**
3334
* 목표 시간 설정
3435
* */
@@ -77,4 +78,60 @@ public void receiveDailyGoal(Member m, Long dailyGoalId, ReceiveDailyGoalRequest
7778
// "휴식은 사치" 미션 성공 판정 (오늘의 학습 종료 시점에 판정)
7879
itemAchievementService.checkRestIsLuxuryOnStudyEnd(m.getId(), dailyGoal.getDailyGoalDate(), dailyGoal);
7980
}
81+
82+
@Transactional
83+
public void receiveDailyGoalV2(Member m, Long dailyGoalId) {
84+
DailyGoal dailyGoal = dailyGoalRepository.findById(dailyGoalId)
85+
.orElseThrow(DailyGoalNotFoundException::new);
86+
Member member = memberRepository.findById(m.getId())
87+
.orElseThrow(MemberNotFoundException::new);
88+
MemberCharacter memberCharacter = memberCharacterRepository.findByMemberIdAndIsDefaultTrue(member.getId())
89+
.orElseThrow(CharacterDefaultNotFoundException::new);
90+
91+
if (!dailyGoal.receiveReward()) {
92+
throw new AlreadyReceivedDailyGoalException();
93+
}
94+
95+
int rewardExp = calculateBonusExp(dailyGoal);
96+
if (rewardExp > 0) {
97+
member.expUp(rewardExp);
98+
member.getMemberInfo().totalExpUp(rewardExp);
99+
memberCharacter.expUp(rewardExp);
100+
}
101+
102+
// 5. "휴식은 사치" 미션 성공 판정
103+
itemAchievementService.checkRestIsLuxuryOnStudyEnd(m.getId(), dailyGoal.getDailyGoalDate(), dailyGoal);
104+
}
105+
106+
/**
107+
* 보상(보너스) 경험치 계산 로직
108+
* (GetDailyGoalResponse에 있던 로직을 서버 내부로 가져옴)
109+
*/
110+
private int calculateBonusExp(DailyGoal dailyGoal) {
111+
int currentMinutes = dailyGoal.getCurrentSeconds() / 60;
112+
int targetMinutes = dailyGoal.getTargetMinutes();
113+
114+
// 1. 계산 기준 시간(x) 산정
115+
int x;
116+
if (currentMinutes >= targetMinutes) {
117+
// 목표 달성 시: x = 목표 시간(시간 단위)
118+
x = targetMinutes / 60;
119+
} else {
120+
// 목표 미달 시: x = (현재 시간 / 60) - 1
121+
x = (currentMinutes / 60) - 1;
122+
}
123+
124+
// 2. 보상 배율 계산 (f(x) = 1.1^(x-2))
125+
float rewardMultiplier = 1.0f;
126+
if (x >= 2) {
127+
double rawMultiplier = Math.pow(1.1, x - 2);
128+
rewardMultiplier = (float) (Math.round(rawMultiplier * 100.0) / 100.0);
129+
}
130+
131+
// 3. 보너스 경험치 계산
132+
int baseExp = currentMinutes * 10;
133+
134+
// 추가로 줄 보너스 경험치 = 기본 * (배율 - 1)
135+
return (int) (baseExp * (rewardMultiplier - 1.0f));
136+
}
80137
}

src/main/java/com/studioedge/focus_to_levelup_server/domain/focus/service/FocusService.java

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import com.studioedge.focus_to_levelup_server.domain.member.dao.MemberSettingRepository;
2626
import com.studioedge.focus_to_levelup_server.domain.member.entity.Member;
2727
import com.studioedge.focus_to_levelup_server.domain.member.entity.MemberInfo;
28-
import com.studioedge.focus_to_levelup_server.domain.member.entity.MemberSetting;
2928
import com.studioedge.focus_to_levelup_server.domain.member.exception.InvalidMemberException;
3029
import com.studioedge.focus_to_levelup_server.domain.member.exception.MemberNotFoundException;
3130
import com.studioedge.focus_to_levelup_server.domain.ranking.dao.RankingRepository;
@@ -43,14 +42,15 @@
4342
import org.springframework.stereotype.Service;
4443
import org.springframework.transaction.annotation.Transactional;
4544

45+
import java.time.Duration;
4646
import java.time.LocalDate;
47+
import java.time.LocalDateTime;
4748
import java.time.LocalTime;
4849
import java.util.ArrayList;
4950
import java.util.List;
5051
import java.util.Map;
5152
import java.util.stream.Collectors;
5253

53-
import static com.studioedge.focus_to_levelup_server.global.common.AppConstants.RANKING_WARNING_FOCUS_MINUTES;
5454
import static com.studioedge.focus_to_levelup_server.global.common.AppConstants.getServiceDate;
5555

5656
@Service
@@ -80,23 +80,44 @@ public void saveFocus(Member m, Long subjectId, SaveFocusRequest request) {
8080
* 대표 캐릭터 친밀도 누적
8181
* 현재 집중중 상태 해제
8282
* */
83-
int focusMinutes = request.focusSeconds() / 60;
84-
int remainSeconds = request.focusSeconds() % 60;
85-
LocalDate serviceDate = getServiceDate();
83+
/*
84+
* 4시 경계 체크 및 시간 보정 로직 (추가된 부분)
85+
* - 시작 시간을 기준으로 '다음 4시'를 구합니다.
86+
* - 종료 시간이 4시를 넘어가면, '시작~4시'까지의 시간만 저장하도록 focusSeconds를 조정합니다.
87+
*/
88+
LocalDateTime startTime = request.startTime();
89+
LocalDateTime endTime = startTime.plusSeconds(request.focusSeconds());
90+
91+
LocalDateTime limitTime;
92+
if (startTime.getHour() < 4) {
93+
limitTime = startTime.toLocalDate().atTime(4, 0);
94+
} else {
95+
limitTime = startTime.toLocalDate().plusDays(1).atTime(4, 0);
96+
}
97+
98+
int savedFocusSeconds = request.focusSeconds();
99+
if (endTime.isAfter(limitTime)) {
100+
long durationUntilLimit = Duration.between(startTime, limitTime).getSeconds();
101+
savedFocusSeconds = (int) Math.max(0, durationUntilLimit);
102+
}
103+
104+
int focusMinutes = savedFocusSeconds / 60;
105+
int remainSeconds = savedFocusSeconds % 60;
106+
LocalDate serviceDate = getServiceDate(startTime);
86107

87108
Member member = memberRepository.findById(m.getId())
88109
.orElseThrow(MemberNotFoundException::new);
89110
MemberInfo memberInfo = memberInfoRepository.findByMemberId(m.getId())
90111
.orElseThrow(InvalidMemberException::new);
91-
MemberSetting memberSetting = memberSettingRepository.findByMemberId(m.getId())
92-
.orElseThrow(InvalidMemberException::new);
93-
if (focusMinutes >= RANKING_WARNING_FOCUS_MINUTES) {
94-
boolean isBanned = memberSetting.warning();
95-
if (isBanned) {
96-
member.banRanking();
97-
rankingRepository.deleteByMemberId(m.getId());
98-
}
99-
}
112+
// MemberSetting memberSetting = memberSettingRepository.findByMemberId(m.getId())
113+
// .orElseThrow(InvalidMemberException::new);
114+
// if (focusMinutes >= RANKING_WARNING_FOCUS_MINUTES) {
115+
// boolean isBanned = memberSetting.warning();
116+
// if (isBanned) {
117+
// member.banRanking();
118+
// rankingRepository.deleteByMemberId(m.getId());
119+
// }
120+
// }
100121

101122
DailyGoal dailyGoal = dailyGoalRepository.findByMemberIdAndDailyGoalDate(m.getId(), serviceDate)
102123
.orElseThrow(DailyGoalNotFoundException::new);
@@ -106,7 +127,6 @@ public void saveFocus(Member m, Long subjectId, SaveFocusRequest request) {
106127
.orElseThrow(CharacterDefaultNotFoundException::new);
107128
DailySubject dailySubject = dailySubjectRepository.findByMemberAndSubjectAndDate(member, subject, serviceDate)
108129
.orElseGet(() -> {
109-
// 오늘 해당 과목으로 공부한 기록이 없으면, 새로 생성
110130
return DailySubject.builder()
111131
.member(member)
112132
.subject(subject)

src/main/java/com/studioedge/focus_to_levelup_server/domain/focus/service/FocusServiceV2.java

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
import org.springframework.stereotype.Service;
3131
import org.springframework.transaction.annotation.Transactional;
3232

33+
import java.time.Duration;
3334
import java.time.LocalDate;
35+
import java.time.LocalDateTime;
3436
import java.time.LocalTime;
3537
import java.util.List;
3638

@@ -59,16 +61,37 @@ public void saveFocus(Member m, Long subjectId, SaveFocusRequestV2 request) {
5961
* 대표 캐릭터 친밀도 누적
6062
* 현재 집중중 상태 해제
6163
* */
62-
int focusMinutes = request.focusSeconds() / 60;
63-
int remainSeconds = request.focusSeconds() % 60;
64-
LocalDate serviceDate = getServiceDate();
64+
/*
65+
* 4시 경계 체크 및 시간 보정 로직 (추가된 부분)
66+
* - 시작 시간을 기준으로 '다음 4시'를 구합니다.
67+
* - 종료 시간이 4시를 넘어가면, '시작~4시'까지의 시간만 저장하도록 focusSeconds를 조정합니다.
68+
*/
69+
LocalDateTime startTime = request.startTime();
70+
LocalDateTime endTime = startTime.plusSeconds(request.focusSeconds());
71+
72+
LocalDateTime limitTime;
73+
if (startTime.getHour() < 4) {
74+
limitTime = startTime.toLocalDate().atTime(4, 0);
75+
} else {
76+
limitTime = startTime.toLocalDate().plusDays(1).atTime(4, 0);
77+
}
78+
79+
int savedFocusSeconds = request.focusSeconds();
80+
if (endTime.isAfter(limitTime)) {
81+
long durationUntilLimit = Duration.between(startTime, limitTime).getSeconds();
82+
savedFocusSeconds = (int) Math.max(0, durationUntilLimit);
83+
}
84+
85+
int focusMinutes = savedFocusSeconds / 60;
86+
int remainSeconds = savedFocusSeconds % 60;
87+
LocalDate serviceDate = getServiceDate(startTime);
6588

6689
Member member = memberRepository.findById(m.getId())
6790
.orElseThrow(MemberNotFoundException::new);
6891
MemberInfo memberInfo = memberInfoRepository.findByMemberId(m.getId())
6992
.orElseThrow(InvalidMemberException::new);
7093

71-
DailyGoal dailyGoal = dailyGoalRepository.findById(request.dailyGoalId())
94+
DailyGoal dailyGoal = dailyGoalRepository.findByMemberIdAndDailyGoalDate(member.getId(), serviceDate)
7295
.orElseThrow(DailyGoalNotFoundException::new);
7396
Subject subject = this.subjectRepository.findByIdAndDeleteAtIsNull(subjectId)
7497
.orElseThrow(SubjectNotFoundException::new);

src/main/java/com/studioedge/focus_to_levelup_server/domain/focus/service/SubjectService.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,22 @@ public List<GetSubjectResponse> getSubjectList(Member member) {
5353

5454
@Transactional
5555
public void createSubject(Member member, CreateSubjectRequest request) {
56-
Subject subject = subjectRepository.findByMemberAndName(member, request.name())
56+
List<Subject> subjects = subjectRepository.findAllByMemberAndName(member, request.name());
57+
Subject targetSubject = subjects.stream()
58+
.filter(s -> s.getDeleteAt() == null) // 활성화된 과목
59+
.findFirst()
5760
.orElseGet(() -> {
58-
return subjectRepository.save(CreateSubjectRequest.from(member, request));
61+
return subjects.stream()
62+
.max((s1, s2) -> s1.getId().compareTo(s2.getId()))
63+
.orElse(null);
5964
});
60-
subject.update(request);
65+
66+
if (targetSubject == null) {
67+
Subject newSubject = CreateSubjectRequest.from(member, request);
68+
subjectRepository.save(newSubject);
69+
} else {
70+
targetSubject.update(request);
71+
}
6172
}
6273

6374
@Transactional

src/main/java/com/studioedge/focus_to_levelup_server/domain/member/entity/Member.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ public class Member extends BaseEntity {
5454
private Boolean isSubscriptionRewarded = false; // 첫 구독시 받는 보상 수령 여부
5555

5656
@Column(nullable = false)
57-
@ColumnDefault("false")
58-
private Boolean isReceivedWeeklyReward = false; // 주간 보상 수령 여부
57+
@ColumnDefault("true")
58+
private Boolean isReceivedWeeklyReward = true; // 주간 보상 수령 여부
5959

6060
@Column(nullable = false)
6161
@ColumnDefault("1")
@@ -149,7 +149,6 @@ public void setFcmToken(String fcmToken) {
149149

150150
public void completeSignUp(String nickname, MemberInfo memberInfo, MemberSetting memberSetting) {
151151
this.nickname = nickname;
152-
this.nicknameUpdatedAt = LocalDateTime.now();
153152
this.memberInfo = memberInfo;
154153
this.memberSetting = memberSetting;
155154
this.status = MemberStatus.ACTIVE;

src/main/java/com/studioedge/focus_to_levelup_server/domain/member/service/MemberServiceImpl.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,13 @@ public GetProfileResponse getMemberProfile(Long memberId) {
130130
@Override
131131
@Transactional
132132
public void updateNickname(Member member, UpdateNicknameRequest request) {
133-
if (memberRepository.existsByNickname(request.nickname())) {
134-
throw new IllegalArgumentException("해당 닉네임은 이미 존재합니다.");
135-
}
136133
LocalDateTime updatedAt = member.getNicknameUpdatedAt();
137134
if (updatedAt != null && updatedAt.isAfter(LocalDateTime.now().minusMonths(1))) {
138135
throw new NicknameUpdateException();
139136
}
137+
if (memberRepository.existsByNickname(request.nickname())) {
138+
throw new IllegalArgumentException("해당 닉네임은 이미 존재합니다.");
139+
}
140140
Member me = memberRepository.findById(member.getId())
141141
.orElseThrow(MemberNotFoundException::new);
142142
me.updateNickname(request.nickname());

src/main/java/com/studioedge/focus_to_levelup_server/domain/system/entity/WeeklyReward.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import lombok.Builder;
99
import lombok.Getter;
1010
import lombok.NoArgsConstructor;
11+
import org.hibernate.annotations.ColumnDefault;
1112

1213
@Entity
1314
@Table(
@@ -40,6 +41,10 @@ public class WeeklyReward extends BaseEntity {
4041
@Column(nullable = false)
4142
private Integer lastLevel;
4243

44+
@Column(nullable = false)
45+
@ColumnDefault("false")
46+
private Boolean isReceived = false;
47+
4348
@Builder
4449
public WeeklyReward(Member member, Character lastCharacter, String lastCharacterImageUrl,
4550
Integer lastLevel, Integer evolution) {
@@ -58,4 +63,8 @@ public WeeklyReward updateInfo(Character lastCharacter, String lastCharacterImag
5863
this.lastCharacterImageUrl = lastCharacterImageUrl;
5964
return this;
6065
}
66+
67+
public void receive() {
68+
this.isReceived = true;
69+
}
6170
}

src/main/java/com/studioedge/focus_to_levelup_server/domain/system/service/WeeklyRewardService.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import com.studioedge.focus_to_levelup_server.domain.system.dto.request.ReceiveWeeklyRewardRequest;
1313
import com.studioedge.focus_to_levelup_server.domain.system.dto.response.WeeklyRewardInfoResponse;
1414
import com.studioedge.focus_to_levelup_server.domain.system.entity.WeeklyReward;
15+
import com.studioedge.focus_to_levelup_server.domain.system.exception.WeeklyRewardAlreadyReceivedException;
1516
import com.studioedge.focus_to_levelup_server.domain.system.exception.WeeklyRewardNotFoundException;
1617
import com.studioedge.focus_to_levelup_server.global.common.enums.Rarity;
1718
import lombok.RequiredArgsConstructor;
@@ -35,7 +36,7 @@ public WeeklyRewardInfoResponse getWeeklyRewardInfo(Member member) {
3536
.orElseThrow(WeeklyRewardNotFoundException::new);
3637

3738
// 이미 수령 여부 확인 (예외 대신 플래그로 전달)
38-
boolean alreadyReceived = !member.getIsReceivedWeeklyReward();
39+
boolean alreadyReceived = weeklyReward.getIsReceived();
3940

4041
// 현재 구독 상태 조회
4142
SubscriptionType subscriptionType = subscriptionRepository.findByMemberIdAndIsActiveTrue(member.getId())
@@ -110,12 +111,18 @@ private int calculateTicketBonus(int ticketCount, int baseAmount) {
110111
@Transactional
111112
public void receiveWeeklyReward(Long memberId, ReceiveWeeklyRewardRequest request) {
112113
Member member = memberRepository.findById(memberId).orElseThrow(MemberNotFoundException::new);
114+
WeeklyReward weeklyReward = weeklyRewardRepository.findById(request.weeklyRewardId())
115+
.orElseThrow(WeeklyRewardNotFoundException::new);
113116
MemberInfo memberInfo = member.getMemberInfo();
117+
118+
if (weeklyReward.getIsReceived() || member.getIsReceivedWeeklyReward()) {
119+
throw new WeeklyRewardAlreadyReceivedException();
120+
}
114121
member.receiveWeeklyReward(true);
122+
weeklyReward.receive();
115123
memberInfo.addDiamond(request.rewardDiamond());
116124
if (memberInfo.getBonusTicketCount() > 0) {
117125
member.getMemberInfo().useBonusTicket();
118126
}
119-
weeklyRewardRepository.deleteById(request.weeklyRewardId());
120127
}
121128
}

0 commit comments

Comments
 (0)