diff --git a/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimeApi.java b/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimeApi.java index 019e029f..58593495 100644 --- a/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimeApi.java +++ b/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimeApi.java @@ -2,10 +2,12 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import gg.agit.konect.domain.studytime.dto.StudyTimeSummaryResponse; import gg.agit.konect.domain.studytime.dto.StudyTimerStopRequest; import gg.agit.konect.domain.studytime.dto.StudyTimerStopResponse; import gg.agit.konect.global.auth.annotation.UserId; @@ -17,6 +19,10 @@ @RequestMapping("/studytimes") public interface StudyTimeApi { + @Operation(summary = "순공 시간(일간, 월간, 통합)을 조회한다.") + @GetMapping("/summary") + ResponseEntity getSummary(@UserId Integer userId); + @Operation(summary = "스터디 타이머를 시작한다.", description = """ ## 설명 - 스터디 타이머를 시작합니다. diff --git a/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimeController.java b/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimeController.java index a8c6d097..cc5f0fa9 100644 --- a/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimeController.java +++ b/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimeController.java @@ -1,14 +1,14 @@ package gg.agit.konect.domain.studytime.controller; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import gg.agit.konect.domain.studytime.dto.StudyTimeSummaryResponse; import gg.agit.konect.domain.studytime.dto.StudyTimerStopRequest; import gg.agit.konect.domain.studytime.dto.StudyTimerStopResponse; +import gg.agit.konect.domain.studytime.service.StudyTimeQueryService; import gg.agit.konect.domain.studytime.service.StudyTimerService; import gg.agit.konect.global.auth.annotation.UserId; import jakarta.validation.Valid; @@ -20,15 +20,23 @@ public class StudyTimeController implements StudyTimeApi { private final StudyTimerService studyTimerService; + private final StudyTimeQueryService studyTimeQueryService; - @PostMapping("/timers") + @Override + public ResponseEntity getSummary(@UserId Integer userId) { + StudyTimeSummaryResponse response = studyTimeQueryService.getSummary(userId); + + return ResponseEntity.ok(response); + } + + @Override public ResponseEntity start(@UserId Integer userId) { studyTimerService.start(userId); return ResponseEntity.ok().build(); } - @DeleteMapping("/timers") + @Override public ResponseEntity stop( @UserId Integer userId, @RequestBody @Valid StudyTimerStopRequest request diff --git a/src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimeSummaryResponse.java b/src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimeSummaryResponse.java new file mode 100644 index 00000000..c8f6059b --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimeSummaryResponse.java @@ -0,0 +1,20 @@ +package gg.agit.konect.domain.studytime.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record StudyTimeSummaryResponse( + @Schema(description = "오늘 누적 공부 시간(누적 초)", example = "45296", requiredMode = REQUIRED) + Long todayStudyTime, + + @Schema(description = "월간 누적 공부 시간(누적 초)", example = "334510", requiredMode = REQUIRED) + Long monthlyStudyTime, + + @Schema(description = "총 누적 공부 시간(누적 초)", example = "564325", requiredMode = REQUIRED) + Long totalStudyTime +) { + public static StudyTimeSummaryResponse of(Long todayStudyTime, Long monthlyStudyTime, Long totalStudyTime) { + return new StudyTimeSummaryResponse(todayStudyTime, monthlyStudyTime, totalStudyTime); + } +} diff --git a/src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimerStopRequest.java b/src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimerStopRequest.java index 839c92be..8eaf5e57 100644 --- a/src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimerStopRequest.java +++ b/src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimerStopRequest.java @@ -3,33 +3,13 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; public record StudyTimerStopRequest( - @NotNull(message = "시간(hour)은 필수 입력입니다.") - @Min(value = 0, message = "시간은 0 이상이어야 합니다.") - @Schema(description = "타이머 시간 - 시간", example = "1", requiredMode = REQUIRED) - Integer hour, - - @NotNull(message = "분(minute)은 필수 입력입니다.") - @Min(value = 0, message = "분은 0 이상이어야 합니다.") - @Max(value = 59, message = "분은 59 이하여야 합니다.") - @Schema(description = "타이머 시간 - 분", example = "30", requiredMode = REQUIRED) - Integer minute, - - @NotNull(message = "초(second)는 필수 입력입니다.") - @Min(value = 0, message = "초는 0 이상이어야 합니다.") - @Max(value = 59, message = "초는 59 이하여야 합니다.") - @Schema(description = "타이머 시간 - 초", example = "15", requiredMode = REQUIRED) - Integer second + @NotNull(message = "누적 초(totalSeconds)는 필수 입력입니다.") + @Min(value = 0, message = "누적 초는 0 이상이어야 합니다.") + @Schema(description = "타이머 누적 시간(초)", example = "5415", requiredMode = REQUIRED) + Long totalSeconds ) { - - private static final long SECONDS_PER_MINUTE = 60L; - private static final long SECONDS_PER_HOUR = 3600L; - - public long toTotalSeconds() { - return hour * SECONDS_PER_HOUR + minute * SECONDS_PER_MINUTE + second; - } } diff --git a/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeAggregate.java b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeAggregate.java new file mode 100644 index 00000000..58ab4e3e --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeAggregate.java @@ -0,0 +1,9 @@ +package gg.agit.konect.domain.studytime.model; + +public record StudyTimeAggregate( + long sessionSeconds, + long dailySeconds, + long monthlySeconds, + long totalSeconds +) { +} diff --git a/src/main/java/gg/agit/konect/domain/studytime/service/StudyTimeQueryService.java b/src/main/java/gg/agit/konect/domain/studytime/service/StudyTimeQueryService.java new file mode 100644 index 00000000..f98d3388 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/studytime/service/StudyTimeQueryService.java @@ -0,0 +1,55 @@ +package gg.agit.konect.domain.studytime.service; + +import java.time.LocalDate; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.agit.konect.domain.studytime.dto.StudyTimeSummaryResponse; +import gg.agit.konect.domain.studytime.model.StudyTimeDaily; +import gg.agit.konect.domain.studytime.model.StudyTimeMonthly; +import gg.agit.konect.domain.studytime.model.StudyTimeTotal; +import gg.agit.konect.domain.studytime.repository.StudyTimeDailyRepository; +import gg.agit.konect.domain.studytime.repository.StudyTimeMonthlyRepository; +import gg.agit.konect.domain.studytime.repository.StudyTimeTotalRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class StudyTimeQueryService { + + private final StudyTimeDailyRepository studyTimeDailyRepository; + private final StudyTimeMonthlyRepository studyTimeMonthlyRepository; + private final StudyTimeTotalRepository studyTimeTotalRepository; + + public StudyTimeSummaryResponse getSummary(Integer userId) { + Long dailyStudyTime = getDailyStudyTime(userId); + Long monthlyStudyTime = getMonthlyStudyTime(userId); + Long totalStudyTime = getTotalStudyTime(userId); + + return StudyTimeSummaryResponse.of(dailyStudyTime, monthlyStudyTime, totalStudyTime); + } + + public long getTotalStudyTime(Integer userId) { + return studyTimeTotalRepository.findByUserId(userId) + .map(StudyTimeTotal::getTotalSeconds) + .orElse(0L); + } + + public long getDailyStudyTime(Integer userId) { + LocalDate today = LocalDate.now(); + + return studyTimeDailyRepository.findByUserIdAndStudyDate(userId, today) + .map(StudyTimeDaily::getTotalSeconds) + .orElse(0L); + } + + public long getMonthlyStudyTime(Integer userId) { + LocalDate month = LocalDate.now().withDayOfMonth(1); + + return studyTimeMonthlyRepository.findByUserIdAndStudyMonth(userId, month) + .map(StudyTimeMonthly::getTotalSeconds) + .orElse(0L); + } +} diff --git a/src/main/java/gg/agit/konect/domain/studytime/service/StudyTimerService.java b/src/main/java/gg/agit/konect/domain/studytime/service/StudyTimerService.java index 5a7f5df2..71ff225d 100644 --- a/src/main/java/gg/agit/konect/domain/studytime/service/StudyTimerService.java +++ b/src/main/java/gg/agit/konect/domain/studytime/service/StudyTimerService.java @@ -35,6 +35,7 @@ public class StudyTimerService { private static final long TIMER_MISMATCH_THRESHOLD_SECONDS = 60L; + private final StudyTimeQueryService studyTimeQueryService; private final StudyTimerRepository studyTimerRepository; private final StudyTimeDailyRepository studyTimeDailyRepository; private final StudyTimeMonthlyRepository studyTimeMonthlyRepository; @@ -66,7 +67,7 @@ public StudyTimerStopResponse stop(Integer userId, StudyTimerStopRequest request LocalDateTime endedAt = LocalDateTime.now(); LocalDateTime startedAt = studyTimer.getStartedAt(); long serverSeconds = Duration.between(startedAt, endedAt).getSeconds(); - long clientSeconds = request.toTotalSeconds(); + long clientSeconds = request.totalSeconds(); if (isElapsedTimeInvalid(serverSeconds, clientSeconds)) { studyTimerRepository.delete(studyTimer); @@ -75,7 +76,7 @@ public StudyTimerStopResponse stop(Integer userId, StudyTimerStopRequest request long sessionSeconds = accumulateStudyTime(studyTimer.getUser(), startedAt, endedAt); studyTimerRepository.delete(studyTimer); - StudyTimeSummary summary = buildSummary(userId, endedAt.toLocalDate(), sessionSeconds); + StudyTimeSummary summary = buildSummary(userId, sessionSeconds); return StudyTimerStopResponse.from(summary); } @@ -160,20 +161,10 @@ private void addTotalSeconds(User user, long seconds) { studyTimeTotalRepository.save(total); } - private StudyTimeSummary buildSummary(Integer userId, LocalDate endDate, long sessionSeconds) { - LocalDate month = endDate.withDayOfMonth(1); - - long dailySeconds = studyTimeDailyRepository.findByUserIdAndStudyDate(userId, endDate) - .map(StudyTimeDaily::getTotalSeconds) - .orElse(0L); - - long monthlySeconds = studyTimeMonthlyRepository.findByUserIdAndStudyMonth(userId, month) - .map(StudyTimeMonthly::getTotalSeconds) - .orElse(0L); - - long totalSeconds = studyTimeTotalRepository.findByUserId(userId) - .map(StudyTimeTotal::getTotalSeconds) - .orElse(0L); + private StudyTimeSummary buildSummary(Integer userId, long sessionSeconds) { + long dailySeconds = studyTimeQueryService.getDailyStudyTime(userId); + long monthlySeconds = studyTimeQueryService.getMonthlyStudyTime(userId); + long totalSeconds = studyTimeQueryService.getTotalStudyTime(userId); return new StudyTimeSummary(sessionSeconds, dailySeconds, monthlySeconds, totalSeconds); } diff --git a/src/main/java/gg/agit/konect/domain/user/dto/UserInfoResponse.java b/src/main/java/gg/agit/konect/domain/user/dto/UserInfoResponse.java index 37f95088..6ea19df0 100644 --- a/src/main/java/gg/agit/konect/domain/user/dto/UserInfoResponse.java +++ b/src/main/java/gg/agit/konect/domain/user/dto/UserInfoResponse.java @@ -3,10 +3,6 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; -import java.time.LocalTime; - -import com.fasterxml.jackson.annotation.JsonFormat; - import gg.agit.konect.domain.user.model.User; import io.swagger.v3.oas.annotations.media.Schema; @@ -32,15 +28,22 @@ public record UserInfoResponse( @Schema(description = "가입 동아리 개수", example = "1", requiredMode = REQUIRED) Integer joinedClubCount, - @Schema(description = "순공 시간", example = "13:13", requiredMode = REQUIRED) - @JsonFormat(pattern = "HH:mm") - LocalTime studyTime, + @Schema(description = "순공 시간(HH:mm)", example = "12:34", requiredMode = REQUIRED) + String studyTime, @Schema(description = "읽지 않은 총 동아리 연합회 공지", example = "1", requiredMode = REQUIRED) Long unreadCouncilNoticeCount ) { - public static UserInfoResponse from(User user, Integer joinedClubCount, Long unreadCouncilNoticeCount) { + private static final long SECONDS_PER_HOUR = 3600; + private static final long SECONDS_PER_MINUTE = 60; + + public static UserInfoResponse from( + User user, + Integer joinedClubCount, + Long studyTime, + Long unreadCouncilNoticeCount + ) { return new UserInfoResponse( user.getName(), user.getUniversity().getKoreanName(), @@ -49,8 +52,15 @@ public static UserInfoResponse from(User user, Integer joinedClubCount, Long unr user.getEmail(), user.getImageUrl(), joinedClubCount, - LocalTime.of(0, 0, 0), + formatSecondsToHHmm(studyTime), unreadCouncilNoticeCount ); } + + private static String formatSecondsToHHmm(Long seconds) { + long h = seconds / SECONDS_PER_HOUR; + long m = (seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE; + + return String.format("%02d:%02d", h, m); + } } diff --git a/src/main/java/gg/agit/konect/domain/user/service/UserService.java b/src/main/java/gg/agit/konect/domain/user/service/UserService.java index 8d295f40..50316352 100644 --- a/src/main/java/gg/agit/konect/domain/user/service/UserService.java +++ b/src/main/java/gg/agit/konect/domain/user/service/UserService.java @@ -14,6 +14,7 @@ import gg.agit.konect.domain.club.repository.ClubApplyRepository; import gg.agit.konect.domain.club.repository.ClubMemberRepository; import gg.agit.konect.domain.notice.repository.CouncilNoticeReadRepository; +import gg.agit.konect.domain.studytime.service.StudyTimeQueryService; import gg.agit.konect.domain.university.model.University; import gg.agit.konect.domain.university.repository.UniversityRepository; import gg.agit.konect.domain.user.dto.SignupRequest; @@ -41,6 +42,7 @@ public class UserService { private final ClubApplyRepository clubApplyRepository; private final ChatMessageRepository chatMessageRepository; private final ChatRoomRepository chatRoomRepository; + private final StudyTimeQueryService studyTimeQueryService; @Transactional public Integer signup(String email, Provider provider, SignupRequest request) { @@ -79,8 +81,9 @@ public UserInfoResponse getUserInfo(Integer userId) { User user = userRepository.getById(userId); int joinedClubCount = clubMemberRepository.findAllByUserId(user.getId()).size(); Long unreadCouncilNoticeCount = councilNoticeReadRepository.countUnreadNoticesByUserId(user.getId()); + Long studyTime = studyTimeQueryService.getTotalStudyTime(userId); - return UserInfoResponse.from(user, joinedClubCount, unreadCouncilNoticeCount); + return UserInfoResponse.from(user, joinedClubCount, studyTime, unreadCouncilNoticeCount); } @Transactional