Skip to content

Commit 0a72cf7

Browse files
jun-0411young-52
andauthored
✨ Authorisation & Login page (#8)
### πŸ“ μž‘μ—… λ‚΄μš© - 둜그인 νŽ˜μ΄μ§€ μž‘μ—…μ„ μ§„ν–‰ν–ˆμŠ΅λ‹ˆλ‹€. - axios 라이브러리λ₯Ό μ΄μš©ν•΄μ„œ api μž‘μ—…μ„ μ§„ν–‰ν–ˆμŠ΅λ‹ˆλ‹€. - zustand 라이브러리λ₯Ό μ΄μš©ν•΄μ„œ λ‘œκ·ΈμΈμ‹œ μœ μ € 정보와 인증 토큰이 둜컬 μŠ€ν† λ¦¬μ§€μ— μ €μž₯되게 ν•˜μ˜€μŠ΅λ‹ˆλ‹€. - μ†Œμ…œ 둜그인 λ‘œμ§μ„ λ§Œλ“€κ³  μ†Œμ…œ λ‘œκ·ΈμΈμ‹œ μ½œλ°±λ˜λŠ” νŽ˜μ΄μ§€λ₯Ό λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€. ### πŸ“Έ μŠ€ν¬λ¦°μƒ· (선택) <img width="2514" height="1315" alt="μŠ€ν¬λ¦°μƒ· 2026-01-01 210036" src="https://github.com/user-attachments/assets/9809afa6-cbdb-4b2c-8a3e-b2f9b978b98b" /> ### πŸš€ 리뷰 μš”κ΅¬μ‚¬ν•­ (선택) - 헀더 μž‘μ—…μ„ λͺ¨λ‹ˆ 인증 토큰을 context둜 μ €μž₯ν•˜μ…¨λŠ”λ°, zustandλ₯Ό λ„μž…ν•˜λŠ”κ±΄ μ–΄λ–¨κΉŒμš”? zustandλ₯Ό μ΄μš©ν•˜λ©΄ μœ μ € 정보와 인증 토큰을 ν•œ λ²ˆμ— 관리할 수 μžˆμ–΄μ§‘λ‹ˆλ‹€. - λ¦¬λ“œλ―Έμ— 적힌 폴더 κ΅¬μ‘°μ—λŠ” μ—†μ§€λ§Œ, type 폴더가 있으면 νŽΈλ¦¬ν•  것 κ°™μ•„μ„œ 폴더λ₯Ό μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€. type 폴더 μ‚¬μš©ν•˜μ§€ μ•Šκ³  contants 폴더에 ν†΅ν•©ν•˜λŠ”κ²Œ 더 μ’‹μ„κΉŒμš”? - utils 폴더에 μ»€μŠ€ν…€ 훅을 μœ„μΉ˜ν–ˆμŠ΅λ‹ˆλ‹€. hooks 폴더λ₯Ό λ§Œλ“€μ–΄μ„œ utils와 κ΅¬λΆ„ν•˜λŠ”κ²Œ μ’‹μ„κΉŒμš”? - 둜그인과 νšŒμ›κ°€μž…μ— λŒ€ν•œ api ꡬ쑰λ₯Ό κ΅¬μƒν•΄λ΄€μŠ΅λ‹ˆλ‹€. 이건 μŠ¬λž™μ— μ˜¬λ¦΄κ²Œμš”! - 컀밋 λ©”μ„Έμ§€ λ‚΄μš©μ΄λ‚˜ κΉƒλͺ¨μ§€μ— λΆ€μ μ ˆν•œκ²Œ μžˆμ—ˆμ„κΉŒμš”...? --------- Co-authored-by: Park Junyoung <bloomwayz@snu.ac.kr>
1 parent 32cf284 commit 0a72cf7

File tree

15 files changed

+649
-8
lines changed

15 files changed

+649
-8
lines changed

β€Ž.gitignoreβ€Ž

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,10 @@ dist-ssr
4040
.ionide
4141

4242
### macOS ###
43-
.DS_Store
43+
.DS_Store
44+
45+
## Environment Variables ###
46+
.env
47+
.env.production
48+
.env.development
49+
*.local

β€Žpackage.jsonβ€Ž

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
},
1515
"dependencies": {
1616
"@tailwindcss/vite": "^4.1.18",
17+
"axios": "^1.13.2",
1718
"react": "^19.1.0",
1819
"react-dom": "^19.1.0",
19-
"react-router": "^7.11.0"
20+
"react-router": "^7.11.0",
21+
"zustand": "^5.0.9"
2022
},
2123
"devDependencies": {
2224
"@biomejs/biome": "1.9.4",

β€Žsrc/api/apiClient.tsβ€Ž

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import axios from 'axios';
2+
import useAuthStore from '../hooks/useAuthStore';
3+
4+
// 곡톡 섀정을 κ°€μ§„ axios μΈμŠ€ν„΄μŠ€ 생성
5+
const apiClient = axios.create({
6+
baseURL: '/api', // λͺ¨λ“  μš”μ²­μ˜ κΈ°λ³Έ 경둜
7+
timeout: 5000,
8+
});
9+
10+
// μš”μ²­ 인터셉터: λͺ¨λ“  μš”μ²­ 직전에 싀행됨
11+
apiClient.interceptors.request.use((config) => {
12+
const token = useAuthStore.getState().token;
13+
14+
if (token) {
15+
config.headers.Authorization = `Bearer ${token}`;
16+
}
17+
18+
return config;
19+
});
20+
21+
export default apiClient;

β€Žsrc/api/auth/login.tsβ€Ž

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { AuthResponse, LoginRequest } from '../../types/auth';
2+
import apiClient from '../apiClient';
3+
4+
export default async function login(data: LoginRequest): Promise<AuthResponse> {
5+
const response = await apiClient.post<AuthResponse>('/auth/login', data);
6+
return response.data;
7+
}

β€Žsrc/api/auth/me.tsβ€Ž

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { User, UserGroup } from '../../types/auth';
2+
import apiClient from '../apiClient';
3+
4+
export default async function getMe(): Promise<User & { groups: UserGroup[] }> {
5+
const response = await apiClient.get('/auth/me');
6+
return response.data;
7+
}

β€Žsrc/api/auth/signup.tsβ€Ž

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { AuthResponse, SignUpRequest } from '../../types/auth';
2+
import apiClient from '../apiClient';
3+
4+
export default async function signup(
5+
data: SignUpRequest
6+
): Promise<AuthResponse> {
7+
const formData = new FormData();
8+
9+
formData.append('username', data.username);
10+
formData.append('password', data.password);
11+
formData.append('name', data.name);
12+
13+
if (data.photo) {
14+
formData.append('photo', data.photo); // λ°±μ—”λ“œ ν•„λ“œλͺ…κ³Ό 'photo' μΌμΉ˜μ‹œν‚€κΈ°
15+
}
16+
17+
const response = await apiClient.post<AuthResponse>('/auth/signup', formData);
18+
return response.data;
19+
}

β€Žsrc/api/auth/social.tsβ€Ž

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { AuthResponse, SocialLoginRequest } from '../../types/auth';
2+
import apiClient from '../apiClient';
3+
4+
export default async function social(
5+
data: SocialLoginRequest
6+
): Promise<AuthResponse> {
7+
const response = await apiClient.post<AuthResponse>('/auth/social', data);
8+
return response.data;
9+
}

β€Žsrc/constants/auth.tsβ€Ž

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// .envμ—μ„œ μ •μ˜ν•œ κΈ°λ³Έ λ¦¬λ‹€μ΄λ ‰νŠΈ μ£Όμ†Œ
2+
const REDIRECT_URI_BASE = import.meta.env.VITE_REDIRECT_URI;
3+
4+
// ꡬ글 인증 URL
5+
export const GOOGLE_AUTH_URL = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${
6+
import.meta.env.VITE_GOOGLE_CLIENT_ID
7+
}&redirect_uri=${REDIRECT_URI_BASE}/google&response_type=code&scope=openid email profile`;
8+
9+
// 카카였 인증 URL
10+
export const KAKAO_AUTH_URL = `https://kauth.kakao.com/oauth/authorize?client_id=${
11+
import.meta.env.VITE_KAKAO_CLIENT_ID
12+
}&redirect_uri=${REDIRECT_URI_BASE}/kakao&response_type=code`;

β€Žsrc/hooks/useAuth.tsβ€Ž

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { useNavigate } from 'react-router';
2+
import loginApi from '../api/auth/login';
3+
import getMeApi from '../api/auth/me';
4+
import signupApi from '../api/auth/signup';
5+
import socialApi from '../api/auth/social';
6+
import type {
7+
LoginRequest,
8+
SignUpRequest,
9+
SocialLoginRequest,
10+
} from '../types/auth';
11+
import useAuthStore from './useAuthStore';
12+
13+
export default function useAuth() {
14+
const navigate = useNavigate();
15+
16+
const { user, isLoggedIn, login, logout, updateUser } = useAuthStore(
17+
(state) => state
18+
);
19+
20+
// 1. 이메일 둜그인 둜직
21+
const handleLogin = async (data: LoginRequest) => {
22+
try {
23+
const { user, token } = await loginApi(data);
24+
login(user, token); // Zustand μŠ€ν† μ–΄ μ—…λ°μ΄νŠΈ
25+
navigate('/'); // 메인 νŽ˜μ΄μ§€λ‘œ 이동
26+
} catch (error) {
27+
console.error('Login failed:', error);
28+
alert('아이디 λ˜λŠ” λΉ„λ°€λ²ˆν˜Έλ₯Ό ν™•μΈν•΄μ£Όμ„Έμš”.');
29+
}
30+
};
31+
32+
// 2. 이메일 νšŒμ›κ°€μž… 둜직
33+
const handleSignUp = async (data: SignUpRequest) => {
34+
try {
35+
const { user, token } = await signupApi(data);
36+
login(user, token);
37+
navigate('/');
38+
} catch (error) {
39+
console.error('Signup failed:', error);
40+
alert('νšŒμ›κ°€μž… 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.');
41+
}
42+
};
43+
44+
// 3. μ†Œμ…œ 둜그인 둜직
45+
const handleSocialLogin = async (data: SocialLoginRequest) => {
46+
try {
47+
const { user, token } = await socialApi(data);
48+
login(user, token);
49+
navigate('/');
50+
} catch (error) {
51+
console.error('Social login failed:', error);
52+
alert('μ†Œμ…œ λ‘œκ·ΈμΈμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€.');
53+
}
54+
};
55+
56+
// 4. λ‚΄ 정보 동기화
57+
const refreshUser = async () => {
58+
if (!isLoggedIn) return;
59+
try {
60+
const userData = await getMeApi();
61+
// μœ μ € 정보와 κ·Έλ£Ή 정보 등이 λ‹΄κΈ΄ λ°μ΄ν„°λ‘œ μŠ€ν† μ–΄ κ°±μ‹ 
62+
updateUser(userData);
63+
} catch (error) {
64+
console.error('Refresh user failed:', error);
65+
handleLogout(); // 토큰이 μœ νš¨ν•˜μ§€ μ•ŠμœΌλ©΄ λ‘œκ·Έμ•„μ›ƒ 처리
66+
alert('μœ μ € 정보 동기화 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.');
67+
}
68+
};
69+
70+
// 5. λ‘œκ·Έμ•„μ›ƒ 둜직
71+
const handleLogout = () => {
72+
logout(); // Zustand μƒνƒœ μ΄ˆκΈ°ν™”
73+
navigate('/login');
74+
};
75+
76+
return {
77+
user,
78+
isLoggedIn,
79+
handleLogin,
80+
handleSignUp,
81+
handleSocialLogin,
82+
refreshUser,
83+
handleLogout,
84+
};
85+
}

β€Žsrc/hooks/useAuthStore.tsβ€Ž

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { create } from 'zustand';
2+
import { persist } from 'zustand/middleware';
3+
import type { User } from '../types/auth';
4+
5+
interface AuthState {
6+
user: User | null;
7+
token: string | null;
8+
isLoggedIn: boolean;
9+
// 둜그인 μ‹œ μœ μ € 정보와 토큰을 ν•¨κ»˜ μ €μž₯
10+
login: (user: User, token: string) => void;
11+
// λ‘œκ·Έμ•„μ›ƒ μ‹œ μƒνƒœ μ΄ˆκΈ°ν™”
12+
logout: () => void;
13+
// μœ μ € μ •λ³΄λ§Œ μ—…λ°μ΄νŠΈν•˜λŠ” μ•‘μ…˜
14+
updateUser: (user: User) => void;
15+
}
16+
17+
const useAuthStore = create<AuthState>()(
18+
persist(
19+
(set) => ({
20+
user: null,
21+
token: null,
22+
isLoggedIn: false,
23+
24+
login: (user, token) => set({ user, token, isLoggedIn: true }),
25+
logout: () => set({ user: null, token: null, isLoggedIn: false }),
26+
updateUser: (user) => set({ user }),
27+
}),
28+
{
29+
name: 'auth-storage', // 둜컬 μŠ€ν† λ¦¬μ§€μ— μ €μž₯될 ν‚€ 이름
30+
}
31+
)
32+
);
33+
34+
export default useAuthStore;

0 commit comments

Comments
Β (0)