Conversation
|
Warning Review limit reached
Next review available in: 36 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 (5)
📒 Files selected for processing (65)
Walkthrough투두 제목/태그 기반으로 Gemini AI를 통해 예상 소요 시간을 추천하는 신규 API( ChangesAI 소요시간 추천 기능
보안/문서/설정 조정
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant AiTodoController
participant AiTodoService
participant AiTodoQueryRepository
participant TodoDurationPromptBuilder
participant AiRequestRateLimiter
participant GeminiService
Client->>AiTodoController: POST /recommend-duration
AiTodoController->>AiTodoService: recommendDuration(userId, request)
AiTodoService->>AiTodoQueryRepository: findActualDurationHistories...
AiTodoQueryRepository-->>AiTodoService: TodoDurationHistory 목록
AiTodoService->>TodoDurationPromptBuilder: build(request, histories)
TodoDurationPromptBuilder-->>AiTodoService: 프롬프트 문자열
AiTodoService->>AiRequestRateLimiter: validate(userId, estimatedTokenCost)
AiTodoService->>GeminiService: generateJson(prompt)
GeminiService-->>AiTodoService: JSON 응답
AiTodoService-->>AiTodoController: RecommendDurationResponse
AiTodoController-->>Client: BaseResponse(DURATION_RECOMMENDED)
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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: 7
🧹 Nitpick comments (6)
src/main/java/com/Timo/Timo/domain/ai/service/AiTodoService.java (3)
97-99: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win추천 소요시간 상한 클램핑 부재
normalizeMinutes는 하한(1분)만 보장하고 상한이 없어, Gemini가 비정상적으로 큰 값(예: 999999)을 반환하면 그대로 클라이언트에 노출됩니다. 상한 클램핑을 추가하면 안전합니다.🤖 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/ai/service/AiTodoService.java` around lines 97 - 99, normalizeMinutes in AiTodoService only clamps the lower bound, so very large Gemini values can pass through unchanged. Update this method to clamp both ends by introducing a reasonable maximum minutes limit and returning a value bounded within that range. Keep the fix localized to normalizeMinutes so all callers automatically receive safe, normalized durations.
74-83: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win마크다운 펜스 제거 로직이 비정상 응답에 취약
stripMarkdownFence는 시작 펜스만 있고 종료 펜스가 없는 등 예상과 다른 형태의 응답이 오면substring인덱스 계산이 잘못되어StringIndexOutOfBoundsException이 발생할 수 있습니다.parseRecommendation의 try-catch로 잡히기는 하지만 근본적으로는 정규식 기반 추출 등 더 견고한 방식으로 개선하는 것을 권장합니다.🔧 제안
- private String stripMarkdownFence(String value) { - String trimmed = value.trim(); - if (trimmed.startsWith("```json")) { - return trimmed.substring(7, trimmed.length() - 3).trim(); - } - if (trimmed.startsWith("```")) { - return trimmed.substring(3, trimmed.length() - 3).trim(); - } - return trimmed; - } + private String stripMarkdownFence(String value) { + String trimmed = value.trim(); + if (trimmed.startsWith("```")) { + int firstNewline = trimmed.indexOf('\n'); + int closingFence = trimmed.lastIndexOf("```"); + if (firstNewline > 0 && closingFence > firstNewline) { + return trimmed.substring(firstNewline + 1, closingFence).trim(); + } + } + return trimmed; + }Also applies to: 105-114
🤖 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/ai/service/AiTodoService.java` around lines 74 - 83, `parseRecommendation` currently relies on `stripMarkdownFence`, which can break on malformed Gemini replies where a code fence is incomplete or missing its closing marker. Update `stripMarkdownFence` in `AiTodoService` to extract fenced content more defensively, using the fence boundaries only when both an opening fence and a valid closing fence are present, and otherwise return the trimmed input unchanged. Keep the existing `parseRecommendation` error handling, but make the fence-stripping logic resilient so it cannot throw `StringIndexOutOfBoundsException` on unexpected responses.
40-61: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueRate limit 검증 전 DB 조회 수행
similarTitleHistories/recentTagHistories조회가 먼저 실행된 뒤rateLimiter.validate가 호출됩니다. 한도 초과 사용자의 경우에도 매번 2건의 DB 쿼리가 낭비됩니다. 토큰 추정치가 프롬프트(히스토리 포함) 길이에 의존하는 구조상 완전한 재정렬은 어렵지만, 최소한 RPM/RPD 같은 기본 한도는 히스토리 조회 전에 먼저 체크하는 방식을 고려해볼 수 있습니다.🤖 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/ai/service/AiTodoService.java` around lines 40 - 61, Rate limit validation is happening after the history lookups in AiTodoService, so over-limit requests still pay for the similarTitleHistories and recentTagHistories queries. Reorder the logic in AiTodoService so a lightweight pre-check for basic limits (before the repository calls) runs ahead of aiTodoQueryRepository.findActualDurationHistoriesBySimilarTitle and findActualDurationHistoriesByTagId, then keep the existing prompt-based estimateTokenCost(prompt) validation after promptBuilder.build for the final token check.src/main/java/com/Timo/Timo/domain/ai/service/AiRequestRateLimiter.java (1)
45-79: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winRedis 장애 시 동작 방식이 불명확
redisTemplate.execute(...)호출 중 Redis 연결 장애(예:RedisConnectionFailureException)가 발생하면 예외가 그대로 전파되어 AI 추천 기능 전체가 실패합니다. Rate limiter는 부가 기능인데 Redis 장애가 핵심 기능 장애로 전이되는 구조입니다. Redis 예외를 잡아 fail-open(제한 없이 통과)할지, fail-closed(요청 거부)할지 명시적으로 정책을 정하는 것을 권장합니다.🤖 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/ai/service/AiRequestRateLimiter.java` around lines 45 - 79, The Redis call in AiRequestRateLimiter.validate currently lets connection failures propagate, so decide an explicit policy for Redis outages and implement it around redisTemplate.execute with the existing LIMIT_SCRIPT. If choosing fail-open, catch RedisConnectionFailureException and other Redis access errors, log the issue, and allow the request to continue; if choosing fail-closed, catch them and translate to the same AI_RATE_LIMIT_EXCEEDED path. Keep the behavior localized to validate and preserve the current bucket/key logic.src/main/java/com/Timo/Timo/domain/ai/dto/request/RecommendDurationRequest.java (1)
13-14: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value
tagId에 대한 값 검증 부재
tagId가 음수나 0으로 들어와도 그대로 서비스 계층까지 전달됩니다. DB 조회 시 단순히 결과가 없게 되어 기능적으로는 크게 문제없지만, 명시적으로@Positive를 추가하면 잘못된 입력을 조기에 차단할 수 있습니다.🔧 제안
+import jakarta.validation.constraints.Positive; + `@Schema`(description = "투두 태그 ID", example = "1", nullable = true) + `@Positive` Long tagId🤖 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/ai/dto/request/RecommendDurationRequest.java` around lines 13 - 14, `RecommendDurationRequest`의 `tagId`에는 현재 양수 검증이 없어 0이나 음수가 그대로 전달됩니다. `tagId` 필드에 입력 검증 애노테이션을 추가해 서비스 계층에 들어가기 전에 잘못된 값을 차단하도록 수정하세요. 이 요청 DTO의 `tagId` 선언 위치를 찾아 `RecommendDurationRequest`에서 명시적으로 양수만 허용되도록 적용하면 됩니다.src/main/java/com/Timo/Timo/domain/ai/repository/AiTodoQueryRepository.java (1)
38-41: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift양방향 LIKE
%...%패턴으로 인해 인덱스를 활용할 수 없습니다.
lower(t.title) like lower(concat('%', :title, '%'))와 그 역방향 조건 모두 선행 와일드카드(%)를 사용하므로title컬럼에 인덱스가 있더라도 활용되지 못하고 풀 스캔이 발생합니다. 사용자별 타이머 기록이 많아질수록 이 쿼리의 비용이 선형으로 증가합니다. 유사도 매칭에 트라이그램 인덱스(pg_trgm) 또는 별도의 검색 인덱스(예: Elasticsearch) 도입을 검토해 보시기 바랍니다.도메인 리포지토리 경로 지침 "쿼리 성능과 N+1 문제가 없는지 확인", "필요한 경우 인덱스 고려가 되어 있는지 확인"에 따라 지적합니다.
🤖 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/ai/repository/AiTodoQueryRepository.java` around lines 38 - 41, The title matching in AiTodoQueryRepository uses leading-wildcard LIKE on both sides, so the query cannot use a title index and will scale poorly. Update the query logic in the repository method to avoid the bilateral lower(... ) like concat('%', ... , '%') pattern; if fuzzy matching is required, switch to a pg_trgm-based search or another indexed search approach and align the repository query with that index strategy.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/controller/AiTodoController.java`:
- Around line 38-43: The INFO log in AiTodoController currently prints the
user-provided todo title via request.title(), which may expose sensitive data in
logs. Update the log statement in the AI duration recommendation endpoint to
omit the title entirely or replace it with a masked/placeholder value, while
keeping non-sensitive context like userId and tagId. Use the AiTodoController
logging call as the target for the change and ensure no plaintext PII is emitted
at INFO level.
In `@src/main/java/com/Timo/Timo/domain/ai/prompt/TodoDurationPromptBuilder.java`:
- Around line 82-89: The escape() helper in TodoDurationPromptBuilder only
escapes backslashes and quotes, leaving newline/control characters intact and
allowing prompt-injection text to break the prompt structure. Update escape() to
also neutralize line breaks and other control characters before values like
title are inserted into the prompt, so user input cannot escape the intended
field context. Keep the fix localized to escape() and ensure the prompt-building
flow that uses it continues to receive safely sanitized text.
In `@src/main/java/com/Timo/Timo/domain/ai/service/AiTodoService.java`:
- Around line 24-25: `AiTodoService` is keeping the whole `recommendDuration()`
flow inside the class-level read-only transaction, which leaves the DB
transaction open during the external Gemini HTTP call. Move the transaction
boundary so only the database lookup work stays inside a transactional method,
and invoke `geminiService.generateJson(prompt)` outside that scope; use the
`AiTodoService` and `recommendDuration()` symbols to separate the
query-building/lookup logic from the Gemini request.
In `@src/main/java/com/Timo/Timo/domain/ai/service/GeminiService.java`:
- Around line 20-21: The Gemini request is currently embedding the API key in
the GENERATE_CONTENT_URL query string, which can leak secrets through logs and
intermediaries. Update GeminiService so the URI only includes the model path and
the API key is sent via the x-goog-api-key header on the request builder. Keep
the model substitution in the URI and move apiKey handling out of the URL
entirely.
- Around line 20-24: The GeminiService RestClient is created without explicit
connect/read timeouts, so update the GeminService initialization to use a
request factory with timeout settings instead of plain RestClient.create().
Adjust the RestClient field setup in GeminiService so the client is built with
configured timeouts, and keep the change localized to the RestClient creation
path used by the Gemini 호출 logic.
In `@src/main/java/com/Timo/Timo/domain/todo/exception/TodoExceptionHandler.java`:
- Line 15: The RestControllerAdvice on TodoExceptionHandler is pointing at a
controller package that does not exist, so the advice is never applied. Update
the basePackages value to match the actual package where the todo controllers
live, or switch to assignableTypes if this handler should only target specific
controller classes. Keep the existing TodoExceptionHandler class and adjust the
`@RestControllerAdvice` configuration so it binds to the intended controllers.
In `@src/main/resources/application-local.yml`:
- Around line 40-50: Fix the typo in the ai.rate-limit configuration so it
matches the property expected by AiRequestRateLimiter; the current user-지rpd key
contains an accidental Korean character and does not match the
`@Value`("${ai.rate-limit.user-rpd}") injection. Update the application-local.yml
entry to use the exact user-rpd symbol alongside the other rate-limit keys so
Spring can resolve it during context initialization.
---
Nitpick comments:
In
`@src/main/java/com/Timo/Timo/domain/ai/dto/request/RecommendDurationRequest.java`:
- Around line 13-14: `RecommendDurationRequest`의 `tagId`에는 현재 양수 검증이 없어 0이나 음수가
그대로 전달됩니다. `tagId` 필드에 입력 검증 애노테이션을 추가해 서비스 계층에 들어가기 전에 잘못된 값을 차단하도록 수정하세요. 이 요청
DTO의 `tagId` 선언 위치를 찾아 `RecommendDurationRequest`에서 명시적으로 양수만 허용되도록 적용하면 됩니다.
In `@src/main/java/com/Timo/Timo/domain/ai/repository/AiTodoQueryRepository.java`:
- Around line 38-41: The title matching in AiTodoQueryRepository uses
leading-wildcard LIKE on both sides, so the query cannot use a title index and
will scale poorly. Update the query logic in the repository method to avoid the
bilateral lower(... ) like concat('%', ... , '%') pattern; if fuzzy matching is
required, switch to a pg_trgm-based search or another indexed search approach
and align the repository query with that index strategy.
In `@src/main/java/com/Timo/Timo/domain/ai/service/AiRequestRateLimiter.java`:
- Around line 45-79: The Redis call in AiRequestRateLimiter.validate currently
lets connection failures propagate, so decide an explicit policy for Redis
outages and implement it around redisTemplate.execute with the existing
LIMIT_SCRIPT. If choosing fail-open, catch RedisConnectionFailureException and
other Redis access errors, log the issue, and allow the request to continue; if
choosing fail-closed, catch them and translate to the same
AI_RATE_LIMIT_EXCEEDED path. Keep the behavior localized to validate and
preserve the current bucket/key logic.
In `@src/main/java/com/Timo/Timo/domain/ai/service/AiTodoService.java`:
- Around line 97-99: normalizeMinutes in AiTodoService only clamps the lower
bound, so very large Gemini values can pass through unchanged. Update this
method to clamp both ends by introducing a reasonable maximum minutes limit and
returning a value bounded within that range. Keep the fix localized to
normalizeMinutes so all callers automatically receive safe, normalized
durations.
- Around line 74-83: `parseRecommendation` currently relies on
`stripMarkdownFence`, which can break on malformed Gemini replies where a code
fence is incomplete or missing its closing marker. Update `stripMarkdownFence`
in `AiTodoService` to extract fenced content more defensively, using the fence
boundaries only when both an opening fence and a valid closing fence are
present, and otherwise return the trimmed input unchanged. Keep the existing
`parseRecommendation` error handling, but make the fence-stripping logic
resilient so it cannot throw `StringIndexOutOfBoundsException` on unexpected
responses.
- Around line 40-61: Rate limit validation is happening after the history
lookups in AiTodoService, so over-limit requests still pay for the
similarTitleHistories and recentTagHistories queries. Reorder the logic in
AiTodoService so a lightweight pre-check for basic limits (before the repository
calls) runs ahead of
aiTodoQueryRepository.findActualDurationHistoriesBySimilarTitle and
findActualDurationHistoriesByTagId, then keep the existing prompt-based
estimateTokenCost(prompt) validation after promptBuilder.build for the final
token check.
🪄 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: fead1de8-6797-4a84-b7a5-4ad091e46ab2
⛔ Files ignored due to path filters (1)
src/main/java/com/Timo/Timo/domain/ai/docs/AiTodoDocs.javais excluded by!**/docs/**
📒 Files selected for processing (16)
src/main/java/com/Timo/Timo/domain/ai/controller/AiTodoController.javasrc/main/java/com/Timo/Timo/domain/ai/dto/request/RecommendDurationRequest.javasrc/main/java/com/Timo/Timo/domain/ai/dto/response/GeminiDurationRecommendation.javasrc/main/java/com/Timo/Timo/domain/ai/dto/response/RecommendDurationResponse.javasrc/main/java/com/Timo/Timo/domain/ai/exception/AiErrorCode.javasrc/main/java/com/Timo/Timo/domain/ai/exception/AiSuccessCode.javasrc/main/java/com/Timo/Timo/domain/ai/prompt/TodoDurationPromptBuilder.javasrc/main/java/com/Timo/Timo/domain/ai/repository/AiTodoQueryRepository.javasrc/main/java/com/Timo/Timo/domain/ai/repository/TodoDurationHistory.javasrc/main/java/com/Timo/Timo/domain/ai/service/AiRequestRateLimiter.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/todo/exception/TodoExceptionHandler.javasrc/main/java/com/Timo/Timo/global/config/SecurityConfig.javasrc/main/java/com/Timo/Timo/global/config/SwaggerConfig.javasrc/main/resources/application-local.yml
관련 이슈 🛠
작업 내용 요약 ✏️
투두명과 사용자의 과거 실제 소요시간 기록을 기반으로 예상 소요 시간을 추천하는 AI API를 추가했습니다.
Gemini API 호출 전 Redis 기반 quota 제한을 적용해 RPM/RPD/TPM 초과 상황을 방어하도록 구성했습니다.
주요 변경 사항 🛠️
트러블 슈팅 ⚽️
테스트 결과 📄
./gradlew compileJava
스크린샷 📷
리뷰 요구사항 📢
📎 참고 자료 (선택)
없음
Summary by CodeRabbit