mirim-app/frontend/src/AuthContext.jsx
AWESOMEDEV 0336213ca2 fix: 기본기 전수 감사(38 에이전트, 172파일)에서 확정된 버그 10건 수정
[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>
2026-07-17 11:42:05 +09:00

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);
}