Skip to content

[FEAT] 독서 기록 API 수정 & 로그인 리팩토링#264

Merged
JiwonLee42 merged 9 commits into
develop-demofrom
feat/#221-login-refactor
May 4, 2026
Merged

[FEAT] 독서 기록 API 수정 & 로그인 리팩토링#264
JiwonLee42 merged 9 commits into
develop-demofrom
feat/#221-login-refactor

Conversation

@JiwonLee42
Copy link
Copy Markdown
Contributor

@JiwonLee42 JiwonLee42 commented May 4, 2026

📄 작업 내용 요약

  • 독서 기록 감정별 필터 반환 API 수정: 개별 책 기반으로 수정, EMPTY 선택지 추가
  • 로그인 시 토큰 재발급 로직 수정: filter에서 처리하던 재발급 로직을 별도 재발급 API로 분리
  • 관련 테스트코드 수정

📎 Issue 번호


✅ 작업 목록

  • 기능 구현
  • 코드 리뷰 반영
  • 테스트 코드 작성
  • 문서 업데이트

📝 기타 참고사항

Summary by CodeRabbit

  • New Features

    • 액세스 토큰 재발급(리프레시 토큰)용 엔드포인트 추가
    • 단건 독서 기록 조회 엔드포인트 추가
  • Improvements

    • 독서 기록 감정 통계가 특정 도서 기준으로 반환되도록 범위 변경(전체 집계 → 도서별 집계)
    • 빈 독서 기록 목록은 일관된 성공 응답으로 반환
    • 감정 값 처리와 반환 형식(문자열 기반) 정교화
  • Documentation / Tests

    • API 문서 및 테스트 케이스 일치화 및 확대 (감정 관련, 토큰 재발급 포함)

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Warning

Rate limit exceeded

@JiwonLee42 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 50 minutes and 9 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a49bc992-8482-4f56-8805-97f1b9c91f45

📥 Commits

Reviewing files that changed from the base of the PR and between 8d133db and f95c626.

📒 Files selected for processing (4)
  • src/main/java/app/nook/user/service/UserService.java
  • src/test/java/app/nook/controller/focus/FocusControllerTest.java
  • src/test/java/app/nook/controller/record/RecordControllerTest.java
  • src/test/java/app/nook/user/service/UserServiceTest.java

Walkthrough

이 PR은 두 개의 독립적인 변경을 포함합니다: (1) 독서 기록 감정 집계를 사용자 전체에서 특정 책(bookId) 범위로 변경하고 DTO/저장소/서비스/컨트롤러/테스트/문서를 연쇄 변경. (2) 리프레시 토큰 기반 액세스 토큰 재발급 흐름(엔드포인트, 서비스, Redis 조회)과 JWT 클레임에서 이메일 대신 userId 사용으로 인증 필터/토큰 제공자 로직을 변경.

Changes

기록 감정별 개수 조회 범위 변경

Layer / File(s) Summary
데이터 형태
src/main/java/app/nook/record/domain/enums/Emotion.java, src/main/java/app/nook/record/dto/BookRecordDto.java
EmotionEMPTY 추가. RecordEmotionDto.emotion 타입을 EmotionString으로 변경.
저장소 계층
src/main/java/app/nook/record/repository/RecordQueryRepository.java, src/main/java/app/nook/record/repository/RecordQueryRepositoryImpl.java
countRecordsByEmotion 시그니처를 (Long userId)(Long userId, Long bookId)로 변경. 쿼리 필터에 bookId 추가 및 그룹 프로젝션을 record.emotion.stringValue()로 변경.
서비스 계층
src/main/java/app/nook/record/service/RecordCommandService.java, src/main/java/app/nook/record/service/RecordQueryService.java
RecordCommandServicenormalizeEmotion 추가(널→EMPTY). getRecordEmotionCounts를 사용자 범위에서 getRecordEmotionCounts(User, Long bookId)로 변경하고 책 존재/소유권 검증 및 "ALL" 항목과 감정 정규화(EMPTY 제외) 로직 추가. parseEmotionFilter에서 EMPTY 입력 거부.
컨트롤러 계층
src/main/java/app/nook/record/controller/RecordController.java
감정 집계 엔드포인트를 GET /records/emotionsGET /records/emotions/{bookId}로 변경. getUserRecords/getBookRecords에서 빈 목록에 대해 항상 SuccessCode.OK 반환(기존 NO_CONTENT 분기 제거). 새로 GET /records/{recordId}(단건 조회) 엔드포인트 추가.
테스트 및 문서
src/test/java/.../RecordControllerTest.java, .../RecordRepositoryTest.java, .../RecordQueryServiceTest.java, .../RecordServiceTest.java, src/docs/asciidoc/record-query.adoc
컨트롤러/레포/서비스 테스트를 bookId 기반 호출과 문자열 감정값 예상으로 업데이트. 빈 응답은 200/빈 배열로 문서화. null 감정 정규화 케이스(EMPTY) 테스트 추가. 문서에서 단건 조회 섹션 및 감정 집계 설명을 책 기준으로 수정.

토큰 재발급 인증 흐름

Layer / File(s) Summary
데이터 형태
src/main/java/app/nook/user/dto/UserDTO.java, src/main/java/app/nook/user/redis/TokenRedis.java
UserDTOTokenReissueRequest/TokenReissueResponse 추가 및 로그인 응답에 refreshToken 포함. TokenRedis.refreshToken@Indexed 추가.
저장소 계층
src/main/java/app/nook/user/redis/TokenRedisRepository.java
findByRefreshToken(String) 메서드 추가(옵셔널 반환).
서비스 계층
src/main/java/app/nook/user/service/UserService.java, src/main/java/app/nook/user/jwt/JwtProvider.java
UserService.reissueAccessToken(String) 추가: 리프레시 토큰 검증, 만료/유효성 판단, Redis 조회, 사용자 조회, 새 액세스·리프레시 토큰 생성 및 Redis 저장, 응답 반환. JwtProvider.createAccessToken에서 subject(이메일) 제거 및 extractEmail 메서드 삭제.
필터 계층
src/main/java/app/nook/user/filter/JwtFilter.java
TokenRedisRepository 의존성 제거. 토큰 유효할 때만 인증 설정, 만료 시 경고 로깅 후 계속 진행, 유효하지 않은 토큰은 예외 발생. 토큰에서 userId 클레임을 읽어 userRepository.findById로 사용자 로드(이전 이메일 기반 로직 제거).
컨트롤러/통합
src/main/java/app/nook/user/controller/AuthController.java, src/main/java/app/nook/user/oauth/OAuthService.java
POST /auth/reissue 엔드포인트 추가로 TokenReissueRequest 처리 및 TokenReissueResponse 반환. OAuth 로그인 응답에 refreshToken 포함.
테스트
src/test/java/.../AuthControllerTest.java, .../JwtFilterTest.java, .../UserServiceTest.java
로그인 응답에 refreshToken 검증 추가. 리프레시 재발급 성공/만료/레디스 없음/유효하지 않은 토큰 케이스 테스트 추가. JwtFilter의 유효/만료/무효/헤더 없음 시 동작 테스트 추가.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Controller
    participant QueryService
    participant Repo
    participant DB

    Client->>Controller: GET /api/v1/records/emotions/{bookId}
    Controller->>QueryService: getRecordEmotionCounts(user, bookId)
    QueryService->>QueryService: validate book exists & ownership
    QueryService->>Repo: countRecordsByEmotion(userId, bookId)
    Repo->>DB: query grouped emotion counts for user+book
    DB-->>Repo: grouped counts (emotion string, count)
    Repo-->>QueryService: RecordEmotionCountResponse
    QueryService->>QueryService: normalize (exclude EMPTY), prepend "ALL"
    QueryService-->>Controller: ApiResponse(200, payload)
    Controller-->>Client: 200 OK
Loading
sequenceDiagram
    actor Client
    participant AuthController
    participant UserService
    participant JwtProvider
    participant Redis
    participant UserRepo

    Client->>AuthController: POST /api/v1/auth/reissue {refreshToken}
    AuthController->>UserService: reissueAccessToken(refreshToken)
    UserService->>JwtProvider: validateToken(refreshToken)
    JwtProvider-->>UserService: valid/expired/invalid
    alt expired
        UserService-->>AuthController: throw TOKEN_EXPIRED
    else valid
        UserService->>Redis: findByRefreshToken(refreshToken)
        Redis-->>UserService: TokenRedis{userId}
        UserService->>UserRepo: findById(userId)
        UserRepo-->>UserService: User
        UserService->>JwtProvider: createAccessToken(user)
        JwtProvider-->>UserService: accessToken
        UserService->>Redis: save(new TokenRedis with tokens)
        Redis-->>UserService: saved
        UserService-->>AuthController: TokenReissueResponse(accessToken, refreshToken)
        AuthController-->>Client: 200 OK
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • UMC-NOOK/Server#247: JWT/토큰 재발급·리프레시 토큰 관리 관련 동일 영역 변경( JwtFilter, JwtProvider, AuthController, TokenRedis 등 ).
  • UMC-NOOK/Server#260: Record/Library/인증 흐름 전반에서 유사 변경(기록 감정 쿼리/컨트롤러와 토큰 처리 관련)과 중복 영역 존재.
  • UMC-NOOK/Server#148: RecordController의 빈 결과 처리(200 vs 204) 변경 관련 연관성 있음.

"🐰
책별 감정 세고, 토큰은 새로 발급해,
페이지를 넘기며 기록은 계속되네.
만료된 키는 경고로 남기고,
새 길 열어주는 건 내 바삭한 발길!
📚✨"

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning PR이 이슈 #221의 '로그인 리팩토링' 일부 항목은 구현했으나, 목표한 RestTemplate→RestClient 전환, 커스텀 어노테이션 적용, 로그아웃/블랙리스트/회원탈퇴 기능은 미구현 상태입니다. 이슈 #221의 나머지 작업 항목(RestTemplate→RestClient 전환, 로그아웃 블랙리스트, 회원탈퇴 기능 등)을 구현하거나, 이번 PR의 범위를 명확히 하기 위해 별도의 서브 이슈로 분할 후 추적하세요.
Docstring Coverage ⚠️ Warning Docstring coverage is 13.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 주요 변경사항인 '독서 기록 API 수정'과 '로그인 리팩토링'을 명확하게 요약하고 있습니다.
Out of Scope Changes check ✅ Passed PR에 독서 기록 API 개선, 토큰 재발급 로직 분리, 포커스 헤더 테스트 리팩토링 등 여러 범위의 변경사항이 포함되어 있으나, 모두 로그인/기록 기능과 관련된 영역 내 변경으로 보입니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#221-login-refactor

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 50 minutes and 9 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@JiwonLee42
Copy link
Copy Markdown
Contributor Author

Overall Project 67.37% -0.13% 🍏
Files changed 95.4% 🍏

File Coverage
RecordController.java 100% 🍏
Emotion.java 100% 🍏
RecordQueryService.java 100% 🍏
AuthController.java 100% 🍏
Library.java 100% 🍏
RecordCommandService.java 96.28% 🍏
JwtFilter.java 90.2% -4.9% 🍏
UserService.java 89.91% -4.59% 🍏
OAuthService.java 63.2% 🍏
JwtProvider.java 30.33% 🍏
RecordQueryRepositoryImpl.java 14.17% 🍏

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/main/java/app/nook/user/dto/UserDTO.java (1)

12-22: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

refreshToken을 JSON 응답 본문에 노출하면 XSS 취약점에 노출될 수 있습니다

refreshToken은 수명이 3일로 길고, 새 액세스 토큰을 계속 발급받는 데 사용되므로 accessToken보다 훨씬 민감한 자산입니다. 응답 본문(JSON)에 포함하면 브라우저의 JavaScript에서 접근 가능해져 XSS 공격으로 탈취될 위험이 있습니다.

보안 권장 사항:

  • refreshTokenHttpOnly, Secure, SameSite=Strict 속성을 갖춘 쿠키로 전달하세요.
  • 응답 JSON(LoginResponse)에서는 refreshToken 필드를 제거하고, 서버에서 쿠키로 직접 설정해야 합니다.
  • /auth/reissue 엔드포인트도 동일하게 요청 바디 대신 쿠키에서 refreshToken을 읽어야 합니다.

웹 클라이언트 외에 모바일 앱만 사용하는 경우라면 응답 본문 전달도 허용될 수 있으나, 이 경우에도 저장 위치(SecureStorage 등)에 대한 클라이언트 측 보안 가이드라인이 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/app/nook/user/dto/UserDTO.java` around lines 12 - 22,
LoginResponse currently exposes the sensitive refreshToken in JSON; remove the
refreshToken field from the LoginResponse DTO (class LoginResponse) and stop
returning it in login responses, and instead set the refresh token as a
HttpOnly, Secure, SameSite=Strict cookie on the server; also update the
`/auth/reissue` flow to read the refresh token from the cookie (not request
body) and adjust any methods that previously relied on
LoginResponse.refreshToken to use the cookie-based token retrieval.
src/main/java/app/nook/user/redis/TokenRedis.java (1)

15-25: ⚠️ Potential issue | 🟠 Major

Redis 메모리 누수: @Indexed 필드의 세컨더리 인덱스가 정리되지 않음

@RedisHash(timeToLive = 60*60*24*3)로 설정된 기본 해시는 3일 후 만료되지만, @Indexed 필드의 세컨더리 인덱스 셋(token:refreshToken:<value>, token:accessToken:<value>)은 TTL이 적용되지 않아 영구적으로 Redis에 잔류합니다.

원인:

  • RedisConfig@EnableRedisRepositories에서 enableKeyspaceEvents가 활성화되지 않음 (현재 기본값)
  • application.ymlspring.data.redis.notify-keyspace-events 설정 없음
  • 코드베이스에 Redis 키 만료 이벤트 리스너 없음

영향:

  • refreshTokenaccessToken 모두 @Indexed로 지정되어 있어 매 로그인마다 새로운 인덱스 항목 두 개씩 누적
  • 고빈도 로그인 환경에서 Redis 메모리 지속적 증가 → 메모리 한계 도달 가능

해결 방법:
@EnableRedisRepositories(enableKeyspaceEvents = "notify-keyspace-events") 활성화 및 Redis 서버의 keyspace 알림 설정(notify-keyspace-events = KEA)과 함께 구성해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/app/nook/user/redis/TokenRedis.java` around lines 15 - 25, The
TokenRedis entity uses `@Indexed` on refreshToken and accessToken which creates
secondary index keys that never expire because keyspace notifications are not
enabled; update your Redis repository configuration (the class with
`@EnableRedisRepositories`) to set enableKeyspaceEvents="notify-keyspace-events"
and ensure your application.yml sets
spring.data.redis.notify-keyspace-events=KEA on the Redis server, then add a
Redis keyspace event listener/bean to react to EXPIRATION/DEL events and remove
the corresponding secondary index entries (or alternatively stop indexing these
fields if you prefer no secondary keys); reference TokenRedis, the
`@EnableRedisRepositories` annotation in RedisConfig, and the application.yml
property when making the changes.
src/docs/asciidoc/record-query.adoc (1)

30-33: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

path-parameters.adoc 누락 — bookId 경로 파라미터가 문서에 표시되지 않음

컨트롤러 테스트(RecordControllerTest.java Lines 360-362)에서 pathParametersbookId를 문서화하고 있어 스니펫이 생성되지만, record-query.adoc에 해당 include가 없습니다. "특정 책 기록 감정 필터 조회" 섹션의 패턴을 참고하여 추가하세요.

📄 제안 수정
 include::{snippets}/record-controller-test/독서_기록_감정별_개수_조회_성공/http-request.adoc[opts=optional]
 include::{snippets}/record-controller-test/독서_기록_감정별_개수_조회_성공/request-headers.adoc[opts=optional]
+include::{snippets}/record-controller-test/독서_기록_감정별_개수_조회_성공/path-parameters.adoc[opts=optional]
 include::{snippets}/record-controller-test/독서_기록_감정별_개수_조회_성공/http-response.adoc[opts=optional]
 include::{snippets}/record-controller-test/독서_기록_감정별_개수_조회_성공/response-fields.adoc[opts=optional]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/docs/asciidoc/record-query.adoc` around lines 30 - 33, The documentation
is missing the path-parameters include for the "독서_기록_감정별_개수_조회_성공" snippets so
the bookId path parameter documented in RecordControllerTest.java (the
pathParameters snippet) doesn't appear; add an include line for the
path-parameters snippet
(include::{snippets}/record-controller-test/독서_기록_감정별_개수_조회_성공/path-parameters.adoc[opts=optional])
into record-query.adoc in the same place/pattern used by the "특정 책 기록 감정 필터 조회"
section so the bookId path parameter is rendered.
🧹 Nitpick comments (3)
src/test/java/app/nook/user/service/UserServiceTest.java (1)

194-206: ⚡ Quick win

validateToken → false, isExpiredToken → false 분기(순수 형식 오류 토큰)에 대한 테스트 누락

UserService.reissueAccessToken Lines 91-92의 throw new CustomException(AuthErrorCode.INVALID_TOKEN) 분기는 현재 테스트에서 검증되지 않습니다. 레디스에없는토큰_예외 테스트는 validateToken → true 경로에서의 INVALID_TOKEN이므로, JWT 자체가 유효하지 않지만(만료는 아닌) 서명 오류·형식 오류 등의 경우에 대한 별도 테스트가 필요합니다.

✅ 누락된 테스트 케이스 추가 제안
`@Test`
void reissueAccessToken_유효하지않은토큰_예외() {
    String refreshToken = "malformed-token";
    given(jwtProvider.validateToken(refreshToken)).willReturn(false);
    given(jwtProvider.isExpiredToken(refreshToken)).willReturn(false); // 만료 아님, 형식 오류

    CustomException ex = assertThrows(
            CustomException.class,
            () -> userService.reissueAccessToken(refreshToken)
    );

    assertThat(ex.getErrorCode()).isEqualTo(AuthErrorCode.INVALID_TOKEN);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/app/nook/user/service/UserServiceTest.java` around lines 194 -
206, Add a unit test for the branch where the JWT is
syntactically/signature-invalid but not expired: in UserServiceTest add a test
(e.g., reissueAccessToken_유효하지않은토큰_예외) that stubs
jwtProvider.validateToken(refreshToken) to return false and
jwtProvider.isExpiredToken(refreshToken) to return false, then calls
userService.reissueAccessToken(refreshToken) and asserts a CustomException with
error code AuthErrorCode.INVALID_TOKEN; this ensures the throw new
CustomException(AuthErrorCode.INVALID_TOKEN) path in
UserService.reissueAccessToken is exercised.
src/test/java/app/nook/user/filter/JwtFilterTest.java (1)

51-130: ⚡ Quick win

userId 클레임 누락 케이스도 테스트로 고정해 두면 좋겠습니다.

지금 스위트는 userId가 있는 정상 케이스만 검증합니다. src/main/java/app/nook/user/filter/JwtFilter.java의 Line 64~Line 67은 이 클레임이 없으면 UNAUTHORIZED를 던지도록 바뀌었으니, validateToken()==true인데 parseClaims()userId가 없는 경우를 하나 추가해 두면 이번 계약 변경을 안정적으로 묶어둘 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/app/nook/user/filter/JwtFilterTest.java` around lines 51 - 130,
Add a test in JwtFilterTest (e.g., method named
유효한_토큰이지만_userId_클레임이_없으면_UNAUTHORIZED를_낸다) that simulates
jwtProvider.validateToken(accessToken) == true but
jwtProvider.parseClaims(accessToken) returns Claims without "userId"; call
jwtFilter.doFilter(...) and assert that
SecurityContextHolder.getContext().getAuthentication() is null, that the
response has the UNAUTHORIZED status (or JwtFilter's expected status), and that
filterChain.doFilter(...) is not invoked; reference jwtProvider.validateToken,
jwtProvider.parseClaims, JwtFilter.doFilter and SecurityContextHolder to locate
the code to modify.
src/test/java/app/nook/record/service/RecordQueryServiceTest.java (1)

232-241: ⚡ Quick win

인덱스 기반 단언이 Emotion 열거형 선언 순서에 의존

get(2), get(5), get(6) 방식의 단언은 Emotion 열거형의 선언 순서가 변경되거나 새 값이 추가될 경우 오탐 또는 잘못된 검증을 유발합니다. 감정 이름 기반으로 조회하는 방식을 권장합니다.

♻️ 리팩터링 제안 (인덱스 대신 감정 이름으로 필터링)
-assertThat(result.emotionCounts().get(0).emotion()).isEqualTo("ALL");
-assertThat(result.emotionCounts().get(0).recordCount()).isEqualTo(8L);
-assertThat(result.emotionCounts().get(1).emotion()).isEqualTo("FUN");
-assertThat(result.emotionCounts().get(1).recordCount()).isEqualTo(5L);
-assertThat(result.emotionCounts().get(2).emotion()).isEqualTo("EMPATHIZING");
-assertThat(result.emotionCounts().get(2).recordCount()).isZero();
-assertThat(result.emotionCounts().get(5).emotion()).isEqualTo("SAD");
-assertThat(result.emotionCounts().get(5).recordCount()).isEqualTo(3L);
-assertThat(result.emotionCounts().get(6).emotion()).isEqualTo("UNCOMFORTABLE");
-assertThat(result.emotionCounts().get(6).recordCount()).isZero();
+assertThat(result.emotionCounts().get(0).emotion()).isEqualTo("ALL");
+assertThat(result.emotionCounts().get(0).recordCount()).isEqualTo(8L);
+assertThat(result.emotionCounts())
+    .filteredOn(item -> item.emotion().equals("FUN"))
+    .singleElement()
+    .extracting(BookRecordDto.RecordEmotionDto::recordCount)
+    .isEqualTo(5L);
+assertThat(result.emotionCounts())
+    .filteredOn(item -> item.emotion().equals("SAD"))
+    .singleElement()
+    .extracting(BookRecordDto.RecordEmotionDto::recordCount)
+    .isEqualTo(3L);
+assertThat(result.emotionCounts())
+    .filteredOn(item -> item.emotion().equals("EMPATHIZING"))
+    .singleElement()
+    .extracting(BookRecordDto.RecordEmotionDto::recordCount)
+    .isZero();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/app/nook/record/service/RecordQueryServiceTest.java` around
lines 232 - 241, Replace index-based assertions on result.emotionCounts() (e.g.,
get(2), get(5), get(6)) with lookups that find entries by emotion name so tests
no longer depend on Emotion enum declaration order; for example, locate the
element whose emotion() equals "EMPATHIZING"/"SAD"/"UNCOMFORTABLE" and assert
its recordCount() (and emotion()) accordingly. Update assertions around
result.emotionCounts().get(0) and get(1) similarly to verify by emotion name
("ALL", "FUN") rather than by position. Ensure you use a clear helper or stream
filter (e.g., filtering result.emotionCounts() by e -> e.emotion().equals(...))
to find the matching element and fail the test if not present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/app/nook/record/domain/enums/Emotion.java`:
- Line 5: RecordQueryServiceTest의 위치 기반 어서션이 현재 Emotion enum 선언 순서와 불일치해 실패합니다;
수정 방법: 테스트 코드(RecordQueryServiceTest)에서 Emotion.values()를 기반으로 하거나 하드코딩된 기대값을 현재
enum 순서로 맞춰 변경하세요 — 즉 enum 선언은 FUN, EMPATHIZING, USEFUL, COMPLICATED, SAD,
UNCOMFORTABLE(EMPTY 제외) 이므로 test의 get(2), get(3), get(4), get(5) 등 위치 기반 기대 문자열을
각각 "EMPATHIZING", "USEFUL", "COMPLICATED", "SAD" 로 업데이트하거나 대신
Emotion.values()[index].name()를 사용하도록 변경해 위치 변화에 강하게 만드세요.

In `@src/main/java/app/nook/record/repository/RecordQueryRepositoryImpl.java`:
- Around line 299-310: The query in RecordQueryRepositoryImpl
(countRecordsByEmotion / the fetch into BookRecordDto.RecordEmotionDto) can
return NULL for record.emotion, causing Emotion.valueOf(item.emotion()) in
RecordQueryService to throw NPE; fix by excluding NULL emotions at query time
(add record.emotion.isNotNull() to the .where(...) clause) so
RecordEmotionDto.emotion() is never null, or if you prefer a defensive approach
adjust RecordQueryService to skip/null-check items before calling
Emotion.valueOf; prefer the query-level change in countRecordsByEmotion().

In `@src/main/java/app/nook/record/service/RecordQueryService.java`:
- Around line 93-110: The code currently excludes Emotion.EMPTY from
normalizedEmotionCounts which makes ALL (response.totalCount()) larger than the
sum of individual emotions; to fix, include EMPTY in the per-emotion list so
sums match: in RecordQueryService change the normalizedEmotionCounts
construction (the stream filtering of Emotion.values()) to include Emotion.EMPTY
(i.e., remove the .filter(emotion -> emotion != Emotion.EMPTY) or explicitly add
a BookRecordDto.RecordEmotionDto for Emotion.EMPTY using
emotionCountMap.getOrDefault(Emotion.EMPTY, 0L)), then keep building
emotionCountsWithAll (with "ALL" using response.totalCount()) so totalCount
equals the sum of the per-emotion entries.

In `@src/main/java/app/nook/user/filter/JwtFilter.java`:
- Around line 43-46: JwtFilter currently only logs expired/invalid token cases
(jwtProvider.isExpiredToken(accessToken)) and continues the filter chain, which
prevents JwtExceptionFilter from returning JWT-specific errors; update JwtFilter
(the doFilter/doFilterInternal branch handling jwtProvider.isExpiredToken(...)
and the invalid token branch) to stop the chain and surface the failure by
either throwing the existing JwtException (or a specific
TokenExpiredException/InvalidTokenException used in the project) or by writing
the JWT error response directly to the HttpServletResponse and returning, so
JwtExceptionFilter can handle or the correct TOKEN_EXPIRED/INVALID_TOKEN
response is returned instead of falling through to generic 401/403 handling.

In `@src/main/java/app/nook/user/service/UserService.java`:
- Around line 101-111: The code stores and reuses the same refreshToken when
issuing a new access token; implement refresh-token rotation by generating a new
refresh token (e.g., via jwtProvider.createRefreshToken or similar) when
reissuing, replace the stored TokenRedis.refreshToken with the newly generated
token in tokenRedisRepository.save (update the existing TokenRedis entry built
by TokenRedis.builder for user.getId()), and return the new refresh token
alongside the new access token in UserDTO.TokenReissueResponse so the client
receives and persists the rotated token; also ensure any logic that checks for
reuse can detect and handle presentation of an old refresh token (invalidate or
log reuse) if present.

---

Outside diff comments:
In `@src/docs/asciidoc/record-query.adoc`:
- Around line 30-33: The documentation is missing the path-parameters include
for the "독서_기록_감정별_개수_조회_성공" snippets so the bookId path parameter documented in
RecordControllerTest.java (the pathParameters snippet) doesn't appear; add an
include line for the path-parameters snippet
(include::{snippets}/record-controller-test/독서_기록_감정별_개수_조회_성공/path-parameters.adoc[opts=optional])
into record-query.adoc in the same place/pattern used by the "특정 책 기록 감정 필터 조회"
section so the bookId path parameter is rendered.

In `@src/main/java/app/nook/user/dto/UserDTO.java`:
- Around line 12-22: LoginResponse currently exposes the sensitive refreshToken
in JSON; remove the refreshToken field from the LoginResponse DTO (class
LoginResponse) and stop returning it in login responses, and instead set the
refresh token as a HttpOnly, Secure, SameSite=Strict cookie on the server; also
update the `/auth/reissue` flow to read the refresh token from the cookie (not
request body) and adjust any methods that previously relied on
LoginResponse.refreshToken to use the cookie-based token retrieval.

In `@src/main/java/app/nook/user/redis/TokenRedis.java`:
- Around line 15-25: The TokenRedis entity uses `@Indexed` on refreshToken and
accessToken which creates secondary index keys that never expire because
keyspace notifications are not enabled; update your Redis repository
configuration (the class with `@EnableRedisRepositories`) to set
enableKeyspaceEvents="notify-keyspace-events" and ensure your application.yml
sets spring.data.redis.notify-keyspace-events=KEA on the Redis server, then add
a Redis keyspace event listener/bean to react to EXPIRATION/DEL events and
remove the corresponding secondary index entries (or alternatively stop indexing
these fields if you prefer no secondary keys); reference TokenRedis, the
`@EnableRedisRepositories` annotation in RedisConfig, and the application.yml
property when making the changes.

---

Nitpick comments:
In `@src/test/java/app/nook/record/service/RecordQueryServiceTest.java`:
- Around line 232-241: Replace index-based assertions on result.emotionCounts()
(e.g., get(2), get(5), get(6)) with lookups that find entries by emotion name so
tests no longer depend on Emotion enum declaration order; for example, locate
the element whose emotion() equals "EMPATHIZING"/"SAD"/"UNCOMFORTABLE" and
assert its recordCount() (and emotion()) accordingly. Update assertions around
result.emotionCounts().get(0) and get(1) similarly to verify by emotion name
("ALL", "FUN") rather than by position. Ensure you use a clear helper or stream
filter (e.g., filtering result.emotionCounts() by e -> e.emotion().equals(...))
to find the matching element and fail the test if not present.

In `@src/test/java/app/nook/user/filter/JwtFilterTest.java`:
- Around line 51-130: Add a test in JwtFilterTest (e.g., method named
유효한_토큰이지만_userId_클레임이_없으면_UNAUTHORIZED를_낸다) that simulates
jwtProvider.validateToken(accessToken) == true but
jwtProvider.parseClaims(accessToken) returns Claims without "userId"; call
jwtFilter.doFilter(...) and assert that
SecurityContextHolder.getContext().getAuthentication() is null, that the
response has the UNAUTHORIZED status (or JwtFilter's expected status), and that
filterChain.doFilter(...) is not invoked; reference jwtProvider.validateToken,
jwtProvider.parseClaims, JwtFilter.doFilter and SecurityContextHolder to locate
the code to modify.

In `@src/test/java/app/nook/user/service/UserServiceTest.java`:
- Around line 194-206: Add a unit test for the branch where the JWT is
syntactically/signature-invalid but not expired: in UserServiceTest add a test
(e.g., reissueAccessToken_유효하지않은토큰_예외) that stubs
jwtProvider.validateToken(refreshToken) to return false and
jwtProvider.isExpiredToken(refreshToken) to return false, then calls
userService.reissueAccessToken(refreshToken) and asserts a CustomException with
error code AuthErrorCode.INVALID_TOKEN; this ensures the throw new
CustomException(AuthErrorCode.INVALID_TOKEN) path in
UserService.reissueAccessToken is exercised.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5902144b-b787-49e8-bb3d-3e4969cdb545

📥 Commits

Reviewing files that changed from the base of the PR and between da3c7b9 and 750ffcc.

📒 Files selected for processing (24)
  • src/docs/asciidoc/record-query.adoc
  • src/main/java/app/nook/library/domain/Library.java
  • src/main/java/app/nook/record/controller/RecordController.java
  • src/main/java/app/nook/record/domain/enums/Emotion.java
  • src/main/java/app/nook/record/dto/BookRecordDto.java
  • src/main/java/app/nook/record/repository/RecordQueryRepository.java
  • src/main/java/app/nook/record/repository/RecordQueryRepositoryImpl.java
  • src/main/java/app/nook/record/service/RecordCommandService.java
  • src/main/java/app/nook/record/service/RecordQueryService.java
  • src/main/java/app/nook/user/controller/AuthController.java
  • src/main/java/app/nook/user/dto/UserDTO.java
  • src/main/java/app/nook/user/filter/JwtFilter.java
  • src/main/java/app/nook/user/jwt/JwtProvider.java
  • src/main/java/app/nook/user/oauth/OAuthService.java
  • src/main/java/app/nook/user/redis/TokenRedis.java
  • src/main/java/app/nook/user/redis/TokenRedisRepository.java
  • src/main/java/app/nook/user/service/UserService.java
  • src/test/java/app/nook/controller/record/RecordControllerTest.java
  • src/test/java/app/nook/controller/user/AuthControllerTest.java
  • src/test/java/app/nook/record/repository/RecordRepositoryTest.java
  • src/test/java/app/nook/record/service/RecordQueryServiceTest.java
  • src/test/java/app/nook/record/service/RecordServiceTest.java
  • src/test/java/app/nook/user/filter/JwtFilterTest.java
  • src/test/java/app/nook/user/service/UserServiceTest.java
💤 Files with no reviewable changes (1)
  • src/main/java/app/nook/user/jwt/JwtProvider.java

Comment thread src/main/java/app/nook/record/domain/enums/Emotion.java
Comment thread src/main/java/app/nook/record/service/RecordQueryService.java
Comment thread src/main/java/app/nook/user/filter/JwtFilter.java
Comment thread src/main/java/app/nook/user/service/UserService.java Outdated
@JiwonLee42
Copy link
Copy Markdown
Contributor Author

Overall Project 67.06% -0.38% 🍏
Files changed 88.59% 🍏

File Coverage
RecordController.java 100% 🍏
Emotion.java 100% 🍏
AuthController.java 100% 🍏
Library.java 100% 🍏
RecordCommandService.java 96.28% 🍏
UserService.java 92.38% -2.24% 🍏
RecordQueryService.java 90.7% -9.3% 🍏
JwtFilter.java 90.65% -4.67% 🍏
OAuthService.java 63.2% 🍏
RecordQueryRepositoryImpl.java 14.17% 🍏
JwtProvider.java 11.48% 🍏

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/test/java/app/nook/controller/focus/FocusControllerTest.java (1)

107-129: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Line 129의 endFocus 스텁에서 eq(1L)로 하드코딩된 userId — @WithCustomUser 기본값과의 암묵적 결합

focusService.endFocus(eq(1L), eq(request))로 스텁을 등록하고 있어, @WithCustomUser가 주입하는 userId가 정확히 1L이어야만 스텁이 매칭됩니다. Line 75의 startFocus 스텁은 any(User.class)를 사용해 User 객체 전체를 무시하고 매칭되므로, 같은 방식으로 endFocus도 개선할 수 있습니다.

현재 @WithCustomUser의 기본값이 userId = 1L이므로 테스트가 통과하지만, 만약 기본값이 변경되면 matcher가 실패하고 stub은 null을 반환하게 됩니다.

🔧 수정 제안
-        given(focusService.endFocus(eq(1L), eq(request))).willReturn(response);
+        given(focusService.endFocus(anyLong(), eq(request))).willReturn(response);

또는 명시적 상수로 참조:

+        // WithCustomUser의 기본 userId 값과 일치해야 함
         given(focusService.endFocus(eq(1L), eq(request))).willReturn(response);
🤖 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/test/java/app/nook/controller/focus/FocusControllerTest.java` around
lines 107 - 129, The stub in FocusControllerTest hardcodes the userId with
eq(1L) when stubbing focusService.endFocus, which couples the test to
`@WithCustomUser`'s default; change the matcher to a non-specific one (e.g.,
anyLong() or any(Long.class)) so the stub doesn't rely on a specific userId.
Update the given call that references focusService.endFocus(eq(1L), eq(request))
to use focusService.endFocus(anyLong(), eq(request)) (or anyLong() for primitive
long / any(Long.class) for Long) to match the startFocus-style flexible
matching.
🤖 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/test/java/app/nook/controller/record/RecordControllerTest.java`:
- Around line 654-658: The response-field documentation for result.emotion (in
the test using fieldWithPath("result.emotion")) omits the possible value
"EMPTY"; update the description passed to fieldWithPath("result.emotion") to
list all allowed values including EMPTY (e.g., "기록 감정 값. EMPTY, FUN,
EMPATHIZING, USEFUL, COMPLICATED, SAD, UNCOMFORTABLE 중 하나") so the generated API
docs match the request-side notes and other tests that state "미입력 시 EMPTY로 저장".
- Around line 363-367: 문서의 응답 필드 설명을 명확히 하도록 수정하세요: ApiResponseSnippet 내
fieldWithPath("result.totalCount") 설명에 EMPTY 감정 레코드가 ALL(totalCount)에 포함되지만 개별
감정 버킷에는 포함되지 않는다는 문구를 추가하고, fieldWithPath("result.emotionCounts[].emotion") 설명에는
EMPTY 값이 별도의 집계 버킷으로 제공되지 않음을 명시(요청에서 emotion 미입력 시 EMPTY로 저장됨도 함께 언급). 해당 변경은
RecordControllerTest의 responseFields 블록에 있는 fieldWithPath
항목들("result.totalCount"와 "result.emotionCounts[].emotion")을 수정하여 반영하세요.

---

Outside diff comments:
In `@src/test/java/app/nook/controller/focus/FocusControllerTest.java`:
- Around line 107-129: The stub in FocusControllerTest hardcodes the userId with
eq(1L) when stubbing focusService.endFocus, which couples the test to
`@WithCustomUser`'s default; change the matcher to a non-specific one (e.g.,
anyLong() or any(Long.class)) so the stub doesn't rely on a specific userId.
Update the given call that references focusService.endFocus(eq(1L), eq(request))
to use focusService.endFocus(anyLong(), eq(request)) (or anyLong() for primitive
long / any(Long.class) for Long) to match the startFocus-style flexible
matching.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2dc0433d-3f65-4e7d-86b1-93a90a2ad768

📥 Commits

Reviewing files that changed from the base of the PR and between 750ffcc and 8d133db.

📒 Files selected for processing (14)
  • src/docs/asciidoc/record-query.adoc
  • src/main/java/app/nook/record/controller/RecordController.java
  • src/main/java/app/nook/record/repository/RecordRepository.java
  • src/main/java/app/nook/record/service/RecordQueryService.java
  • src/main/java/app/nook/user/dto/UserDTO.java
  • src/main/java/app/nook/user/filter/JwtFilter.java
  • src/main/java/app/nook/user/service/UserService.java
  • src/test/java/app/nook/controller/focus/FocusControllerTest.java
  • src/test/java/app/nook/controller/focus/FocusThemeControllerTest.java
  • src/test/java/app/nook/controller/library/LibraryControllerTest.java
  • src/test/java/app/nook/controller/record/RecordControllerTest.java
  • src/test/java/app/nook/controller/user/AuthControllerTest.java
  • src/test/java/app/nook/user/filter/JwtFilterTest.java
  • src/test/java/app/nook/user/service/UserServiceTest.java
✅ Files skipped from review due to trivial changes (2)
  • src/test/java/app/nook/controller/library/LibraryControllerTest.java
  • src/docs/asciidoc/record-query.adoc
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/test/java/app/nook/controller/user/AuthControllerTest.java
  • src/main/java/app/nook/user/service/UserService.java
  • src/main/java/app/nook/record/controller/RecordController.java
  • src/test/java/app/nook/user/service/UserServiceTest.java
  • src/main/java/app/nook/record/service/RecordQueryService.java

Comment thread src/test/java/app/nook/controller/record/RecordControllerTest.java
Comment thread src/test/java/app/nook/controller/record/RecordControllerTest.java
@JiwonLee42
Copy link
Copy Markdown
Contributor Author

Overall Project 67.07% -0.38% 🍏
Files changed 88.68% 🍏

File Coverage
RecordController.java 100% 🍏
Emotion.java 100% 🍏
AuthController.java 100% 🍏
Library.java 100% 🍏
RecordCommandService.java 96.28% 🍏
UserService.java 92.48% -2.21% 🍏
RecordQueryService.java 90.7% -9.3% 🍏
JwtFilter.java 90.65% -4.67% 🍏
OAuthService.java 63.2% 🍏
RecordQueryRepositoryImpl.java 14.17% 🍏
JwtProvider.java 11.48% 🍏

@JiwonLee42 JiwonLee42 merged commit 4dd7d58 into develop-demo May 4, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] 로그인 리팩토링

1 participant