-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 순공 시간 타이머 실행/종료 API 추가 #98
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
Merged
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
5eefbb7
feat: 순공 시간 및 타이머 DB 스키마 작성
dh2906 8c8adba
feat: 순공 시간 관련 엔티티 생성
dh2906 490c410
refactor: 도메인 패키지 명 수정
dh2906 4d71ef1
feat: 순공 시간 타이머 API 명세 작성
dh2906 b44c0a4
feat: 순공 시간 타이머 API 구현
dh2906 d8f9039
feat: 순공 시간 타이머 서비스 로직 구현
dh2906 821479e
fix: 순공 시간 타이머 PK 구조 변경
dh2906 3c2c381
fix: 순공 시간 집계 응답 순서 수정
dh2906 31d2f78
refactor: 스터디 타이머 종료 응답 매핑을 서비스로 이동
dh2906 8fa352e
fix: 동시 요청에 대한 에러 방지
dh2906 a349977
fix: 클래스 명, 엔드포인트 수정
dh2906 f3766f9
fix: 타이머 종료 시 유효성 검증을 위해 누적 시간을 요청으로 받도록 수정
dh2906 24f17a1
feat: 타이머 종료 누적 시간 DTO 추가
dh2906 f7b5661
fix: 매직 넘버 상수화
dh2906 801776d
fix: 순공 시간 타이머 엔드포인트 수정
dh2906 4d2deec
fix: 타이머 종료 시 시간, 분, 초 각각 요청으로 받도록 수정
dh2906 a8444d3
fix: StudyTimeAggregate -> StudyTimeSummary 네이밍 수정
dh2906 eb5943b
fix: 타이머 경과 시간 검증 메소드 명 수정
dh2906 4383a8d
fix: 공부 시간 반영하는 메소드 명 수정
dh2906 eb58ad1
refactor: 시간 누적 로직 메소드 분할
dh2906 caf492e
refactor: accumulateStudyTime 반환형 long으로 변경
dh2906 4d03665
refactor: 엔티티 생성을 정적 팩토리 메소드 호출하는 방식으로 수정
dh2906 ccb3cfa
chore: 코드 포맷팅
dh2906 d7b4df4
chore: 매직넘버 상수화
dh2906 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
46 changes: 46 additions & 0 deletions
46
src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimeApi.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| 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 gg.agit.konect.domain.studytime.dto.StudyTimerStopRequest; | ||
| import gg.agit.konect.domain.studytime.dto.StudyTimerStopResponse; | ||
| import gg.agit.konect.global.auth.annotation.UserId; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import jakarta.validation.Valid; | ||
|
|
||
| @Tag(name = "(Normal) Study Time: 순공 시간", description = "순공 시간 API") | ||
| @RequestMapping("/studytimes") | ||
| public interface StudyTimeApi { | ||
|
|
||
| @Operation(summary = "스터디 타이머를 시작한다.", description = """ | ||
| ## 설명 | ||
| - 스터디 타이머를 시작합니다. | ||
| - 사용자당 동시에 1개의 타이머만 허용됩니다. | ||
|
|
||
| ## 에러 | ||
| - `ALREADY_RUNNING_STUDY_TIMER` (409): 이미 실행 중인 타이머가 있는 경우 | ||
| """) | ||
| @PostMapping("/timers") | ||
| ResponseEntity<Void> start(@UserId Integer userId); | ||
|
|
||
| @Operation(summary = "스터디 타이머를 종료한다.", description = """ | ||
| ## 설명 | ||
| - 실행 중인 타이머를 종료하고 공부 시간을 집계합니다. | ||
| - 시간이 자정을 넘기면 날짜별로 분할 집계합니다. | ||
| - 일간, 월간, 총 누적 시간을 함께 갱신합니다. | ||
|
|
||
| ## 에러 | ||
| - `STUDY_TIMER_TIME_MISMATCH` (400): 클라이언트 누적 시간과 서버 시간 차이가 1분 이상인 경우 | ||
| - `STUDY_TIMER_NOT_RUNNING` (400): 실행 중인 타이머가 없는 경우 | ||
| """) | ||
| @DeleteMapping("/timers") | ||
| ResponseEntity<StudyTimerStopResponse> stop( | ||
| @UserId Integer userId, | ||
| @RequestBody @Valid StudyTimerStopRequest request | ||
| ); | ||
| } |
40 changes: 40 additions & 0 deletions
40
src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimeController.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| 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.StudyTimerStopRequest; | ||
| import gg.agit.konect.domain.studytime.dto.StudyTimerStopResponse; | ||
| import gg.agit.konect.domain.studytime.service.StudyTimerService; | ||
| import gg.agit.konect.global.auth.annotation.UserId; | ||
| import jakarta.validation.Valid; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/studytimes") | ||
| public class StudyTimeController implements StudyTimeApi { | ||
|
|
||
| private final StudyTimerService studyTimerService; | ||
|
|
||
| @PostMapping("/timers") | ||
| public ResponseEntity<Void> start(@UserId Integer userId) { | ||
| studyTimerService.start(userId); | ||
|
|
||
| return ResponseEntity.ok().build(); | ||
| } | ||
|
|
||
| @DeleteMapping("/timers") | ||
| public ResponseEntity<StudyTimerStopResponse> stop( | ||
| @UserId Integer userId, | ||
| @RequestBody @Valid StudyTimerStopRequest request | ||
| ) { | ||
| StudyTimerStopResponse response = studyTimerService.stop(userId, request); | ||
|
|
||
| return ResponseEntity.ok(response); | ||
| } | ||
| } |
35 changes: 35 additions & 0 deletions
35
src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimerStopRequest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| 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; | ||
| 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 | ||
| ) { | ||
|
|
||
| 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; | ||
| } | ||
| } |
29 changes: 29 additions & 0 deletions
29
src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimerStopResponse.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package gg.agit.konect.domain.studytime.dto; | ||
|
|
||
| import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; | ||
|
|
||
| import gg.agit.konect.domain.studytime.model.StudyTimeSummary; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| public record StudyTimerStopResponse( | ||
| @Schema(description = "이번 세션 공부 시간(초)", example = "3600", requiredMode = REQUIRED) | ||
| Long sessionSeconds, | ||
|
|
||
| @Schema(description = "오늘 누적 공부 시간(초)", example = "7200", requiredMode = REQUIRED) | ||
| Long dailySeconds, | ||
|
|
||
| @Schema(description = "이번 달 누적 공부 시간(초)", example = "120000", requiredMode = REQUIRED) | ||
| Long monthlySeconds, | ||
|
|
||
| @Schema(description = "총 누적 공부 시간(초)", example = "360000", requiredMode = REQUIRED) | ||
| Long totalSeconds | ||
| ) { | ||
| public static StudyTimerStopResponse from(StudyTimeSummary summary) { | ||
| return new StudyTimerStopResponse( | ||
| summary.sessionSeconds(), | ||
| summary.dailySeconds(), | ||
| summary.monthlySeconds(), | ||
| summary.totalSeconds() | ||
| ); | ||
| } | ||
| } | ||
70 changes: 70 additions & 0 deletions
70
src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeDaily.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package gg.agit.konect.domain.studytime.model; | ||
|
|
||
| import static jakarta.persistence.FetchType.LAZY; | ||
| import static jakarta.persistence.GenerationType.IDENTITY; | ||
| import static lombok.AccessLevel.PROTECTED; | ||
|
|
||
| import java.time.LocalDate; | ||
|
|
||
| import gg.agit.konect.domain.user.model.User; | ||
| import gg.agit.konect.global.model.BaseEntity; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.ManyToOne; | ||
| import jakarta.persistence.Table; | ||
| import jakarta.persistence.UniqueConstraint; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @Entity | ||
| @Table( | ||
| name = "study_time_daily", | ||
| uniqueConstraints = { | ||
| @UniqueConstraint(name = "uq_study_time_daily_user_date", columnNames = {"user_id", "study_date"}) | ||
| } | ||
| ) | ||
| @NoArgsConstructor(access = PROTECTED) | ||
| public class StudyTimeDaily extends BaseEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = IDENTITY) | ||
| @Column(name = "id", nullable = false, updatable = false, unique = true) | ||
| private Integer id; | ||
|
|
||
| @ManyToOne(fetch = LAZY) | ||
| @JoinColumn(name = "user_id", nullable = false, updatable = false) | ||
| private User user; | ||
|
|
||
| @NotNull | ||
| @Column(name = "study_date", nullable = false) | ||
| private LocalDate studyDate; | ||
|
|
||
| @NotNull | ||
| @Column(name = "total_seconds", nullable = false) | ||
| private Long totalSeconds; | ||
|
|
||
| @Builder | ||
| private StudyTimeDaily(User user, LocalDate studyDate, Long totalSeconds) { | ||
| this.user = user; | ||
| this.studyDate = studyDate; | ||
| this.totalSeconds = totalSeconds; | ||
| } | ||
|
|
||
| public static StudyTimeDaily of(User user, LocalDate studyDate, Long totalSeconds) { | ||
| return StudyTimeDaily.builder() | ||
| .user(user) | ||
| .studyDate(studyDate) | ||
| .totalSeconds(totalSeconds) | ||
| .build(); | ||
| } | ||
|
|
||
| public void addSeconds(long seconds) { | ||
| this.totalSeconds += seconds; | ||
| } | ||
| } |
70 changes: 70 additions & 0 deletions
70
src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeMonthly.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| package gg.agit.konect.domain.studytime.model; | ||
|
|
||
| import static jakarta.persistence.FetchType.LAZY; | ||
| import static jakarta.persistence.GenerationType.IDENTITY; | ||
| import static lombok.AccessLevel.PROTECTED; | ||
|
|
||
| import java.time.LocalDate; | ||
|
|
||
| import gg.agit.konect.domain.user.model.User; | ||
| import gg.agit.konect.global.model.BaseEntity; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.ManyToOne; | ||
| import jakarta.persistence.Table; | ||
| import jakarta.persistence.UniqueConstraint; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @Entity | ||
| @Table( | ||
| name = "study_time_monthly", | ||
| uniqueConstraints = { | ||
| @UniqueConstraint(name = "uq_study_time_monthly_user_month", columnNames = {"user_id", "study_month"}) | ||
| } | ||
| ) | ||
| @NoArgsConstructor(access = PROTECTED) | ||
| public class StudyTimeMonthly extends BaseEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = IDENTITY) | ||
| @Column(name = "id", nullable = false, updatable = false, unique = true) | ||
| private Integer id; | ||
|
|
||
| @ManyToOne(fetch = LAZY) | ||
| @JoinColumn(name = "user_id", nullable = false, updatable = false) | ||
| private User user; | ||
|
|
||
| @NotNull | ||
| @Column(name = "study_month", nullable = false) | ||
| private LocalDate studyMonth; | ||
|
|
||
| @NotNull | ||
| @Column(name = "total_seconds", nullable = false) | ||
| private Long totalSeconds; | ||
|
|
||
| @Builder | ||
| private StudyTimeMonthly(User user, LocalDate studyMonth, Long totalSeconds) { | ||
| this.user = user; | ||
| this.studyMonth = studyMonth; | ||
| this.totalSeconds = totalSeconds; | ||
| } | ||
|
|
||
| public static StudyTimeMonthly of(User user, LocalDate studyMonth, Long totalSeconds) { | ||
| return StudyTimeMonthly.builder() | ||
| .user(user) | ||
| .studyMonth(studyMonth) | ||
| .totalSeconds(totalSeconds) | ||
| .build(); | ||
| } | ||
|
|
||
| public void addSeconds(long seconds) { | ||
| this.totalSeconds += seconds; | ||
| } | ||
| } |
9 changes: 9 additions & 0 deletions
9
src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeSummary.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package gg.agit.konect.domain.studytime.model; | ||
|
|
||
| public record StudyTimeSummary( | ||
| long sessionSeconds, | ||
| long dailySeconds, | ||
| long monthlySeconds, | ||
| long totalSeconds | ||
| ) { | ||
| } |
62 changes: 62 additions & 0 deletions
62
src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeTotal.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| package gg.agit.konect.domain.studytime.model; | ||
|
|
||
| import static jakarta.persistence.FetchType.LAZY; | ||
| import static jakarta.persistence.GenerationType.IDENTITY; | ||
| import static lombok.AccessLevel.PROTECTED; | ||
|
|
||
| import gg.agit.konect.domain.user.model.User; | ||
| import gg.agit.konect.global.model.BaseEntity; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.OneToOne; | ||
| import jakarta.persistence.Table; | ||
| import jakarta.persistence.UniqueConstraint; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Getter | ||
| @Entity | ||
| @Table( | ||
| name = "study_time_total", | ||
| uniqueConstraints = { | ||
| @UniqueConstraint(name = "uq_study_time_total_user", columnNames = {"user_id"}) | ||
| } | ||
| ) | ||
| @NoArgsConstructor(access = PROTECTED) | ||
| public class StudyTimeTotal extends BaseEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = IDENTITY) | ||
| @Column(name = "id", nullable = false, updatable = false, unique = true) | ||
| private Integer id; | ||
|
|
||
| @OneToOne(fetch = LAZY) | ||
| @JoinColumn(name = "user_id", nullable = false, unique = true) | ||
| private User user; | ||
|
|
||
| @NotNull | ||
| @Column(name = "total_seconds", nullable = false) | ||
| private Long totalSeconds; | ||
|
|
||
| @Builder | ||
| private StudyTimeTotal(User user, Long totalSeconds) { | ||
| this.user = user; | ||
| this.totalSeconds = totalSeconds; | ||
| } | ||
|
|
||
| public static StudyTimeTotal of(User user, Long totalSeconds) { | ||
| return StudyTimeTotal.builder() | ||
| .user(user) | ||
| .totalSeconds(totalSeconds) | ||
| .build(); | ||
| } | ||
|
|
||
| public void addSeconds(long seconds) { | ||
| this.totalSeconds += seconds; | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.