Skip to content

Conversation

@Hminkyung
Copy link
Contributor

@Hminkyung Hminkyung commented Jan 25, 2026

#️⃣ 연관된 이슈

관련된 이슈 번호를 적어주세요.
Close #60

✨ 작업 내용 (Summary)

이번 PR에서 작업한 내용을 간략히 설명해주세요. (이미지 첨부 가능)
기존에 토큰을 갱신해도 이전에 토큰이 재사용이 가능한 문제를 해결합니다.

  • 토큰 재발급 성공
  • 토큰 재발급 시 기존 rt는 블랙리스트 처리
  • 토큰 재발급 시 기존 RT로 재검증 시도하면 실패
  • 유효하지 않은 RT로 재발급 시도 시 예외 발생
  • 로그아웃 시 AT 블랙리스트 처리 및 RT 삭제
  • 로그아웃 후 블랙리스트 된 AT로 요청 시 실패해야함
  • 로그아웃 후 RT로 재발급 시도 시 실패
  • ACCESS TOKEN 생성 및 검증 성공
  • REFRESH TOKEN 생성 시 REDIS에 저장
  • REFRESH TOKEN 검증 성공
  • 잘못된 REFRESH TOKEN 검증 실패
  • 토큰 블랙리스트 등록 및 확인
  • 블랙리스트에 등록된 토큰은 검증 시 무효 처리
  • REFRESH TOKEN 삭제 성공
  • 유효하지 않은 토큰 검증 시 예외 발생
  • 남은 만료 시간 계산 정확성
  • 새 REFRESH TOKEN 발급 시 기존 토큰 교체됨

✅ 변경 사항 체크리스트

다음 항목들을 확인하고 체크해주세요.

  • 코드에 영향이 있는 모든 부분에 대한 테스트를 작성하고 실행했나요?
  • 문서를 작성하거나 수정했나요? (필요한 경우)
  • 중요한 변경 사항이 팀에 공유되었나요?

🧪 테스트 결과

코드 변경에 대해 테스트를 수행한 결과를 요약해주세요.

  • 테스트 환경: (예: 로컬, 개발 서버 등) 로컬
  • 테스트 방법: (예: Postman, 단위 테스트, 수동 기능 테스트 등) 다뉘테스트
  • 결과 요약: (예: 모든 테스트 통과, 새로운 테스트 케이스 3개 추가 완료) 모든 테스트 통과

📸 스크린샷

관련된 스크린샷 또는 GIF가 있다면 여기에 첨부해주세요.

스크린샷 2026-01-25 162817 스크린샷 2026-01-25 162848

💬 리뷰 요구사항

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요.


📎 참고 자료

관련 문서, 레퍼런스 링크 등이 있다면 여기에 첨부해주세요.

Summary by CodeRabbit

  • 새로운 기능

    • 카카오·애플·네이버 소셜 로그인 엔드포인트 추가
    • 로그아웃, 회원탈퇴(Authorization 헤더 수신 포함), 소셜 연결 해제 기능 추가
    • 토큰 재발급 응답에 리프레시 토큰 포함
  • 보안 개선

    • 리프레시 토큰 Redis 저장·검증 도입 및 기존 리프레시 토큰 대체 로직 추가
    • 토큰 블랙리스트 도입(유효기간 기반) 및 토큰 검증·예외 처리 강화
  • 테스트

    • 인증 흐름과 JWT 동작(재발급·블랙리스트·삭제·로그아웃·탈퇴) 통합/단위 테스트 추가

@Hminkyung Hminkyung self-assigned this Jan 25, 2026
@Hminkyung Hminkyung added 🐞 bug 버그 이슈 ✔️ test 테스트 민경 labels Jan 25, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 25, 2026

📝 Walkthrough

Walkthrough

소셜 로그인(kakao, Apple, Naver) 엔드포인트와 로그아웃/탈퇴/연결해제(revoke) 로직이 추가되었고, Redis 기반 리프레시 토큰 저장소 및 토큰 블랙리스트가 도입되어 리프레시 재발급·로그아웃·탈퇴 흐름이 재구성되었습니다.

Changes

Cohort / File(s) Summary
DTO 변경
src/main/java/com/hrr/backend/domain/auth/dto/AuthResponseDto.java
TokenReissueResponse 레코드 시그니처가 (String accessToken)(String accessToken, String refreshToken)으로 변경 (공개 API 변경). LoginResponse 인덴트만 조정.
Auth 서비스 변경/추가
src/main/java/com/hrr/backend/domain/auth/service/AuthService.java
kakaoLogin, appleLogin, naverLogin 추가; reissueToken 로직 강화(블랙리스트 체크, Redis에 저장된 리프레시 확인, 새 액세스·리프레시 발급, 기존 리프레시 블랙리스트 처리); logout, withdraw, revoke 메서드 추가 및 트랜잭션/오류 처리 보강.
JWT/토큰 관리 (Redis 연동)
src/main/java/com/hrr/backend/domain/auth/service/JwtService.java
REFRESH_TOKEN_PREFIX 추가. 리프레시 토큰을 refresh_token:{userId}로 저장/교체/삭제, 액세스 토큰 블랙리스트 저장/확인, 토큰 만료 잔여시간 계산, 토큰→userId 추출(만료 토큰 포함) 등 Redis 기반 관리 API 추가. 검증 에러 처리 강화.
컨트롤러 시그니처 변경
src/main/java/com/hrr/backend/domain/auth/controller/AuthController.java
withdraw 엔드포인트에 @RequestHeader("Authorization") String authorizationHeader 추가 및 서비스에 전달하도록 시그니처·호출 변경.
테스트 추가/수정
src/test/java/com/hrr/backend/domain/auth/service/*.java
AuthServiceTest, JwtServiceTest 추가 및 AuthServiceWithdrawTest 수정: 리이슈(액세스+리프레시 발급), 리프레시 교체 시 블랙리스트 처리, 로그아웃 시 블랙리스트·리프레시 삭제, Redis 저장/삭제 및 잔여 TTL 기반 블랙리스트 동작 검증 등. Redis 초기화/스텁 사용.

Sequence Diagram(s)

sequenceDiagram
    actor User as "사용자"
    participant Client as "클라이언트"
    participant AuthService as "AuthService"
    participant JwtService as "JwtService"
    participant Redis as "Redis"

    User->>Client: 리프레시로 토큰 재발급 요청 (Authorization header + refresh token)
    Client->>AuthService: reissueToken(header)
    AuthService->>JwtService: getUserIdFromToken(header) / validateToken
    JwtService->>Redis: GET refresh_token:{userId}
    Redis-->>JwtService: storedRefreshToken
    JwtService-->>AuthService: storedRefreshToken 확인

    AuthService->>JwtService: generateAccessToken(userId)
    JwtService-->>AuthService: newAccessToken
    AuthService->>JwtService: generateRefreshToken(userId)
    JwtService->>Redis: SET refresh_token:{userId} = newRefreshToken (TTL)
    Redis-->>JwtService: OK

    AuthService->>JwtService: getRemainingExpiration(oldRefreshToken)
    JwtService-->>AuthService: remainingTTL
    alt remainingTTL > 0
        AuthService->>JwtService: blacklistToken(oldRefreshToken, remainingTTL)
        JwtService->>Redis: SET blacklist:{hash(oldRefreshToken)} = true (TTL)
        Redis-->>JwtService: OK
    end

    AuthService-->>Client: { accessToken, refreshToken }
Loading
sequenceDiagram
    actor User as "사용자"
    participant Client as "클라이언트"
    participant AuthService as "AuthService"
    participant ExternalOAuth as "ExternalOAuth (Kakao/Apple/Naver)"
    participant UserRepo as "UserRepo"
    participant JwtService as "JwtService"
    participant Redis as "Redis"

    User->>Client: 소셜 로그인 요청 (code / provider token)
    Client->>AuthService: socialLogin(...)
    AuthService->>ExternalOAuth: 프로필/토큰 검증 요청
    ExternalOAuth-->>AuthService: profile (id, email, name)
    AuthService->>UserRepo: upsertUser(profile)
    UserRepo-->>AuthService: User
    AuthService->>JwtService: generateAccessToken(userId)
    JwtService-->>AuthService: accessToken
    AuthService->>JwtService: generateRefreshToken(userId)
    JwtService->>Redis: SET refresh_token:{userId} = refreshToken (TTL)
    Redis-->>JwtService: OK
    AuthService-->>Client: { accessToken, refreshToken, nextStep }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

오래된 토큰엔 검은 표식, 새 토큰은 Redis 둥지에 🕊️
카카오·애플·네이버가 노크하면 사용자 기록이 깨어나고,
로그아웃 한 번이면 액세스는 잠기고 리프레시는 사라지네 🚪
테스트가 문을 두드려 흐름을 점검하니, 빈틈은 남기지 않았네 🔍
축하해요 — 토큰 관리가 더 단단해졌습니다 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목 '[BUG] 토큰 만료 API 수정'은 변경사항의 핵심을 정확히 반영하고 있습니다. 토큰 재발급 시 기존 토큰 무효화 기능을 구현한 것이 명확하게 드러나 있습니다.
Linked Issues check ✅ Passed 이슈 #60의 요구사항(새 토큰 발급 시 기존 토큰 즉시 무효화)이 완전히 충족되었습니다. 토큰 블랙리스트 메커니즘, Refresh Token 검증, 로그아웃 처리 등이 모두 구현되었으며 테스트로 검증되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 토큰 만료 및 무효화 기능 구현에 직접 관련되어 있습니다. 소셜 로그인(Kakao, Apple, Naver) 메서드 추가 및 withdraw, revoke 기능도 토큰 관리 체계와 일관성 있게 통합되었으며 이슈 범위 내입니다.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/#60-token-expired

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

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

Copy link

@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: 4

🤖 Fix all issues with AI agents
In `@src/main/java/com/hrr/backend/domain/auth/service/AuthService.java`:
- Around line 196-213: Call jwtService.validateToken(token) explicitly at the
start of logout (after stripping "Bearer ") to make intent clear before any
parsing, then call jwtService.extractUserId(token) as before; after validation,
check for prior blacklisting (e.g., add and use a
jwtService.isBlacklisted(token) or equivalent) and return early if already
blacklisted to avoid duplicate logout work, otherwise proceed to compute
jwtService.getRemainingExpiration(token) and jwtService.blacklistToken(token,
remainingExpiration) and finally jwtService.deleteRefreshToken(userId).

In `@src/main/java/com/hrr/backend/domain/auth/service/JwtService.java`:
- Around line 154-158: The validateRefreshToken method can throw an NPE if
refreshToken is null; change the comparison to a null-safe check (e.g., use
Objects.equals(refreshToken, storedToken) or explicitly null-check both values)
inside validateRefreshToken so it returns false when either token is null or
they don't match; update references to REFRESH_TOKEN_PREFIX + userId and
storedToken as needed and ensure the method still returns a boolean.

In `@src/test/java/com/hrr/backend/domain/auth/service/JwtServiceTest.java`:
- Around line 71-80: The test JwtServiceTest.validateRefreshTokenFail declares
an unused variable realToken which is unnecessary; either remove the realToken
declaration or use it in the assertion to make the test intent clear. Update the
test method (validateRefreshTokenFail) to eliminate the unused realToken
variable if the test only needs fakeToken, or change the assertion to compare
behavior between realToken and fakeToken via
jwtService.validateRefreshToken(realToken, TEST_USER_ID) as appropriate.
- Around line 30-34: The test profile lacks Redis configuration so tests try to
connect to localhost:6379; add proper Redis test settings in
application-test.yml or spin up a Testcontainers/embedded Redis for tests and
point the test profile to it, remove the unused realToken variable in
JwtServiceTest, replace the hardcoded "30 minutes" usage with the configured
constant (use the same config field used by JwtService, e.g.,
accessTokenValidity / 60000) instead of a literal, and stop using
redisTemplate.keys("*") in JwtServiceTest#setUp — switch to a non-blocking
cleanup strategy or isolate tests to a dedicated test Redis instance and clear
only known test keys or use flushdb on the test instance.
🧹 Nitpick comments (5)
src/main/java/com/hrr/backend/domain/auth/service/JwtService.java (1)

127-129: Boolean 언박싱 주의

redisTemplate.hasKey()Boolean 객체를 반환하며, Redis 연결 문제 시 null이 반환될 수 있습니다. Boolean.TRUE.equals()를 사용하면 더 안전합니다.

♻️ 안전한 null 처리
 public boolean isTokenBlacklisted(String token) {
-    return redisTemplate.hasKey(BLACKLIST_PREFIX + token);
+    return Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX + token));
 }
src/main/java/com/hrr/backend/domain/auth/service/AuthService.java (1)

188-193: 예외 로깅 시 스택 트레이스 전체 노출 주의

log.error("네이버 로그인 중 오류 발생: ", e)에서 전체 예외 스택을 로깅합니다. 운영 환경에서는 민감한 정보가 포함될 수 있으니, 필요한 정보만 로깅하거나 로그 레벨 정책을 확인하세요.

src/test/java/com/hrr/backend/domain/auth/service/JwtServiceTest.java (2)

96-105: 테스트 의도 중복 확인 필요

blacklistedTokenIsInvalid 테스트가 blacklistToken 테스트와 동일한 검증을 수행합니다. 테스트명에 따르면 블랙리스트된 토큰이 "검증 시 무효 처리"되어야 하는데, 실제로는 validateToken 호출 없이 isTokenBlacklisted만 확인합니다.

♻️ 테스트 의도에 맞는 검증 추가
 `@Test`
 `@DisplayName`("블랙리스트에 등록된 토큰은 검증 시 무효 처리")
 void blacklistedTokenIsInvalid() {
     // given
     String accessToken = jwtService.generateAccessToken(TEST_USER_ID);
     jwtService.blacklistToken(accessToken, Duration.ofMinutes(5));

     // when & then
     assertThat(jwtService.isTokenBlacklisted(accessToken)).isTrue();
+    // Note: validateToken 자체는 블랙리스트를 체크하지 않으므로,
+    // 실제 요청 처리 시 isTokenBlacklisted를 먼저 확인하는 로직이 필요합니다.
 }

참고: validateToken이 블랙리스트를 체크하지 않는다면, 이 테스트는 의미가 약합니다. Filter/Interceptor 레벨에서 블랙리스트 체크가 이루어지는지 확인하세요.


134-146: 하드코딩된 만료 시간 가정

assertThat(remaining.toMinutes()).isLessThanOrEqualTo(30)에서 30분이라는 값이 하드코딩되어 있습니다. 설정값이 변경되면 테스트가 실패할 수 있으니, 가능하다면 설정값을 주입받아 검증하세요.

src/test/java/com/hrr/backend/domain/auth/service/AuthServiceTest.java (1)

112-123: 테스트 검증 범위 확장 고려

cannotUseBlacklistedAccessToken 테스트는 블랙리스트 등록 여부만 확인합니다. 실제로 블랙리스트된 토큰으로 API 요청 시 거부되는지는 통합 테스트나 Filter 레벨 테스트에서 검증이 필요합니다.

Copy link

@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: 3

🤖 Fix all issues with AI agents
In `@src/main/java/com/hrr/backend/domain/auth/service/AuthService.java`:
- Around line 80-83: The blacklist check in AuthService is inverted: currently
it throws when a token is NOT blacklisted; change the condition so it throws
only when jwtService.isTokenBlacklisted(refreshToken) is true (i.e., remove the
negation). Update the conditional in the method containing that snippet
(AuthService token/refresh handling) to call
jwtService.isTokenBlacklisted(refreshToken) and throw new
GlobalException(ErrorCode.AUTH_INVALID_TOKEN) only when that returns true so
blacklisted refresh tokens are rejected and normal tokens pass.

In `@src/main/java/com/hrr/backend/domain/auth/service/JwtService.java`:
- Around line 127-129: The isTokenBlacklisted method currently returns the
result of redisTemplate.hasKey(...) which can be null and cause NPE on unboxing;
change the implementation to defensively handle a nullable Boolean by comparing
it to Boolean.TRUE (e.g., use
Boolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX + token))) so the
method always returns a primitive boolean without risk of NPE and still
correctly indicates blacklist membership.

In `@src/test/java/com/hrr/backend/domain/auth/service/JwtServiceTest.java`:
- Around line 106-115: The test's DisplayName claims "blacklisted token is
invalid during validation" but the test only calls
jwtService.isTokenBlacklisted; fix by either (A) changing the test to actually
call jwtService.validateToken(accessToken) and assert it throws the expected
exception (and ensure JwtService.validateToken consults isTokenBlacklisted and
throws when true), referencing methods: JwtService.validateToken,
JwtService.isTokenBlacklisted, JwtService.blacklistToken, or (B) if you don't
want validation to check blacklist, update the test DisplayName to reflect that
it only verifies isTokenBlacklisted (and keep assertions against
jwtService.isTokenBlacklisted(accessToken)). Ensure the test method
name/displayName and assertions are consistent.
🧹 Nitpick comments (3)
src/main/java/com/hrr/backend/domain/auth/service/JwtService.java (1)

131-152: getRemainingExpiration에서 모든 예외를 Duration.ZERO로 처리하면 디버깅이 어려울 수 있습니다.

catch (Exception e)가 만료된 토큰 외의 예외(서명 오류, 파싱 오류 등)도 조용히 삼키고 있습니다. 최소한 로깅을 추가하면 운영 시 문제 추적에 도움이 됩니다.

🛠️ 제안된 수정
     } catch (Exception e) {
+        log.warn("토큰 만료 시간 계산 실패: {}", e.getMessage());
         return Duration.ZERO;
     }

참고: JwtService 클래스에 @Slf4j 어노테이션이 없으므로 추가가 필요합니다.

src/main/java/com/hrr/backend/domain/auth/service/AuthService.java (1)

107-196: 소셜 로그인 메서드들(kakaoLogin, appleLogin, naverLogin)에 상당한 코드 중복이 존재합니다.

JWT 토큰 생성, nextStep 결정, LoginResponse 구성 로직이 4개 메서드(socialLogin 포함)에서 반복됩니다. 공통 로직을 private 헬퍼 메서드로 추출하면 유지보수성이 향상됩니다.

예시:

private AuthResponseDto.LoginResponse buildLoginResponse(User user) {
    String accessToken = jwtService.generateAccessToken(user.getId());
    String refreshToken = jwtService.generateRefreshToken(user.getId());
    String nextStep = user.determineNextStep();
    return new AuthResponseDto.LoginResponse(
            user.getId(), accessToken, refreshToken,
            user.getDisplayName(), user.getDisplayNickname(),
            user.getLoginStatus(), nextStep);
}
src/test/java/com/hrr/backend/domain/auth/service/JwtServiceTest.java (1)

81-90: generateRefreshToken 호출 없이 테스트하면 "잘못된 토큰" 시나리오가 아닌 "저장된 토큰 없음" 시나리오입니다.

현재 Redis에 토큰이 저장되지 않은 상태에서 fakeToken을 검증하므로, storedToken == nullfalse가 반환됩니다. "잘못된 Refresh Token 검증 실패"라는 의도에 맞추려면 실제 토큰을 먼저 저장한 후 다른 토큰으로 검증하는 것이 더 정확한 테스트입니다.

🛠️ 개선 제안
 void validateRefreshTokenFail() {
     // given
+    jwtService.generateRefreshToken(TEST_USER_ID); // Redis에 실제 토큰 저장
     String fakeToken = "fake.refresh.token";

     // when & then
     assertThat(jwtService.validateRefreshToken(fakeToken, TEST_USER_ID)).isFalse();
 }

Copy link

@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: 1

🤖 Fix all issues with AI agents
In `@src/main/java/com/hrr/backend/domain/auth/service/AuthService.java`:
- Around line 228-242: The revoke method only unlinks social accounts but does
not clear the app's refresh token, leaving RTs in Redis and causing
inconsistency with withdraw; update revoke (in AuthService.revoke) to call
jwtService.deleteRefreshToken(userId) after the social revocation/unlink switch
so the user's refresh token is removed the same way withdraw does, ensuring both
flows consistently invalidate RTs.
🧹 Nitpick comments (5)
src/main/java/com/hrr/backend/domain/auth/service/AuthService.java (3)

77-104: reissueToken에서 블랙리스트 중복 검사

Line 78의 jwtService.validateToken(refreshToken)이 내부적으로 이미 블랙리스트 검사를 수행합니다 (JwtService Line 71-73). 따라서 Line 81-83의 isTokenBlacklisted 재검사는 중복입니다.

코드 의도를 명확히 하기 위해 중복 검사를 제거하거나, 주석으로 방어적 검사임을 명시하세요.

♻️ 중복 제거 제안
         // Refresh Token 유효성 검증
         jwtService.validateToken(refreshToken);
 
-        // Redis에 저장된 Refresh Token과 비교 검증
-        if (jwtService.isTokenBlacklisted(refreshToken)) {
-            throw new GlobalException(ErrorCode.AUTH_INVALID_TOKEN);
-        }
-
         // userId 추출
         Long userId = jwtService.extractUserId(refreshToken);

107-196: 로그인 메서드 간 코드 중복이 상당합니다

kakaoLogin, appleLogin, naverLogin, socialLogin 모두 동일한 패턴을 반복합니다:

  1. 외부 API로 사용자 정보 조회
  2. upsert 처리
  3. AT/RT 생성
  4. LoginResponse 구성 및 반환
  5. 동일한 try-catch 구조

토큰 생성 + 응답 구성 로직을 private 헬퍼 메서드로 추출하면 중복을 줄이고, 향후 응답 구조 변경 시 수정 포인트가 하나로 줄어듭니다.

♻️ 헬퍼 메서드 추출 예시
private AuthResponseDto.LoginResponse buildLoginResponse(User user) {
    String accessToken = jwtService.generateAccessToken(user.getId());
    String refreshToken = jwtService.generateRefreshToken(user.getId());
    String nextStep = user.determineNextStep();

    return new AuthResponseDto.LoginResponse(
            user.getId(),
            accessToken,
            refreshToken,
            user.getDisplayName(),
            user.getDisplayNickname(),
            user.getLoginStatus(),
            nextStep
    );
}

198-215: logout에서 이미 블랙리스트된 토큰의 중복 처리 가능성

extractUserId가 JWT 파싱 시 암묵적으로 토큰을 검증하지만, 블랙리스트 확인은 하지 않습니다. 따라서 이미 블랙리스트된 AT로 로그아웃을 다시 호출하면 중복으로 블랙리스트에 등록됩니다 (Redis SET은 덮어쓰므로 에러는 아니지만 불필요한 작업).

방어적으로 처리하려면 블랙리스트 확인 후 early return을 추가할 수 있습니다:

♻️ 방어적 처리 제안
     public void logout(String tokenHeader) {
         String token = tokenHeader.startsWith("Bearer ")
                 ? tokenHeader.substring(7)
                 : tokenHeader;
 
+        // 이미 블랙리스트된 토큰이면 중복 처리 방지
+        if (jwtService.isTokenBlacklisted(token)) {
+            return;
+        }
+
         Long userId = jwtService.extractUserId(token);
src/main/java/com/hrr/backend/domain/auth/service/JwtService.java (2)

70-85: validateTokenfalse를 반환하지 않는 boolean 메서드

이 메서드는 성공 시 true, 실패 시 항상 예외를 던지므로 절대 false를 반환하지 않습니다. boolean 리턴 타입은 호출자에게 "false가 반환될 수 있다"는 잘못된 기대를 줍니다.

두 가지 개선 방향이 있습니다:

  1. 리턴 타입을 void로 변경 (검증 실패 시 예외만 던지는 패턴)
  2. 예외 대신 false 반환 (호출자가 분기 처리하는 패턴)

현재 호출자들(AuthService.reissueToken, 테스트 코드)을 보면 반환값을 사용하는 곳과 무시하는 곳이 혼재하므로, 일관성을 위해 void로 통일하는 것을 권장합니다.


134-155: getRemainingExpiration에서 모든 예외를 무시하면 디버깅이 어려워질 수 있습니다

Line 152의 catch (Exception e)가 모든 예외를 삼키고 Duration.ZERO를 반환합니다. 만료된 토큰은 Duration.ZERO가 적절하지만, 서명 오류나 파싱 오류 등 예상치 못한 문제도 동일하게 처리됩니다.

최소한 예외를 로그로 남기면 운영 중 문제 추적에 도움이 됩니다.

♻️ 로깅 추가 제안
         } catch (Exception e) {
+            log.warn("토큰 만료 시간 계산 실패: {}", e.getMessage());
             return Duration.ZERO;
         }

참고: @Slf4j 어노테이션이 이 클래스에 없으므로, 로깅 추가 시 @Slf4j도 함께 추가해야 합니다.

Copy link

@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: 3

🤖 Fix all issues with AI agents
In `@src/main/java/com/hrr/backend/domain/auth/service/AuthService.java`:
- Around line 217-226: The withdraw method only deletes the refresh token and
doesn't blacklist the active access token, leaving a window where a deleted user
can still call APIs; update withdraw(Long userId) to accept the incoming token
header (same parameter used by logout) and apply the same access-token blacklist
logic used in logout (call the jwtService method that blacklists/saves the
access token to the blacklist) before deleting the refresh token and completing
user.withdraw(); reference the withdraw method, logout method, and
jwtService.deleteRefreshToken/use of the jwtService blacklist method to locate
where to add the tokenHeader handling and blacklist call.
- Around line 80-102: The refresh-token flow in AuthService has a TOCTOU race:
separate checks (jwtService.isTokenBlacklisted, jwtService.validateRefreshToken)
allow two concurrent requests to both succeed and issue duplicate token pairs;
change the logic to atomically validate-and-consume the stored refresh token
(e.g., implement jwtService.validateAndConsumeRefreshToken or similar) so that
Redis performs a single atomic get-and-delete (GETDEL or Lua script) for the key
tied to userId (use jwtService.extractUserId to locate it), throw
GlobalException(ErrorCode.AUTH_INVALID_TOKEN) if the consumed value doesn't
match the presented refreshToken, then proceed to
generateAccessToken/generateRefreshToken and blacklist the old token only if
needed; alternatively serialize issuance per userId with a distributed lock
(Redisson) to prevent concurrent issuance.
- Around line 139-147: The apple refresh_token from
appleTokens.get("refresh_token") is not validated and null can be passed into
socialUserService.upsertAppleUser and later into appleAuthService.revoke; update
AuthService to guard the refresh token: extract appleRefreshToken =
appleTokens.get("refresh_token"), check for null/empty and normalize it (e.g.,
use Optional.empty / empty string / don't overwrite existing DB token) before
calling upsertAppleUser(socialId, appleRefreshToken, request.getName()), and
ensure any revoke logic in appleAuthService.revoke is only called when a
non-null token is present.
🧹 Nitpick comments (3)
src/main/java/com/hrr/backend/domain/auth/service/AuthService.java (3)

107-196: 4개의 로그인 메서드에서 토큰 생성 및 응답 구성 로직이 중복됩니다.

socialLogin, kakaoLogin, appleLogin, naverLogin 모두 동일한 패턴을 반복합니다:

  1. generateAccessToken + generateRefreshToken
  2. determineNextStep
  3. LoginResponse 생성

이를 private 헬퍼로 추출하면 변경 지점이 하나로 수렴되어 유지보수성이 크게 향상됩니다. 특히 토큰 발급 로직이 변경될 때(예: 추가 클레임, 새 필드) 4곳을 수정할 필요가 없어집니다.

♻️ 헬퍼 메서드 추출 예시
+ private AuthResponseDto.LoginResponse buildLoginResponse(User user) {
+     String accessToken = jwtService.generateAccessToken(user.getId());
+     String refreshToken = jwtService.generateRefreshToken(user.getId());
+     String nextStep = user.determineNextStep();
+     return new AuthResponseDto.LoginResponse(
+             user.getId(), accessToken, refreshToken,
+             user.getDisplayName(), user.getDisplayNickname(),
+             user.getLoginStatus(), nextStep);
+ }

72-75: refreshHeadernull일 경우 NullPointerException이 발생합니다.

startsWith("Bearer ")null 입력에서 NPE를 던집니다. 컨트롤러의 @RequestHeader가 기본적으로 필수이므로 보통 null이 전달되지 않지만, 서비스 레이어에서 방어적 검증을 두는 것이 안전합니다. logout (Line 200)에서도 동일합니다.

🛡️ 공통 유틸 추출 제안
+ private String resolveToken(String header) {
+     if (header == null || header.isBlank()) {
+         throw new GlobalException(ErrorCode.AUTH_INVALID_TOKEN);
+     }
+     return header.startsWith("Bearer ") ? header.substring(7) : header;
+ }

38-38: userChallengeRepository 필드를 제거하세요.

이 필드는 어떤 메서드에서도 사용되지 않습니다. @RequiredArgsConstructor를 통해 주입되고 있지만 실제로 참조되지 않으므로 불필요한 Spring 빈 와이어링을 유발하고, 클래스의 책임 범위를 모호하게 만듭니다.

개선 방법:

  • 선언되지 않은 의존성은 제거해야 하며, 향후 필요하다면 그때 추가하세요. Spring 프레임워크의 dependency injection 가이드에서도 필요한 의존성만 주입할 것을 권장합니다.

Copy link

@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: 3

Caution

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

⚠️ Outside diff range comments (1)
src/main/java/com/hrr/backend/domain/auth/service/JwtService.java (1)

70-85: ⚠️ Potential issue | 🟡 Minor

Redis 블랙리스트 검증에 Redis 연결 실패 처리 추가 필요

Line 130의 isTokenBlacklisted 메서드는 redisTemplate.hasKey()를 직접 호출하며, Redis 연결 실패 시 RedisConnectionFailureException 등이 처리되지 않은 채 전파되어 모든 토큰 검증이 실패합니다. 이는 Redis 장애가 전체 인증 시스템을 차단하는 단일 장애 지점(SPOF)이 됩니다.

토큰 블랙리스트는 보안상 중요한 상태이므로 fail-closed(예외 전파)가 안전하지만, Redis 타임아웃으로 인한 무한 대기나 서비스 정지를 피하려면 다음을 권장합니다:

  1. Redis 클라이언트 타임아웃 설정: LettuceConnectionFactory에서 setTimeout()setConnectTimeout()으로 빠른 실패(fast-fail) 구현
  2. Circuit Breaker 추가: Resilience4j @CircuitBreaker로 연속 실패 시 Redis 호출을 일시 중단하고 503 응답 반환
  3. 선택사항: Redis 장애 시 제한된 시간 동안만 토큰 검증을 건너뛰는 하이브리드 방식(모니터링 필수)

현재는 설정이 보이지 않으므로, Spring Boot 환경설정에서 Redis 타임아웃을 명시적으로 설정하는 것부터 시작하세요.

🤖 Fix all issues with AI agents
In `@src/main/java/com/hrr/backend/domain/auth/service/AuthService.java`:
- Around line 206-223: The logout method currently calls
jwtService.extractUserId(token) which throws on expired tokens; catch
ExpiredJwtException around that call and extract the userId from
exception.getClaims() (or fallback to a safe parse method) so logout proceeds
for expired ATs; then continue to compute remainingExpiration, blacklistToken
(if any) and deleteRefreshToken(userId) as before, ensuring you only rethrow
other JWT errors. Reference: method logout, jwtService.extractUserId,
jwtService.getRemainingExpiration, jwtService.blacklistToken,
jwtService.deleteRefreshToken.
- Around line 253-268: The revoke method currently lets exceptions from external
social calls (naverAuthService.revoke, appleAuthService.revoke,
kakaoAuthService.unlink) prevent jwtService.deleteRefreshToken(userId) from
running; modify AuthService.revoke to ensure refresh-token deletion always
occurs by wrapping the social-service call(s) in a try-catch or try-finally
block: call the appropriate branch (based on socialAuth.getSocialType()) inside
a try, log any exception with context (including socialAuth identifiers) and
suppress or rethrow as appropriate, and then call
jwtService.deleteRefreshToken(userId) in the finally (or after catching) so
token cleanup always runs even when social unlink/revoke fails.

In
`@src/test/java/com/hrr/backend/domain/auth/service/AuthServiceWithdrawTest.java`:
- Around line 81-106: The test withdraw_success_no_active_challenges is missing
verification that jwtService.blacklistToken is called when
jwtService.getRemainingExpiration returns Duration.ofMinutes(10); update the
test (method withdraw_success_no_active_challenges) to verify that
authService.withdraw invokes jwtService.blacklistToken with the same token
(dummyToken) and the expected duration (Duration.ofMinutes(10)), e.g., add a
verify(jwtService).blacklistToken(...) after calling authService.withdraw
alongside the existing verify(jwtService).deleteRefreshToken(userId) and
verify(user).withdraw().
🧹 Nitpick comments (3)
src/main/java/com/hrr/backend/domain/auth/service/AuthService.java (1)

113-138: 로그인 메서드들의 중복 코드 — 공통 헬퍼 추출을 고려해 주세요

kakaoLogin, appleLogin, naverLogin 세 메서드 모두 사용자 조회 후 토큰 생성 → nextStep 결정 → LoginResponse 빌드 패턴이 동일합니다. 소셜 사용자 조회 부분만 다르고 나머지 로직은 완전히 동일하므로, 공통 부분을 별도 private 메서드로 추출하면 유지보수성이 크게 향상됩니다.

♻️ 리팩토링 예시
private AuthResponseDto.LoginResponse buildLoginResponse(User user) {
    String accessToken = jwtService.generateAccessToken(user.getId());
    String refreshToken = jwtService.generateRefreshToken(user.getId());
    String nextStep = user.determineNextStep();

    return new AuthResponseDto.LoginResponse(
            user.getId(), accessToken, refreshToken,
            user.getDisplayName(), user.getDisplayNickname(),
            user.getLoginStatus(), nextStep
    );
}

각 로그인 메서드에서:

public AuthResponseDto.LoginResponse kakaoLogin(String kakaoAccessToken) {
    try {
        KakaoUserResponse kakaoUser = kakaoAuthService.fetchUser(kakaoAccessToken);
        User user = socialUserService.upsertKakaoUser(kakaoUser);
        return buildLoginResponse(user);
    } catch (GlobalException e) {
        throw e;
    } catch (Exception e) {
        throw new GlobalException(ErrorCode.AUTH_EXTERNAL_API_ERROR);
    }
}

Also applies to: 140-176, 178-204

src/main/java/com/hrr/backend/domain/auth/service/JwtService.java (1)

134-155: getRemainingExpiration이 모든 예외를 Duration.ZERO로 무시합니다

현재 로직은 만료된 토큰뿐 아니라 변조된(malformed) 토큰, 서명 불일치 등의 경우에도 Duration.ZERO를 반환합니다. 호출부(logout/withdraw)에서는 이미 유효성 검증을 거친 후 호출하므로 실질적 문제는 아니지만, 예상치 못한 경로에서 호출될 경우 오류를 조용히 삼키게 됩니다.

최소한 catch 블록에서 경고 로그를 남기는 것을 권장합니다:

🔧 로깅 추가 제안
         } catch (Exception e) {
+            log.warn("토큰 만료 시간 계산 실패: {}", e.getMessage());
             return Duration.ZERO;
         }
src/test/java/com/hrr/backend/domain/auth/service/AuthServiceWithdrawTest.java (1)

27-38: AuthService의 모든 의존성이 Mock 선언되지 않았습니다 — 현재는 동작하지만 취약한 구조

AuthService는 8개의 final 필드를 가지고 있지만, 이 테스트에서는 3개(userRepository, userChallengeRepository, jwtService)만 @Mock으로 선언되어 있습니다. Mockito의 @InjectMocks는 생성자 주입 실패 시 필드 주입으로 fallback하여 동작하지만, 이는 명시적이지 않습니다.

나머지 의존성도 @Mock으로 선언하면 의도가 명확해지고, 향후 withdraw 메서드가 다른 의존성을 사용하게 될 때 테스트가 깨지는 것을 방지할 수 있습니다.

Copy link

@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

🤖 Fix all issues with AI agents
In `@src/main/java/com/hrr/backend/domain/auth/service/AuthService.java`:
- Around line 150-155: Currently appleRefreshToken is converted from null to an
empty string before calling socialUserService.upsertAppleUser, which can later
cause appleAuthService.revoke(socialAuth.getSocialRefreshToken()) to send an
empty string to Apple; instead preserve null (do not replace with "") and update
the revoke flow and/or upsert logic: leave appleRefreshToken as null when
absent, modify socialUserService.upsertAppleUser(socialId, appleRefreshToken,
...) to treat null as “no change” (preserve existing DB token) or ensure revoke
callers (appleAuthService.revoke and any use of
socialAuth.getSocialRefreshToken()) perform null/empty checks before calling the
Apple API to avoid sending empty strings.

In `@src/main/java/com/hrr/backend/domain/auth/service/JwtService.java`:
- Around line 173-182: The validateRefreshToken method in JwtService is dead
code (used only by tests) because AuthService.reissueToken performs the same
inline comparison after fetching the token via getAndDeleteRefreshToken; either
remove JwtService.validateRefreshToken to eliminate the duplicate check, or
refactor AuthService.reissueToken to call JwtService.validateRefreshToken (pass
refreshToken and userId) and rely on that centralized null-safe comparison
instead of repeating storedRefreshToken.equals(refreshToken); update tests
accordingly if you remove or relocate the method.
🧹 Nitpick comments (6)
src/main/java/com/hrr/backend/domain/auth/service/AuthService.java (2)

75-111: reissueToken에서 블랙리스트 검사가 중복 수행됩니다.

Line 81의 validateToken(refreshToken)은 내부적으로 isTokenBlacklisted를 이미 호출합니다 (JwtService Line 71-73). Line 84에서 다시 isTokenBlacklisted를 호출하므로 동일한 Redis 조회가 2회 발생합니다. 또한 같은 토큰에 대해 JWT 파싱이 4회(validateToken, isTokenBlacklistedextractUserId, 그리고 getRemainingExpiration) 수행됩니다.

validateToken에서 이미 블랙리스트+유효성 검증을 모두 처리하므로, Line 83-86의 중복 블랙리스트 검사를 제거하고, extractUserId 대신 이미 파싱된 claims를 재사용하는 방식을 고려해 보세요.

♻️ 중복 블랙리스트 검사 제거 제안
         // Refresh Token 유효성 검증
         jwtService.validateToken(refreshToken);
 
-        // Redis에 저장된 Refresh Token과 비교 검증
-        if (jwtService.isTokenBlacklisted(refreshToken)) {
-            throw new GlobalException(ErrorCode.AUTH_INVALID_TOKEN);
-        }
-
         // userId 추출
         Long userId = jwtService.extractUserId(refreshToken);

43-72: socialLoginkakaoLogin의 로직이 거의 동일합니다.

두 메서드 모두 카카오 사용자 조회 → upsert → 토큰 생성 → LoginResponse 반환 패턴이 동일합니다. socialLogin은 추가로 exchangeToken을 통해 authorization code → access token 변환을 수행하는 차이만 있습니다. DRY 원칙에 따라 공통 로직을 private 헬퍼 메서드로 추출하면 4개 login 메서드 모두 간결해집니다.

Also applies to: 113-138

src/main/java/com/hrr/backend/domain/auth/service/JwtService.java (4)

101-128: extractUserIdgetUserIdFromToken은 거의 동일한 역할을 합니다.

두 메서드의 유일한 차이는 ExpiredJwtException 처리뿐입니다. extractUserId는 만료된 토큰에서 예외를 던지고, getUserIdFromToken은 만료된 토큰에서도 userId를 추출합니다. 현재 extractUserIdreissueToken에서, getUserIdFromTokenlogout에서 사용되는데, 호출자가 어떤 메서드를 써야 하는지 이름만으로는 구분이 어렵습니다.

하나의 메서드로 통합하거나, 메서드 이름을 의도가 명확하게 드러나도록 변경하는 것을 권장합니다 (예: extractUserIdAllowExpired 등).


150-171: getRemainingExpiration에서 모든 예외를 무시하고 Duration.ZERO를 반환합니다.

Line 168-170에서 catch (Exception e)로 모든 예외를 삼키면, 서명 불일치·변조된 토큰 등 유효하지 않은 토큰에 대해서도 조용히 Duration.ZERO를 반환합니다. 호출자(reissueToken, logout, withdraw)는 이 경우 블랙리스트 등록을 건너뛰게 되는데, 변조된 토큰이라면 이미 앞단에서 걸러지므로 현재 흐름상 큰 문제는 아닙니다.

다만 디버깅 편의를 위해 최소한 log.warn을 추가하면 운영 시 추적이 용이합니다.

🔍 로깅 추가 제안
         } catch (Exception e) {
+            log.warn("토큰 만료 시간 계산 실패: {}", e.getMessage());
             return Duration.ZERO;
         }

70-85: validateToken은 항상 true를 반환하거나 예외를 던집니다.

boolean 반환 타입이지만 false를 반환하는 경로가 없어, 호출자가 반환값을 검사하는 것이 무의미합니다. 현재 AuthService.reissueToken에서도 반환값을 사용하지 않고 있습니다. void로 변경하면 메서드의 계약(contract)이 더 명확해집니다.


36-38: getSigningKey()가 호출마다 새로운 SecretKey 인스턴스를 생성합니다.

JWT 파싱/서명 시마다 Keys.hmacShaKeyFor()를 호출하여 바이트 변환 + 키 생성을 반복합니다. secret 값은 변하지 않으므로, 키를 한 번만 생성하여 캐싱하면 불필요한 객체 할당을 줄일 수 있습니다. @PostConstruct로 초기화하거나 lazy 패턴을 사용하세요.

♻️ 키 캐싱 제안
+import jakarta.annotation.PostConstruct;
+
 `@Service`
-@RequiredArgsConstructor
 public class JwtService {
     `@Value`("${jwt.secret}")
     private String secret;
 
+    private SecretKey signingKey;
+
+    `@PostConstruct`
+    void init() {
+        this.signingKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
+    }
+
-    private SecretKey getSigningKey() {
-        return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
-    }
+    private SecretKey getSigningKey() {
+        return signingKey;
+    }

참고: @PostConstruct를 사용하면 @RequiredArgsConstructor와 함께 사용할 때 생성자 주입 순서와 무관하게 @Value 필드가 초기화된 후 호출됩니다.

Copy link

@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

🤖 Fix all issues with AI agents
In `@src/main/java/com/hrr/backend/domain/auth/service/AuthService.java`:
- Around line 203-220: The logout method lacks a null check for tokenHeader and
can NPE when tokenHeader is null; mirror the guard used in withdraw by first
verifying tokenHeader != null (or handling empty/invalid headers) and return
early before calling jwtService.getUserIdFromToken,
jwtService.getRemainingExpiration, jwtService.blacklistToken, and
jwtService.deleteRefreshToken; ensure the method extracts the token only after
the null/empty check and preserves existing blacklist and refresh-token deletion
behavior.
- Around line 259-262: The APPLE branch in AuthService currently calls
appleAuthService.revoke(socialAuth.getSocialRefreshToken()) without guarding
against a null refresh token which causes Apple API errors; before invoking
appleAuthService.revoke(...) in the switch (or inside AuthService's APPLE case),
check if socialAuth.getSocialRefreshToken() is non-null and only call revoke
when present (otherwise call kakao-like unlink with socialAuth.getSocialId() or
skip revoke and log a specific warning that the token is null), and additionally
add a null guard inside AppleAuthService.revoke(...) to no-op and emit a clear
log entry when given a null token so the external API is never called with
"token": null.
🧹 Nitpick comments (5)
src/main/java/com/hrr/backend/domain/auth/service/JwtService.java (2)

70-85: validateToken에 블랙리스트 검증이 추가되었으나, AuthService.reissueToken과 중복 체크됩니다.

validateToken 내부에서 isTokenBlacklisted를 호출하고(Line 71), AuthService.reissueToken에서도 동일한 체크를 수행합니다(AuthService Line 84). 현재 동작상 문제는 없지만, 방어 로직이 두 레이어에 분산되어 있어 유지보수 시 혼란의 소지가 있습니다. 한쪽으로 통일하거나, reissueToken 측 체크에 의도를 명시하는 주석을 추가하면 좋겠습니다.


150-171: getRemainingExpiration에서 모든 예외를 무시하면 디버깅이 어려울 수 있습니다.

catch (Exception e)에서 Duration.ZERO를 반환하는 것은 만료된 토큰에 대해 올바른 동작이지만, 예상치 못한 파싱 오류까지 조용히 삼키면 문제 추적이 어렵습니다. 최소한 debug/warn 레벨 로그를 남기는 것을 권장합니다.

🔍 로깅 추가 제안
         } catch (Exception e) {
+            log.debug("토큰 만료 시간 계산 실패: {}", e.getMessage());
             return Duration.ZERO;
         }

참고: JwtService에 현재 @Slf4j가 없으므로 클래스 레벨에 추가가 필요합니다.

src/main/java/com/hrr/backend/domain/auth/service/AuthService.java (3)

75-111: reissueToken 흐름이 견고하게 개선되었습니다. 원자적 getAndDeleteRefreshToken + 비교 검증 + 블랙리스트 처리의 3중 방어가 훌륭합니다.

한 가지 작은 포인트: Line 84의 isTokenBlacklisted 체크는 Line 81의 validateToken 내부에서 이미 수행됩니다(JwtService Line 71). 블랙리스트된 토큰은 validateToken에서 먼저 거부되므로 Line 84-86은 도달 불가능한 코드입니다. 의도적인 방어 계층이라면 주석으로 명시하면 좋겠습니다.


89-89: extractUserId 대신 getUserIdFromToken 사용을 권장합니다.

JwtService에서 extractUserIdgetUserIdFromToken 통합을 제안드렸는데, 이 호출부도 함께 교체 대상입니다. 현재 validateToken을 통과한 후이므로 동작상 문제는 없지만, API 일관성을 위해 통일하는 것이 좋습니다.


113-201: 로그인 메서드 4개(socialLogin, kakaoLogin, appleLogin, naverLogin)에 토큰 발급 및 응답 생성 로직이 중복됩니다.

각 메서드에서 generateAccessTokengenerateRefreshTokendetermineNextStepnew LoginResponse(...) 패턴이 반복되고 있습니다. 공통 부분을 헬퍼 메서드로 추출하면 변경 포인트를 줄이고 유지보수성을 높일 수 있습니다. 예를 들어 LoginResponse 필드가 변경되면 4곳을 모두 수정해야 하는 상황을 방지할 수 있습니다.

♻️ 헬퍼 메서드 추출 예시
private AuthResponseDto.LoginResponse buildLoginResponse(User user) {
    String accessToken = jwtService.generateAccessToken(user.getId());
    String refreshToken = jwtService.generateRefreshToken(user.getId());
    String nextStep = user.determineNextStep();

    return new AuthResponseDto.LoginResponse(
            user.getId(),
            accessToken,
            refreshToken,
            user.getDisplayName(),
            user.getDisplayNickname(),
            user.getLoginStatus(),
            nextStep
    );
}

각 로그인 메서드에서는 return buildLoginResponse(user);로 호출하면 됩니다.

@Hminkyung Hminkyung requested a review from yc3697 February 7, 2026 07:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 bug 버그 이슈 ✔️ test 테스트 민경

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] 사용자 토큰 발급 API

1 participant