Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/api/auth.ts
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;
}
15 changes: 15 additions & 0 deletions src/api/users.ts
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;
}
117 changes: 72 additions & 45 deletions src/context/authProvider.tsx
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);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

해당 패턴이 계속 반복되고 있어서 추후 시간이 남는다면 유틸함수로 컨트롤하게 리팩토링 할 수 있을것 같습니다! (제안)


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,
Copy link
Contributor

Choose a reason for hiding this comment

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

추후 시간이 남는다면 useMemo로 리팩토링을 해볼 수 있을것 같습니다! (제안)
isLogin, role, user 와 같은 값은 다시 계산이 일어나지 않게 할수도 있을것 같습니다!
모든 함수에 useCallback 을 넣는다면 모두다 디펜던시로 넣을 수도 있을것 같구요 !

Expand All @@ -76,4 +102,5 @@ const AuthProvider = ({ children }: { children: ReactNode }) => {

return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

export default AuthProvider;
19 changes: 19 additions & 0 deletions src/lib/axios/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Copy link
Contributor

Choose a reason for hiding this comment

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

eslint 규칙에 any 타입을 쓰면 vercel 에서 error 를 발생하면서 배포가 막혀있습니다! axios 포함, testAuth 페이지의 any 타입을 변경해야할것 같습니다!

그리고 axios v1 + any 타입 해결책으로는 new AxiosHeaders() 인스턴스로 생성하여 통일하는 방법이 있습니다.

import axios, { AxiosInstance, AxiosHeaders, InternalAxiosRequestConfig } from 'axios';

const axiosInstance: AxiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  headers: new AxiosHeaders({ 'Content-Type': 'application/json' }), 
// 타입을 확정지을 수 있도록 AxiosHeaders로 초기화
});


axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  // SSR 방어
  if (typeof window === 'undefined') return config;

  const token = localStorage.getItem('thejulge_token');
  if (token) {
    config.headers.set('Authorization', `Bearer ${token}`);
  }

  return config;
});

참고했던 블로그도 같이 첨부드릴게요!
블로그


export default axiosInstance;
Loading
Loading