Skip to content

Commit 9e43dfd

Browse files
authored
Merge pull request #35 from next-engineer/feature/30-api
Feature/30 api
2 parents b1345ac + 691267d commit 9e43dfd

File tree

17 files changed

+252
-454
lines changed

17 files changed

+252
-454
lines changed

src/App.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ import MyMatchesPage from './pages/MyMatchesPage';
2323
import CreateMatchPage from './pages/CreateMatchPage';
2424

2525
export default function App() {
26+
const { syncFromStorage } = useAuthStore();
27+
2628
useEffect(() => {
27-
useAuthStore.getState().syncFromStorage();
28-
}, []);
29+
syncFromStorage();
30+
}, [syncFromStorage]);
2931

3032
return (
3133
<BrowserRouter>
@@ -39,6 +41,7 @@ export default function App() {
3941
<Route path="/home" element={<HomePage />} />
4042
<Route path="/notfound" element={<NotFoundPage />} />
4143
<Route path="/matches" element={<MatchesPage />} />
44+
<Route path="/unauthorized" element={<NotFoundPage />} />
4245

4346
{/* 로그인하지 않은 사용자만 접근 가능 */}
4447
<Route
@@ -57,13 +60,6 @@ export default function App() {
5760
</PublicOnlyRoute>
5861
}
5962
/>
60-
{/* <Route
61-
path="/auth/callback"
62-
element={
63-
<ProtectedRoute>
64-
<AuthCallbackPage />
65-
</ProtectedRoute>}
66-
/> */}
6763

6864
{/* 로그인한 사용자만 접근 가능 */}
6965
<Route
@@ -75,7 +71,7 @@ export default function App() {
7571
}
7672
/>
7773
<Route
78-
path="/mymatches"
74+
path="/matches/me"
7975
element={
8076
<ProtectedRoute>
8177
<MyMatchesPage />
@@ -85,7 +81,7 @@ export default function App() {
8581

8682
{/* 관리자만 접근 가능 */}
8783
<Route
88-
path="/createMatch"
84+
path="/admin/matches/new"
8985
element={
9086
<AdminRoute>
9187
<CreateMatchPage />

src/api/axiosInstance.ts

Lines changed: 36 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
// src/api/axiosInstance.ts
21
import axios, { AxiosError } from 'axios';
32
import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
43
import { useAuthStore } from '../store/useAuthStore';
54

6-
// --- 기본 설정 & 환경 변수 검증 ---
5+
// 기본 환경 변수와 설정
76
const baseURL = import.meta.env.VITE_API_BASE_URL as string | undefined;
87
if (!baseURL) {
98
throw new Error('VITE_API_BASE_URL environment variable is required');
@@ -15,7 +14,7 @@ export const api = axios.create({
1514
timeout: 10000,
1615
});
1716

18-
// --- 유틸들 ---
17+
// 토큰 관리 함수
1918
function getAccessToken(): string | null {
2019
const { tokens } = useAuthStore.getState();
2120
return tokens?.accessToken ?? localStorage.getItem('access_token');
@@ -26,50 +25,32 @@ function getIdToken(): string | null {
2625
return tokens?.idToken ?? localStorage.getItem('id_token');
2726
}
2827

29-
function toAbsoluteURL(url?: string, cfgBaseURL?: string): URL | null {
30-
if (!url) return null;
31-
try {
32-
return new URL(url); // 절대 URL
33-
} catch {
34-
try {
35-
return new URL(url, cfgBaseURL ?? baseURL); // 상대 → 절대
36-
} catch {
37-
return null;
38-
}
39-
}
40-
}
41-
42-
const SKIP_AUTH_PATHS = ['/auth/refresh', '/auth/logout', '/health'];
28+
// 인증이 필요하지 않은 경로
29+
const SKIP_AUTH_PATHS = ['/', '/auth/refresh', '/auth/logout', '/health'];
4330

4431
function shouldSkipAuth(config: InternalAxiosRequestConfig): boolean {
45-
// 사용자가 직접 Authorization을 준 경우 존중
46-
const hasAuthHeader =
47-
(config.headers?.Authorization as string | undefined) ??
48-
(config.headers as Record<string, unknown> | undefined)?.authorization;
32+
// Authorization 헤더가 이미 있으면 스킵
33+
const hasAuthHeader = config.headers?.Authorization || config.headers?.authorization;
4934
if (hasAuthHeader) return true;
5035

51-
const reqUrl = toAbsoluteURL(config.url, config.baseURL ?? baseURL);
52-
if (!reqUrl) return false;
53-
54-
// baseURL과 동일한 오리진에만 토큰 부착
55-
const base = new URL(baseURL!);
56-
if (reqUrl.origin !== base.origin) return true;
57-
58-
// baseURL에 path prefix가 있는 경우(예: https://host/api) 그 하위 경로만 적용
59-
if (!reqUrl.pathname.startsWith(base.pathname)) return true;
60-
61-
// 스킵 리스트 적용 (base pathname 이후의 상대 경로 기준)
62-
const relPath = reqUrl.pathname.slice(base.pathname.length) || '/';
63-
return SKIP_AUTH_PATHS.some((p) => relPath.startsWith(p));
36+
// 스킵 경로 확인
37+
const url = config.url || '';
38+
return SKIP_AUTH_PATHS.some((path) => url.startsWith(path));
6439
}
6540

41+
// 로그 함수들 (토큰 마스킹)
6642
function safeRequestLog(config: InternalAxiosRequestConfig) {
6743
if (!DEBUG) return;
6844
const headers = { ...(config.headers as Record<string, unknown>) };
6945
if ('Authorization' in headers) headers.Authorization = 'Bearer ***';
7046
if ('authorization' in headers) headers.authorization = 'Bearer ***';
7147
if ('X-ID-Token' in headers) headers['X-ID-Token'] = '***';
7248
if ('x-id-token' in headers) headers['x-id-token'] = '***';
49+
console.log('API Request:', {
50+
method: config.method?.toUpperCase(),
51+
url: config.url,
52+
headers,
53+
});
7354
}
7455

7556
function safeResponseLog(res: AxiosResponse) {
@@ -91,41 +72,45 @@ function safeErrorLog(err: AxiosError) {
9172
});
9273
}
9374

94-
// --- 인터셉터들 ---
95-
// Request: Bearer 자동 부착(+ 안전 로깅)
75+
// Request 인터셉터 - 자동 토큰 부착
9676
api.interceptors.request.use((config) => {
9777
if (!shouldSkipAuth(config)) {
98-
const token = getAccessToken();
99-
if (token) {
78+
const accessToken = getAccessToken();
79+
const idToken = getIdToken();
80+
81+
if (accessToken) {
10082
config.headers = config.headers ?? {};
101-
(config.headers as Record<string, string>).Authorization = `Bearer ${token}`;
83+
config.headers.Authorization = `Bearer ${accessToken}`;
10284
}
103-
const idToken = getIdToken();
85+
10486
if (idToken) {
10587
config.headers = config.headers ?? {};
106-
(config.headers as Record<string, string>)['X-ID-Token'] = idToken;
88+
config.headers['X-ID-Token'] = idToken;
10789
}
10890
}
91+
10992
safeRequestLog(config);
11093
return config;
11194
});
11295

113-
// Response: 공통 로깅 + 401 처리(토큰 정리)
96+
// Response 인터셉터 - 401 에러 처리
11497
api.interceptors.response.use(
115-
(res) => {
116-
safeResponseLog(res);
117-
return res;
98+
(response) => {
99+
safeResponseLog(response);
100+
return response;
118101
},
119-
(err: AxiosError) => {
120-
if (err.response?.status === 401) {
121-
// 토큰 정리
102+
(error: AxiosError) => {
103+
safeErrorLog(error);
104+
105+
// 401 에러 시 토큰 정리
106+
if (error.response?.status === 401) {
122107
const { clearTokens } = useAuthStore.getState();
123-
if (clearTokens) clearTokens();
108+
clearTokens?.();
124109
localStorage.removeItem('access_token');
125110
localStorage.removeItem('id_token');
126111
localStorage.removeItem('refresh_token');
127112
}
128-
safeErrorLog(err);
129-
return Promise.reject(err);
113+
114+
return Promise.reject(error);
130115
},
131116
);

src/api/matchApi.ts

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,48 @@
11
import { api } from './axiosInstance';
2-
import type { Match, CreateMatchRequest, MatchParticipant } from '../types';
2+
import type {
3+
Match,
4+
CreateMatchRequest,
5+
MatchParticipant,
6+
MatchInfo,
7+
MyMatchesResponse,
8+
} from '../types';
39

4-
// API: GET /api/matches
5-
// (Authorization: Bearer <access_token>)
10+
// GET /api/matches - 전체 매치 조회
611
export async function fetchAllMatches(): Promise<Match[]> {
7-
const { data } = await api.get<Match[]>('/api/matches');
12+
const { data } = await api.get<Match[]>('matches');
813
return data;
914
}
1015

11-
// API: GET /api/matches/me
12-
// (Authorization: Bearer <access_token>)
13-
export async function fetchMyMatches(): Promise<{
14-
items: Match[];
15-
page: number;
16-
size: number;
17-
total: number;
18-
}> {
19-
const { data } = await api.get('/api/matches/me');
20-
return data;
16+
// GET /api/matches/me - 내 참여 매치 조회
17+
export async function fetchMyMatches(): Promise<MatchInfo[]> {
18+
const { data } = await api.get<MyMatchesResponse>('matches/me');
19+
return data.matches;
2120
}
2221

23-
// API: POST /api/matches
24-
// (Authorization: Bearer <access_token>)
22+
// POST /api/matches - 매치 생성 (관리자)
2523
export async function createMatch(matchData: CreateMatchRequest): Promise<Match> {
26-
const { data } = await api.post<Match>('/api/matches', matchData);
24+
const { data } = await api.post<Match>('matches', matchData);
2725
return data;
2826
}
2927

30-
// API: POST /api/matches/{matchId}/participants
31-
// (Authorization: Bearer <access_token>)
28+
// POST /api/matches/{matchId}/participants - 매치 참여 (일반 사용자)
3229
export async function joinMatch(matchId: number): Promise<MatchParticipant> {
33-
const { data } = await api.post<MatchParticipant>(`/api/matches/${matchId}/participants`, {});
30+
const { data } = await api.post<MatchParticipant>(`matches/${matchId}/participants`);
3431
return data;
3532
}
3633

37-
// API: DELETE /api/matches/{matchId}/participants
38-
// (Authorization: Bearer <access_token>)
34+
// DELETE /api/matches/{matchId}/participants - 매치 참여 취소 (일반 사용자)
3935
export async function leaveMatch(matchId: number): Promise<void> {
40-
await api.delete(`/api/matches/${matchId}/participants`);
36+
await api.delete(`matches/${matchId}/participants`);
4137
}
4238

43-
// API: DELETE /api/matches/{matchId}
44-
// (Authorization: Bearer <access_token>)
39+
// DELETE /api/matches/{matchId} - 매치 삭제 (관리자)
4540
export async function deleteMatch(matchId: number): Promise<void> {
46-
await api.delete(`/api/matches/${matchId}`);
41+
await api.delete(`matches/${matchId}`);
4742
}
4843

49-
// API: PUT /api/matches/{matchId}/status
50-
// (Authorization: Bearer <access_token>)
51-
export async function updateMatchStatus(
52-
matchId: number,
53-
status: Match['matchStatus'],
54-
): Promise<Match> {
55-
const { data } = await api.put<Match>(`/api/matches/${matchId}/status`, { status });
44+
// GET /api/matches/{matchId}/participants - 매칭 참가자 조회 (관리자)
45+
export async function fetchMatchParticipants(matchId: number): Promise<MatchParticipant[]> {
46+
const { data } = await api.get<MatchParticipant[]>(`matches/${matchId}/participants`);
5647
return data;
5748
}

src/api/userApi.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,30 @@ export async function postUserMe(): Promise<void> {
55
await api.post('users/signin-up', null);
66
}
77

8-
export async function getMyProfile(): Promise<User> {
8+
// GET /api/users/me - 내 프로필 조회
9+
export async function fetchUserProfile(): Promise<User> {
910
const { data } = await api.get<User>('users/me');
1011
return data;
1112
}
13+
14+
// PUT /api/users/me - 프로필 수정
15+
export async function updateUserProfile(profileData: Partial<User>): Promise<User> {
16+
const { data } = await api.put<User>('users/me', profileData);
17+
return data;
18+
}
19+
20+
// POST /api/uploads/profile-image - 프로필 이미지 업로드
21+
export async function uploadProfileImage(formData: FormData): Promise<{ url: string }> {
22+
const { data } = await api.post<{ url: string }>('uploads/profile-image', formData, {
23+
headers: {
24+
'Content-Type': 'multipart/form-data',
25+
},
26+
});
27+
return data;
28+
}
29+
30+
// PUT /api/users/me - 랭크 업데이트 (별도 함수로 분리)
31+
export async function updateUserRank(rank: string): Promise<User> {
32+
const { data } = await api.put<User>('users/me', { rank });
33+
return data;
34+
}

src/auth/callback.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { COGNITO } from './config';
22
import { useAuthStore } from '../store/useAuthStore';
3-
import { postUserMe, getMyProfile } from '../api/userApi';
3+
import { fetchUserProfile, postUserMe } from '../api/userApi';
44

55
export type TokenResponse = {
66
id_token: string;
@@ -57,7 +57,7 @@ export async function handleAuthCallback(): Promise<TokenResponse | null> {
5757

5858
try {
5959
await postUserMe();
60-
const me = await getMyProfile();
60+
const me = await fetchUserProfile();
6161
useAuthStore.getState().setUser(me);
6262
} catch (e) {
6363
console.error('[callback] user sync failed', e);

src/components/AppLayout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export default function AppLayout() {
5353

5454
const navigationItems = [
5555
{ label: '전체 매칭', path: '/matches', icon: '⚽' },
56-
...(user?.role === 'USER' ? [{ label: '내 매칭', path: '/matches/mine', icon: '📋' }] : []),
56+
...(user?.role === 'USER' ? [{ label: '내 매칭', path: '/matches/me', icon: '📋' }] : []),
5757
...(user?.role === 'ADMIN'
5858
? [{ label: '매칭 개설', path: '/admin/matches/new', icon: '➕' }]
5959
: []),

0 commit comments

Comments
 (0)