Skip to content

Commit d238dfc

Browse files
committed
refactor: enhance request and response validation using Zod schemas
1 parent 0152e68 commit d238dfc

File tree

6 files changed

+62
-62
lines changed

6 files changed

+62
-62
lines changed

apps/client/src/features/auth/auth.api.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import axios from 'axios';
22

33
import {
44
PostLoginRequestDTO,
5+
PostLoginRequestSchema,
56
PostLoginResponseDTO,
7+
PostLoginResponseSchema,
68
PostRefreshResponseDTO,
79
PostRefreshResponseSchema,
810
} from '@/features/auth/auth.dto';
@@ -11,8 +13,8 @@ const AUTH_BASE_URL = '/api/auth';
1113

1214
export const login = (body: PostLoginRequestDTO) =>
1315
axios
14-
.post<PostLoginResponseDTO>(`${AUTH_BASE_URL}/login`, body)
15-
.then((res) => res.data);
16+
.post<PostLoginResponseDTO>(`${AUTH_BASE_URL}/login`, PostLoginRequestSchema.parse(body))
17+
.then((res) => PostLoginResponseSchema.parse(res.data));
1618

1719
export const logout = () => axios.post(`${AUTH_BASE_URL}/logout`);
1820

apps/client/src/features/auth/auth.hook.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useMutation } from '@tanstack/react-query';
22
import { useRouterState } from '@tanstack/react-router';
33
import { isAxiosError } from 'axios';
44
import { useState } from 'react';
5+
import { ZodError } from 'zod';
56

67
import { login } from '@/features/auth/auth.api';
78
import { useAuthStore } from '@/features/auth/auth.store';
@@ -25,8 +26,7 @@ export function useSignInForm() {
2526
});
2627

2728
const { mutate: loginQuery, isPending } = useMutation({
28-
mutationFn: (credentials: { email: string; password: string }) =>
29-
login(credentials),
29+
mutationFn: (credentials: { email: string; password: string }) => login(credentials),
3030
onSuccess: (response) => {
3131
addToast({
3232
type: 'SUCCESS',
@@ -51,14 +51,29 @@ export function useSignInForm() {
5151
message: error.response.data.message,
5252
});
5353
}
54+
} else if (error instanceof ZodError) {
55+
const { issues } = error;
56+
const emailIssue = issues.find((issue) => issue.path.includes('email'));
57+
const passwordIssue = issues.find((issue) => issue.path.includes('password'));
58+
59+
if (emailIssue) {
60+
setLoginFailed({
61+
status: 'INVALID',
62+
message: '올바른 이메일 형식이 아닙니다.',
63+
});
64+
} else if (passwordIssue) {
65+
setLoginFailed({
66+
status: 'INVALID',
67+
message: '비밀번호는 8-20자여야 합니다.',
68+
});
69+
}
5470
}
5571
},
5672
});
5773

5874
const isLoginEnabled = email.length > 0 && password.length > 7 && !isPending;
5975

60-
const handleLogin = (callback: () => void) =>
61-
loginQuery({ email, password }, { onSuccess: callback });
76+
const handleLogin = (callback: () => void) => loginQuery({ email, password }, { onSuccess: callback });
6277

6378
return {
6479
email,

apps/client/src/features/auth/auth.store.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,13 @@ interface AuthStore {
44
userId?: number;
55
accessToken?: string;
66
isLogin: () => boolean;
7-
setAuthInformation: ({
8-
userId,
9-
accessToken,
10-
}: {
11-
userId?: number;
12-
accessToken?: string;
13-
}) => void;
7+
setAuthInformation: ({ userId, accessToken }: { userId?: number; accessToken?: string }) => void;
148
clearAuthInformation: () => void;
159
}
1610

1711
export const useAuthStore = create<AuthStore>((set, get) => ({
1812
isLogin: () => get().accessToken != null,
19-
setAuthInformation: ({
20-
userId,
21-
accessToken,
22-
}: {
23-
userId?: number;
24-
accessToken?: string;
25-
}) => set({ userId, accessToken }),
26-
clearAuthInformation: () =>
27-
set({ userId: undefined, accessToken: undefined }),
13+
setAuthInformation: ({ userId, accessToken }: { userId?: number; accessToken?: string }) =>
14+
set({ userId, accessToken }),
15+
clearAuthInformation: () => set({ userId: undefined, accessToken: undefined }),
2816
}));
Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,26 @@
11
import axios from 'axios';
22

33
import {
4-
GetVerifyEmailDTO,
5-
GetVerifyEmailSchema,
6-
GetVerifyNicknameDTO,
7-
GetVerifyNicknameSchema,
4+
GetVerifyEmailRequestSchema,
5+
GetVerifyEmailResponseDTO,
6+
GetVerifyEmailResponseSchema,
7+
GetVerifyNicknameRequestSchema,
8+
GetVerifyNicknameResponseDTO,
9+
GetVerifyNicknameResponseSchema,
810
PostUserDTO,
911
PostUserSchema,
1012
} from '@/features/user/user.dto';
1113

12-
const USER_BASE_URL = `/api/users`;
13-
14-
export const postUser = (body: PostUserDTO) =>
15-
axios.post(USER_BASE_URL, PostUserSchema.parse(body));
14+
export const postUser = (body: PostUserDTO) => axios.post('/api/users', PostUserSchema.parse(body));
1615

1716
export const getVerifyEmail = (email: string) =>
1817
axios
19-
.get<GetVerifyEmailDTO>(
20-
`${USER_BASE_URL}/emails/${encodeURIComponent(email)}`,
21-
)
22-
.then((res) => GetVerifyEmailSchema.parse(res.data));
18+
.get<GetVerifyEmailResponseDTO>(`/api/users/emails/${encodeURIComponent(GetVerifyEmailRequestSchema.parse(email))}`)
19+
.then((res) => GetVerifyEmailResponseSchema.parse(res.data));
2320

2421
export const getVerifyNickname = (nickname: string) =>
2522
axios
26-
.get<GetVerifyNicknameDTO>(
27-
`${USER_BASE_URL}/nicknames/${encodeURIComponent(nickname)}`,
23+
.get<GetVerifyNicknameResponseDTO>(
24+
`/api/users/nicknames/${encodeURIComponent(GetVerifyNicknameRequestSchema.parse(nickname))}`,
2825
)
29-
.then((res) => GetVerifyNicknameSchema.parse(res.data));
26+
.then((res) => GetVerifyNicknameResponseSchema.parse(res.data));

apps/client/src/features/user/user.dto.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@ export const PostUserSchema = z.object({
66
nickname: z.string().min(3).max(20),
77
});
88

9-
export const GetVerifyEmailSchema = z.object({
10-
exists: z.boolean(),
11-
});
9+
export const GetVerifyEmailRequestSchema = z.string().email();
1210

13-
export const GetVerifyNicknameSchema = z.object({
11+
export const GetVerifyEmailResponseSchema = z.object({
1412
exists: z.boolean(),
1513
});
1614

17-
export type GetVerifyEmailDTO = z.infer<typeof GetVerifyEmailSchema>;
15+
export const GetVerifyNicknameRequestSchema = z.string().min(3).max(20);
16+
17+
export const GetVerifyNicknameResponseSchema = z.object({ exists: z.boolean() });
18+
1819
export type PostUserDTO = z.infer<typeof PostUserSchema>;
19-
export type GetVerifyNicknameDTO = z.infer<typeof GetVerifyNicknameSchema>;
20+
export type GetVerifyEmailRequestDTO = z.infer<typeof GetVerifyEmailRequestSchema>;
21+
export type GetVerifyEmailResponseDTO = z.infer<typeof GetVerifyEmailResponseSchema>;
22+
export type GetVerifyNicknameRequestDTO = z.infer<typeof GetVerifyNicknameRequestSchema>;
23+
export type GetVerifyNicknameResponseDTO = z.infer<typeof GetVerifyNicknameResponseSchema>;

apps/client/src/features/user/user.hook.ts

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,9 @@ import { isAxiosError } from 'axios';
22
import { debounce } from 'es-toolkit';
33
import { useCallback, useEffect, useMemo, useState } from 'react';
44

5-
import { getVerifyEmail, getVerifyNickname } from '@/features/user/index';
5+
import { getVerifyEmail, getVerifyNickname } from '@/features/user';
66

7-
import {
8-
validateEmail,
9-
validateNickname,
10-
validatePassword,
11-
ValidationStatusWithMessage,
12-
} from '@/shared';
7+
import { validateEmail, validateNickname, validatePassword, ValidationStatusWithMessage } from '@/shared';
138

149
export function useSignUpForm() {
1510
const [email, setEmail] = useState('');
@@ -18,14 +13,17 @@ export function useSignUpForm() {
1813

1914
const [password, setPassword] = useState('');
2015

21-
const [emailValidationStatus, setEmailValidationStatus] =
22-
useState<ValidationStatusWithMessage>({ status: 'INITIAL' });
16+
const [emailValidationStatus, setEmailValidationStatus] = useState<ValidationStatusWithMessage>({
17+
status: 'INITIAL',
18+
});
2319

24-
const [nicknameValidationStatus, setNicknameValidationStatus] =
25-
useState<ValidationStatusWithMessage>({ status: 'INITIAL' });
20+
const [nicknameValidationStatus, setNicknameValidationStatus] = useState<ValidationStatusWithMessage>({
21+
status: 'INITIAL',
22+
});
2623

27-
const [passwordValidationStatus, setPasswordValidationStatus] =
28-
useState<ValidationStatusWithMessage>({ status: 'INITIAL' });
24+
const [passwordValidationStatus, setPasswordValidationStatus] = useState<ValidationStatusWithMessage>({
25+
status: 'INITIAL',
26+
});
2927

3028
const isSignUpEnabled = useMemo(
3129
() =>
@@ -50,9 +48,7 @@ export function useSignUpForm() {
5048
if (!isAxiosError(error)) return;
5149

5250
const message =
53-
error.response?.data.message ??
54-
error.response?.data.messages.shift() ??
55-
'알 수 없는 오류가 발생했습니다.';
51+
error.response?.data.message ?? error.response?.data.messages.shift() ?? '알 수 없는 오류가 발생했습니다.';
5652

5753
setEmailValidationStatus({
5854
status: 'INVALID',
@@ -78,9 +74,7 @@ export function useSignUpForm() {
7874
if (!isAxiosError(error)) return;
7975

8076
const message =
81-
error.response?.data.message ??
82-
error.response?.data.messages.shift() ??
83-
'알 수 없는 오류가 발생했습니다.';
77+
error.response?.data.message ?? error.response?.data.messages.shift() ?? '알 수 없는 오류가 발생했습니다.';
8478

8579
setNicknameValidationStatus({
8680
status: 'INVALID',

0 commit comments

Comments
 (0)