Repository navigation
[FE] [FIX] auth state prob, login page - #226
Conversation
워크스루이메일 기반에서 학번 기반 로그인으로 전환하고, 중앙화된 AuthContext를 통해 인증 상태를 관리하며, ProtectedRoute로 경로 보호를 추가하고, 로그인/비밀번호 재설정 흐름을 업데이트하고, API 프록싱 및 baseURL 설정을 변경하는 인증 시스템 마이그레이션입니다. 변경사항
시퀀스 다이어그램sequenceDiagram
participant User as 사용자
participant App as App/Router
participant AuthCtx as AuthContext
participant API as API 서버
participant ProtRoute as ProtectedRoute
User->>App: 앱 방문
App->>AuthCtx: AuthProvider 초기화
AuthCtx->>API: GET /api/user/details (로그인 확인)
alt 인증됨
API-->>AuthCtx: 성공
AuthCtx->>AuthCtx: isLoggedIn = true
else 미인증
API-->>AuthCtx: 실패
AuthCtx->>AuthCtx: isLoggedIn = false
end
User->>App: 보호된 경로 접근 시도
App->>ProtRoute: 경로 확인
alt isLoggedIn === true
ProtRoute->>ProtRoute: Outlet 렌더링 (자식 경로 허용)
ProtRoute-->>User: 보호된 콘텐츠 표시
else isLoggedIn === false
ProtRoute->>App: Navigate to /login
App-->>User: 로그인 페이지로 리다이렉트
end
User->>App: 로그인 폼 제출 (학번 + 비밀번호)
App->>API: POST /api/auth/login (studentId, password)
API-->>App: 인증 성공 (쿠키 설정)
App->>AuthCtx: login() 호출
AuthCtx->>AuthCtx: isLoggedIn = true
AuthCtx-->>App: 로그인 완료
App-->>User: 대시보드로 리다이렉트
예상 코드 리뷰 노력🎯 4 (Complex) | ⏱️ ~50 minutes 관련 이슈
관련 PR
추천 리뷰어
시
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/components/login/LoginForm.jsx (1)
37-49:⚠️ Potential issue | 🔴 Critical필수:
AuthContext의login함수에서 로그인 후isLoggedIn상태 업데이트 필요
frontend/src/contexts/AuthContext.jsx의login함수(Line 25-33)를 보면, API 호출 후setIsLoggedIn(true)를 호출하지 않습니다. 따라서 로그인이 성공하고nav('/')로 이동해도,isLoggedIn상태는 여전히false로 유지되어 라우팅 및 상태 관리에서 문제가 발생합니다.또한 Line 46-49의 catch 블록에서
AbortError를 필터링하지 않아, 이전 요청이 abort될 때도 에러 토스트가 표시됩니다.수정 필요
frontend/src/contexts/AuthContext.jsx의 login 함수에서:const login = async ({ studentId, password }, signal) => { const paylaod = { studentId, password }; const res = await api.post('/api/auth/login', paylaod, { signal }); + setIsLoggedIn(true); return res.data; };
frontend/src/components/login/LoginForm.jsx의 catch 블록에서:} catch (err) { + if (err.name === 'AbortError') return; console.dir(err); toast.error('로그인에 실패하였습니다. 학번과 비밀번호를 확인해주세요.'); }
🤖 Fix all issues with AI agents
In `@frontend/package.json`:
- Line 17: package.json currently lists the qrcode.react dependency but there is
no usage in the codebase; either remove the "qrcode.react" entry from
frontend/package.json to avoid shipping an unused package, or add the actual QR
code implementation that uses that dependency (for example create a QRCode React
component that imports { QRCode } from "qrcode.react" and use it where you need
to render a QR code, e.g., in the PaymentPage or ShareLink component). Ensure
you update any related imports and run the build to confirm no unused-dependency
warnings remain.
In `@frontend/src/components/attendancemanage/SessionManagementCard.jsx`:
- Around line 37-39: selectedSessionId is a string (from e.target.value) but
session.attendanceSessionId in sessionList is a UUID object, so the ===
comparison in currentSession (const currentSession = sessionList.find((session)
=> session.attendanceSessionId === selectedSessionId)) always fails; fix by
normalizing types: when handling the HTML select change assign selectedSessionId
as the string value (keep it as e.target.value) and ensure sessionList IDs are
compared as strings (e.g., map/convert session.attendanceSessionId to
session.attendanceSessionId.toString() before find) or alternatively serialize
API responses so session.attendanceSessionId is already a string; apply the same
normalization in RoundDayPicker.jsx and AddUsersModal.jsx where you compare
selected IDs.
In `@frontend/src/components/Header.jsx`:
- Around line 14-18: The logout flow doesn't flip the AuthContext state so
isLoggedIn remains true; update the AuthContext's logout function (in
frontend/src/contexts/AuthContext.jsx) to call setIsLoggedIn(false) after any
auth cleanup (and before/after clearing tokens or calling backend) so the
context reflects the logged-out state; ensure the logout function exported/used
by Header.jsx is the same function that updates isLoggedIn and, if necessary,
persist the change (e.g., clear storage) so nav('/login') leads to a true
unauthenticated state.
In `@frontend/src/components/login/LoginForm.jsx`:
- Around line 107-112: 현재 로그인 폼의 "비밀번호 초기화" 링크는 클릭 핸들러만 있고 href가 없어 키보드 접근성이
떨어집니다; LoginForm.jsx의 해당 <a>를 대체하거나 수정하여 접근성을 보장하세요: 더 나은 fix는 요소를 <button>으로
교체하고 기존 onClick={() => setModalStep('resetPassword')} 핸들러를 유지하는 것이며, 만약 앵커를 유지해야
한다면 href="#"와 role="button"을 추가하고 onClick에서 event.preventDefault()를 호출해 키보드 포커스와
스크린리더 상호작용을 정상화하세요.
In `@frontend/src/components/login/ResetPasswordModal.jsx`:
- Around line 19-35: The catch block for the reset flow should ignore
intentional AbortError and avoid showing the failure toast or printing
console.dir for aborted requests: inside the try/catch around resetPassword
(which uses abortRef.current.signal), change the catch to detect and return
early for an AbortError (check err.name === 'AbortError' or use DOMException
detection) so that toast.error('비밀번호 재설정 메일 전송에 실패했습니다...') is not shown for
cancellations; remove or wrap console.dir(err) so only real, non-abort errors
are logged (and still trigger the error toast), keeping references to abortRef,
resetPassword, isValidEmail, onClose, toast.success and toast.error to locate
the code to change.
In `@frontend/src/components/protectedRoute.jsx`:
- Around line 4-14: The auth loading flag is initialized incorrectly causing
ProtectedRoute (which uses useAuth()) to redirect to /login before async
checkLogin completes; in AuthContext.jsx change the loading useState initial
value from false to true so the initial render preserves the loading state until
the checkLogin useEffect resolves, ensuring ProtectedRoute's loading check
prevents premature redirection.
In `@frontend/src/contexts/AuthContext.jsx`:
- Around line 7-8: The initial state causes a flicker because loading starts as
false; change the AuthContext initial state so authentication check doesn't
treat the user as unauthenticated during async check — for example, set loading
to true (or isLoggedIn to null) in AuthContext.jsx, ensure checkLogin sets
setIsLoggedIn(...) and then setLoading(false) only after the async result, and
update ProtectedRoute to rely on loading (or null isLoggedIn) to render a loader
instead of redirecting while loading; update uses of isLoggedIn and loading
accordingly to avoid the temporary redirect.
- Around line 25-33: The login function in AuthContext.jsx never updates the
context state after a successful API call; after the API returns (inside login
in AuthContext.jsx) call setIsLoggedIn(true) to update authentication state, and
remove or delegate this logic to the existing login implementation in
frontend/src/utils/auth.js to avoid duplication (either call utils.login from
AuthContext.jsx or consolidate the logic into one login function); ensure you
reference the AuthContext.jsx login function and the setIsLoggedIn state updater
when making the change.
- Around line 35-45: The logout function in AuthContext.jsx removes the user
from localStorage but never updates the auth state, so isLoggedIn remains true;
update the logout handler (logout) to call the context state setters (e.g.,
setIsLoggedIn(false) and setUser(null)) in the finally block after
localStorage.removeItem('user') so the React state reflects logout immediately
(also consider triggering any existing redirect/cleanup logic the context
exposes).
In `@frontend/src/utils/auth.js`:
- Around line 20-23: Rename the misspelled variable `paylaod` to `payload` in
the `login` function (export const login) so the object passed to
`api.post('/api/auth/login', ...)` is correctly named, and update any other
occurrences of `paylaod` (including in AuthContext.jsx around the auth
call/usage) to `payload` so all references match the corrected identifier.
In `@frontend/src/utils/axios.js`:
- Around line 3-6: The api axios instance is created with baseURL: '' causing
inconsistent endpoints and failed token reissue calls that directly use
import.meta.env.VITE_API_URL; update the axios.create call (symbol: api) to use
a single source of truth for the backend URL (e.g. set baseURL to
import.meta.env.VITE_API_URL or a safe fallback) and refactor the token reissue
logic to call the api instance rather than constructing URLs with VITE_API_URL
directly so all requests (including reissue) use the same baseURL configuration.
🧹 Nitpick comments (8)
frontend/vite.config.js (1)
37-45: 하드코딩된 백엔드 IP 주소는 환경 변수로 관리하는 것을 권장합니다.
54.180.175.139:8080이 직접 코드에 포함되어 있어, 서버 IP 변경 시 코드 수정이 필요합니다. 환경 변수(예:VITE_API_URL)를 활용하면 유지보수가 용이합니다. 또한axios.js의 토큰 재발급 요청에서는 여전히import.meta.env.VITE_API_URL을 참조하고 있어 불일치가 발생할 수 있습니다.제안
server: { proxy: { '/api': { - target: 'http://54.180.175.139:8080', // 예: http://54.180.175.139 + target: process.env.VITE_API_URL || 'http://54.180.175.139:8080', changeOrigin: true, secure: false, }, }, },frontend/src/utils/attendanceManage.js (1)
21-22: 디버그용console.log가 남아있습니다.프로덕션 코드에 불필요한 로깅이 포함되어 있습니다. 출석 세션 데이터가 콘솔에 노출될 수 있으므로 제거를 권장합니다.
제안
- // console.log('API BASE URL:', import.meta.env.VITE_API_URL); - console.log(res.data); return res.data;frontend/src/components/login/LoginForm.jsx (1)
6-9: 주석 처리된 import는 제거하는 것을 권장합니다.
SocialLoginButtons,VerificationModal,FindEmailResultModalimport가 주석 처리되어 있습니다. PR 설명에 "임시 주석 처리"라고 되어 있지만, VCS가 히스토리를 관리하므로 코드에서 제거하고 필요 시 복원하는 것이 더 깔끔합니다.frontend/src/components/attendancemanage/SessionManagementCard.jsx (2)
58-66: 버튼 클릭 핸들러가 아무 동작도 하지 않습니다.
openAddRoundsModal()이 주석 처리되어 있어, 세션을 선택한 후 "출석일자 추가" 버튼을 눌러도 아무 일이 일어나지 않습니다. WIP 상태라면 버튼을disabled처리하거나 TODO 주석을 명확히 남기는 것이 좋습니다.
42-50: 라운드 조회 TODO — 추후 연결 필요 확인.
selectedSessionId변경 시 라운드를 비우기만 하고 있으며, 실제getRoundsAPI 연결이 필요합니다. 이 작업을 추적하기 위한 이슈를 생성할까요?frontend/src/components/Header.jsx (1)
4-5: 사용하지 않는 import가 남아있습니다.
useState,useEffect(Line 4),api(Line 5)는 AuthContext로 전환 후 더 이상 사용되지 않습니다. 정리하세요.♻️ 미사용 import 제거
- import { useState, useEffect } from 'react'; - import { api } from '../utils/axios';frontend/src/components/Sidebar.jsx (2)
3-4: 사용하지 않는 import가 남아있습니다.
useEffect(Line 3),api(Line 4)는 AuthContext로 전환 후 더 이상 사용되지 않습니다. Header.jsx와 동일한 문제입니다.♻️ 미사용 import 제거
- import { useState, useEffect } from 'react'; + import { useState } from 'react'; - import { api } from '../utils/axios';
31-35: Header와 Sidebar에서 로그아웃 로직이 중복됩니다.
handleLogout함수가Header.jsx와 동일하게 구현되어 있습니다. 추후useAuth훅에 navigation과 toast를 포함한handleLogout을 제공하거나, 커스텀 훅으로 추출하는 것을 고려해보세요.또한 Line 174에서
handleLogout()이 async 함수인데await없이 호출되고 있습니다. 현재 AuthContext의 logout이 내부적으로 에러를 catch하므로 문제는 없지만, 일관성을 위해await를 추가하거나.catch()처리를 권장합니다.Also applies to: 167-176
| "immer": "^10.2.0", | ||
| "lucide-react": "^0.555.0", | ||
| "pretendard": "^1.3.9", | ||
| "qrcode.react": "^4.2.0", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# qrcode.react 및 qrcode 사용처 검색
rg -i "qrcode" --type-list | head -20
rg -i "from.*qrcode|import.*qrcode|require.*qrcode" -g '!node_modules' -g '!.next'Repository: SISC-IT/sisc-web
Length of output: 449
🏁 Script executed:
#!/bin/bash
# qrcode.react 및 qrcode 사용처 검색
rg "qrcode" --type js --type jsx --type ts --type tsx -iRepository: SISC-IT/sisc-web
Length of output: 87
🏁 Script executed:
#!/bin/bash
# qrcode 사용처 검색
rg "qrcode" --type js --type ts -i
echo "---"
# 다른 방식으로도 검색
rg "import.*qrcode|require.*qrcode" -iRepository: SISC-IT/sisc-web
Length of output: 63
🏁 Script executed:
#!/bin/bash
# package.json에서 qrcode 의존성 확인
echo "=== package.json에서 qrcode 관련 의존성 ==="
rg "qrcode" frontend/package.json
echo -e "\n=== 모든 QR 관련 참조 확인 ==="
rg -i "qr" --type js --type ts
echo -e "\n=== 변경된 파일 목록 ==="
git ls-files frontend/src --cachedRepository: SISC-IT/sisc-web
Length of output: 10561
qrcode.react 의존성을 제거하거나 사용처를 추가하세요.
qrcode.react 패키지가 package.json에 추가되었지만 코드베이스 전체에서 사용되지 않습니다. 불필요한 의존성은 번들 크기를 증가시키므로 제거하거나, 실제 QR코드 기능이 필요하다면 해당 구현을 함께 추가해주세요.
🤖 Prompt for AI Agents
In `@frontend/package.json` at line 17, package.json currently lists the
qrcode.react dependency but there is no usage in the codebase; either remove the
"qrcode.react" entry from frontend/package.json to avoid shipping an unused
package, or add the actual QR code implementation that uses that dependency (for
example create a QRCode React component that imports { QRCode } from
"qrcode.react" and use it where you need to render a QR code, e.g., in the
PaymentPage or ShareLink component). Ensure you update any related imports and
run the build to confirm no unused-dependency warnings remain.
| const currentSession = sessionList.find( | ||
| (session) => session.attendanceSessionId === selectedSessionId | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find the getAttendanceSessions function to see its return type
rg -n "getAttendanceSessions" --type=js --type=jsx --type=ts --type=tsx -A 10 -B 2Repository: SISC-IT/sisc-web
Length of output: 87
🏁 Script executed:
# Look for TypeScript type definitions related to attendance sessions
fd -e ts -e tsx | xargs rg -l "attendanceSessionId" 2>/dev/null || echo "No TS files found with attendanceSessionId"Repository: SISC-IT/sisc-web
Length of output: 570
🏁 Script executed:
# Check the complete SessionManagementCard.jsx file to understand the context better
cat -n frontend/src/components/attendancemanage/SessionManagementCard.jsx | head -100Repository: SISC-IT/sisc-web
Length of output: 3759
🏁 Script executed:
# Search for how selectedSessionId is initialized and used throughout the codebase
rg -n "selectedSessionId" frontend/src/components/attendancemanage/SessionManagementCard.jsx -B 2 -A 2Repository: SISC-IT/sisc-web
Length of output: 1012
🏁 Script executed:
# Check the attendanceManage utility file to understand the return type of getAttendanceSessions
cat -n frontend/src/utils/attendanceManage.jsRepository: SISC-IT/sisc-web
Length of output: 6351
🏁 Script executed:
# Look at the backend entity to understand the data structure
cat -n backend/src/main/java/org/sejongisc/backend/attendance/entity/AttendanceSession.java | head -50Repository: SISC-IT/sisc-web
Length of output: 1514
🏁 Script executed:
# Check if there are any TypeScript type definitions for API responses
fd -type f \( -name "*.ts" -o -name "*.tsx" \) | xargs rg -l "Session" 2>/dev/null | head -5Repository: SISC-IT/sisc-web
Length of output: 457
🏁 Script executed:
# Search for how attendanceSessionId is used in the option element
rg -n "value={session.attendanceSessionId}" --type-add 'js:*.jsx' -B 3 -A 3Repository: SISC-IT/sisc-web
Length of output: 184
🏁 Script executed:
# Let's check if there's any mock data or API response structure defined
rg -n "attendanceSessionId" --type-add 'jsx:*.jsx' -B 2 -A 2 frontend/src/Repository: SISC-IT/sisc-web
Length of output: 3838
🏁 Script executed:
# Let's look at how the select option values are rendered
cat -n frontend/src/components/attendancemanage/SessionManagementCard.jsx | sed -n '76,95p'Repository: SISC-IT/sisc-web
Length of output: 816
🏁 Script executed:
# Check if there are any tests or example data that shows the actual data structure
fd -type f \( -name "*.test.*" -o -name "*.mock.*" -o -name "*.spec.*" \) -path "*attendance*" 2>/dev/null | head -10Repository: SISC-IT/sisc-web
Length of output: 42
🏁 Script executed:
# Let's check how UUID is serialized in the API response by looking at other components that use sessions
rg -n "session\." frontend/src/components/attendancemanage/ -A 1 | head -30Repository: SISC-IT/sisc-web
Length of output: 2276
🏁 Script executed:
# Let's check how the API response is being used in AttendanceManage.jsx (where console.log is called)
cat -n frontend/src/pages/AttendanceManage.jsx | sed -n '60,75p'Repository: SISC-IT/sisc-web
Length of output: 575
🏁 Script executed:
# Check if there's a development/local testing setup that shows actual API response structure
fd -type f -name "*.json" | xargs rg -l "attendanceSessionId" 2>/dev/null | head -5Repository: SISC-IT/sisc-web
Length of output: 517
🏁 Script executed:
# Let's verify the actual behavior by checking if UUID objects could be passed without string conversion
# Look for any custom JSON serializers or type configurations
rg -n "JsonSerialize\|@Serializable\|ObjectMapper" --type=javaRepository: SISC-IT/sisc-web
Length of output: 42
🏁 Script executed:
# Check if there are any actual errors or issues reported related to session selection in other files
rg -n "selectedSessionId\|currentSession" frontend/src/components/attendancemanage/ -B 3 -A 3 | head -50Repository: SISC-IT/sisc-web
Length of output: 42
🏁 Script executed:
# Let's verify by checking how the select value gets serialized and what the actual API returns
# Check if there's any type declaration or interface for the API response
find frontend/src -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.d.ts" \) | head -10Repository: SISC-IT/sisc-web
Length of output: 42
🏁 Script executed:
# Check if the sessionList data is actually being used with type checking anywhere
rg -n "sessionList" frontend/src/components/attendancemanage/SessionManagementCard.jsx -B 2 -A 2Repository: SISC-IT/sisc-web
Length of output: 701
🏁 Script executed:
# Let's verify by looking at a real scenario - how the selected sessionId is used after selection
cat -n frontend/src/components/attendancemanage/SessionManagementCard.jsx | sed -n '40,70p'Repository: SISC-IT/sisc-web
Length of output: 1171
🏁 Script executed:
# Check if there are any type mismatches or issues reported in other similar components
cat -n frontend/src/components/attendancemanage/RoundDayPicker.jsx | head -50Repository: SISC-IT/sisc-web
Length of output: 1850
🏁 Script executed:
# Let's check how the sessions context uses selectedSessionId
cat -n frontend/src/contexts/AttendanceContext.jsx | head -80Repository: SISC-IT/sisc-web
Length of output: 3024
🏁 Script executed:
# Verify if there's any actual runtime error or console warning about the comparison
rg -n "console" frontend/src/components/attendancemanage/SessionManagementCard.jsxRepository: SISC-IT/sisc-web
Length of output: 42
🏁 Script executed:
# Let's check the full implementation to see if currentSession is actually being used correctly
cat -n frontend/src/components/attendancemanage/SessionManagementCard.jsx | sed -n '36,120p'Repository: SISC-IT/sisc-web
Length of output: 3124
🏁 Script executed:
# Check what the actual JSON serialization of UUID looks like by examining test or sample data
rg -n "uuid\|UUID" frontend/src --type-add 'json:*.json' | head -20Repository: SISC-IT/sisc-web
Length of output: 42
🏁 Script executed:
# Let's verify how React renders the select value when passed a UUID
# Check if there's any explicit toString() or String() conversion
rg -n "toString\|String(" frontend/src/components/attendancemanage/SessionManagementCard.jsxRepository: SISC-IT/sisc-web
Length of output: 144
🏁 Script executed:
# Let's look at the axios configuration to see if there's any custom serialization
cat -n frontend/src/utils/axios.jsRepository: SISC-IT/sisc-web
Length of output: 1747
🏁 Script executed:
# Check if there's a .env or config file that might show the API structure
fd -type f \( -name ".env*" -o -name "vite.config.*" -o -name "config.*" \) | head -10Repository: SISC-IT/sisc-web
Length of output: 230
🏁 Script executed:
# Let's check AddUsersModal to see if it has the same pattern and how it uses selectedSessionId
cat -n frontend/src/components/attendancemanage/AddUsersModal.jsx | sed -n '1,80p'Repository: SISC-IT/sisc-web
Length of output: 2835
🏁 Script executed:
# Check how selectedSessionId is being set in the context (line 70 initialized it)
cat -n frontend/src/contexts/AttendanceContext.jsx | sed -n '65,150p'Repository: SISC-IT/sisc-web
Length of output: 3566
🏁 Script executed:
# Let's verify the actual behavior by checking if there's any explicit conversion happening elsewhere
rg -n "setSelectedSessionId" frontend/src --type-add 'jsx:*.jsx' -B 2 -A 2Repository: SISC-IT/sisc-web
Length of output: 2446
selectedSessionId 타입 불일치 — HTML 선택 요소의 값이 UUID 객체를 문자열로 변환하지만 비교 시 UUID 객체와 비교되는 버그
Line 82의 e.target.value는 항상 문자열이며, 백엔드에서 반환되는 session.attendanceSessionId는 UUID 객체입니다. HTML 속성에서 UUID 객체가 렌더링될 때 문자열로 변환되지만, sessionList의 데이터는 여전히 UUID 객체이므로 Line 38의 === 비교가 항상 실패합니다. 결과적으로 currentSession은 항상 undefined가 되어 세션 선택이 작동하지 않습니다.
같은 문제가 RoundDayPicker.jsx(Line 31-33)와 AddUsersModal.jsx(Line 39-41)에서도 발생합니다.
🐛 수정 방안
방법 1: selectedSessionId를 UUID 문자열로 유지 (권장)
- onChange={(e) => setSelectedSessionId(e.target.value)}
+ onChange={(e) => setSelectedSessionId(e.target.value)}
// sessionList의 UUID를 문자열로 변환하거나, find 비교를 수정
const currentSession = sessionList.find(
- (session) => session.attendanceSessionId === selectedSessionId
+ (session) => String(session.attendanceSessionId) === selectedSessionId
);방법 2: API 응답에서 UUID를 문자열로 직렬화
백엔드에서 UUID를 JSON 응답 시 문자열로 자동 직렬화하도록 구성하여 프론트엔드에서 문자열로 받기
🤖 Prompt for AI Agents
In `@frontend/src/components/attendancemanage/SessionManagementCard.jsx` around
lines 37 - 39, selectedSessionId is a string (from e.target.value) but
session.attendanceSessionId in sessionList is a UUID object, so the ===
comparison in currentSession (const currentSession = sessionList.find((session)
=> session.attendanceSessionId === selectedSessionId)) always fails; fix by
normalizing types: when handling the HTML select change assign selectedSessionId
as the string value (keep it as e.target.value) and ensure sessionList IDs are
compared as strings (e.g., map/convert session.attendanceSessionId to
session.attendanceSessionId.toString() before find) or alternatively serialize
API responses so session.attendanceSessionId is already a string; apply the same
normalization in RoundDayPicker.jsx and AddUsersModal.jsx where you compare
selected IDs.
| const handleLogout = async () => { | ||
| await logout(); | ||
| toast.success('로그아웃 되었습니다.'); | ||
| nav('/login'); | ||
| }; |
There was a problem hiding this comment.
logout() 호출 후 isLoggedIn 상태가 false로 변경되지 않습니다.
AuthContext의 logout 함수(frontend/src/contexts/AuthContext.jsx)가 setIsLoggedIn(false)를 호출하지 않습니다. 로그아웃 후 /login으로 이동하지만, isLoggedIn이 여전히 true이므로 뒤로가기 등으로 보호된 라우트에 접근하면 여전히 인증된 것으로 처리됩니다.
이 문제는 AuthContext의 logout 함수에서 수정해야 합니다.
🐛 AuthContext의 logout 함수에 setIsLoggedIn(false) 추가 필요
frontend/src/contexts/AuthContext.jsx:
const logout = async () => {
try {
await api.post('/api/auth/logout');
} catch (error) {
console.log('로그아웃 API 호출 실패:', error.message);
} finally {
localStorage.removeItem('user');
+ setIsLoggedIn(false);
}
};🤖 Prompt for AI Agents
In `@frontend/src/components/Header.jsx` around lines 14 - 18, The logout flow
doesn't flip the AuthContext state so isLoggedIn remains true; update the
AuthContext's logout function (in frontend/src/contexts/AuthContext.jsx) to call
setIsLoggedIn(false) after any auth cleanup (and before/after clearing tokens or
calling backend) so the context reflects the logged-out state; ensure the logout
function exported/used by Header.jsx is the same function that updates
isLoggedIn and, if necessary, persist the change (e.g., clear storage) so
nav('/login') leads to a true unauthenticated state.
| <a | ||
| className={styles.text} | ||
| onClick={() => setModalStep('verifyPhoneForEmail')} | ||
| onClick={() => setModalStep('resetPassword')} | ||
| > | ||
| 이메일 찾기 | ||
| </a> | ||
| <span className={styles.divider} aria-hidden="true"> | ||
| | | ||
| </span> | ||
| <a | ||
| className={styles.text} | ||
| onClick={() => setModalStep('verifyPhoneForPassword')} | ||
| > | ||
| 비밀번호 찾기 | ||
| 비밀번호 초기화 | ||
| </a> |
There was a problem hiding this comment.
<a> 태그에 href 없이 사용 — 접근성 개선 필요.
<a> 태그가 href 없이 onClick만으로 사용되고 있어 키보드 내비게이션이 불가능합니다. <button> 태그를 사용하거나 최소한 href="#"와 role="button"을 추가하세요.
♻️ 접근성 개선 제안
- <a
- className={styles.text}
- onClick={() => setModalStep('resetPassword')}
- >
- 비밀번호 초기화
- </a>
+ <button
+ type="button"
+ className={styles.text}
+ onClick={() => setModalStep('resetPassword')}
+ >
+ 비밀번호 초기화
+ </button>📝 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.
| <a | |
| className={styles.text} | |
| onClick={() => setModalStep('verifyPhoneForEmail')} | |
| onClick={() => setModalStep('resetPassword')} | |
| > | |
| 이메일 찾기 | |
| </a> | |
| <span className={styles.divider} aria-hidden="true"> | |
| | | |
| </span> | |
| <a | |
| className={styles.text} | |
| onClick={() => setModalStep('verifyPhoneForPassword')} | |
| > | |
| 비밀번호 찾기 | |
| 비밀번호 초기화 | |
| </a> | |
| <button | |
| type="button" | |
| className={styles.text} | |
| onClick={() => setModalStep('resetPassword')} | |
| > | |
| 비밀번호 초기화 | |
| </button> |
🤖 Prompt for AI Agents
In `@frontend/src/components/login/LoginForm.jsx` around lines 107 - 112, 현재 로그인
폼의 "비밀번호 초기화" 링크는 클릭 핸들러만 있고 href가 없어 키보드 접근성이 떨어집니다; LoginForm.jsx의 해당 <a>를
대체하거나 수정하여 접근성을 보장하세요: 더 나은 fix는 요소를 <button>으로 교체하고 기존 onClick={() =>
setModalStep('resetPassword')} 핸들러를 유지하는 것이며, 만약 앵커를 유지해야 한다면 href="#"와
role="button"을 추가하고 onClick에서 event.preventDefault()를 호출해 키보드 포커스와 스크린리더 상호작용을
정상화하세요.
| abortRef.current?.abort(); | ||
| abortRef.current = new AbortController(); | ||
|
|
||
| if (!isValidEmail(email)) { | ||
| toast.error('이메일 형식이 올바르지 않습니다.'); | ||
| return; | ||
| } | ||
| if (password.length < 8) { | ||
| toast.error('비밀번호는 8자 이상이어야 합니다.'); | ||
| return; | ||
| try { | ||
| await resetPassword({ email }, abortRef.current.signal); | ||
| toast.success('비밀번호 재설정 메일을 전송했습니다.'); | ||
| onClose(); | ||
| } catch (err) { | ||
| console.dir(err); | ||
| toast.error( | ||
| '비밀번호 재설정 메일 전송에 실패했습니다. 다시 시도해주세요.' | ||
| ); | ||
| } |
There was a problem hiding this comment.
AbortError가 사용자에게 에러 토스트로 노출됩니다.
Line 19에서 이전 요청을 abort하면, 이전 요청의 catch 블록에서 AbortError가 잡히면서 "비밀번호 재설정 메일 전송에 실패했습니다" 토스트가 표시됩니다. AbortError는 의도된 취소이므로 필터링해야 합니다.
또한 Line 31의 console.dir(err)는 디버그 코드이므로 제거하거나 조건부로 변경하는 것을 권장합니다.
🐛 AbortError 필터링 수정 제안
} catch (err) {
- console.dir(err);
- toast.error(
- '비밀번호 재설정 메일 전송에 실패했습니다. 다시 시도해주세요.'
- );
+ if (err.name === 'AbortError') return;
+ toast.error(
+ '비밀번호 재설정 메일 전송에 실패했습니다. 다시 시도해주세요.'
+ );
}📝 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.
| abortRef.current?.abort(); | |
| abortRef.current = new AbortController(); | |
| if (!isValidEmail(email)) { | |
| toast.error('이메일 형식이 올바르지 않습니다.'); | |
| return; | |
| } | |
| if (password.length < 8) { | |
| toast.error('비밀번호는 8자 이상이어야 합니다.'); | |
| return; | |
| try { | |
| await resetPassword({ email }, abortRef.current.signal); | |
| toast.success('비밀번호 재설정 메일을 전송했습니다.'); | |
| onClose(); | |
| } catch (err) { | |
| console.dir(err); | |
| toast.error( | |
| '비밀번호 재설정 메일 전송에 실패했습니다. 다시 시도해주세요.' | |
| ); | |
| } | |
| abortRef.current?.abort(); | |
| abortRef.current = new AbortController(); | |
| if (!isValidEmail(email)) { | |
| toast.error('이메일 형식이 올바르지 않습니다.'); | |
| return; | |
| } | |
| try { | |
| await resetPassword({ email }, abortRef.current.signal); | |
| toast.success('비밀번호 재설정 메일을 전송했습니다.'); | |
| onClose(); | |
| } catch (err) { | |
| if (err.name === 'AbortError') return; | |
| toast.error( | |
| '비밀번호 재설정 메일 전송에 실패했습니다. 다시 시도해주세요.' | |
| ); | |
| } |
🤖 Prompt for AI Agents
In `@frontend/src/components/login/ResetPasswordModal.jsx` around lines 19 - 35,
The catch block for the reset flow should ignore intentional AbortError and
avoid showing the failure toast or printing console.dir for aborted requests:
inside the try/catch around resetPassword (which uses abortRef.current.signal),
change the catch to detect and return early for an AbortError (check err.name
=== 'AbortError' or use DOMException detection) so that toast.error('비밀번호 재설정 메일
전송에 실패했습니다...') is not shown for cancellations; remove or wrap console.dir(err)
so only real, non-abort errors are logged (and still trigger the error toast),
keeping references to abortRef, resetPassword, isValidEmail, onClose,
toast.success and toast.error to locate the code to change.
| const [isLoggedIn, setIsLoggedIn] = useState(false); | ||
| const [loading, setLoading] = useState(false); |
There was a problem hiding this comment.
loading 초기값이 false로 설정되어 인증 확인 전에 비인증 상태로 렌더링됩니다.
checkLogin이 비동기로 실행되는 동안 loading=false, isLoggedIn=false 상태이므로, ProtectedRoute가 로그인 페이지로 잠깐 리다이렉트하는 플리커 현상이 발생합니다.
수정
- const [loading, setLoading] = useState(false);
+ const [loading, setLoading] = useState(true);🤖 Prompt for AI Agents
In `@frontend/src/contexts/AuthContext.jsx` around lines 7 - 8, The initial state
causes a flicker because loading starts as false; change the AuthContext initial
state so authentication check doesn't treat the user as unauthenticated during
async check — for example, set loading to true (or isLoggedIn to null) in
AuthContext.jsx, ensure checkLogin sets setIsLoggedIn(...) and then
setLoading(false) only after the async result, and update ProtectedRoute to rely
on loading (or null isLoggedIn) to render a loader instead of redirecting while
loading; update uses of isLoggedIn and loading accordingly to avoid the
temporary redirect.
| const login = async ({ studentId, password }, signal) => { | ||
| const paylaod = { studentId, password }; | ||
|
|
||
| const res = await api.post('/api/auth/login', paylaod, { signal }); | ||
| // 토큰은 자동으로 쿠키에 저장됨 (백엔드가 Set-Cookie 헤더로 전송) | ||
| // res.data에는 유저 정보만 있음 (accessToken, refreshToken 제거됨) | ||
|
|
||
| return res.data; | ||
| }; |
There was a problem hiding this comment.
login() 성공 후 isLoggedIn 상태가 업데이트되지 않습니다.
로그인 API 호출이 성공해도 setIsLoggedIn(true)가 호출되지 않아, 인증 상태가 갱신되지 않습니다. 또한 이 로직은 frontend/src/utils/auth.js의 login 함수와 중복됩니다.
수정
const login = async ({ studentId, password }, signal) => {
- const paylaod = { studentId, password };
- const res = await api.post('/api/auth/login', paylaod, { signal });
+ const payload = { studentId, password };
+ const res = await api.post('/api/auth/login', payload, { signal });
+ setIsLoggedIn(true);
return res.data;
};🤖 Prompt for AI Agents
In `@frontend/src/contexts/AuthContext.jsx` around lines 25 - 33, The login
function in AuthContext.jsx never updates the context state after a successful
API call; after the API returns (inside login in AuthContext.jsx) call
setIsLoggedIn(true) to update authentication state, and remove or delegate this
logic to the existing login implementation in frontend/src/utils/auth.js to
avoid duplication (either call utils.login from AuthContext.jsx or consolidate
the logic into one login function); ensure you reference the AuthContext.jsx
login function and the setIsLoggedIn state updater when making the change.
| const logout = async () => { | ||
| try { | ||
| await api.post('/api/auth/logout'); | ||
| } catch (error) { | ||
| // 로그아웃 API 실패해도 무시 (토큰이 없을 수 있음) | ||
| console.log('로그아웃 API 호출 실패:', error.message); | ||
| } finally { | ||
| // localStorage 유저 정보 삭제 | ||
| localStorage.removeItem('user'); | ||
| } | ||
| }; |
There was a problem hiding this comment.
logout() 후 isLoggedIn 상태가 false로 설정되지 않습니다.
로그아웃 후에도 isLoggedIn이 true로 유지되어 보호된 라우트에 접근 가능한 상태가 됩니다. 이 PR의 목적이 로그아웃/인증 상태 버그 수정인 점을 고려하면 핵심적인 누락입니다.
수정
const logout = async () => {
try {
await api.post('/api/auth/logout');
} catch (error) {
console.log('로그아웃 API 호출 실패:', error.message);
} finally {
localStorage.removeItem('user');
+ setIsLoggedIn(false);
}
};📝 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.
| const logout = async () => { | |
| try { | |
| await api.post('/api/auth/logout'); | |
| } catch (error) { | |
| // 로그아웃 API 실패해도 무시 (토큰이 없을 수 있음) | |
| console.log('로그아웃 API 호출 실패:', error.message); | |
| } finally { | |
| // localStorage 유저 정보 삭제 | |
| localStorage.removeItem('user'); | |
| } | |
| }; | |
| const logout = async () => { | |
| try { | |
| await api.post('/api/auth/logout'); | |
| } catch (error) { | |
| // 로그아웃 API 실패해도 무시 (토큰이 없을 수 있음) | |
| console.log('로그아웃 API 호출 실패:', error.message); | |
| } finally { | |
| // localStorage 유저 정보 삭제 | |
| localStorage.removeItem('user'); | |
| setIsLoggedIn(false); | |
| } | |
| }; |
🤖 Prompt for AI Agents
In `@frontend/src/contexts/AuthContext.jsx` around lines 35 - 45, The logout
function in AuthContext.jsx removes the user from localStorage but never updates
the auth state, so isLoggedIn remains true; update the logout handler (logout)
to call the context state setters (e.g., setIsLoggedIn(false) and setUser(null))
in the finally block after localStorage.removeItem('user') so the React state
reflects logout immediately (also consider triggering any existing
redirect/cleanup logic the context exposes).
| export const login = async ({ studentId, password }, signal) => { | ||
| const paylaod = { studentId, password }; | ||
|
|
||
| const res = await api.post('/api/auth/login', paylaod, { signal }); |
There was a problem hiding this comment.
오타: paylaod → payload
Line 21과 Line 23에서 paylaod로 오타가 있습니다. 동일한 오타가 AuthContext.jsx Line 26에도 존재합니다.
수정
export const login = async ({ studentId, password }, signal) => {
- const paylaod = { studentId, password };
- const res = await api.post('/api/auth/login', paylaod, { signal });
+ const payload = { studentId, password };
+ const res = await api.post('/api/auth/login', payload, { signal });📝 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.
| export const login = async ({ studentId, password }, signal) => { | |
| const paylaod = { studentId, password }; | |
| const res = await api.post('/api/auth/login', paylaod, { signal }); | |
| export const login = async ({ studentId, password }, signal) => { | |
| const payload = { studentId, password }; | |
| const res = await api.post('/api/auth/login', payload, { signal }); |
🤖 Prompt for AI Agents
In `@frontend/src/utils/auth.js` around lines 20 - 23, Rename the misspelled
variable `paylaod` to `payload` in the `login` function (export const login) so
the object passed to `api.post('/api/auth/login', ...)` is correctly named, and
update any other occurrences of `paylaod` (including in AuthContext.jsx around
the auth call/usage) to `payload` so all references match the corrected
identifier.
| export const api = axios.create({ | ||
| baseURL: import.meta.env.VITE_API_URL, | ||
| baseURL: '', | ||
| withCredentials: true, | ||
| }); |
There was a problem hiding this comment.
baseURL과 토큰 재발급 요청 간의 URL 불일치 문제
api 인스턴스는 baseURL: ''으로 설정되어 Vite 프록시를 통해 요청하지만, 토큰 재발급(Line 20)은 import.meta.env.VITE_API_URL을 직접 사용합니다. VITE_API_URL이 미설정 시 "undefined/api/auth/reissue"로 요청되어 토큰 갱신이 실패합니다.
또한 프로덕션 빌드에서는 Vite 프록시가 동작하지 않으므로, baseURL: ''로는 실제 백엔드에 요청이 도달하지 않습니다.
제안: 일관된 URL 관리
export const api = axios.create({
- baseURL: '',
+ baseURL: import.meta.env.VITE_API_URL || '',
withCredentials: true,
});또는 재발급 요청도 api 인스턴스를 통하도록 변경:
- await axios.post(
- `${import.meta.env.VITE_API_URL}/api/auth/reissue`,
- {},
+ await axios.post(
+ '/api/auth/reissue',
+ {},
{ withCredentials: true }
);Also applies to: 19-23
🤖 Prompt for AI Agents
In `@frontend/src/utils/axios.js` around lines 3 - 6, The api axios instance is
created with baseURL: '' causing inconsistent endpoints and failed token reissue
calls that directly use import.meta.env.VITE_API_URL; update the axios.create
call (symbol: api) to use a single source of truth for the backend URL (e.g. set
baseURL to import.meta.env.VITE_API_URL or a safe fallback) and refactor the
token reissue logic to call the api instance rather than constructing URLs with
VITE_API_URL directly so all requests (including reissue) use the same baseURL
configuration.
Summary by CodeRabbit
릴리스 노트
새로운 기능
개선 사항