[major]
- client.js 401 인터셉터: /auth/** 전체를 예외로 — 비밀번호 변경에서 현재 비번
틀리면 세션 만료로 오인해 /login으로 튕기던 버그 (에러 안내 도달 불가였음)
- Submission (assignment_id,user_id) 유니크 제약 추가(엔티티+운영DB) —
동시 제출 시 중복 행 → 이후 해당 과제 재제출 영구 500 방지. 자매 엔티티와 일관화
[minor]
- AuthContext: 마운트 시 /auth/me의 늦은 401이 로그인 성공을 덮는 경쟁 → 함수형 업데이트로 보존
- AssignmentsPage: key에 제출 id 포함 → 첫 제출 직후 카드 리마운트로 성공 메시지 소실
→ key를 과제 id로 고정, 메시지는 접히는 영역 밖으로
- CodeEditor: 텍스트 선택 후 Tab 시 커서가 선택 길이만큼 밀림 (selectionEnd+2 → selectionStart+2)
- div.olist>ol 마크업 오류로 목록 번호 이중 표시 — 7개 강좌 10곳 전부 ol.olist로 통일
(감사가 3파일 발견, 전역 grep으로 4파일 추가 확보)
- SignupPage 검증을 서버 규칙과 일치: 아이디 4자+영숫자, 생년월일 과거만
- LoginRequest @NotBlank + @Valid: username 누락 시 NPE 500 → 400
- server.error.include-message: always — 서버의 한국어 에러 사유가 화면까지 전달
('이미 사용 중인 아이디입니다' 등, 그동안 폴백 문구만 보였음)
검증: 테스트 22개 통과, 운영에서 400/409 message/비번변경 에러표시 확인.
각 버그의 원리를 소스 주석에 학습 포인트로 기록.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
62 lines
2.9 KiB
JavaScript
62 lines
2.9 KiB
JavaScript
// 이 파일이 하는 일: "지금 누가 로그인해 있는가"를 앱 전체에서 공유하는 Context.
|
|
// 앱이 처음 뜰 때 GET /api/auth/me로 세션을 확인하고,
|
|
// login()/logout() 함수를 하위 컴포넌트에 내려준다.
|
|
import { createContext, useContext, useEffect, useState } from 'react';
|
|
import client from './api/client';
|
|
|
|
// 학습 포인트: Context는 "props를 여러 단계 내려보내는 번거로움"을 해결한다.
|
|
// 사용자 정보처럼 앱 전역에서 필요한 값에 딱 맞는 도구다.
|
|
// Context가 없다면? App → Layout → 각 페이지 → 버튼까지 user를 props로
|
|
// 일일이 전달해야 하고, 중간 컴포넌트들은 쓰지도 않는 값을 나르기만 하게 된다.
|
|
// (이걸 "prop drilling"이라고 부른다.)
|
|
const AuthContext = createContext(null);
|
|
|
|
export function AuthProvider({ children }) {
|
|
// user: 로그인한 사용자 객체 {id, username, name, role, track} 또는 null
|
|
const [user, setUser] = useState(null);
|
|
// loading: 새로고침 직후 /auth/me 확인이 끝나기 전인지 여부.
|
|
// 학습 포인트: 이 플래그가 없으면 새로고침할 때마다
|
|
// "아직 확인 중인데 user가 null이니까" 로그인 페이지로 튕겨버린다.
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
// 앱 최초 로딩 시 세션 쿠키가 살아 있는지 서버에 물어본다.
|
|
//
|
|
// 학습 포인트 — 비동기 경쟁(race condition) 방어.
|
|
// 이 요청이 끝나기 전에 사용자가 로그인을 마칠 수 있다(자동완성 + 빠른 제출).
|
|
// 그때 뒤늦게 도착한 /auth/me의 401이 setUser(null)로 로그인 결과를 덮으면,
|
|
// 로그인은 성공했는데 화면은 로그인 페이지로 튕기는 황당한 버그가 된다.
|
|
// 그래서 함수형 업데이트로 "이미 로그인돼 있으면 건드리지 않는다":
|
|
// prev가 있으면(=login()이 먼저 끝남) 그대로 두고, 없을 때만 결과를 적용한다.
|
|
client
|
|
.get('/auth/me')
|
|
.then((res) => setUser((prev) => prev ?? res.data))
|
|
.catch(() => setUser((prev) => prev)) // 401 = 비로그인. 단, 먼저 끝난 로그인은 보존
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
async function login(username, password) {
|
|
// 실패하면 axios가 예외를 던지므로 호출한 쪽(LoginPage)에서 try/catch로 잡는다.
|
|
const res = await client.post('/auth/login', { username, password });
|
|
setUser(res.data);
|
|
return res.data;
|
|
}
|
|
|
|
async function logout() {
|
|
await client.post('/auth/logout');
|
|
setUser(null);
|
|
}
|
|
|
|
return (
|
|
<AuthContext.Provider value={{ user, loading, login, logout }}>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
}
|
|
|
|
// 학습 포인트: useContext를 감싼 커스텀 훅.
|
|
// 각 페이지는 `const { user } = useAuth()` 한 줄이면 된다.
|
|
export function useAuth() {
|
|
return useContext(AuthContext);
|
|
}
|