[feat] #72 - 타임박스 조회 #86
Conversation
|
Warning Review limit reached
Next review available in: 38 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (8)
Walkthrough타임박스 조회 API(컨트롤러, 서비스, DTO, 날짜 파서, 세션 조회 쿼리)가 신규 추가됨. 타이머 연장/완료/중지 엔드포인트와 관련 도메인 로직, AI 피드백 생성 흐름(프롬프트 빌더, DTO, 파싱/검증)이 추가됨. AI 히스토리 조회가 LocalDate에서 사용자 타임존 기반 LocalDateTime 경계로 변경되고, AiTodoController 라우팅 경로가 수정됨. Changes타임박스 조회 API
Estimated code review effort: 3 (Moderate) | ~25 minutes 타이머 연장/완료/중지 및 AI 피드백 연동
Estimated code review effort: 4 (Complex) | ~55 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
src/main/java/com/Timo/Timo/domain/timer/repository/TimerSessionRepository.java (1)
16-31: 🚀 Performance & Scalability | 🔵 Trivial쿼리 성능을 위한 인덱스 확인 권장
tr.user.id = :userId와ts.startedAt < :toExclusive조건으로 결과 집합이 좁혀지지만, 대량 데이터 환경에서는 다음 인덱스 확인이 필요합니다:
TimerSession.startedAt— 상한 조건(< :toExclusive)에 사용TimerRecord.user_id(FK 인덱스) — 사용자 필터링에 사용coalesce(ts.pausedAt, :nowUtc)는 함수 호출로 인해pausedAt인덱스를 활용할 수 없으나, 앞선 조건들로 충분히 필터링될 것으로 예상됩니다.fetch join으로 N+1 문제를 잘 방지하고 있습니다. As per path instructions, 필요한 경우 인덱스 고려가 되어 있는지 확인해 주세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/Timo/Timo/domain/timer/repository/TimerSessionRepository.java` around lines 16 - 31, `TimerSessionRepository.findTimeBoxSessions`의 조회 조건(`tr.user.id`, `ts.startedAt`, `ts.pausedAt`)에 맞는 인덱스가 대량 데이터에서 성능을 좌우하므로, `TimerSession`과 `TimerRecord`의 매핑을 기준으로 실제 DB 스키마에 `startedAt`과 `user_id` 인덱스가 있는지 확인하고 필요하면 추가하세요. 특히 `coalesce(ts.pausedAt, :nowUtc)`는 인덱스를 타기 어렵다는 점을 감안해, `pausedAt` 인덱스는 보조적으로 검토하되 우선적으로 `findTimeBoxSessions`의 필터링 경로가 효율적인지 점검해 주세요.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/java/com/Timo/Timo/domain/ai/repository/AiTodoQueryRepository.java`:
- Around line 158-176: The Timestamp handling in AiTodoQueryRepository is using
the JVM default timezone via toLocalDateTime(), which can shift the date on
non-UTC servers. Update the Timestamp branch in toUserLocalDate(Object, ZoneId)
to convert from timestamp.toInstant() directly into the userZoneId and derive
the LocalDate there, while keeping the LocalDateTime overload consistent with
the same user timezone conversion logic.
In
`@src/main/java/com/Timo/Timo/domain/timer/dto/request/TimerExtendRequest.java`:
- Around line 1-12: `TimerExtendRequest`의 `extendMinutes`에 하한만 있고 상한이 없어
`extendMinutes * 60` 계산에서 `int` 오버플로우가 날 수 있으므로, 이 record 필드에 합리적인 `@Max` 제약을
추가해 입력 범위를 제한하세요. `TimerExtendRequest`의 `extendMinutes`와 이를 사용하는 서비스의 시간 변환 로직을
함께 확인해, 오버플로우가 발생하지 않는 최대 분 단위를 기준으로 검증 메시지도 함께 맞춰 주세요.
In `@src/main/java/com/Timo/Timo/domain/timer/entity/TimerRecord.java`:
- Around line 98-105: `TimerRecord.finish()` currently accepts any
`targetStatus` and only checks `isFinished()`, unlike `pause()` and `resume()`
which enforce state transitions. Update `finish(TimerStatus targetStatus,
LocalDateTime endedAt, int actualSeconds)` to validate that `targetStatus` is
only one of the allowed terminal statuses (`COMPLETED` or `STOPPED`) and throw
the appropriate domain exception for invalid values. Keep the rule encapsulated
inside `TimerRecord` alongside the other status transition methods.
- Around line 111-116: `TimerRecord.extend()` only checks `isFinished()` and
allows non-positive values, so add a defensive domain validation before updating
`extendedSeconds`. Update the `extend(int extendedSeconds)` method to reject
zero or negative input by throwing an appropriate `CustomException`/timer error,
keeping the rule enforced even when called outside the DTO path. Use the
`extend` method and `isFinished` as the main points to locate and adjust this
state change logic.
In `@src/main/java/com/Timo/Timo/domain/timer/service/TimerService.java`:
- Around line 196-197: `finishTimer`에서 `generateAiFeedback`를 같은 트랜잭션 안에서 바로 호출해
외부 AI 요청이 DB 커넥션을 오래 점유하고 있습니다. `TimerService.finishTimer`와
`generateAiFeedback`의 결합을 끊고, 타이머 저장/종료는 트랜잭션에서만 완료한 뒤 AI 피드백 생성은 커밋 이후로 분리하세요.
`@TransactionalEventListener` 같은 이벤트 기반 방식으로 `timerRecordRepository.flush()` 이후에
비동기/후처리로 실행되도록 옮기고, 관련 도메인 로직은 `TimerService`에서 외부 호출과 데이터 저장 책임이 섞이지 않게 정리하세요.
---
Nitpick comments:
In
`@src/main/java/com/Timo/Timo/domain/timer/repository/TimerSessionRepository.java`:
- Around line 16-31: `TimerSessionRepository.findTimeBoxSessions`의 조회
조건(`tr.user.id`, `ts.startedAt`, `ts.pausedAt`)에 맞는 인덱스가 대량 데이터에서 성능을 좌우하므로,
`TimerSession`과 `TimerRecord`의 매핑을 기준으로 실제 DB 스키마에 `startedAt`과 `user_id` 인덱스가
있는지 확인하고 필요하면 추가하세요. 특히 `coalesce(ts.pausedAt, :nowUtc)`는 인덱스를 타기 어렵다는 점을 감안해,
`pausedAt` 인덱스는 보조적으로 검토하되 우선적으로 `findTimeBoxSessions`의 필터링 경로가 효율적인지 점검해 주세요.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 83273829-a51a-4861-a15e-e9c653c5f65f
⛔ Files ignored due to path filters (5)
src/main/java/com/Timo/Timo/domain/ai/docs/AiTodoDocs.javais excluded by!**/docs/**src/main/java/com/Timo/Timo/domain/timebox/docs/TimeBoxControllerDocs.javais excluded by!**/docs/**src/main/java/com/Timo/Timo/domain/timer/docs/TimerCompleteControllerDocs.javais excluded by!**/docs/**src/main/java/com/Timo/Timo/domain/timer/docs/TimerExtendControllerDocs.javais excluded by!**/docs/**src/main/java/com/Timo/Timo/domain/timer/docs/TimerStopControllerDocs.javais excluded by!**/docs/**
📒 Files selected for processing (33)
src/main/java/com/Timo/Timo/domain/ai/controller/AiTodoController.javasrc/main/java/com/Timo/Timo/domain/ai/dto/TodoDurationHistory.javasrc/main/java/com/Timo/Timo/domain/ai/dto/TodoFeedbackSource.javasrc/main/java/com/Timo/Timo/domain/ai/dto/request/RecommendDurationRequest.javasrc/main/java/com/Timo/Timo/domain/ai/dto/response/GeminiTodoFeedback.javasrc/main/java/com/Timo/Timo/domain/ai/exception/AiErrorCode.javasrc/main/java/com/Timo/Timo/domain/ai/prompt/TodoDurationPromptBuilder.javasrc/main/java/com/Timo/Timo/domain/ai/prompt/TodoFeedbackPromptBuilder.javasrc/main/java/com/Timo/Timo/domain/ai/repository/AiTodoQueryRepository.javasrc/main/java/com/Timo/Timo/domain/ai/service/AiRequestRateLimiter.javasrc/main/java/com/Timo/Timo/domain/ai/service/AiTodoHistories.javasrc/main/java/com/Timo/Timo/domain/ai/service/AiTodoHistoryService.javasrc/main/java/com/Timo/Timo/domain/ai/service/AiTodoService.javasrc/main/java/com/Timo/Timo/domain/ai/service/GeminiService.javasrc/main/java/com/Timo/Timo/domain/tag/entity/Tag.javasrc/main/java/com/Timo/Timo/domain/timebox/controller/TimeBoxController.javasrc/main/java/com/Timo/Timo/domain/timebox/dto/response/TimeBoxResponse.javasrc/main/java/com/Timo/Timo/domain/timebox/enums/TimeBoxAction.javasrc/main/java/com/Timo/Timo/domain/timebox/exception/TimeBoxErrorCode.javasrc/main/java/com/Timo/Timo/domain/timebox/exception/TimeBoxSuccessCode.javasrc/main/java/com/Timo/Timo/domain/timebox/service/TimeBoxService.javasrc/main/java/com/Timo/Timo/domain/timebox/support/TimeBoxDateParser.javasrc/main/java/com/Timo/Timo/domain/timer/controller/TimerController.javasrc/main/java/com/Timo/Timo/domain/timer/dto/request/TimerExtendRequest.javasrc/main/java/com/Timo/Timo/domain/timer/dto/response/TimerExtendResponse.javasrc/main/java/com/Timo/Timo/domain/timer/dto/response/TimerFinishResponse.javasrc/main/java/com/Timo/Timo/domain/timer/entity/TimerRecord.javasrc/main/java/com/Timo/Timo/domain/timer/exception/TimerErrorCode.javasrc/main/java/com/Timo/Timo/domain/timer/exception/TimerSuccessCode.javasrc/main/java/com/Timo/Timo/domain/timer/repository/TimerRecordRepository.javasrc/main/java/com/Timo/Timo/domain/timer/repository/TimerSessionRepository.javasrc/main/java/com/Timo/Timo/domain/timer/service/TimerService.javasrc/main/java/com/Timo/Timo/domain/todo/exception/TodoErrorCode.java
💤 Files with no reviewable changes (1)
- src/main/java/com/Timo/Timo/domain/timer/repository/TimerRecordRepository.java
| public void finish(TimerStatus targetStatus, LocalDateTime endedAt, int actualSeconds) { | ||
| if (isFinished()) { | ||
| throw new CustomException(TimerErrorCode.TIMER_ALREADY_FINISHED); | ||
| } | ||
| this.status = targetStatus; | ||
| this.endedAt = endedAt; | ||
| this.actualSeconds = actualSeconds; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
finish() 메서드가 targetStatus를 검증하지 않습니다.
기존 pause()와 resume()은 상태 전환을 검증하는 패턴을 따르지만, finish()는 targetStatus가 COMPLETED 또는 STOPPED인지 확인하지 않고 어떤 상태든 허용합니다. 도메인 규칙 캡슐화를 위해 targetStatus 검증을 추가하세요.
🛡️ 제안 수정
public void finish(TimerStatus targetStatus, LocalDateTime endedAt, int actualSeconds) {
if (isFinished()) {
throw new CustomException(TimerErrorCode.TIMER_ALREADY_FINISHED);
}
+ if (targetStatus != TimerStatus.COMPLETED && targetStatus != TimerStatus.STOPPED) {
+ throw new CustomException(TimerErrorCode.TIMER_INVALID_STATUS_TRANSITION);
+ }
this.status = targetStatus;
this.endedAt = endedAt;
this.actualSeconds = actualSeconds;
}As per path instructions, "Entity에 필요한 도메인 규칙과 상태 변경 메서드가 적절히 캡슐화되어 있는지 확인해 주세요."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public void finish(TimerStatus targetStatus, LocalDateTime endedAt, int actualSeconds) { | |
| if (isFinished()) { | |
| throw new CustomException(TimerErrorCode.TIMER_ALREADY_FINISHED); | |
| } | |
| this.status = targetStatus; | |
| this.endedAt = endedAt; | |
| this.actualSeconds = actualSeconds; | |
| } | |
| public void finish(TimerStatus targetStatus, LocalDateTime endedAt, int actualSeconds) { | |
| if (isFinished()) { | |
| throw new CustomException(TimerErrorCode.TIMER_ALREADY_FINISHED); | |
| } | |
| if (targetStatus != TimerStatus.COMPLETED && targetStatus != TimerStatus.STOPPED) { | |
| throw new CustomException(TimerErrorCode.TIMER_INVALID_STATUS_TRANSITION); | |
| } | |
| this.status = targetStatus; | |
| this.endedAt = endedAt; | |
| this.actualSeconds = actualSeconds; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/Timo/Timo/domain/timer/entity/TimerRecord.java` around
lines 98 - 105, `TimerRecord.finish()` currently accepts any `targetStatus` and
only checks `isFinished()`, unlike `pause()` and `resume()` which enforce state
transitions. Update `finish(TimerStatus targetStatus, LocalDateTime endedAt, int
actualSeconds)` to validate that `targetStatus` is only one of the allowed
terminal statuses (`COMPLETED` or `STOPPED`) and throw the appropriate domain
exception for invalid values. Keep the rule encapsulated inside `TimerRecord`
alongside the other status transition methods.
Source: Path instructions
| public void extend(int extendedSeconds){ | ||
| if (isFinished()){ | ||
| throw new CustomException(TimerErrorCode.TIMER_ALREADY_FINISHED); | ||
| } | ||
| this.extendedSeconds += extendedSeconds; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
extend() 메서드가 양수 입력을 검증하지 않습니다.
DTO에 @Min(1) 검증이 있으나, 도메인 메서드 자체도 방어적으로 검증해야 합니다. 다른 서비스 코드에서 직접 호출 시 음수가 전달되면 extendedSeconds가 감소하는 문제가 발생할 수 있습니다.
🛡️ 제안 수정
-public void extend(int extendedSeconds){
+public void extend(int extendedSeconds) {
+ if (extendedSeconds <= 0) {
+ throw new CustomException(TimerErrorCode.TIMER_INVALID_STATUS_TRANSITION);
+ }
if (isFinished()){
throw new CustomException(TimerErrorCode.TIMER_ALREADY_FINISHED);
}
this.extendedSeconds += extendedSeconds;
}As per path instructions, "Entity에 필요한 도메인 규칙과 상태 변경 메서드가 적절히 캡슐화되어 있는지 확인해 주세요."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public void extend(int extendedSeconds){ | |
| if (isFinished()){ | |
| throw new CustomException(TimerErrorCode.TIMER_ALREADY_FINISHED); | |
| } | |
| this.extendedSeconds += extendedSeconds; | |
| } | |
| public void extend(int extendedSeconds) { | |
| if (extendedSeconds <= 0) { | |
| throw new CustomException(TimerErrorCode.TIMER_INVALID_STATUS_TRANSITION); | |
| } | |
| if (isFinished()) { | |
| throw new CustomException(TimerErrorCode.TIMER_ALREADY_FINISHED); | |
| } | |
| this.extendedSeconds += extendedSeconds; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/Timo/Timo/domain/timer/entity/TimerRecord.java` around
lines 111 - 116, `TimerRecord.extend()` only checks `isFinished()` and allows
non-positive values, so add a defensive domain validation before updating
`extendedSeconds`. Update the `extend(int extendedSeconds)` method to reject
zero or negative input by throwing an appropriate `CustomException`/timer error,
keeping the rule enforced even when called outside the DTO path. Use the
`extend` method and `isFinished` as the main points to locate and adjust this
state change logic.
Source: Path instructions
5ee70b3 to
657b1b6
Compare
관련 이슈 🛠
작업 내용 요약 ✏️
지정한 날짜의 타임라인에 표시할 타임박스 조회 API를 구현했습니다.
사용자의 저장된 타임존 기준으로 조회 날짜를 계산하고 타이머 세션 기록을 기반으로 시작/정지/재개/완료 시점을 타임박스 응답에 포함하도록 구성했습니다.
주요 변경 사항 🛠️
GET /api/v1/time-boxes?date={date}타임박스 조회 API 추가START,PAUSE,RESUME,COMPLETE액션 응답 구성endedAt,endAction을null로 응답actualMinutes를 응답트러블 슈팅 ⚽️
없음
테스트 결과 📄
스크린샷 📷
<img width="976" height="658" alt="스크린샷 2026-07-09 오후 11 29 06" src="https://github.com/user-attachments/assets/1

2b8a078-9419-4f16-831f-bfd86473fdd9" />
타이머 시작, 재개, 종료시 화면입니다.
리뷰 요구사항 📢
없음
📎 참고 자료 (선택)
없음
Summary by CodeRabbit
New Features
Bug Fixes