Skip to content

Commit b5e235c

Browse files
committed
feat: 작성한 지원서 확인 및 수정 API 연결 완료
1 parent dd9e285 commit b5e235c

File tree

18 files changed

+2376
-172
lines changed

18 files changed

+2376
-172
lines changed

src/api/application.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import {
2+
API_URLS,
3+
getAuthHeaders,
4+
handleFetchResponse,
5+
} from "./config";
6+
import type {
7+
RecruitmentItemType,
8+
SubmitApplicationRequest,
9+
} from "./recruitment";
10+
11+
export type ApplicationAnswerDetail = {
12+
recruitmentItemId?: number;
13+
itemType: RecruitmentItemType;
14+
title: string;
15+
order: number;
16+
description?: string;
17+
multiple?: boolean;
18+
selectedOptionIds?: number[];
19+
selectedOptionTitles?: string[];
20+
text?: string;
21+
answer?: string;
22+
value?: string;
23+
date?: string;
24+
};
25+
26+
export type ApplicationDetailData = {
27+
applicationId: number;
28+
recruitmentId: number;
29+
recruitmentName: string;
30+
name: string;
31+
email: string;
32+
tel: string;
33+
answers: ApplicationAnswerDetail[];
34+
};
35+
36+
export type ApplicationDetailResponse = {
37+
code: number;
38+
message: string;
39+
data: ApplicationDetailData;
40+
};
41+
42+
export const getApplicationDetail = async (
43+
applicationId: number
44+
): Promise<ApplicationDetailResponse> => {
45+
const headers = getAuthHeaders();
46+
47+
const response = await fetch(
48+
API_URLS.APPLICATION_DETAIL.replace(
49+
":applicationId",
50+
String(applicationId)
51+
),
52+
{
53+
method: "GET",
54+
headers,
55+
}
56+
);
57+
58+
return handleFetchResponse<ApplicationDetailResponse>(response);
59+
};
60+
61+
export type UpdateApplicationRequest = SubmitApplicationRequest;
62+
63+
export type UpdateApplicationResponse = {
64+
code: number;
65+
message: string;
66+
data: {
67+
applicationId: number;
68+
};
69+
};
70+
71+
export const updateApplication = async (
72+
applicationId: number,
73+
payload: UpdateApplicationRequest
74+
): Promise<UpdateApplicationResponse> => {
75+
const headers = getAuthHeaders();
76+
77+
const response = await fetch(
78+
API_URLS.APPLICATION_DETAIL.replace(
79+
":applicationId",
80+
String(applicationId)
81+
),
82+
{
83+
method: "PATCH",
84+
headers,
85+
body: JSON.stringify(payload),
86+
}
87+
);
88+
89+
return handleFetchResponse<UpdateApplicationResponse>(response);
90+
};
91+

src/api/auth.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { useMutation } from "@tanstack/react-query";
2-
import axios from "axios";
3-
import { API_URLS } from "./config";
2+
import { API_URLS, apiClient } from "./config";
43

54
export type ManagerRegisterRequest = {
65
name: string;
@@ -35,8 +34,11 @@ export type ManagerLoginResponse = {
3534
async function registerManager(
3635
payload: ManagerRegisterRequest
3736
): Promise<ManagerRegisterResponse> {
38-
const { data } = await axios.post(API_URLS.MANAGER_REGISTER, payload, {
39-
headers: { "Content-Type": "application/json", Accept: "application/json" },
37+
const { data } = await apiClient.post(API_URLS.MANAGER_REGISTER, payload, {
38+
headers: {
39+
"Content-Type": "application/json",
40+
Accept: "application/json",
41+
},
4042
withCredentials: true,
4143
});
4244

@@ -51,8 +53,11 @@ async function registerManager(
5153
async function loginManager(
5254
payload: ManagerLoginRequest
5355
): Promise<ManagerLoginResponse> {
54-
const { data } = await axios.post(API_URLS.MANAGER_LOGIN, payload, {
55-
headers: { "Content-Type": "application/json", Accept: "application/json" },
56+
const { data } = await apiClient.post(API_URLS.MANAGER_LOGIN, payload, {
57+
headers: {
58+
"Content-Type": "application/json",
59+
Accept: "application/json",
60+
},
5661
withCredentials: true,
5762
});
5863

src/api/config.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,95 @@ export const API_URLS = {
1010
CREATE_CLUB: "/backend/councils",
1111
CHANGE_COUNCIL_NAME: "/backend/councils/:councilId/names",
1212
GET_COUNCIL_MEMBERS: "/backend/councils/:councilId/members",
13+
APPLICATIONS: "/backend/applications/:recruitmentId",
14+
APPLICATION_DETAIL: "/backend/applications/:applicationId",
15+
MY_APPLICATIONS: "/backend/applications/mine",
1316
} as const;
1417

1518
export const getApiUrl = (endpoint: string): string => {
1619
return `${SERVER_URI}${endpoint}`;
1720
};
1821

22+
const TOKEN_EXPIRED_CODE = 10002;
23+
const TOKEN_EXPIRED_MESSAGE = "만료된 토큰입니다";
24+
25+
let hasRedirectedForTokenExpiration = false;
26+
27+
const clearAuthTokens = () => {
28+
localStorage.removeItem("accessToken");
29+
localStorage.removeItem("token");
30+
};
31+
32+
export const handleTokenExpiration = () => {
33+
if (hasRedirectedForTokenExpiration) {
34+
return;
35+
}
36+
hasRedirectedForTokenExpiration = true;
37+
clearAuthTokens();
38+
if (typeof window !== "undefined" && window.location) {
39+
if (typeof window.history?.replaceState === "function") {
40+
window.history.replaceState(null, "", window.location.href);
41+
}
42+
window.location.replace("/admin/login");
43+
}
44+
};
45+
46+
type TokenErrorPayload = {
47+
code?: number;
48+
message?: string;
49+
};
50+
51+
const isTokenErrorPayload = (value: unknown): value is TokenErrorPayload => {
52+
return typeof value === "object" && value !== null;
53+
};
54+
55+
export const checkTokenExpiration = (payload: unknown) => {
56+
if (!isTokenErrorPayload(payload)) {
57+
return;
58+
}
59+
const { code, message } = payload;
60+
if (
61+
code === TOKEN_EXPIRED_CODE ||
62+
(typeof message === "string" && message.includes(TOKEN_EXPIRED_MESSAGE))
63+
) {
64+
handleTokenExpiration();
65+
}
66+
};
67+
68+
export async function handleFetchResponse<T>(response: Response): Promise<T> {
69+
const text = await response.text();
70+
let parsed: unknown = text;
71+
72+
if (text) {
73+
try {
74+
parsed = JSON.parse(text);
75+
} catch {
76+
// JSON 이 아닌 응답은 그대로 사용합니다.
77+
}
78+
} else {
79+
parsed = {};
80+
}
81+
82+
checkTokenExpiration(parsed);
83+
84+
if (!response.ok) {
85+
const errorMessage =
86+
typeof parsed === "object" && parsed !== null && "message" in parsed
87+
? String((parsed as { message?: string }).message ?? "")
88+
: text || response.statusText;
89+
90+
const logPayload =
91+
typeof parsed === "string" ? parsed : JSON.stringify(parsed);
92+
console.error("API Error Response:", logPayload);
93+
94+
throw new Error(
95+
`HTTP error! status: ${response.status} - ${errorMessage || logPayload}`
96+
);
97+
}
98+
99+
return parsed as T;
100+
}
101+
19102
/**
20103
* 인증 토큰이 포함된 헤더를 반환합니다.
21104
*/
@@ -54,3 +137,18 @@ apiClient.interceptors.request.use(
54137
return config;
55138
}
56139
);
140+
141+
apiClient.interceptors.response.use(
142+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
143+
(response: any) => {
144+
checkTokenExpiration(response?.data);
145+
return response;
146+
},
147+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
148+
(error: any) => {
149+
if (error?.response?.data) {
150+
checkTokenExpiration(error.response.data);
151+
}
152+
return Promise.reject(error);
153+
}
154+
);

0 commit comments

Comments
 (0)