Skip to content

[#258][fe][user] Refresh Token 프론트 작업#260

Open
letsgilit wants to merge 3 commits intodevfrom
feat/#258
Open

[#258][fe][user] Refresh Token 프론트 작업#260
letsgilit wants to merge 3 commits intodevfrom
feat/#258

Conversation

@letsgilit
Copy link
Copy Markdown
Member

@letsgilit letsgilit commented Oct 27, 2025

#️⃣연관된 이슈

변경 타입

  • 신규 기능 추가/수정
  • 버그 수정
  • 리팩토링
  • 설정
  • 비기능 (주석 등 기능에 영향을 주지 않음)

변경 내용

  • to-be(변경 후 설명을 여기에 작성)

    • Refresh Token 프론트 작업
    • TODO
    • TODO

체크리스트

  • 코드가 제대로 동작하는지 확인했습니다.
  • 관련 테스트를 추가했습니다.
  • 문서(코드, 주석, README 등)를 업데이트했습니다.

코멘트

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 자동 토큰 갱신 메커니즘 추가 - 세션 만료 시 자동으로 토큰을 갱신하여 사용자 경험 개선
    • 토큰 관리 체계 개선 - 액세스 토큰과 갱신 토큰을 분리하여 보안 강화
    • 토큰 만료 시간 추적 기능 추가 - 사용자 세션 상태를 더 정확하게 관리
  • 버그 수정

    • 인증 오류 발생 시 로그인 페이지로의 자동 리디렉션 처리 개선

@letsgilit letsgilit self-assigned this Oct 27, 2025
@letsgilit letsgilit added ✨ feature 기능 구현 🎨 frontend 프론트 마크업 및 스타일링 labels Oct 27, 2025
@letsgilit letsgilit linked an issue Oct 27, 2025 that may be closed by this pull request
3 tasks
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 27, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

이 변경사항은 자동 토큰 갱신 기능을 구현합니다. JWT 유틸리티 모듈을 추가하여 토큰 디코딩 및 만료 여부 판단을 수행하고, Axios 요청 인터셉터를 수정하여 만료된 토큰을 자동으로 갱신합니다. 인증 서비스 및 API에 갱신 기능을 추가하고, 인증 저장소를 리팩토링하여 액세스 토큰, 새로고침 토큰, 만료 시간을 별도로 관리합니다. Firebase 설정에서는 탭 간 이벤트 중복 제거 기능을 추가합니다.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Interceptor
    participant AuthService
    participant Backend
    participant AuthStore

    Client->>Interceptor: 요청 발송
    Interceptor->>Interceptor: 토큰 만료 여부 확인
    
    alt 토큰이 만료됨
        Interceptor->>Interceptor: 갱신 중인가?
        
        alt 갱신이 진행 중
            Interceptor->>Interceptor: 요청을 대기열에 추가
        else 갱신이 필요함
            Interceptor->>Interceptor: isRefreshing = true
            Interceptor->>AuthService: refresh() 호출
            AuthService->>Backend: POST /v1/auth/refresh
            Backend-->>AuthService: 새 토큰 반환
            AuthService->>AuthStore: accessToken, refreshToken 업데이트
            Interceptor->>Interceptor: 대기 중인 요청 모두 처리
            Interceptor->>Interceptor: isRefreshing = false
        end
        
        Interceptor->>Interceptor: 새 토큰으로 Authorization 헤더 설정
        Interceptor->>Backend: 요청 재시도
    else 토큰이 유효함
        Interceptor->>Interceptor: Authorization 헤더 설정
        Interceptor->>Backend: 요청 발송
    end
    
    Backend-->>Client: 응답
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45-60 minutes

주의 깊게 검토할 부분:

  • front/src/api/axios.js: 토큰 갱신 로직의 정확성 및 구독자 대기열 메커니즘이 경쟁 조건을 방지하는지 확인 필요. 401 응답 처리 로직이 완전히 구현되었는지 검토 필요
  • front/src/shared/utils/jwtUtils.js: JWT 디코딩 시 예외 처리가 안전한지, exp 클레임 기반 만료 판단 로직이 올바른지 확인 필요
  • front/src/stores/useAuthStore.js: token에서 accessToken/refreshToken으로의 마이그레이션이 모든 참조처에서 일관되게 처리되었는지 확인 필요. 저장소 인터페이스 변경이 다른 모듈과 호환되는지 검증 필요
  • front/src/features/auth/authService.js: login 흐름에서 새로운 토큰 저장 스키마가 올바르게 적용되었는지 검증 필요
  • front/src/shared/firebase/config.js: BroadcastChannel 리스너 제거의 의도 및 영향 범위 확인 필요

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning JWT 유틸리티, Axios 인터셉터, 인증 저장소, Refresh API/서비스 등의 변경사항은 Refresh Token 구현에 직접적으로 필요합니다. 하지만 front/src/shared/firebase/config.js의 변경사항, 특히 markAndCheckSeen(id) 함수 추가, BroadcastChannel 메시지 리스너 제거, 형식 조정 등은 Refresh Token 기능과 직접적인 관련성이 없으며 이벤트 중복 제거 및 탭 간 통신과 관련된 별도의 기능으로 보입니다. 이러한 변경사항은 범위를 벗어난 것으로 판단됩니다. front/src/shared/firebase/config.js에 대한 변경사항(markAndCheckSeen 함수 추가, BroadcastChannel 리스너 제거, 형식 조정)을 별도의 PR로 분리하시거나, 이 변경사항이 Refresh Token 구현과 어떤 구체적인 의존성이나 필요성이 있는지 명확히 설명하고 정당화하시기 바랍니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Linked Issues Check ❓ Inconclusive 연결된 이슈 #258은 "refresh token추가"라는 매우 모호한 요구사항과 구체적 내용 없는 3개의 미완료 TODO 항목만 포함하고 있습니다. PR은 JWT 유틸리티, Axios 인터셉터 수정, 인증 저장소 업데이트, Refresh API/서비스 구현 등을 통해 Refresh Token 기능을 프론트엔드에 추가하고 있으나, PR 설명에서 "Two TODOs remaining"이 명시되어 있어 작업이 완료되지 않았음을 나타냅니다. 이슈의 구체적 요구사항이 정의되지 않아 완전한 준수 여부를 판단할 수 없습니다. 연결된 이슈의 3개 TODO 항목에 대한 구체적인 요구사항을 명확히 정의하고, 남은 TODO들이 완료되었거나 명시적으로 범위 외로 표시되었는지 확인하시기 바랍니다. 모든 요구사항이 충족되었는지 명확한 검증이 필요합니다.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed PR 제목 "[#258][fe][user]feat: Refresh Token 프론트 작업"은 변경 세트의 주요 목표와 명확하게 일치합니다. 제공된 변경 사항들(axios 인터셉터 수정, 인증 서비스 업데이트, JWT 유틸리티 추가, 저장소 구조 변경 등)이 모두 Refresh Token 기능 구현을 목표로 하고 있으며, 제목에서 "Refresh Token"이라는 구체적인 기능명을 명시하고 있습니다. 팀원이 커밋 히스토리를 스캔할 때 이 PR의 주요 변경 내용을 명확하게 파악할 수 있습니다.

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.

@letsgilit letsgilit changed the title [#258][fe][user]feat: Refresh Token 프론트 작업 [#258][fe][user] Refresh Token 프론트 작업 Oct 27, 2025
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: 13

Caution

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

⚠️ Outside diff range comments (1)
front/src/shared/firebase/config.js (1)

85-86: postMessage 실패에 대한 에러 처리를 추가하는 것을 고려하세요.

BroadcastChannel 또는 서비스 워커로의 메시지 전송이 실패할 경우 에러가 발생할 수 있습니다. 현재는 조용히 실패하는데, 최소한 콘솔 로그를 추가하거나 에러를 처리하는 것이 디버깅에 도움이 됩니다.

- if (bc) bc.postMessage({ id, ts: now })
- else navigator.serviceWorker?.controller?.postMessage({ id, ts: now })
+ try {
+   if (bc) bc.postMessage({ id, ts: now })
+   else navigator.serviceWorker?.controller?.postMessage({ id, ts: now })
+ } catch (err) {
+   console.warn('Failed to broadcast event deduplication:', err)
+ }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b3c48b4 and 374f204.

📒 Files selected for processing (7)
  • front/src/api/axios.js (2 hunks)
  • front/src/features/auth/api.js (1 hunks)
  • front/src/features/auth/authService.js (3 hunks)
  • front/src/features/auth/dto.js (1 hunks)
  • front/src/shared/firebase/config.js (1 hunks)
  • front/src/shared/utils/jwtUtils.js (1 hunks)
  • front/src/stores/useAuthStore.js (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: letsgilit
PR: HI-dle/rouby#230
File: back/src/main/java/com/rouby/batch/config/BatchConfig.java:68-73
Timestamp: 2025-09-09T11:11:40.513Z
Learning: letsgilit prefers simpler configurations over complex ones when the current solution is sufficient for the use case.
🧬 Code graph analysis (5)
front/src/features/auth/api.js (1)
front/src/features/auth/authService.js (2)
  • refresh (87-101)
  • refresh (87-101)
front/src/api/axios.js (4)
front/src/features/auth/authService.js (2)
  • refresh (87-101)
  • refresh (87-101)
front/src/features/auth/api.js (2)
  • refresh (47-49)
  • refresh (47-49)
front/src/shared/utils/jwtUtils.js (3)
  • decoded (8-8)
  • decoded (17-17)
  • decodeJwt (1-14)
front/src/stores/useAuthStore.js (2)
  • useAuthStore (6-68)
  • useAuthStore (6-68)
front/src/shared/utils/jwtUtils.js (1)
front/src/api/axios.js (2)
  • token (61-61)
  • decoded (31-31)
front/src/stores/useAuthStore.js (4)
front/src/features/auth/useLoginForm.js (1)
  • staySignedIn (11-11)
front/src/stores/useScheduleStore.js (1)
  • reset (94-97)
front/src/stores/useDatePickStore.js (1)
  • reset (41-45)
front/src/stores/useUserInfoStore.js (1)
  • reset (81-93)
front/src/features/auth/authService.js (4)
front/src/features/auth/api.js (2)
  • refresh (47-49)
  • refresh (47-49)
front/src/shared/utils/errorUtils.js (1)
  • wrapApi (37-45)
front/src/features/auth/dto.js (1)
  • toRefreshPayload (36-40)
front/src/shared/utils/jwtUtils.js (1)
  • getTokenExpiration (16-19)
🔇 Additional comments (4)
front/src/shared/firebase/config.js (4)

63-68: 이벤트 중복 제거 설정이 적절합니다.

MAX_SEENTRIM_COUNT 값이 메모리 관리에 적절하며, BroadcastChannel의 기능 감지 처리도 잘 되어 있습니다.


71-88: 크로스 탭 중복 제거 로직에서 경쟁 상태 가능성을 확인하세요.

두 개의 탭이 거의 동시에 같은 이벤트를 수신하면, 둘 다 seen.has(id) 체크를 통과한 후 브로드캐스트를 보내기 전에 이벤트를 처리할 수 있습니다. 이는 짧은 시간 동안 중복 알림이 표시될 수 있는 TOCTOU 경쟁 상태입니다.

알림 시스템의 특성상 이는 허용 가능한 수준이지만, 실제 환경에서 중복 알림이 발생하는지 확인해 보시기 바랍니다.


91-99: 탭 간 이벤트 전파 처리가 잘 구현되어 있습니다.

resolvePropagatedEvent는 타임스탬프 비교를 통해 최신 정보를 유지하고, BroadcastChannel과 서비스 워커 모두에 리스너를 등록하여 적절한 폴백을 제공합니다.


108-110: 중복 제거 통합이 깔끔하게 구현되었습니다.

markAndCheckSeen 함수를 사용한 조기 반환 패턴이 명확하며, 알림 표시 로직과 잘 통합되어 있습니다.

Comment thread front/src/api/axios.js
Comment on lines +17 to +24
function onRefreshed(newAccessToken) {
refreshSubscribers.forEach((cb) => cb(newAccessToken))
refreshSubscribers = []
}

function addRefreshSubscriber(callback) {
refreshSubscribers.push(callback)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

refresh 실패 시 대기중인 요청이 영원히 대기 상태로 남음

실패 시 onRefreshed가 호출되지 않아 구독자 Promise가 resolve/reject되지 않습니다. 메모리 누수/무한 대기 위험.

실패 브로드캐스트를 추가하고 구독자 구조를 resolve/reject로 변경하세요:

-let refreshSubscribers = []
+let refreshSubscribers = []

-function onRefreshed(newAccessToken) {
-  refreshSubscribers.forEach((cb) => cb(newAccessToken))
-  refreshSubscribers = []
-}
+function onRefreshed(newAccessToken) {
+  refreshSubscribers.forEach(({ resolve }) => resolve(newAccessToken))
+  refreshSubscribers = []
+}
+
+function onRefreshFailed(error) {
+  refreshSubscribers.forEach(({ reject }) => reject(error))
+  refreshSubscribers = []
+}

-function addRefreshSubscriber(callback) {
-  refreshSubscribers.push(callback)
-}
+function addRefreshSubscriber(handlers /* { resolve, reject } */) {
+  refreshSubscribers.push(handlers)
+}

 async function refreshAccessToken(authStore) {
   try {
     const { accessToken: newAccessToken, refreshToken: newRefreshToken } = await refresh({
       refreshToken: authStore.getRefreshToken(),
     })
@@
     onRefreshed(newAccessToken)
     return newAccessToken
   } catch (error) {
     console.error('Refresh 실패:', error)
+    onRefreshFailed(error)
     authStore.reset()
     window.location.href = '/login'
     throw error
   } finally {
     isRefreshing = false
   }
 }

Also applies to: 26-52

🤖 Prompt for AI Agents
In front/src/api/axios.js around lines 17-24 (and also apply same change to
lines 26-52), the current refresh subscriber pattern only supports successful
broadcasts so when token refresh fails waiting requests remain pending; change
the subscriber model to register objects with both resolve and reject (e.g.,
push {resolve, reject}) and make addRefreshSubscriber return a Promise that
stores its resolve/reject in that list; implement a new broadcaster function
(e.g., onRefreshFailed) that iterates subscribers calling reject(error) and then
clears the list, and update onRefreshed to call each
subscriber.resolve(newAccessToken) and then clear the list; ensure all branches
(success and failure) always clear subscribers to avoid memory leaks.

Comment thread front/src/api/axios.js
Comment on lines +31 to +37
const decoded = decodeJwt(newAccessToken)
const expiredAt = decoded?.exp ? decoded.exp * 1000 : null

// 새 토큰 및 만료 시간 저장
authStore.setAccessToken(newAccessToken)
authStore.setAccessTokenExpirationTime(expiredAt)
if (newRefreshToken) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

만료시각 계산 유틸 통일 제안

여기서는 decodeJwt로 직접 계산, 다른 곳(authService)은 getTokenExpiration을 사용. 한 유틸로 통일하면 버그 표면적이 줄어듭니다.

-    const decoded = decodeJwt(newAccessToken)
-    const expiredAt = decoded?.exp ? decoded.exp * 1000 : null
+    const { getTokenExpiration } = await import('@/shared/utils/jwtUtils.js')
+    const expiredAt = getTokenExpiration(newAccessToken)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const decoded = decodeJwt(newAccessToken)
const expiredAt = decoded?.exp ? decoded.exp * 1000 : null
// 새 토큰 및 만료 시간 저장
authStore.setAccessToken(newAccessToken)
authStore.setAccessTokenExpirationTime(expiredAt)
if (newRefreshToken) {
const { getTokenExpiration } = await import('@/shared/utils/jwtUtils.js')
const expiredAt = getTokenExpiration(newAccessToken)
// 새 토큰 및 만료 시간 저장
authStore.setAccessToken(newAccessToken)
authStore.setAccessTokenExpirationTime(expiredAt)
if (newRefreshToken) {
🤖 Prompt for AI Agents
In front/src/api/axios.js around lines 31 to 37, the code computes token expiry
by calling decodeJwt and multiplying exp by 1000, while other modules use
getTokenExpiration; replace the direct decodeJwt usage with the shared
getTokenExpiration utility: import getTokenExpiration from the shared utils,
call expiredAt = getTokenExpiration(newAccessToken) (ensuring it returns null
when unavailable), then continue storing
authStore.setAccessTokenExpirationTime(expiredAt); keep existing handling for
newRefreshToken unchanged.

Comment thread front/src/api/axios.js
Comment on lines +45 to +47
console.error('Refresh 실패:', error)
authStore.reset()
window.location.href = '/login'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

authStore.reset()가 refreshToken/만료시각을 지우지 않을 가능성

제공된 스토어 스니펫에 따르면 reset이 accessToken만 초기화합니다. 실패 후 refreshToken이 남으면 보안/정합성 문제입니다.

스토어의 reset에서 refreshToken과 accessTokenExpirationTime도 함께 초기화해 주세요. 아래 스크립트로 실제 구현을 확인해 주실 수 있나요?


🏁 Script executed:

#!/bin/bash
# useAuthStore.reset이 어떤 필드를 초기화하는지 확인
set -euo pipefail
# 대상 파일 탐색
fd -t f useAuthStore.js
echo "---- reset 구현 ----"
rg -n -C3 'reset\s*=\s*\(\)\s*=>' $(fd -t f useAuthStore.js)
echo "---- refreshToken 초기화 위치 ----"
rg -n -C1 'refreshToken\.value\s*=' $(fd -t f useAuthStore.js)
echo "---- accessTokenExpirationTime 초기화 위치 ----"
rg -n -C1 'accessTokenExpirationTime\.value\s*=' $(fd -t f useAuthStore.js)

Length of output: 495


useAuthStore.reset()에서 refreshToken과 accessTokenExpirationTime 초기화 필요

검증 결과, reset() 함수(line 42-45)는 staySignedInaccessToken만 초기화하고 있습니다. refreshTokenaccessTokenExpirationTime은 초기화되지 않고 있습니다.

토큰 갱신 실패 후 logout 흐름에서 이 필드들이 남아있으면 보안 및 데이터 정합성 문제로 이어질 수 있습니다.

수정 사항:
front/src/stores/useAuthStore.js의 reset() 함수에서 다음 필드도 함께 초기화하세요:

  • refreshToken.value = ''
  • accessTokenExpirationTime.value = 0 (또는 적절한 초기값)
🤖 Prompt for AI Agents
In front/src/stores/useAuthStore.js around lines 42-45, the reset() function
only clears staySignedIn and accessToken but leaves refreshToken and
accessTokenExpirationTime populated; update reset() to also set
refreshToken.value = '' and accessTokenExpirationTime.value = 0 (or another
appropriate initial value) so both tokens and their expiration are cleared
during logout/refresh-failure flows.

Comment thread front/src/api/axios.js
Comment on lines +46 to +48
authStore.reset()
window.location.href = '/login'
throw error
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

라이브러리 레벨에서의 하드 리다이렉트 지양

공용 axios 모듈에서 window.location.href를 직접 호출하면 앱 라우터/상태 전환과 충돌할 수 있습니다. 응답 인터셉터 401 처리와 동일하게 라우터 이벤트(예: 전역 이벤트 버스)로 위임하는 방식을 권장합니다.

Also applies to: 96-105

🤖 Prompt for AI Agents
In front/src/api/axios.js around lines 46-48 and 96-105, remove the hard
redirect (window.location.href) in the axios response interceptor; instead
delegate navigation by emitting a global event or calling a provided router
callback so the app router handles redirects. Specifically, replace the direct
window.location.href with emitting an "unauthorized" event (or invoking a
passed-in router.push('/login')) after calling authStore.reset(), and ensure the
interceptor returns/throws the error (Promise.reject(error)) so callers can
handle it; do the same for the other 401 handling block at lines 96-105.

Comment thread front/src/api/axios.js
Comment on lines +73 to +80
if (isRefreshing) {
return new Promise((resolve) => {
addRefreshSubscriber((newAccessToken) => {
config.headers.Authorization = `Bearer ${newAccessToken}`
resolve(config)
})
})
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

명시적 Authorization 헤더 덮어쓰기 버그 (회원가입 등 깨질 수 있음)

요청 측에서 이미 Authorization을 명시한 경우(예: 가입 검증 토큰) 인터셉터가 Bearer 액세스 토큰으로 덮어씁니다. 결과적으로 API 요구 헤더가 바뀌어 실패할 수 있습니다.

다음과 같이 “이미 설정된 Authorization은 보존”하도록 가드해 주세요:

   const token = authStore.getAccessToken()
   const expiredAt = authStore.getAccessTokenExpirationTime()

   if (!token) return config
+  // 호출자가 명시적으로 Authorization을 설정한 경우(특수 토큰) 덮어쓰지 않음
+  if (config.headers?.Authorization) return config

-  const now = Date.now()
+  const now = Date.now()

   // 만료된 토큰이면 refresh 시도
-  if (expiredAt && now >= expiredAt) {
+  const SKEW_MS = 30_000
+  if (expiredAt && now >= expiredAt - SKEW_MS) {
     console.warn('AccessToken 만료 → refresh 요청 시작')

     // 이미 갱신 중이라면 기다림
-    if (isRefreshing) {
-      return new Promise((resolve) => {
-        addRefreshSubscriber((newAccessToken) => {
-          config.headers.Authorization = `Bearer ${newAccessToken}`
-          resolve(config)
-        })
-      })
-    }
+    if (isRefreshing) {
+      return new Promise((resolve, reject) => {
+        addRefreshSubscriber({
+          resolve: (newAccessToken) => {
+            if (!config.headers?.Authorization) {
+              config.headers.Authorization = `Bearer ${newAccessToken}`
+            }
+            resolve(config)
+          },
+          reject,
+        })
+      })
+    }

     // refresh 수행
     isRefreshing = true
     const newAccessToken = await refreshAccessToken(authStore)
-    config.headers.Authorization = `Bearer ${newAccessToken}`
+    if (!config.headers?.Authorization) {
+      config.headers.Authorization = `Bearer ${newAccessToken}`
+    }
     return config
   }

   // 만료되지 않았으면 그대로 요청
-  config.headers.Authorization = `Bearer ${token}`
+  if (!config.headers?.Authorization) {
+    config.headers.Authorization = `Bearer ${token}`
+  }

Also applies to: 82-87, 89-91

🤖 Prompt for AI Agents
In front/src/api/axios.js around lines 73-80 (also apply same change to 82-87
and 89-91): the interceptor unconditionally overwrites
config.headers.Authorization when a refresh is in progress, which clobbers any
explicit Authorization set by the caller (e.g., signup/verification tokens). Fix
by checking if config.headers and config.headers.Authorization are already set
and only replace Authorization when it is missing; for the refresh-subscriber
path, only set Authorization = `Bearer ${newAccessToken}` if no pre-existing
Authorization exists, and do the same guard in the other two similar blocks so
caller-provided Authorization headers are preserved.

Comment on lines +36 to +40
export function toRefreshPayload(form) {
return {
refreshToken: form.refreshToken?.trim(),
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

refreshToken 유효성 체크 누락

빈 값/누락 시 서버에 빈 페이로드가 전송될 수 있습니다. 클라이언트에서 즉시 검증/실패시키면 UX와 로깅이 명확해집니다.

다음과 같이 보완을 권고합니다:

 export function toRefreshPayload(form) {
-  return {
-    refreshToken: form.refreshToken?.trim(),
-  }
+  const token = typeof form?.refreshToken === 'string' ? form.refreshToken.trim() : ''
+  if (!token) {
+    // wrapApi 에서 적절히 잡혀 사용자 친화적 메시지로 변환됨
+    throw new Error('MISSING_REFRESH_TOKEN')
+  }
+  return { refreshToken: token }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function toRefreshPayload(form) {
return {
refreshToken: form.refreshToken?.trim(),
}
}
export function toRefreshPayload(form) {
const token = typeof form?.refreshToken === 'string' ? form.refreshToken.trim() : ''
if (!token) {
// wrapApi 에서 적절히 잡혀 사용자 친화적 메시지로 변환됨
throw new Error('MISSING_REFRESH_TOKEN')
}
return { refreshToken: token }
}
🤖 Prompt for AI Agents
In front/src/features/auth/dto.js around lines 36 to 40, toRefreshPayload
currently returns a possibly empty refreshToken; add a validation step that
trims form.refreshToken, checks that it's a non-empty string (length > 0) and if
valid returns { refreshToken }, otherwise throw or return a synchronous
validation error (e.g., throw new Error("refreshToken is required") or return
null/validation result) so callers immediately fail client-side and avoid
sending empty payloads to the server; ensure the calling code handles the
thrown/returned error consistently and update any tests/usages accordingly.

Comment on lines +1 to +14
export function decodeJwt(token) {
try {
if (!token) return null

const payload = token.split('.')[1]
if (!payload) return null

const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'))
return JSON.parse(decoded)
} catch (e) {
console.error('JWT 디코딩 실패:', e)
return null
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

base64url 패딩 누락으로 디코딩 실패 가능 (exp 계산 오동작 유발)

JWT payload는 '=' 패딩이 생략될 수 있습니다. 현재 구현은 패딩을 보완하지 않아 atob에서 실패할 수 있습니다. SSR 환경에서 atob가 없는 경우도 대비하세요.

안전한 base64url 디코딩으로 교체를 권장합니다:

+function base64UrlToBase64(input) {
+  const b64 = input.replace(/-/g, '+').replace(/_/g, '/')
+  const pad = b64.length % 4
+  return pad ? b64 + '='.repeat(4 - pad) : b64
+}
+
 export function decodeJwt(token) {
   try {
     if (!token) return null

     const payload = token.split('.')[1]
     if (!payload) return null

-    const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'))
+    const b64 = base64UrlToBase64(payload)
+    const decoded =
+      typeof atob === 'function'
+        ? atob(b64)
+        : Buffer.from(b64, 'base64').toString('binary')
     return JSON.parse(decoded)
   } catch (e) {
     console.error('JWT 디코딩 실패:', e)
     return null
   }
 }
🤖 Prompt for AI Agents
In front/src/shared/utils/jwtUtils.js around lines 1 to 14, the decodeJwt
implementation can fail because it doesn't restore '=' base64url padding and
assumes atob exists (breaks in SSR); replace the current logic with a safe
base64url decoder that (1) converts '-' and '_' back to '+' and '/', (2) adds
the required '=' padding based on payload length, (3) uses a robust decoding
fallback when atob is unavailable (e.g., Buffer.from for Node/SSR), and (4)
parses the JSON payload inside try/catch and logs errors as before.

Comment on lines +21 to +25
export function isAccessTokenExpired(token) {
const exp = getTokenExpiration(token)
if (!exp) return true
return Date.now() >= exp
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

만료 정보가 없으면 항상 만료로 간주하는 정책 재확인

isAccessTokenExpired가 exp 없을 때 true를 반환합니다. 현재 인터셉터는 exp 타임스탬프를 따로 저장/사용하므로 이 함수 사용 시 동작이 달라질 수 있습니다. 사용 지점을 명확히 정하세요.

🤖 Prompt for AI Agents
In front/src/shared/utils/jwtUtils.js around lines 21 to 25, the current
isAccessTokenExpired returns true when no exp is present which can conflict with
interceptors that store/use exp separately; change the function signature to
accept an options parameter (e.g., { assumeExpiredIfNoExp: true } defaulting to
current behavior) and implement logic to return the configured value instead of
always true, then update callers: keep default behavior for general use, but
modify interceptor call sites to pass assumeExpiredIfNoExp: false (or explicitly
handle missing exp) so behavior is consistent and documented.

Comment on lines +30 to +40
const getAccessToken = () => {
return accessToken.value
}

const getRefreshToken = () => {
return refreshToken.value
}

const getAccessTokenExpirationTime = () => {
return accessTokenExpirationTime.value
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

getter 함수들이 불필요할 수 있습니다.

Vue 3와 Pinia에서는 ref가 이미 반응형이고 return 객체에서 직접 노출되므로, getAccessToken(), getRefreshToken(), getAccessTokenExpirationTime() 같은 단순 getter 함수들은 중복될 수 있습니다. 소비자 코드에서 authStore.accessToken 형태로 직접 접근 가능합니다.

다만, 기존 코드베이스에서 이미 getter 패턴을 사용 중이거나 일관성을 위해 유지하는 경우라면 현재 구현도 충분히 타당합니다.

🤖 Prompt for AI Agents
In front/src/stores/useAuthStore.js around lines 30 to 40, the simple getter
functions getAccessToken(), getRefreshToken(), and
getAccessTokenExpirationTime() are redundant because Pinia exposes refs
directly; remove these three functions and expose the ref properties
(accessToken, refreshToken, accessTokenExpirationTime) from the store so
consumers use authStore.accessToken etc., or if you must keep the getters for
consistency, mark them as legacy and add a TODO comment to remove later; update
any call sites to read the properties directly.

Comment on lines 42 to 45
const reset = () => {
staySignedIn.value = false
token.value = ''
accessToken.value = ''
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

reset() 함수가 모든 토큰 관련 상태를 초기화하지 않습니다.

reset() 함수가 accessToken만 초기화하고 refreshTokenaccessTokenExpirationTime은 초기화하지 않습니다. 이는 로그아웃이나 세션 종료 시 민감한 토큰 정보가 영구 저장소에 남아있을 수 있는 보안 문제를 야기합니다.

다음 diff를 적용하여 모든 토큰 관련 상태를 초기화하세요:

 const reset = () => {
   staySignedIn.value = false
   accessToken.value = ''
+  refreshToken.value = ''
+  accessTokenExpirationTime.value = 0
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const reset = () => {
staySignedIn.value = false
token.value = ''
accessToken.value = ''
}
const reset = () => {
staySignedIn.value = false
accessToken.value = ''
refreshToken.value = ''
accessTokenExpirationTime.value = 0
}
🤖 Prompt for AI Agents
In front/src/stores/useAuthStore.js around lines 42 to 45, the reset() function
only clears accessToken but leaves refreshToken and accessTokenExpirationTime
intact; update reset() to also clear refreshToken and accessTokenExpirationTime
(e.g., set refreshToken to an empty string and accessTokenExpirationTime to
null/0) and ensure any persisted token entries (localStorage/sessionStorage) are
removed as part of the reset.

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

🚨🚨🚨Test failed: 코드를 다시 확인해주세요.🚨🚨🚨

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feature 기능 구현 🎨 frontend 프론트 마크업 및 스타일링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[user][front] refresh token 추가

1 participant