Skip to content
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

Feat : 퀘스트 목표 변경 기능을 구현한다 #25 #59

Merged
merged 6 commits into from
Feb 6, 2025
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 @@ -35,7 +35,7 @@ public ResponseEntity<ApiResponse<?>> currentStreak(

@Operation(
summary = "오늘의 퀘스트 완료 여부 조회",
description = "오늘의 퀘스트 완료 여부를 조회합니다. 퍼센트로 반환됩니다. (0~100)")
description = "오늘의 퀘스트 완료 여부를 조회합니다. 퍼센트로 반환됩니다 (0~100). 매일 0시 1분 0초에 자동으로 초기화됩니다.")
@GetMapping("/today-quest")
public ResponseEntity<ApiResponse<?>> todayQuest(
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
Expand All @@ -45,7 +45,9 @@ public ResponseEntity<ApiResponse<?>> todayQuest(
.body(ApiResponse.from(QuestResponse.toQuestResponse(todayQuest)));
}

@Operation(summary = "요일별 출석 현황 조회", description = "요일별 출석 현황을 조회합니다. 매주 자동으로 초기화됩니다.")
@Operation(
summary = "요일별 출석 현황 조회",
description = "요일별 출석 현황을 조회합니다. 매주 월요일 0시 0분 0초에 자동으로 초기화됩니다.")
@GetMapping("/weekly-attendance")
public ResponseEntity<ApiResponse<?>> weeklyAttendance(
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/com/ripple/BE/user/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
import com.ripple.BE.user.domain.CustomUserDetails;
import com.ripple.BE.user.domain.type.Level;
import com.ripple.BE.user.dto.ProgressDTO;
import com.ripple.BE.user.dto.UserGoalDTO;
import com.ripple.BE.user.dto.UserInfoDTO;
import com.ripple.BE.user.dto.request.UpdateUserProfileRequest;
import com.ripple.BE.user.dto.request.UserGoalRequest;
import com.ripple.BE.user.dto.response.ProgressResponse;
import com.ripple.BE.user.dto.response.UserGoalResponse;
import com.ripple.BE.user.dto.response.UserInfoResponse;
import com.ripple.BE.user.service.MyPageService;
import com.ripple.BE.user.service.UserProgressService;
Expand Down Expand Up @@ -241,4 +244,25 @@ public ResponseEntity<ApiResponse<Object>> getUserInfo(
return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.from(UserInfoResponse.toUserInfoResponse(userInfo)));
}

@Operation(summary = "사용자 퀘스트 목표 조회", description = "로그인한 유저의 퀘스트 목표를 조회합니다.")
@GetMapping("/goal")
public ResponseEntity<ApiResponse<Object>> getUserGoal(
@AuthenticationPrincipal CustomUserDetails customUserDetails) {
UserGoalDTO userGoal = userService.getUserGoal(customUserDetails.getId());

return ResponseEntity.status(HttpStatus.OK)
.body(ApiResponse.from(UserGoalResponse.toUserGoalResponse(userGoal)));
}

@Operation(summary = "사용자 퀘스트 목표 수정", description = "로그인한 유저의 퀘스트 목표를 수정합니다. 목표는 1 이상이어야 합니다.")
@PostMapping("/goal")
public ResponseEntity<ApiResponse<?>> updateUserGoal(
@Valid @RequestBody UserGoalRequest userGoalRequest,
@AuthenticationPrincipal CustomUserDetails customUserDetails) {

userService.updateUserGoal(userGoalRequest, customUserDetails.getId());

return ResponseEntity.status(HttpStatus.OK).body(ApiResponse.EMPTY_RESPONSE);
}
}
18 changes: 9 additions & 9 deletions src/main/java/com/ripple/BE/user/domain/Quest.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,27 @@ public class Quest extends BaseEntity {
@JoinColumn(name = "user_id", nullable = false)
private User user;

private boolean quizCompleted;
private int quizCompletedCount;

private boolean conceptCompleted;
private int conceptCompletedCount;

private long articleCompletedCount;
private int articleCompletedCount;

private LocalDate lastUpdatedDate; // 퀘스트 완료 날짜

public void resetQuests() {
this.quizCompleted = false;
this.conceptCompleted = false;
this.quizCompletedCount = 0;
this.conceptCompletedCount = 0;
this.articleCompletedCount = 0;
this.lastUpdatedDate = LocalDate.now();
}

public void updateQuizCompleted() {
this.quizCompleted = true;
public void updateQuizCompletedCount() {
this.quizCompletedCount++;
}

public void updateConceptCompleted() {
this.conceptCompleted = true;
public void updateConceptCompletedCount() {
this.conceptCompletedCount++;
}

public void updateArticleCompletedCount() {
Expand Down
47 changes: 47 additions & 0 deletions src/main/java/com/ripple/BE/user/domain/UserGoal.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.ripple.BE.user.domain;

import com.ripple.BE.global.entity.BaseEntity;
import com.ripple.BE.user.dto.UserGoalDTO;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Table(name = "user_goals")
@Getter
@Builder
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class UserGoal extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_goal_id")
private Long id;

@OneToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;

private int quizGoal;

private int conceptGoal;

private int articleGoal;

public void updateQuizGoal(UserGoalDTO userGoalDTO) {
this.quizGoal = userGoalDTO.quizGoal();
this.conceptGoal = userGoalDTO.conceptGoal();
this.articleGoal = userGoalDTO.articleGoal();
}
}
4 changes: 2 additions & 2 deletions src/main/java/com/ripple/BE/user/dto/QuestDTO.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.ripple.BE.user.dto;

public record QuestDTO(Long conceptProgress, Long quizProgress, Long articleProgress) {
public record QuestDTO(int conceptProgress, int quizProgress, int articleProgress) {

public static QuestDTO toQuestDTO(
final Long conceptProgress, final Long quizProgress, final Long articleProgress) {
final int conceptProgress, final int quizProgress, final int articleProgress) {
return new QuestDTO(conceptProgress, quizProgress, articleProgress);
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/ripple/BE/user/dto/UserGoalDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.ripple.BE.user.dto;

import com.ripple.BE.user.dto.request.UserGoalRequest;

public record UserGoalDTO(int conceptGoal, int quizGoal, int articleGoal) {

public static UserGoalDTO toUserGoalDTO(UserGoalRequest userGoalRequest) {
return new UserGoalDTO(
userGoalRequest.conceptGoal(), userGoalRequest.quizGoal(), userGoalRequest.articleGoal());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.ripple.BE.user.dto.request;

import jakarta.validation.constraints.Min;

public record UserGoalRequest(
@Min(value = 1, message = "1 이상의 값을 입력해주세요.") int conceptGoal,
@Min(value = 1, message = "1 이상의 값을 입력해주세요.") int quizGoal,
@Min(value = 1, message = "1 이상의 값을 입력해주세요.") int articleGoal) {}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.ripple.BE.user.dto.QuestDTO;

public record QuestResponse(Long conceptProgress, Long quizProgress, Long articleProgress) {
public record QuestResponse(int conceptProgress, int quizProgress, int articleProgress) {

public static QuestResponse toQuestResponse(QuestDTO questDTO) {
return new QuestResponse(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ripple.BE.user.dto.response;

import com.ripple.BE.user.dto.UserGoalDTO;

public record UserGoalResponse(int conceptGoal, int quizGoal, int articleGoal) {
public static UserGoalResponse toUserGoalResponse(UserGoalDTO userGoalDTO) {
return new UserGoalResponse(
userGoalDTO.conceptGoal(), userGoalDTO.quizGoal(), userGoalDTO.articleGoal());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public enum UserErrorCode implements ErrorCode {
INVALID_QUEST_TYPE(HttpStatus.BAD_REQUEST, "Invalid Quest Type"),
INVALID_EMAIL(HttpStatus.BAD_REQUEST, "Already exist Email"),
QUEST_NOT_FOUND(HttpStatus.NOT_FOUND, "Quest not found"),
USER_GOAL_NOT_FOUND(HttpStatus.NOT_FOUND, "User Goal not found"),
ATTENDANCE_NOT_FOUND(HttpStatus.NOT_FOUND, "Attendance not found");

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.ripple.BE.user.repository;

import com.ripple.BE.user.domain.UserGoal;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserGoalRepository extends JpaRepository<UserGoal, Long> {
Optional<UserGoal> findByUserId(Long userId);
}
39 changes: 10 additions & 29 deletions src/main/java/com/ripple/BE/user/service/AttendanceScheduler.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
package com.ripple.BE.user.service;

import com.ripple.BE.user.domain.AttendanceLog;
import com.ripple.BE.user.domain.Quest;
import com.ripple.BE.user.repository.AttendanceLogRepository;
import com.ripple.BE.user.repository.AttendanceRepository;
import com.ripple.BE.user.repository.QuestRepository;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
Expand All @@ -15,36 +9,23 @@
@RequiredArgsConstructor
public class AttendanceScheduler {

private final AttendanceLogRepository attendanceLogRepository;
private final QuestRepository questRepository;
private final AttendanceRepository attendanceRepository;
private final AttendanceService attendanceService;

@Scheduled(cron = "0 0 0 * * MON") // 매주 월요일 0시 0분 0초에 실행
public void resetWeeklyAttendance() {
attendanceLogRepository.deleteAll();
}

@Scheduled(cron = "0 0 0 * * ?") // 매일 0시 0분 0초에 실행
@Transactional
public void resetAllQuests() {
questRepository.findAll().forEach(Quest::resetQuests);
public void resetWeeklyAttendanceLog() {
attendanceService.resetWeeklyAttendanceLog();
}

@Scheduled(cron = "0 0 0 * * ?") // 매일 0시 0분 0초에 실행
@Scheduled(cron = "30 0 0 * * ?") // 매일 0시 0분 30초에 실행
Copy link
Member

Choose a reason for hiding this comment

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

오 세심하네요

@Transactional
public void recordAttendanceLog() {
LocalDate today = LocalDate.now();
attendanceService.recordAttendanceLog();
}

attendanceRepository
.findAll()
.forEach(
attendance -> {
attendanceLogRepository.save(
AttendanceLog.builder()
.attendance(attendance)
.date(today)
.isAttended(false)
.build());
});
@Scheduled(cron = "0 1 0 * * ?") // 매일 0시 1분 0초에 실행
@Transactional
public void resetAllQuests() {
attendanceService.resetAllQuests();
}
}
62 changes: 53 additions & 9 deletions src/main/java/com/ripple/BE/user/service/AttendanceService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
import com.ripple.BE.user.domain.AttendanceLog;
import com.ripple.BE.user.domain.Quest;
import com.ripple.BE.user.domain.User;
import com.ripple.BE.user.domain.UserGoal;
import com.ripple.BE.user.dto.AttendanceDTO;
import com.ripple.BE.user.dto.QuestDTO;
import com.ripple.BE.user.exception.UserException;
import com.ripple.BE.user.repository.AttendanceLogRepository;
import com.ripple.BE.user.repository.AttendanceRepository;
import com.ripple.BE.user.repository.QuestRepository;
import com.ripple.BE.user.repository.UserGoalRepository;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
Expand All @@ -30,43 +32,58 @@ public class AttendanceService {
private final QuestRepository questRepository;
private final AttendanceRepository attendanceRepository;
private final AttendanceLogRepository attendanceLogRepository;
private final UserGoalRepository userGoalRepository;

@Transactional
public Long getCurrentStreak(Long id) {
Attendance attendance =
attendanceRepository
.findByUserId(id)
.orElseThrow(() -> new UserException(ATTENDANCE_NOT_FOUND));

if (attendance.getLastAttendedDate() != null
&& attendance.getLastAttendedDate().isBefore(LocalDate.now().minusDays(1))) {
attendance.updateCurrentStreak(1L);
}

return attendance.getCurrentStreak();
}

public QuestDTO getTodayQuest(Long id) {
Quest quest =
questRepository.findByUserId(id).orElseThrow(() -> new UserException(QUEST_NOT_FOUND));
UserGoal userGoal =
userGoalRepository
.findByUserId(id)
.orElseThrow(() -> new UserException(USER_GOAL_NOT_FOUND));

return QuestDTO.toQuestDTO(
quest.isConceptCompleted() ? 100L : 0L,
quest.isQuizCompleted() ? 100L : 0L,
quest.getArticleCompletedCount() / 3 * 100);
Math.min(100, quest.getConceptCompletedCount() * 100 / userGoal.getConceptGoal()),
Math.min(100, quest.getQuizCompletedCount() * 100 / userGoal.getQuizGoal()),
Math.min(100, quest.getArticleCompletedCount() * 100 / userGoal.getArticleGoal()));
}

@Transactional
public void completeQuest(Long userId, String questType) {
Quest quest =
questRepository.findByUserId(userId).orElseThrow(() -> new UserException(QUEST_NOT_FOUND));
UserGoal userGoal =
userGoalRepository
.findByUserId(userId)
.orElseThrow(() -> new UserException(USER_GOAL_NOT_FOUND));

// 퀘스트 타입에 따라 완료 처리
switch (questType.toUpperCase()) {
case "QUIZ" -> quest.updateQuizCompleted();
case "CONCEPT" -> quest.updateConceptCompleted();
case "QUIZ" -> quest.updateQuizCompletedCount();
case "CONCEPT" -> quest.updateConceptCompletedCount();
case "ARTICLE" -> quest.updateArticleCompletedCount();
default -> throw new UserException(INVALID_QUEST_TYPE);
}

// 퀘스트 3개 완료 시 출석 완료 처리
if (quest.getArticleCompletedCount() >= 3
&& quest.isConceptCompleted()
&& quest.isQuizCompleted()) {
// 퀘스트 완료 시 출석 처리
if (quest.getArticleCompletedCount() >= userGoal.getArticleGoal()
&& quest.getConceptCompletedCount() >= userGoal.getConceptGoal()
&& quest.getQuizCompletedCount() >= userGoal.getQuizGoal()) {
completeAttendance(userId, LocalDate.now());
}
}
Expand Down Expand Up @@ -108,6 +125,8 @@ public void createAttendance(User user) {
questRepository.save(Quest.builder().user(user).lastUpdatedDate(today).build());
attendanceLogRepository.save(
AttendanceLog.builder().date(today).isAttended(false).attendance(attendance).build());
userGoalRepository.save(
UserGoal.builder().user(user).quizGoal(1).articleGoal(3).conceptGoal(1).build());
Copy link
Member

Choose a reason for hiding this comment

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

이게 사용자 목표 기본 설정값인거가요?

}

@Transactional
Expand Down Expand Up @@ -136,4 +155,29 @@ public AttendanceDTO getWeeklyAttendance(Long userId) {
.sunday(weeklyAttendance.get(6))
.build();
}

@Transactional
public void resetWeeklyAttendanceLog() {
attendanceLogRepository.deleteAll();
}

@Transactional
public void resetAllQuests() {
questRepository.findAll().forEach(Quest::resetQuests);
}

@Transactional
public void recordAttendanceLog() {
attendanceRepository
.findAll()
.forEach(
attendance -> {
attendanceLogRepository.save(
AttendanceLog.builder()
.attendance(attendance)
.date(LocalDate.now())
.isAttended(false)
.build());
});
}
}
Loading