-
Notifications
You must be signed in to change notification settings - Fork 4
[feature] 공용 axios 인스턴스 및 Authorization 자동 첨부 #56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
8916578
a6d2e54
780726d
80436cc
3137093
948df73
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // 목적: 로그인/회원가입 API만 담당(다른 리소스와 분리) | ||
| import axios from '@/lib/axios'; | ||
| import type { LoginRequest, LoginResponse, UserRequest } from '@/types/user'; | ||
|
|
||
| // 로그인: POST /token | ||
| export async function apiLogin(body: LoginRequest) { | ||
| const { data } = await axios.post<LoginResponse>('/token', body); | ||
| return data; // data.item.token, data.item.user.item.id 사용 | ||
| } | ||
|
|
||
| // 회원가입: POST /users (알바생이면 type: 'employee') | ||
| export async function apiSignup(body: UserRequest) { | ||
| const { data } = await axios.post('/users', body); | ||
| return data; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // 목적: 유저 리소스 전용(내 정보 조회/수정) | ||
| import axios from '@/lib/axios'; | ||
| import type { User } from '@/types/user'; | ||
|
|
||
| // 내 정보 조회: GET /users/{user_id} | ||
| export async function apiGetUser(userId: string) { | ||
| const { data } = await axios.get<{ item: User }>(`/users/${userId}`); | ||
| return data.item; | ||
| } | ||
|
|
||
| // 내 정보 수정: PUT /users/{user_id} | ||
| export async function apiUpdateUser(userId: string, patch: Partial<User>) { | ||
| const { data } = await axios.put<{ item: User }>(`/users/${userId}`, patch); | ||
| return data.item; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,72 +1,98 @@ | ||
| import { LoginRequest, User, UserRequest, UserRole } from '@/types/user'; | ||
| import { createContext, ReactNode, useState } from 'react'; | ||
|
|
||
| /** | ||
| * @TODO | ||
| * token은 AuthProvider 내부에서만 관리하고 외부에는 파생된 상태(isLogin, user 같은 권한)만 소비하는 설계 | ||
| * 컴포넌트에서 토큰 문자열을 직접 다룰일을 막을 수 있음 (보안/ 유지보수측면) | ||
| * 토큰 저장 위치(Storage, cookie 등)를 변경해야 할때도 컨텍스트 안에서만 토큰이 사용되기 때문에 외부 코드를 건드릴 일이 없음 | ||
| * token을 간접적으로 사용하는 저장,삭제,업데이트와 같은 액션은 내부 함수로 작성하고 Provider 외부로 노출하면 관리에 용이함 | ||
| * 외부는 파생된 상태값만 받아서 적절한 권한 처리만 하면 된다. | ||
| * | ||
| * AuthContextValue의 함수는 전부 void 값으로 지정했으나 구현에 따라 | ||
| * return 값이 필요할 경우 type/user 에서 맞는 타입 지정 | ||
| * | ||
| * 초반 설계는 AuthContext에서 구현을 하다가 | ||
| * 추후 AuthContext (토큰,로그인 상태 관리) / UserContext (프로필 전용) 로 관심사 분리 리팩토링 고려 | ||
| * | ||
| */ | ||
|
|
||
| type AuthState = { | ||
| user: User | null; | ||
| isPending: boolean; | ||
| }; | ||
| interface AuthContextValue extends AuthState { | ||
| isLogin: boolean; | ||
| role: UserRole; | ||
| // 목적: 앱 전역에서 로그인/유저 상태를 관리하고, 인증 관련 함수를 제공한다. | ||
| // 특징: 토큰과 사용자 ID를 내부에서 관리하고, 외부는 파생 상태와 동작 함수만 사용한다. | ||
|
|
||
| import { apiLogin, apiSignup } from '@/api/auth'; // 로그인/회원가입 API | ||
| import { apiGetUser, apiUpdateUser } from '@/api/users'; // 내 정보 API | ||
| import type { LoginRequest, User, UserRequest, UserRole } from '@/types/user'; | ||
| import { createContext, ReactNode, useEffect, useMemo, useState } from 'react'; | ||
|
|
||
| type AuthContextValue = { | ||
| isLogin: boolean; // 토큰 보유 여부 | ||
| role: UserRole; // 'guest' | 'employee' | 'employer' | ||
| user: User | null; // 로그인 상태일 때의 내 정보 | ||
| login: (credentials: LoginRequest) => Promise<void>; | ||
| logout: () => void; | ||
| signup: (data: UserRequest) => Promise<void>; | ||
| getUser: () => Promise<void>; | ||
| updateUser: (data: Partial<User>) => Promise<void>; | ||
| } | ||
| const initialState: AuthState = { | ||
| user: null, | ||
| isPending: true, | ||
| updateUser: (patch: Partial<User>) => Promise<void>; | ||
| }; | ||
| export const AuthContext = createContext<AuthContextValue | undefined>(undefined); | ||
|
|
||
| export const AuthContext = createContext<AuthContextValue | null>(null); | ||
|
|
||
| // 로컬 스토리지 키: 읽기 쉬운 이름으로 고정 | ||
| const TOKEN_KEY = 'thejulge_token'; | ||
| const USER_ID_KEY = 'thejulge_user_id'; | ||
|
|
||
| const AuthProvider = ({ children }: { children: ReactNode }) => { | ||
| const [values, setValues] = useState<AuthState>(initialState); | ||
| // 핵심 상태: 토큰, 사용자 ID, 사용자 정보 | ||
| const [token, setToken] = useState<string | null>(null); | ||
| const [userId, setUserId] = useState<string | null>(null); | ||
| const [user, setUser] = useState<User | null>(null); | ||
|
|
||
| // 파생 상태: 로그인 여부, 역할 | ||
| const isLogin = !!token; | ||
| const role: UserRole = !isLogin | ||
| ? 'guest' | ||
| : values.user?.type === 'employer' | ||
| ? 'employer' | ||
| : 'employee'; | ||
| const role: UserRole = useMemo(() => (user ? user.type : 'guest'), [user]); | ||
|
|
||
| // 앱 시작 시, 저장소에 남아있는 토큰/사용자 ID 복원 | ||
| useEffect(() => { | ||
| if (typeof window === 'undefined') return; | ||
| const storedToken = localStorage.getItem(TOKEN_KEY); | ||
| const storedUserId = localStorage.getItem(USER_ID_KEY); | ||
| if (storedToken) setToken(storedToken); | ||
| if (storedUserId) setUserId(storedUserId); | ||
| }, []); | ||
|
|
||
| // 로그인: /token → 토큰과 사용자 ID 저장 → 즉시 내 정보 동기화 | ||
| const login: AuthContextValue['login'] = async credentials => { | ||
| // TODO: 로그인 구현 (API 요청 후 setValues, setToken) | ||
| const res = await apiLogin(credentials); | ||
| const newToken = res.item.token; | ||
| const newUserId = res.item.user.item.id; | ||
|
|
||
| setToken(newToken); | ||
| setUserId(newUserId); | ||
| if (typeof window !== 'undefined') { | ||
| localStorage.setItem(TOKEN_KEY, newToken); | ||
| localStorage.setItem(USER_ID_KEY, newUserId); | ||
| } | ||
|
|
||
| const me = await apiGetUser(newUserId); | ||
| setUser(me); | ||
| }; | ||
|
|
||
| // 로그아웃: 상태와 저장소 모두 비우기 | ||
| const logout: AuthContextValue['logout'] = () => { | ||
| // TODO: 로그아웃 구현 (setValues, setToken 초기화) | ||
| setToken(null); | ||
| setUser(null); | ||
| setUserId(null); | ||
| if (typeof window !== 'undefined') { | ||
| localStorage.removeItem(TOKEN_KEY); | ||
| localStorage.removeItem(USER_ID_KEY); | ||
| } | ||
| }; | ||
|
|
||
| // 회원가입: /users 성공만 확인(페이지에서 라우팅 처리) | ||
| const signup: AuthContextValue['signup'] = async data => { | ||
| // TODO: 회원가입 구현 | ||
| await apiSignup(data); | ||
| }; | ||
|
|
||
| // 내 정보 재조회 | ||
| const getUser: AuthContextValue['getUser'] = async () => { | ||
| // TODO: 유저 조회 구현 | ||
| if (!userId) throw new Error('로그인이 필요합니다'); | ||
| const me = await apiGetUser(userId); | ||
| setUser(me); | ||
| }; | ||
| const updateUser: AuthContextValue['updateUser'] = async data => { | ||
| // TODO: 유저 업데이트 구현 | ||
|
|
||
| // 내 정보 수정 | ||
| const updateUser: AuthContextValue['updateUser'] = async patch => { | ||
| if (!userId) throw new Error('로그인이 필요합니다'); | ||
| const updated = await apiUpdateUser(userId, patch); | ||
| setUser(updated); | ||
| }; | ||
|
|
||
| const value: AuthContextValue = { | ||
| ...values, | ||
| isLogin, | ||
| role, | ||
| user, | ||
| login, | ||
| logout, | ||
| signup, | ||
|
||
|
|
@@ -76,4 +102,5 @@ const AuthProvider = ({ children }: { children: ReactNode }) => { | |
|
|
||
| return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; | ||
| }; | ||
|
|
||
| export default AuthProvider; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,4 +6,23 @@ const axiosInstance: AxiosInstance = baseAxios.create({ | |
| 'Content-Type': 'application/json', | ||
| }, | ||
| }); | ||
|
|
||
| axiosInstance.interceptors.request.use(config => { | ||
| // SSR에서는 localStorage가 없으니 브라우저에서만 읽기 | ||
| const token = typeof window !== 'undefined' ? localStorage.getItem('thejulge_token') : null; | ||
| if (token) { | ||
| // Axios v1: headers가 AxiosHeaders(클래스)일 수 있음 | ||
| const h = config.headers as any; | ||
| if (h?.set && typeof h.set === 'function') { | ||
| // 1) AxiosHeaders 인스턴스인 경우: set API 지원 | ||
| h.set('Authorization', `Bearer ${token}`); | ||
| } else { | ||
| // 2) 평범한 객체인 경우: 속성만 추가 (전체 재할당 금지) | ||
| config.headers = config.headers ?? {}; | ||
| (config.headers as any).Authorization = `Bearer ${token}`; | ||
| } | ||
| } | ||
| return config; | ||
| }); | ||
|
||
|
|
||
| export default axiosInstance; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 패턴이 계속 반복되고 있어서 추후 시간이 남는다면 유틸함수로 컨트롤하게 리팩토링 할 수 있을것 같습니다! (제안)