From 5eefbb7b29b7c0195fc050e6dcd8672f415032ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 31 Dec 2025 18:14:13 +0900 Subject: [PATCH 01/24] =?UTF-8?q?feat:=20=EC=88=9C=EA=B3=B5=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EB=B0=8F=20=ED=83=80=EC=9D=B4=EB=A8=B8=20DB=20?= =?UTF-8?q?=EC=8A=A4=ED=82=A4=EB=A7=88=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/schema.sql | 52 +++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 6fef261..410eef4 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -255,3 +255,55 @@ CREATE TABLE chat_message FOREIGN KEY (chat_room_id) REFERENCES chat_room (id) ON DELETE CASCADE, FOREIGN KEY (sender_id) REFERENCES users (id) ON DELETE SET NULL ); + +CREATE TABLE study_timer +( + user_id INT NOT NULL, + started_at TIMESTAMP NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + + PRIMARY KEY (user_id), + + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); + +CREATE TABLE study_time_daily +( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + study_date DATE NOT NULL, + total_seconds BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + + UNIQUE (user_id, study_date), + + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); + +CREATE TABLE study_time_monthly +( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + study_month DATE NOT NULL, + total_seconds BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + + UNIQUE (user_id, study_month), + + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); + +CREATE TABLE study_time_total +( + user_id INT NOT NULL, + total_seconds BIGINT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, + + PRIMARY KEY (user_id), + + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); From 8c8adba6b49b00719f4075106840fd78e09a7770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 31 Dec 2025 18:15:20 +0900 Subject: [PATCH 02/24] =?UTF-8?q?feat:=20=EC=88=9C=EA=B3=B5=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EA=B4=80=EB=A0=A8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/study/model/StudyTimeDaily.java | 62 +++++++++++++++++++ .../domain/study/model/StudyTimeMonthly.java | 62 +++++++++++++++++++ .../domain/study/model/StudyTimeTotal.java | 49 +++++++++++++++ .../konect/domain/study/model/StudyTimer.java | 47 ++++++++++++++ 4 files changed, 220 insertions(+) create mode 100644 src/main/java/gg/agit/konect/domain/study/model/StudyTimeDaily.java create mode 100644 src/main/java/gg/agit/konect/domain/study/model/StudyTimeMonthly.java create mode 100644 src/main/java/gg/agit/konect/domain/study/model/StudyTimeTotal.java create mode 100644 src/main/java/gg/agit/konect/domain/study/model/StudyTimer.java diff --git a/src/main/java/gg/agit/konect/domain/study/model/StudyTimeDaily.java b/src/main/java/gg/agit/konect/domain/study/model/StudyTimeDaily.java new file mode 100644 index 0000000..87331f5 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/study/model/StudyTimeDaily.java @@ -0,0 +1,62 @@ +package gg.agit.konect.domain.study.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 void addSeconds(long seconds) { + this.totalSeconds += seconds; + } +} diff --git a/src/main/java/gg/agit/konect/domain/study/model/StudyTimeMonthly.java b/src/main/java/gg/agit/konect/domain/study/model/StudyTimeMonthly.java new file mode 100644 index 0000000..474cd49 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/study/model/StudyTimeMonthly.java @@ -0,0 +1,62 @@ +package gg.agit.konect.domain.study.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 void addSeconds(long seconds) { + this.totalSeconds += seconds; + } +} diff --git a/src/main/java/gg/agit/konect/domain/study/model/StudyTimeTotal.java b/src/main/java/gg/agit/konect/domain/study/model/StudyTimeTotal.java new file mode 100644 index 0000000..c26a348 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/study/model/StudyTimeTotal.java @@ -0,0 +1,49 @@ +package gg.agit.konect.domain.study.model; + +import static jakarta.persistence.FetchType.LAZY; +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.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "study_time_total") +@NoArgsConstructor(access = PROTECTED) +public class StudyTimeTotal extends BaseEntity { + + @Id + @Column(name = "user_id", nullable = false, updatable = false) + private Integer userId; + + @MapsId + @OneToOne(fetch = LAZY) + @JoinColumn(name = "user_id", nullable = false, updatable = false) + private User user; + + @NotNull + @Column(name = "total_seconds", nullable = false) + private Long totalSeconds; + + @Builder + private StudyTimeTotal(User user, Long totalSeconds) { + this.user = user; + this.userId = user.getId(); + this.totalSeconds = totalSeconds; + } + + public void addSeconds(long seconds) { + this.totalSeconds += seconds; + } +} diff --git a/src/main/java/gg/agit/konect/domain/study/model/StudyTimer.java b/src/main/java/gg/agit/konect/domain/study/model/StudyTimer.java new file mode 100644 index 0000000..3eeebf5 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/study/model/StudyTimer.java @@ -0,0 +1,47 @@ +package gg.agit.konect.domain.study.model; + +import static jakarta.persistence.FetchType.LAZY; +import static lombok.AccessLevel.PROTECTED; + +import java.time.LocalDateTime; + +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.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "study_timer") +@NoArgsConstructor(access = PROTECTED) +public class StudyTimer extends BaseEntity { + + @Id + @Column(name = "user_id", nullable = false, updatable = false) + private Integer userId; + + @MapsId + @OneToOne(fetch = LAZY) + @JoinColumn(name = "user_id", nullable = false, updatable = false) + private User user; + + @NotNull + @Column(name = "started_at", nullable = false) + private LocalDateTime startedAt; + + @Builder + private StudyTimer(User user, LocalDateTime startedAt) { + this.user = user; + this.userId = user.getId(); + this.startedAt = startedAt; + } +} From 490c410b53efafc65ab2b192a66e0d7d7841ea5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 31 Dec 2025 19:54:27 +0900 Subject: [PATCH 03/24] =?UTF-8?q?refactor:=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=20=ED=8C=A8=ED=82=A4=EC=A7=80=20=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/{study => studytime}/model/StudyTimeDaily.java | 2 +- .../domain/{study => studytime}/model/StudyTimeMonthly.java | 2 +- .../domain/{study => studytime}/model/StudyTimeTotal.java | 2 +- .../konect/domain/{study => studytime}/model/StudyTimer.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename src/main/java/gg/agit/konect/domain/{study => studytime}/model/StudyTimeDaily.java (97%) rename src/main/java/gg/agit/konect/domain/{study => studytime}/model/StudyTimeMonthly.java (97%) rename src/main/java/gg/agit/konect/domain/{study => studytime}/model/StudyTimeTotal.java (96%) rename src/main/java/gg/agit/konect/domain/{study => studytime}/model/StudyTimer.java (96%) diff --git a/src/main/java/gg/agit/konect/domain/study/model/StudyTimeDaily.java b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeDaily.java similarity index 97% rename from src/main/java/gg/agit/konect/domain/study/model/StudyTimeDaily.java rename to src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeDaily.java index 87331f5..d49a3ed 100644 --- a/src/main/java/gg/agit/konect/domain/study/model/StudyTimeDaily.java +++ b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeDaily.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.study.model; +package gg.agit.konect.domain.studytime.model; import static jakarta.persistence.FetchType.LAZY; import static jakarta.persistence.GenerationType.IDENTITY; diff --git a/src/main/java/gg/agit/konect/domain/study/model/StudyTimeMonthly.java b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeMonthly.java similarity index 97% rename from src/main/java/gg/agit/konect/domain/study/model/StudyTimeMonthly.java rename to src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeMonthly.java index 474cd49..9c04421 100644 --- a/src/main/java/gg/agit/konect/domain/study/model/StudyTimeMonthly.java +++ b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeMonthly.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.study.model; +package gg.agit.konect.domain.studytime.model; import static jakarta.persistence.FetchType.LAZY; import static jakarta.persistence.GenerationType.IDENTITY; diff --git a/src/main/java/gg/agit/konect/domain/study/model/StudyTimeTotal.java b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeTotal.java similarity index 96% rename from src/main/java/gg/agit/konect/domain/study/model/StudyTimeTotal.java rename to src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeTotal.java index c26a348..f4da2bb 100644 --- a/src/main/java/gg/agit/konect/domain/study/model/StudyTimeTotal.java +++ b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeTotal.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.study.model; +package gg.agit.konect.domain.studytime.model; import static jakarta.persistence.FetchType.LAZY; import static lombok.AccessLevel.PROTECTED; diff --git a/src/main/java/gg/agit/konect/domain/study/model/StudyTimer.java b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimer.java similarity index 96% rename from src/main/java/gg/agit/konect/domain/study/model/StudyTimer.java rename to src/main/java/gg/agit/konect/domain/studytime/model/StudyTimer.java index 3eeebf5..6a7f2e3 100644 --- a/src/main/java/gg/agit/konect/domain/study/model/StudyTimer.java +++ b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimer.java @@ -1,4 +1,4 @@ -package gg.agit.konect.domain.study.model; +package gg.agit.konect.domain.studytime.model; import static jakarta.persistence.FetchType.LAZY; import static lombok.AccessLevel.PROTECTED; From 4d71ef1a254b692833fff3ee2fc1ff79718c6d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 31 Dec 2025 20:02:12 +0900 Subject: [PATCH 04/24] =?UTF-8?q?feat:=20=EC=88=9C=EA=B3=B5=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=ED=83=80=EC=9D=B4=EB=A8=B8=20API=20=EB=AA=85?= =?UTF-8?q?=EC=84=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studytime/controller/StudyTimerApi.java | 38 +++++++++++++++++++ .../konect/global/code/ApiResponseCode.java | 2 + 2 files changed, 40 insertions(+) create mode 100644 src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerApi.java diff --git a/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerApi.java b/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerApi.java new file mode 100644 index 0000000..286e78f --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerApi.java @@ -0,0 +1,38 @@ +package gg.agit.konect.domain.studytime.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +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; + +@Tag(name = "(Normal) Study Timer: 공부 타이머", description = "공부 타이머 API") +@RequestMapping("/study-timers") +public interface StudyTimerApi { + + @Operation(summary = "스터디 타이머를 시작한다.", description = """ + ## 설명 + - 스터디 타이머를 시작합니다. + - 사용자당 동시에 1개의 타이머만 허용됩니다. + + ## 에러 + - `ALREADY_RUNNING_STUDY_TIMER` (409): 이미 실행 중인 타이머가 있는 경우 + """) + @PostMapping("/start") + ResponseEntity start(@UserId Integer userId); + + @Operation(summary = "스터디 타이머를 종료한다.", description = """ + ## 설명 + - 실행 중인 타이머를 종료하고 공부 시간을 집계합니다. + - 시간이 자정을 넘기면 날짜별로 분할 집계합니다. + - 일간, 월간, 총 누적 시간을 함께 갱신합니다. + + ## 에러 + - `STUDY_TIMER_NOT_RUNNING` (400): 실행 중인 타이머가 없는 경우 + """) + @PostMapping("/stop") + ResponseEntity stop(@UserId Integer userId); +} diff --git a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java index 1ec0918..b4fa481 100644 --- a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java +++ b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java @@ -22,6 +22,7 @@ public enum ApiResponseCode { CANNOT_CREATE_CHAT_ROOM_WITH_SELF(HttpStatus.BAD_REQUEST, "자기 자신과는 채팅방을 만들 수 없습니다."), REQUIRED_CLUB_APPLY_ANSWER_MISSING(HttpStatus.BAD_REQUEST, "필수 가입 답변이 누락되었습니다."), CANNOT_DELETE_CLUB_PRESIDENT(HttpStatus.BAD_REQUEST, "동아리 회장인 경우 회장을 양도하고 탈퇴해야 합니다."), + STUDY_TIMER_NOT_RUNNING(HttpStatus.BAD_REQUEST, "실행 중인 스터디 타이머가 없습니다."), // 401 Unauthorized INVALID_SESSION(HttpStatus.UNAUTHORIZED, "올바르지 않은 인증 정보 입니다."), @@ -56,6 +57,7 @@ public enum ApiResponseCode { DUPLICATE_PHONE_NUMBER(HttpStatus.CONFLICT, "이미 사용 중인 전화번호입니다."), ALREADY_APPLIED_CLUB(HttpStatus.CONFLICT, "이미 동아리에 가입 신청을 완료했습니다."), DUPLICATE_CLUB_APPLY_QUESTION(HttpStatus.CONFLICT, "중복된 가입 문항이 포함되어 있습니다."), + ALREADY_RUNNING_STUDY_TIMER(HttpStatus.CONFLICT, "이미 실행 중인 스터디 타이머가 있습니다."), // 500 Internal Server Error (서버 오류) CLIENT_ABORTED(HttpStatus.INTERNAL_SERVER_ERROR, "클라이언트에 의해 연결이 중단되었습니다."), From b44c0a40ad33b442b61e341ea16b5f66355db727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 31 Dec 2025 20:06:13 +0900 Subject: [PATCH 05/24] =?UTF-8?q?feat:=20=EC=88=9C=EA=B3=B5=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=ED=83=80=EC=9D=B4=EB=A8=B8=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/StudyTimerController.java | 39 +++++++++++++++++++ .../studytime/dto/StudyTimerStopResponse.java | 28 +++++++++++++ .../studytime/model/StudyTimeAggregate.java | 9 +++++ 3 files changed, 76 insertions(+) create mode 100644 src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerController.java create mode 100644 src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimerStopResponse.java create mode 100644 src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeAggregate.java diff --git a/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerController.java b/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerController.java new file mode 100644 index 0000000..863608d --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerController.java @@ -0,0 +1,39 @@ +package gg.agit.konect.domain.studytime.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import gg.agit.konect.domain.studytime.dto.StudyTimerStopResponse; +import gg.agit.konect.domain.studytime.model.StudyTimeAggregate; +import gg.agit.konect.domain.studytime.service.StudyTimerService; +import gg.agit.konect.global.auth.annotation.UserId; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/study-timers") +public class StudyTimerController implements StudyTimerApi { + + private final StudyTimerService studyTimerService; + + @PostMapping("/start") + public ResponseEntity start(@UserId Integer userId) { + studyTimerService.start(userId); + + return ResponseEntity.ok().build(); + } + + @PostMapping("/stop") + public ResponseEntity stop(@UserId Integer userId) { + StudyTimeAggregate aggregate = studyTimerService.stop(userId); + + return ResponseEntity.ok(StudyTimerStopResponse.of( + aggregate.sessionSeconds(), + aggregate.dailySeconds(), + aggregate.monthlySeconds(), + aggregate.totalSeconds() + )); + } +} diff --git a/src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimerStopResponse.java b/src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimerStopResponse.java new file mode 100644 index 0000000..4eb2bb2 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimerStopResponse.java @@ -0,0 +1,28 @@ +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 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 of( + long sessionSeconds, + long dailySeconds, + long monthlySeconds, + long totalSeconds + ) { + return new StudyTimerStopResponse(sessionSeconds, dailySeconds, monthlySeconds, totalSeconds); + } +} 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 0000000..58ab4e3 --- /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 +) { +} From d8f90390bebf752409714a081e7540e5005e0303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 31 Dec 2025 20:21:35 +0900 Subject: [PATCH 06/24] =?UTF-8?q?feat:=20=EC=88=9C=EA=B3=B5=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=ED=83=80=EC=9D=B4=EB=A8=B8=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/StudyTimeDailyRepository.java | 15 ++ .../StudyTimeMonthlyRepository.java | 15 ++ .../repository/StudyTimeTotalRepository.java | 14 ++ .../repository/StudyTimerRepository.java | 26 ++++ .../studytime/service/StudyTimerService.java | 146 ++++++++++++++++++ 5 files changed, 216 insertions(+) create mode 100644 src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimeDailyRepository.java create mode 100644 src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimeMonthlyRepository.java create mode 100644 src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimeTotalRepository.java create mode 100644 src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimerRepository.java create mode 100644 src/main/java/gg/agit/konect/domain/studytime/service/StudyTimerService.java diff --git a/src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimeDailyRepository.java b/src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimeDailyRepository.java new file mode 100644 index 0000000..05d6399 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimeDailyRepository.java @@ -0,0 +1,15 @@ +package gg.agit.konect.domain.studytime.repository; + +import java.time.LocalDate; +import java.util.Optional; + +import org.springframework.data.repository.Repository; + +import gg.agit.konect.domain.studytime.model.StudyTimeDaily; + +public interface StudyTimeDailyRepository extends Repository { + + Optional findByUserIdAndStudyDate(Integer userId, LocalDate studyDate); + + StudyTimeDaily save(StudyTimeDaily studyTimeDaily); +} diff --git a/src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimeMonthlyRepository.java b/src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimeMonthlyRepository.java new file mode 100644 index 0000000..34e007a --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimeMonthlyRepository.java @@ -0,0 +1,15 @@ +package gg.agit.konect.domain.studytime.repository; + +import java.time.LocalDate; +import java.util.Optional; + +import org.springframework.data.repository.Repository; + +import gg.agit.konect.domain.studytime.model.StudyTimeMonthly; + +public interface StudyTimeMonthlyRepository extends Repository { + + Optional findByUserIdAndStudyMonth(Integer userId, LocalDate studyMonth); + + StudyTimeMonthly save(StudyTimeMonthly studyTimeMonthly); +} diff --git a/src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimeTotalRepository.java b/src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimeTotalRepository.java new file mode 100644 index 0000000..2ca6aae --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimeTotalRepository.java @@ -0,0 +1,14 @@ +package gg.agit.konect.domain.studytime.repository; + +import java.util.Optional; + +import org.springframework.data.repository.Repository; + +import gg.agit.konect.domain.studytime.model.StudyTimeTotal; + +public interface StudyTimeTotalRepository extends Repository { + + Optional findByUserId(Integer userId); + + StudyTimeTotal save(StudyTimeTotal studyTimeTotal); +} diff --git a/src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimerRepository.java b/src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimerRepository.java new file mode 100644 index 0000000..8a3b705 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/studytime/repository/StudyTimerRepository.java @@ -0,0 +1,26 @@ +package gg.agit.konect.domain.studytime.repository; + +import static gg.agit.konect.global.code.ApiResponseCode.STUDY_TIMER_NOT_RUNNING; + +import java.util.Optional; + +import org.springframework.data.repository.Repository; + +import gg.agit.konect.domain.studytime.model.StudyTimer; +import gg.agit.konect.global.exception.CustomException; + +public interface StudyTimerRepository extends Repository { + + Optional findByUserId(Integer userId); + + default StudyTimer getByUserId(Integer userId) { + return findByUserId(userId) + .orElseThrow(() -> CustomException.of(STUDY_TIMER_NOT_RUNNING)); + } + + Boolean existsByUserId(Integer userId); + + StudyTimer save(StudyTimer studyTimer); + + void delete(StudyTimer studyTimer); +} 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 new file mode 100644 index 0000000..8b2fe9b --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/studytime/service/StudyTimerService.java @@ -0,0 +1,146 @@ +package gg.agit.konect.domain.studytime.service; + +import static gg.agit.konect.global.code.ApiResponseCode.ALREADY_RUNNING_STUDY_TIMER; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.agit.konect.domain.studytime.model.StudyTimeAggregate; +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.model.StudyTimer; +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 gg.agit.konect.domain.studytime.repository.StudyTimerRepository; +import gg.agit.konect.domain.user.model.User; +import gg.agit.konect.domain.user.repository.UserRepository; +import gg.agit.konect.global.exception.CustomException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class StudyTimerService { + + private final StudyTimerRepository studyTimerRepository; + private final StudyTimeDailyRepository studyTimeDailyRepository; + private final StudyTimeMonthlyRepository studyTimeMonthlyRepository; + private final StudyTimeTotalRepository studyTimeTotalRepository; + private final UserRepository userRepository; + + @Transactional + public void start(Integer userId) { + if (studyTimerRepository.existsByUserId(userId)) { + throw CustomException.of(ALREADY_RUNNING_STUDY_TIMER); + } + + User user = userRepository.getById(userId); + LocalDateTime startedAt = LocalDateTime.now(); + + studyTimerRepository.save(StudyTimer.builder() + .user(user) + .startedAt(startedAt) + .build()); + } + + @Transactional + public StudyTimeAggregate stop(Integer userId) { + StudyTimer studyTimer = studyTimerRepository.getByUserId(userId); + + LocalDateTime endedAt = LocalDateTime.now(); + LocalDateTime startedAt = studyTimer.getStartedAt(); + StudyTimeAggregate aggregate = applyStudyTime(studyTimer.getUser(), startedAt, endedAt); + + studyTimerRepository.delete(studyTimer); + + return aggregate; + } + + private StudyTimeAggregate applyStudyTime(User user, LocalDateTime startedAt, LocalDateTime endedAt) { + LocalDateTime cursor = startedAt; + long sessionSeconds = 0L; + + while (cursor.toLocalDate().isBefore(endedAt.toLocalDate())) { + LocalDateTime nextDayStart = cursor.toLocalDate().plusDays(1).atStartOfDay(); + long seconds = Duration.between(cursor, nextDayStart).getSeconds(); + sessionSeconds += addSegment(user, cursor.toLocalDate(), seconds); + cursor = nextDayStart; + } + + if (cursor.isBefore(endedAt)) { + long seconds = Duration.between(cursor, endedAt).getSeconds(); + sessionSeconds += addSegment(user, cursor.toLocalDate(), seconds); + } + + if (sessionSeconds > 0) { + addTotalSeconds(user, sessionSeconds); + } + + LocalDate endDate = endedAt.toLocalDate(); + + return buildAggregate(user.getId(), endDate, sessionSeconds); + } + + private long addSegment(User user, LocalDate date, long seconds) { + if (seconds <= 0) { + return 0L; + } + + StudyTimeDaily daily = studyTimeDailyRepository.findByUserIdAndStudyDate(user.getId(), date) + .orElseGet(() -> StudyTimeDaily.builder() + .user(user) + .studyDate(date) + .totalSeconds(0L) + .build()); + + daily.addSeconds(seconds); + studyTimeDailyRepository.save(daily); + + LocalDate month = date.withDayOfMonth(1); + StudyTimeMonthly monthly = studyTimeMonthlyRepository.findByUserIdAndStudyMonth(user.getId(), month) + .orElseGet(() -> StudyTimeMonthly.builder() + .user(user) + .studyMonth(month) + .totalSeconds(0L) + .build()); + + monthly.addSeconds(seconds); + studyTimeMonthlyRepository.save(monthly); + + return seconds; + } + + private void addTotalSeconds(User user, long seconds) { + StudyTimeTotal total = studyTimeTotalRepository.findByUserId(user.getId()) + .orElseGet(() -> StudyTimeTotal.builder() + .user(user) + .totalSeconds(0L) + .build()); + total.addSeconds(seconds); + studyTimeTotalRepository.save(total); + } + + private StudyTimeAggregate buildAggregate(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); + + return new StudyTimeAggregate(dailySeconds, monthlySeconds, totalSeconds, sessionSeconds); + } +} From 821479e0cb598104097b130e4acf6a9d06f42940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 31 Dec 2025 20:42:15 +0900 Subject: [PATCH 07/24] =?UTF-8?q?fix:=20=EC=88=9C=EA=B3=B5=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=ED=83=80=EC=9D=B4=EB=A8=B8=20PK=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studytime/model/StudyTimeTotal.java | 20 ++++++++++++------- .../domain/studytime/model/StudyTimer.java | 12 +++++------ src/main/resources/schema.sql | 6 ++++-- 3 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeTotal.java b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeTotal.java index f4da2bb..824c2ff 100644 --- a/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeTotal.java +++ b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeTotal.java @@ -1,17 +1,19 @@ 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.MapsId; import jakarta.persistence.OneToOne; import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Getter; @@ -19,17 +21,22 @@ @Getter @Entity -@Table(name = "study_time_total") +@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 - @Column(name = "user_id", nullable = false, updatable = false) - private Integer userId; + @GeneratedValue(strategy = IDENTITY) + @Column(name = "id", nullable = false, updatable = false, unique = true) + private Integer id; - @MapsId @OneToOne(fetch = LAZY) - @JoinColumn(name = "user_id", nullable = false, updatable = false) + @JoinColumn(name = "user_id", nullable = false, unique = true) private User user; @NotNull @@ -39,7 +46,6 @@ public class StudyTimeTotal extends BaseEntity { @Builder private StudyTimeTotal(User user, Long totalSeconds) { this.user = user; - this.userId = user.getId(); this.totalSeconds = totalSeconds; } diff --git a/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimer.java b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimer.java index 6a7f2e3..f49f365 100644 --- a/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimer.java +++ b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimer.java @@ -1,6 +1,7 @@ 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.LocalDateTime; @@ -9,9 +10,9 @@ 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.MapsId; import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; @@ -26,12 +27,12 @@ public class StudyTimer extends BaseEntity { @Id - @Column(name = "user_id", nullable = false, updatable = false) - private Integer userId; + @GeneratedValue(strategy = IDENTITY) + @Column(name = "id", nullable = false, updatable = false, unique = true) + private Integer id; - @MapsId @OneToOne(fetch = LAZY) - @JoinColumn(name = "user_id", nullable = false, updatable = false) + @JoinColumn(name = "user_id", nullable = false, unique = true) private User user; @NotNull @@ -41,7 +42,6 @@ public class StudyTimer extends BaseEntity { @Builder private StudyTimer(User user, LocalDateTime startedAt) { this.user = user; - this.userId = user.getId(); this.startedAt = startedAt; } } diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index 410eef4..7b2ae73 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -258,12 +258,13 @@ CREATE TABLE chat_message CREATE TABLE study_timer ( + id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, started_at TIMESTAMP NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY (user_id), + UNIQUE (user_id), FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ); @@ -298,12 +299,13 @@ CREATE TABLE study_time_monthly CREATE TABLE study_time_total ( + id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, total_seconds BIGINT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY (user_id), + UNIQUE (user_id), FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE ); From 3c2c381e036a1eac505c40224a9b29ca9c8faae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 31 Dec 2025 20:42:36 +0900 Subject: [PATCH 08/24] =?UTF-8?q?fix:=20=EC=88=9C=EA=B3=B5=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=A7=91=EA=B3=84=20=EC=9D=91=EB=8B=B5=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agit/konect/domain/studytime/service/StudyTimerService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8b2fe9b..71348c2 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 @@ -141,6 +141,6 @@ private StudyTimeAggregate buildAggregate(Integer userId, LocalDate endDate, lon .map(StudyTimeTotal::getTotalSeconds) .orElse(0L); - return new StudyTimeAggregate(dailySeconds, monthlySeconds, totalSeconds, sessionSeconds); + return new StudyTimeAggregate(sessionSeconds, dailySeconds, monthlySeconds, totalSeconds); } } From 31d2f78099170822defd4b772eaefc7a52afbdfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 31 Dec 2025 21:03:51 +0900 Subject: [PATCH 09/24] =?UTF-8?q?refactor:=20=EC=8A=A4=ED=84=B0=EB=94=94?= =?UTF-8?q?=20=ED=83=80=EC=9D=B4=EB=A8=B8=20=EC=A2=85=EB=A3=8C=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EB=A7=A4=ED=95=91=EC=9D=84=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studytime/controller/StudyTimerController.java | 12 +++--------- .../domain/studytime/service/StudyTimerService.java | 10 ++++++++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerController.java b/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerController.java index 863608d..6ba1fb2 100644 --- a/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerController.java +++ b/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerController.java @@ -6,7 +6,6 @@ import org.springframework.web.bind.annotation.RestController; import gg.agit.konect.domain.studytime.dto.StudyTimerStopResponse; -import gg.agit.konect.domain.studytime.model.StudyTimeAggregate; import gg.agit.konect.domain.studytime.service.StudyTimerService; import gg.agit.konect.global.auth.annotation.UserId; import lombok.RequiredArgsConstructor; @@ -27,13 +26,8 @@ public ResponseEntity start(@UserId Integer userId) { @PostMapping("/stop") public ResponseEntity stop(@UserId Integer userId) { - StudyTimeAggregate aggregate = studyTimerService.stop(userId); - - return ResponseEntity.ok(StudyTimerStopResponse.of( - aggregate.sessionSeconds(), - aggregate.dailySeconds(), - aggregate.monthlySeconds(), - aggregate.totalSeconds() - )); + StudyTimerStopResponse response = studyTimerService.stop(userId); + + return ResponseEntity.ok(response); } } 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 71348c2..474b276 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 @@ -9,6 +9,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import gg.agit.konect.domain.studytime.dto.StudyTimerStopResponse; import gg.agit.konect.domain.studytime.model.StudyTimeAggregate; import gg.agit.konect.domain.studytime.model.StudyTimeDaily; import gg.agit.konect.domain.studytime.model.StudyTimeMonthly; @@ -50,7 +51,7 @@ public void start(Integer userId) { } @Transactional - public StudyTimeAggregate stop(Integer userId) { + public StudyTimerStopResponse stop(Integer userId) { StudyTimer studyTimer = studyTimerRepository.getByUserId(userId); LocalDateTime endedAt = LocalDateTime.now(); @@ -59,7 +60,12 @@ public StudyTimeAggregate stop(Integer userId) { studyTimerRepository.delete(studyTimer); - return aggregate; + return StudyTimerStopResponse.of( + aggregate.sessionSeconds(), + aggregate.dailySeconds(), + aggregate.monthlySeconds(), + aggregate.totalSeconds() + ); } private StudyTimeAggregate applyStudyTime(User user, LocalDateTime startedAt, LocalDateTime endedAt) { From 8fa352e6a98d99ee851f6bdee882229f832044b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 31 Dec 2025 21:51:24 +0900 Subject: [PATCH 10/24] =?UTF-8?q?fix:=20=EB=8F=99=EC=8B=9C=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studytime/service/StudyTimerService.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) 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 474b276..235ebe5 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 @@ -6,6 +6,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,6 +23,7 @@ import gg.agit.konect.domain.user.model.User; import gg.agit.konect.domain.user.repository.UserRepository; import gg.agit.konect.global.exception.CustomException; +import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; @Service @@ -34,6 +36,7 @@ public class StudyTimerService { private final StudyTimeMonthlyRepository studyTimeMonthlyRepository; private final StudyTimeTotalRepository studyTimeTotalRepository; private final UserRepository userRepository; + private final EntityManager entityManager; @Transactional public void start(Integer userId) { @@ -44,10 +47,15 @@ public void start(Integer userId) { User user = userRepository.getById(userId); LocalDateTime startedAt = LocalDateTime.now(); - studyTimerRepository.save(StudyTimer.builder() - .user(user) - .startedAt(startedAt) - .build()); + try { + studyTimerRepository.save(StudyTimer.builder() + .user(user) + .startedAt(startedAt) + .build()); + entityManager.flush(); + } catch (DataIntegrityViolationException e) { + throw CustomException.of(ALREADY_RUNNING_STUDY_TIMER); + } } @Transactional From a349977aff1caa097e33a5ecb605be27eb270b9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 31 Dec 2025 22:20:39 +0900 Subject: [PATCH 11/24] =?UTF-8?q?fix:=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EB=AA=85,=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/{StudyTimerApi.java => StudyTimeApi.java} | 6 +++--- .../{StudyTimerController.java => StudyTimeController.java} | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) rename src/main/java/gg/agit/konect/domain/studytime/controller/{StudyTimerApi.java => StudyTimeApi.java} (90%) rename src/main/java/gg/agit/konect/domain/studytime/controller/{StudyTimerController.java => StudyTimeController.java} (86%) diff --git a/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerApi.java b/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimeApi.java similarity index 90% rename from src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerApi.java rename to src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimeApi.java index 286e78f..9a032b3 100644 --- a/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerApi.java +++ b/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimeApi.java @@ -9,9 +9,9 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -@Tag(name = "(Normal) Study Timer: 공부 타이머", description = "공부 타이머 API") -@RequestMapping("/study-timers") -public interface StudyTimerApi { +@Tag(name = "(Normal) Study Time: 순공 시간", description = "순공 시간 API") +@RequestMapping("/study-times") +public interface StudyTimeApi { @Operation(summary = "스터디 타이머를 시작한다.", description = """ ## 설명 diff --git a/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerController.java b/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimeController.java similarity index 86% rename from src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerController.java rename to src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimeController.java index 6ba1fb2..8c33327 100644 --- a/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimerController.java +++ b/src/main/java/gg/agit/konect/domain/studytime/controller/StudyTimeController.java @@ -2,7 +2,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import gg.agit.konect.domain.studytime.dto.StudyTimerStopResponse; @@ -12,8 +11,7 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/study-timers") -public class StudyTimerController implements StudyTimerApi { +public class StudyTimeController implements StudyTimeApi { private final StudyTimerService studyTimerService; From f3766f9c2a5b461ecfa66354ef79d9efdad57a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 31 Dec 2025 23:40:49 +0900 Subject: [PATCH 12/24] =?UTF-8?q?fix:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EC=A2=85=EB=A3=8C=20=EC=8B=9C=20=EC=9C=A0=ED=9A=A8=EC=84=B1=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EC=9D=84=20=EC=9C=84=ED=95=B4=20=EB=88=84?= =?UTF-8?q?=EC=A0=81=20=EC=8B=9C=EA=B0=84=EC=9D=84=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studytime/controller/StudyTimeApi.java | 9 +++++- .../controller/StudyTimeController.java | 12 ++++++-- .../studytime/service/StudyTimerService.java | 29 +++++++++++++++++-- .../konect/global/code/ApiResponseCode.java | 1 + 4 files changed, 46 insertions(+), 5 deletions(-) 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 9a032b3..83161bb 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,12 +2,15 @@ import org.springframework.http.ResponseEntity; 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("/study-times") @@ -31,8 +34,12 @@ public interface StudyTimeApi { - 일간, 월간, 총 누적 시간을 함께 갱신합니다. ## 에러 + - `STUDY_TIMER_TIME_MISMATCH` (400): 클라이언트 누적 시간과 서버 시간 차이가 1분 이상인 경우 - `STUDY_TIMER_NOT_RUNNING` (400): 실행 중인 타이머가 없는 경우 """) @PostMapping("/stop") - ResponseEntity stop(@UserId Integer userId); + ResponseEntity stop( + @UserId Integer userId, + @RequestBody @Valid StudyTimerStopRequest request + ); } 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 8c33327..a965662 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 @@ -2,15 +2,20 @@ import org.springframework.http.ResponseEntity; 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("/study-times") public class StudyTimeController implements StudyTimeApi { private final StudyTimerService studyTimerService; @@ -23,8 +28,11 @@ public ResponseEntity start(@UserId Integer userId) { } @PostMapping("/stop") - public ResponseEntity stop(@UserId Integer userId) { - StudyTimerStopResponse response = studyTimerService.stop(userId); + public ResponseEntity stop( + @UserId Integer userId, + @RequestBody @Valid StudyTimerStopRequest request + ) { + StudyTimerStopResponse response = studyTimerService.stop(userId, request); return ResponseEntity.ok(response); } 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 235ebe5..f72d735 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 @@ -1,6 +1,7 @@ package gg.agit.konect.domain.studytime.service; import static gg.agit.konect.global.code.ApiResponseCode.ALREADY_RUNNING_STUDY_TIMER; +import static gg.agit.konect.global.code.ApiResponseCode.STUDY_TIMER_TIME_MISMATCH; import java.time.Duration; import java.time.LocalDate; @@ -10,6 +11,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import gg.agit.konect.domain.studytime.dto.StudyTimerStopRequest; import gg.agit.konect.domain.studytime.dto.StudyTimerStopResponse; import gg.agit.konect.domain.studytime.model.StudyTimeAggregate; import gg.agit.konect.domain.studytime.model.StudyTimeDaily; @@ -31,6 +33,8 @@ @Transactional(readOnly = true) public class StudyTimerService { + private static final long TIMER_MISMATCH_THRESHOLD_SECONDS = 60L; + private final StudyTimerRepository studyTimerRepository; private final StudyTimeDailyRepository studyTimeDailyRepository; private final StudyTimeMonthlyRepository studyTimeMonthlyRepository; @@ -58,12 +62,20 @@ public void start(Integer userId) { } } - @Transactional - public StudyTimerStopResponse stop(Integer userId) { + @Transactional(noRollbackFor = CustomException.class) + public StudyTimerStopResponse stop(Integer userId, StudyTimerStopRequest request) { StudyTimer studyTimer = studyTimerRepository.getByUserId(userId); LocalDateTime endedAt = LocalDateTime.now(); LocalDateTime startedAt = studyTimer.getStartedAt(); + long serverSeconds = Duration.between(startedAt, endedAt).getSeconds(); + long clientSeconds = parseElapsedSeconds(request.elapsedTime()); + + if (isMismatch(serverSeconds, clientSeconds)) { + studyTimerRepository.delete(studyTimer); + throw CustomException.of(STUDY_TIMER_TIME_MISMATCH); + } + StudyTimeAggregate aggregate = applyStudyTime(studyTimer.getUser(), startedAt, endedAt); studyTimerRepository.delete(studyTimer); @@ -157,4 +169,17 @@ private StudyTimeAggregate buildAggregate(Integer userId, LocalDate endDate, lon return new StudyTimeAggregate(sessionSeconds, dailySeconds, monthlySeconds, totalSeconds); } + + private boolean isMismatch(long serverSeconds, long clientSeconds) { + return Math.abs(serverSeconds - clientSeconds) >= TIMER_MISMATCH_THRESHOLD_SECONDS; + } + + private long parseElapsedSeconds(String elapsedTime) { + String[] parts = elapsedTime.split(":"); + long hours = Long.parseLong(parts[0]); + long minutes = Long.parseLong(parts[1]); + long seconds = Long.parseLong(parts[2]); + + return hours * 3600 + minutes * 60 + seconds; + } } diff --git a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java index b4fa481..74f4b09 100644 --- a/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java +++ b/src/main/java/gg/agit/konect/global/code/ApiResponseCode.java @@ -23,6 +23,7 @@ public enum ApiResponseCode { REQUIRED_CLUB_APPLY_ANSWER_MISSING(HttpStatus.BAD_REQUEST, "필수 가입 답변이 누락되었습니다."), CANNOT_DELETE_CLUB_PRESIDENT(HttpStatus.BAD_REQUEST, "동아리 회장인 경우 회장을 양도하고 탈퇴해야 합니다."), STUDY_TIMER_NOT_RUNNING(HttpStatus.BAD_REQUEST, "실행 중인 스터디 타이머가 없습니다."), + STUDY_TIMER_TIME_MISMATCH(HttpStatus.BAD_REQUEST, "스터디 타이머 시간이 유효하지 않습니다."), // 401 Unauthorized INVALID_SESSION(HttpStatus.UNAUTHORIZED, "올바르지 않은 인증 정보 입니다."), From 24f17a1ebe9f1e585209b59cc83cc40a76ca8e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 31 Dec 2025 23:41:38 +0900 Subject: [PATCH 13/24] =?UTF-8?q?feat:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EC=A2=85=EB=A3=8C=20=EB=88=84=EC=A0=81=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?DTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studytime/dto/StudyTimerStopRequest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimerStopRequest.java 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 new file mode 100644 index 0000000..44f177c --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimerStopRequest.java @@ -0,0 +1,15 @@ +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.NotEmpty; +import jakarta.validation.constraints.Pattern; + +public record StudyTimerStopRequest( + @NotEmpty(message = "타이머 누적 시간은 필수 입력입니다.") + @Pattern(regexp = "^[0-9]{2}:[0-5][0-9]:[0-5][0-9]$", message = "타이머 누적 시간 형식이 올바르지 않습니다.") + @Schema(description = "타이머 누적 시간(HH:mm:ss)", example = "01:30:15", requiredMode = REQUIRED) + String elapsedTime +) { +} From f7b5661ee14c3475f3490b59e82ec094c5de74ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Wed, 31 Dec 2025 23:45:44 +0900 Subject: [PATCH 14/24] =?UTF-8?q?fix:=20=EB=A7=A4=EC=A7=81=20=EB=84=98?= =?UTF-8?q?=EB=B2=84=20=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/domain/studytime/service/StudyTimerService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 f72d735..81fd94c 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 @@ -34,6 +34,8 @@ public class StudyTimerService { private static final long TIMER_MISMATCH_THRESHOLD_SECONDS = 60L; + private static final int SECONDS_PER_MINUTE = 60; + private static final int SECONDS_PER_HOUR = 3600; private final StudyTimerRepository studyTimerRepository; private final StudyTimeDailyRepository studyTimeDailyRepository; @@ -180,6 +182,6 @@ private long parseElapsedSeconds(String elapsedTime) { long minutes = Long.parseLong(parts[1]); long seconds = Long.parseLong(parts[2]); - return hours * 3600 + minutes * 60 + seconds; + return hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE + seconds; } } From 801776db538e9b92ce2fd85b0dff9d9a027b0988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 1 Jan 2026 16:43:13 +0900 Subject: [PATCH 15/24] =?UTF-8?q?fix:=20=EC=88=9C=EA=B3=B5=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=ED=83=80=EC=9D=B4=EB=A8=B8=20=EC=97=94=EB=93=9C?= =?UTF-8?q?=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/domain/studytime/controller/StudyTimeApi.java | 7 ++++--- .../domain/studytime/controller/StudyTimeController.java | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) 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 83161bb..019e029 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 @@ -1,6 +1,7 @@ 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; @@ -13,7 +14,7 @@ import jakarta.validation.Valid; @Tag(name = "(Normal) Study Time: 순공 시간", description = "순공 시간 API") -@RequestMapping("/study-times") +@RequestMapping("/studytimes") public interface StudyTimeApi { @Operation(summary = "스터디 타이머를 시작한다.", description = """ @@ -24,7 +25,7 @@ public interface StudyTimeApi { ## 에러 - `ALREADY_RUNNING_STUDY_TIMER` (409): 이미 실행 중인 타이머가 있는 경우 """) - @PostMapping("/start") + @PostMapping("/timers") ResponseEntity start(@UserId Integer userId); @Operation(summary = "스터디 타이머를 종료한다.", description = """ @@ -37,7 +38,7 @@ public interface StudyTimeApi { - `STUDY_TIMER_TIME_MISMATCH` (400): 클라이언트 누적 시간과 서버 시간 차이가 1분 이상인 경우 - `STUDY_TIMER_NOT_RUNNING` (400): 실행 중인 타이머가 없는 경우 """) - @PostMapping("/stop") + @DeleteMapping("/timers") ResponseEntity stop( @UserId Integer userId, @RequestBody @Valid StudyTimerStopRequest request 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 a965662..a8c6d09 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,6 +1,7 @@ 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; @@ -15,19 +16,19 @@ @RestController @RequiredArgsConstructor -@RequestMapping("/study-times") +@RequestMapping("/studytimes") public class StudyTimeController implements StudyTimeApi { private final StudyTimerService studyTimerService; - @PostMapping("/start") + @PostMapping("/timers") public ResponseEntity start(@UserId Integer userId) { studyTimerService.start(userId); return ResponseEntity.ok().build(); } - @PostMapping("/stop") + @DeleteMapping("/timers") public ResponseEntity stop( @UserId Integer userId, @RequestBody @Valid StudyTimerStopRequest request From 4d2deec4f60dcfd224bd65c838bb360ca90424db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 1 Jan 2026 16:52:25 +0900 Subject: [PATCH 16/24] =?UTF-8?q?fix:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EC=A2=85=EB=A3=8C=20=EC=8B=9C=20=EC=8B=9C=EA=B0=84,=20?= =?UTF-8?q?=EB=B6=84,=20=EC=B4=88=20=EA=B0=81=EA=B0=81=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=EC=9C=BC=EB=A1=9C=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studytime/dto/StudyTimerStopRequest.java | 29 +++++++++++++++---- .../studytime/service/StudyTimerService.java | 13 +-------- 2 files changed, 24 insertions(+), 18 deletions(-) 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 44f177c..49f959e 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,13 +3,30 @@ import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; public record StudyTimerStopRequest( - @NotEmpty(message = "타이머 누적 시간은 필수 입력입니다.") - @Pattern(regexp = "^[0-9]{2}:[0-5][0-9]:[0-5][0-9]$", message = "타이머 누적 시간 형식이 올바르지 않습니다.") - @Schema(description = "타이머 누적 시간(HH:mm:ss)", example = "01:30:15", requiredMode = REQUIRED) - String elapsedTime + @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 ) { + + public long toTotalSeconds() { + return hour * 3600L + minute * 60L + second; + } } 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 81fd94c..4229b79 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 @@ -34,8 +34,6 @@ public class StudyTimerService { private static final long TIMER_MISMATCH_THRESHOLD_SECONDS = 60L; - private static final int SECONDS_PER_MINUTE = 60; - private static final int SECONDS_PER_HOUR = 3600; private final StudyTimerRepository studyTimerRepository; private final StudyTimeDailyRepository studyTimeDailyRepository; @@ -71,7 +69,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 = parseElapsedSeconds(request.elapsedTime()); + long clientSeconds = request.toTotalSeconds(); if (isMismatch(serverSeconds, clientSeconds)) { studyTimerRepository.delete(studyTimer); @@ -175,13 +173,4 @@ private StudyTimeAggregate buildAggregate(Integer userId, LocalDate endDate, lon private boolean isMismatch(long serverSeconds, long clientSeconds) { return Math.abs(serverSeconds - clientSeconds) >= TIMER_MISMATCH_THRESHOLD_SECONDS; } - - private long parseElapsedSeconds(String elapsedTime) { - String[] parts = elapsedTime.split(":"); - long hours = Long.parseLong(parts[0]); - long minutes = Long.parseLong(parts[1]); - long seconds = Long.parseLong(parts[2]); - - return hours * SECONDS_PER_HOUR + minutes * SECONDS_PER_MINUTE + seconds; - } } From a8444d3e63f86bbf2f82d8424e17d90dd6212e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 1 Jan 2026 17:02:02 +0900 Subject: [PATCH 17/24] =?UTF-8?q?fix:=20StudyTimeAggregate=20->=20StudyTim?= =?UTF-8?q?eSummary=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{StudyTimeAggregate.java => StudyTimeSummary.java} | 2 +- .../domain/studytime/service/StudyTimerService.java | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) rename src/main/java/gg/agit/konect/domain/studytime/model/{StudyTimeAggregate.java => StudyTimeSummary.java} (81%) diff --git a/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeAggregate.java b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeSummary.java similarity index 81% rename from src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeAggregate.java rename to src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeSummary.java index 58ab4e3..a368754 100644 --- a/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeAggregate.java +++ b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeSummary.java @@ -1,6 +1,6 @@ package gg.agit.konect.domain.studytime.model; -public record StudyTimeAggregate( +public record StudyTimeSummary( long sessionSeconds, long dailySeconds, long monthlySeconds, 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 4229b79..ac58dd1 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 @@ -13,7 +13,7 @@ import gg.agit.konect.domain.studytime.dto.StudyTimerStopRequest; import gg.agit.konect.domain.studytime.dto.StudyTimerStopResponse; -import gg.agit.konect.domain.studytime.model.StudyTimeAggregate; +import gg.agit.konect.domain.studytime.model.StudyTimeSummary; import gg.agit.konect.domain.studytime.model.StudyTimeDaily; import gg.agit.konect.domain.studytime.model.StudyTimeMonthly; import gg.agit.konect.domain.studytime.model.StudyTimeTotal; @@ -76,7 +76,7 @@ public StudyTimerStopResponse stop(Integer userId, StudyTimerStopRequest request throw CustomException.of(STUDY_TIMER_TIME_MISMATCH); } - StudyTimeAggregate aggregate = applyStudyTime(studyTimer.getUser(), startedAt, endedAt); + StudyTimeSummary aggregate = applyStudyTime(studyTimer.getUser(), startedAt, endedAt); studyTimerRepository.delete(studyTimer); @@ -88,7 +88,7 @@ public StudyTimerStopResponse stop(Integer userId, StudyTimerStopRequest request ); } - private StudyTimeAggregate applyStudyTime(User user, LocalDateTime startedAt, LocalDateTime endedAt) { + private StudyTimeSummary applyStudyTime(User user, LocalDateTime startedAt, LocalDateTime endedAt) { LocalDateTime cursor = startedAt; long sessionSeconds = 0L; @@ -152,7 +152,7 @@ private void addTotalSeconds(User user, long seconds) { studyTimeTotalRepository.save(total); } - private StudyTimeAggregate buildAggregate(Integer userId, LocalDate endDate, long sessionSeconds) { + private StudyTimeSummary buildAggregate(Integer userId, LocalDate endDate, long sessionSeconds) { LocalDate month = endDate.withDayOfMonth(1); long dailySeconds = studyTimeDailyRepository.findByUserIdAndStudyDate(userId, endDate) @@ -167,7 +167,7 @@ private StudyTimeAggregate buildAggregate(Integer userId, LocalDate endDate, lon .map(StudyTimeTotal::getTotalSeconds) .orElse(0L); - return new StudyTimeAggregate(sessionSeconds, dailySeconds, monthlySeconds, totalSeconds); + return new StudyTimeSummary(sessionSeconds, dailySeconds, monthlySeconds, totalSeconds); } private boolean isMismatch(long serverSeconds, long clientSeconds) { From eb5943bf61b6aa9fb2ef344e9d2721f79439bca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 1 Jan 2026 17:03:12 +0900 Subject: [PATCH 18/24] =?UTF-8?q?fix:=20=ED=83=80=EC=9D=B4=EB=A8=B8=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=BC=20=EC=8B=9C=EA=B0=84=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=20=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/domain/studytime/service/StudyTimerService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ac58dd1..492e659 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 @@ -71,7 +71,7 @@ public StudyTimerStopResponse stop(Integer userId, StudyTimerStopRequest request long serverSeconds = Duration.between(startedAt, endedAt).getSeconds(); long clientSeconds = request.toTotalSeconds(); - if (isMismatch(serverSeconds, clientSeconds)) { + if (isElapsedTimeInvalid(serverSeconds, clientSeconds)) { studyTimerRepository.delete(studyTimer); throw CustomException.of(STUDY_TIMER_TIME_MISMATCH); } @@ -170,7 +170,7 @@ private StudyTimeSummary buildAggregate(Integer userId, LocalDate endDate, long return new StudyTimeSummary(sessionSeconds, dailySeconds, monthlySeconds, totalSeconds); } - private boolean isMismatch(long serverSeconds, long clientSeconds) { + private boolean isElapsedTimeInvalid(long serverSeconds, long clientSeconds) { return Math.abs(serverSeconds - clientSeconds) >= TIMER_MISMATCH_THRESHOLD_SECONDS; } } From 4383a8dfff26d7dc73d6a984305b94d923e4a26f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Thu, 1 Jan 2026 17:07:15 +0900 Subject: [PATCH 19/24] =?UTF-8?q?fix:=20=EA=B3=B5=EB=B6=80=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EB=B0=98=EC=98=81=ED=95=98=EB=8A=94=20=EB=A9=94?= =?UTF-8?q?=EC=86=8C=EB=93=9C=20=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/domain/studytime/service/StudyTimerService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 492e659..56c527c 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 @@ -76,7 +76,7 @@ public StudyTimerStopResponse stop(Integer userId, StudyTimerStopRequest request throw CustomException.of(STUDY_TIMER_TIME_MISMATCH); } - StudyTimeSummary aggregate = applyStudyTime(studyTimer.getUser(), startedAt, endedAt); + StudyTimeSummary aggregate = accumulateStudyTime(studyTimer.getUser(), startedAt, endedAt); studyTimerRepository.delete(studyTimer); @@ -88,7 +88,7 @@ public StudyTimerStopResponse stop(Integer userId, StudyTimerStopRequest request ); } - private StudyTimeSummary applyStudyTime(User user, LocalDateTime startedAt, LocalDateTime endedAt) { + private StudyTimeSummary accumulateStudyTime(User user, LocalDateTime startedAt, LocalDateTime endedAt) { LocalDateTime cursor = startedAt; long sessionSeconds = 0L; From eb58ad1146e913d5c1f76e2d969f06e6977d4f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 2 Jan 2026 10:12:59 +0900 Subject: [PATCH 20/24] =?UTF-8?q?refactor:=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EB=88=84=EC=A0=81=20=EB=A1=9C=EC=A7=81=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=20=EB=B6=84=ED=95=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studytime/service/StudyTimerService.java | 76 +++++++++++++------ 1 file changed, 52 insertions(+), 24 deletions(-) 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 56c527c..c1acae5 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 @@ -76,49 +76,68 @@ public StudyTimerStopResponse stop(Integer userId, StudyTimerStopRequest request throw CustomException.of(STUDY_TIMER_TIME_MISMATCH); } - StudyTimeSummary aggregate = accumulateStudyTime(studyTimer.getUser(), startedAt, endedAt); + StudyTimeSummary summary = accumulateStudyTime(studyTimer.getUser(), startedAt, endedAt); studyTimerRepository.delete(studyTimer); return StudyTimerStopResponse.of( - aggregate.sessionSeconds(), - aggregate.dailySeconds(), - aggregate.monthlySeconds(), - aggregate.totalSeconds() + summary.sessionSeconds(), + summary.dailySeconds(), + summary.monthlySeconds(), + summary.totalSeconds() ); } private StudyTimeSummary accumulateStudyTime(User user, LocalDateTime startedAt, LocalDateTime endedAt) { + long sessionSeconds = accumulateDailyAndMonthlySeconds(user, startedAt, endedAt); + updateTotalSecondsIfNeeded(user, sessionSeconds); + LocalDate endDate = endedAt.toLocalDate(); + + return buildAggregate(user.getId(), endDate, sessionSeconds); + } + + private long accumulateDailyAndMonthlySeconds(User user, LocalDateTime startedAt, LocalDateTime endedAt) { LocalDateTime cursor = startedAt; long sessionSeconds = 0L; + LocalDate endDate = endedAt.toLocalDate(); - while (cursor.toLocalDate().isBefore(endedAt.toLocalDate())) { - LocalDateTime nextDayStart = cursor.toLocalDate().plusDays(1).atStartOfDay(); - long seconds = Duration.between(cursor, nextDayStart).getSeconds(); - sessionSeconds += addSegment(user, cursor.toLocalDate(), seconds); - cursor = nextDayStart; - } + while (cursor.isBefore(endedAt)) { + LocalDateTime segmentEnd; - if (cursor.isBefore(endedAt)) { - long seconds = Duration.between(cursor, endedAt).getSeconds(); - sessionSeconds += addSegment(user, cursor.toLocalDate(), seconds); - } + if (cursor.toLocalDate().isBefore(endDate)) { + segmentEnd = cursor.toLocalDate().plusDays(1).atStartOfDay(); + } else { + segmentEnd = endedAt; + } - if (sessionSeconds > 0) { - addTotalSeconds(user, sessionSeconds); + sessionSeconds += accumulateDailyAndMonthlySegment(user, cursor, segmentEnd); + cursor = segmentEnd; } - LocalDate endDate = endedAt.toLocalDate(); - - return buildAggregate(user.getId(), endDate, sessionSeconds); + return sessionSeconds; } - private long addSegment(User user, LocalDate date, long seconds) { + private long accumulateDailyAndMonthlySegment(User user, LocalDateTime segmentStart, LocalDateTime segmentEnd) { + if (!segmentStart.isBefore(segmentEnd)) { + return 0L; + } + + long seconds = Duration.between(segmentStart, segmentEnd).getSeconds(); + if (seconds <= 0) { return 0L; } - StudyTimeDaily daily = studyTimeDailyRepository.findByUserIdAndStudyDate(user.getId(), date) + LocalDate date = segmentStart.toLocalDate(); + addDailySegment(user, date, seconds); + addMonthlySegment(user, date, seconds); + + return seconds; + } + + private void addDailySegment(User user, LocalDate date, long seconds) { + StudyTimeDaily daily = studyTimeDailyRepository + .findByUserIdAndStudyDate(user.getId(), date) .orElseGet(() -> StudyTimeDaily.builder() .user(user) .studyDate(date) @@ -127,9 +146,13 @@ private long addSegment(User user, LocalDate date, long seconds) { daily.addSeconds(seconds); studyTimeDailyRepository.save(daily); + } + private void addMonthlySegment(User user, LocalDate date, long seconds) { LocalDate month = date.withDayOfMonth(1); - StudyTimeMonthly monthly = studyTimeMonthlyRepository.findByUserIdAndStudyMonth(user.getId(), month) + + StudyTimeMonthly monthly = studyTimeMonthlyRepository + .findByUserIdAndStudyMonth(user.getId(), month) .orElseGet(() -> StudyTimeMonthly.builder() .user(user) .studyMonth(month) @@ -138,8 +161,12 @@ private long addSegment(User user, LocalDate date, long seconds) { monthly.addSeconds(seconds); studyTimeMonthlyRepository.save(monthly); + } - return seconds; + private void updateTotalSecondsIfNeeded(User user, long sessionSeconds) { + if (sessionSeconds > 0) { + addTotalSeconds(user, sessionSeconds); + } } private void addTotalSeconds(User user, long seconds) { @@ -148,6 +175,7 @@ private void addTotalSeconds(User user, long seconds) { .user(user) .totalSeconds(0L) .build()); + total.addSeconds(seconds); studyTimeTotalRepository.save(total); } From caf492e9d1cb5fcc5937cba796572ada10e4c046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 2 Jan 2026 10:30:34 +0900 Subject: [PATCH 21/24] =?UTF-8?q?refactor:=20accumulateStudyTime=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=98=95=20long=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studytime/dto/StudyTimerStopResponse.java | 15 ++++++++------- .../studytime/service/StudyTimerService.java | 18 ++++++------------ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimerStopResponse.java b/src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimerStopResponse.java index 4eb2bb2..eed11bb 100644 --- a/src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimerStopResponse.java +++ b/src/main/java/gg/agit/konect/domain/studytime/dto/StudyTimerStopResponse.java @@ -2,6 +2,7 @@ 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( @@ -17,12 +18,12 @@ public record StudyTimerStopResponse( @Schema(description = "총 누적 공부 시간(초)", example = "360000", requiredMode = REQUIRED) Long totalSeconds ) { - public static StudyTimerStopResponse of( - long sessionSeconds, - long dailySeconds, - long monthlySeconds, - long totalSeconds - ) { - return new StudyTimerStopResponse(sessionSeconds, dailySeconds, monthlySeconds, totalSeconds); + public static StudyTimerStopResponse from(StudyTimeSummary summary) { + return new StudyTimerStopResponse( + summary.sessionSeconds(), + summary.dailySeconds(), + summary.monthlySeconds(), + summary.totalSeconds() + ); } } 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 c1acae5..cdd4e6c 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 @@ -76,24 +76,18 @@ public StudyTimerStopResponse stop(Integer userId, StudyTimerStopRequest request throw CustomException.of(STUDY_TIMER_TIME_MISMATCH); } - StudyTimeSummary summary = accumulateStudyTime(studyTimer.getUser(), startedAt, endedAt); - + long sessionSeconds = accumulateStudyTime(studyTimer.getUser(), startedAt, endedAt); studyTimerRepository.delete(studyTimer); + StudyTimeSummary summary = buildSummary(userId, endedAt.toLocalDate(), sessionSeconds); - return StudyTimerStopResponse.of( - summary.sessionSeconds(), - summary.dailySeconds(), - summary.monthlySeconds(), - summary.totalSeconds() - ); + return StudyTimerStopResponse.from(summary); } - private StudyTimeSummary accumulateStudyTime(User user, LocalDateTime startedAt, LocalDateTime endedAt) { + private long accumulateStudyTime(User user, LocalDateTime startedAt, LocalDateTime endedAt) { long sessionSeconds = accumulateDailyAndMonthlySeconds(user, startedAt, endedAt); updateTotalSecondsIfNeeded(user, sessionSeconds); - LocalDate endDate = endedAt.toLocalDate(); - return buildAggregate(user.getId(), endDate, sessionSeconds); + return sessionSeconds; } private long accumulateDailyAndMonthlySeconds(User user, LocalDateTime startedAt, LocalDateTime endedAt) { @@ -180,7 +174,7 @@ private void addTotalSeconds(User user, long seconds) { studyTimeTotalRepository.save(total); } - private StudyTimeSummary buildAggregate(Integer userId, LocalDate endDate, long sessionSeconds) { + private StudyTimeSummary buildSummary(Integer userId, LocalDate endDate, long sessionSeconds) { LocalDate month = endDate.withDayOfMonth(1); long dailySeconds = studyTimeDailyRepository.findByUserIdAndStudyDate(userId, endDate) From 4d03665c875ba9d9bd54d521ed4afa4e34ba2b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 2 Jan 2026 10:42:09 +0900 Subject: [PATCH 22/24] =?UTF-8?q?refactor:=20=EC=97=94=ED=8B=B0=ED=8B=B0?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=EC=9D=84=20=EC=A0=95=EC=A0=81=20=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EB=A9=94=EC=86=8C=EB=93=9C=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=ED=95=98=EB=8A=94=20=EB=B0=A9=EC=8B=9D=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studytime/model/StudyTimeDaily.java | 8 +++++++ .../studytime/model/StudyTimeMonthly.java | 8 +++++++ .../studytime/model/StudyTimeTotal.java | 7 ++++++ .../domain/studytime/model/StudyTimer.java | 7 ++++++ .../studytime/service/StudyTimerService.java | 22 ++++--------------- 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeDaily.java b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeDaily.java index d49a3ed..fa42032 100644 --- a/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeDaily.java +++ b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeDaily.java @@ -56,6 +56,14 @@ private StudyTimeDaily(User user, LocalDate studyDate, Long totalSeconds) { 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; } diff --git a/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeMonthly.java b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeMonthly.java index 9c04421..58c37ba 100644 --- a/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeMonthly.java +++ b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeMonthly.java @@ -56,6 +56,14 @@ private StudyTimeMonthly(User user, LocalDate studyMonth, Long totalSeconds) { 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; } diff --git a/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeTotal.java b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeTotal.java index 824c2ff..b791412 100644 --- a/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeTotal.java +++ b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimeTotal.java @@ -49,6 +49,13 @@ private StudyTimeTotal(User user, Long totalSeconds) { 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; } diff --git a/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimer.java b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimer.java index f49f365..ad8d87f 100644 --- a/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimer.java +++ b/src/main/java/gg/agit/konect/domain/studytime/model/StudyTimer.java @@ -44,4 +44,11 @@ private StudyTimer(User user, LocalDateTime startedAt) { this.user = user; this.startedAt = startedAt; } + + public static StudyTimer of(User user, LocalDateTime startedAt) { + return StudyTimer.builder() + .user(user) + .startedAt(startedAt) + .build(); + } } 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 cdd4e6c..60024ee 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 @@ -52,10 +52,7 @@ public void start(Integer userId) { LocalDateTime startedAt = LocalDateTime.now(); try { - studyTimerRepository.save(StudyTimer.builder() - .user(user) - .startedAt(startedAt) - .build()); + studyTimerRepository.save(StudyTimer.of(user, startedAt)); entityManager.flush(); } catch (DataIntegrityViolationException e) { throw CustomException.of(ALREADY_RUNNING_STUDY_TIMER); @@ -132,11 +129,7 @@ private long accumulateDailyAndMonthlySegment(User user, LocalDateTime segmentSt private void addDailySegment(User user, LocalDate date, long seconds) { StudyTimeDaily daily = studyTimeDailyRepository .findByUserIdAndStudyDate(user.getId(), date) - .orElseGet(() -> StudyTimeDaily.builder() - .user(user) - .studyDate(date) - .totalSeconds(0L) - .build()); + .orElseGet(() -> StudyTimeDaily.of(user, date, 0L)); daily.addSeconds(seconds); studyTimeDailyRepository.save(daily); @@ -147,11 +140,7 @@ private void addMonthlySegment(User user, LocalDate date, long seconds) { StudyTimeMonthly monthly = studyTimeMonthlyRepository .findByUserIdAndStudyMonth(user.getId(), month) - .orElseGet(() -> StudyTimeMonthly.builder() - .user(user) - .studyMonth(month) - .totalSeconds(0L) - .build()); + .orElseGet(() -> StudyTimeMonthly.of(user, month, 0L)); monthly.addSeconds(seconds); studyTimeMonthlyRepository.save(monthly); @@ -165,10 +154,7 @@ private void updateTotalSecondsIfNeeded(User user, long sessionSeconds) { private void addTotalSeconds(User user, long seconds) { StudyTimeTotal total = studyTimeTotalRepository.findByUserId(user.getId()) - .orElseGet(() -> StudyTimeTotal.builder() - .user(user) - .totalSeconds(0L) - .build()); + .orElseGet(() -> StudyTimeTotal.of(user, 0L)); total.addSeconds(seconds); studyTimeTotalRepository.save(total); From ccb3cfab66e3fa6fe67a4cfcb9d5426dea0c463f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 2 Jan 2026 10:42:51 +0900 Subject: [PATCH 23/24] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agit/konect/domain/studytime/service/StudyTimerService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 60024ee..5a7f5df 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 @@ -13,9 +13,9 @@ import gg.agit.konect.domain.studytime.dto.StudyTimerStopRequest; import gg.agit.konect.domain.studytime.dto.StudyTimerStopResponse; -import gg.agit.konect.domain.studytime.model.StudyTimeSummary; import gg.agit.konect.domain.studytime.model.StudyTimeDaily; import gg.agit.konect.domain.studytime.model.StudyTimeMonthly; +import gg.agit.konect.domain.studytime.model.StudyTimeSummary; import gg.agit.konect.domain.studytime.model.StudyTimeTotal; import gg.agit.konect.domain.studytime.model.StudyTimer; import gg.agit.konect.domain.studytime.repository.StudyTimeDailyRepository; From d7b4df4265d1df26d0c38da695fff60db0d9bd7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Fri, 2 Jan 2026 10:46:11 +0900 Subject: [PATCH 24/24] =?UTF-8?q?chore:=20=EB=A7=A4=EC=A7=81=EB=84=98?= =?UTF-8?q?=EB=B2=84=20=EC=83=81=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konect/domain/studytime/dto/StudyTimerStopRequest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 49f959e..839c92b 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 @@ -26,7 +26,10 @@ public record StudyTimerStopRequest( Integer second ) { + private static final long SECONDS_PER_MINUTE = 60L; + private static final long SECONDS_PER_HOUR = 3600L; + public long toTotalSeconds() { - return hour * 3600L + minute * 60L + second; + return hour * SECONDS_PER_HOUR + minute * SECONDS_PER_MINUTE + second; } }