Skip to content

Commit 4f2e4f1

Browse files
authored
✨ 메일 인증, 코드 검증, 중복 이메일 검증 API 추가 (#26)
* 🤡 누락된 메일 인증 관련 API mock 추가 * ✨ 메일 인증, 코드 검증, 중복 이메일 검증 API 추가 * ✨ 메일 인증 관련 API 추가 * 🧪 메일 인증에 따른 성공/실패 컴포넌트 테스트 추가 * 🎨 포매팅 적용
1 parent 452d498 commit 4f2e4f1

17 files changed

Lines changed: 1140 additions & 312 deletions

File tree

src/feature/auth/application/auth-query.ts

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,58 @@ import { createErrorMessage } from '@/feature/shared/error/create-error-message'
55
import { useRouteNavigation } from '@/feature/shared/routes/use-route-navigation';
66
import type { UserRole } from '../domain/user-role';
77

8+
export const useSendVerificationEmail = ({
9+
onSuccess,
10+
setResponseMessage,
11+
}: {
12+
onSuccess: () => void;
13+
setResponseMessage: (message: string) => void;
14+
}) => {
15+
const { authUsecase } = useGuardContext(UsecaseContext);
16+
const { mutate: sendVerificationEmail, isPending } = useMutation({
17+
mutationFn: async ({ email }: { email: string }) => {
18+
return await authUsecase.sendVerificationEmail({ email });
19+
},
20+
onSuccess: (response) => {
21+
if (response.type === 'success') {
22+
onSuccess();
23+
return;
24+
}
25+
setResponseMessage(
26+
createErrorMessage(response.code, '인증 이메일 발송에 실패했습니다.')
27+
);
28+
},
29+
});
30+
31+
return { sendVerificationEmail, isPending };
32+
};
33+
34+
export const useValidateEmailCode = ({
35+
onSuccess,
36+
setCodeError,
37+
}: {
38+
onSuccess: (verificationToken: string) => void;
39+
setCodeError: (message: string) => void;
40+
}) => {
41+
const { authUsecase } = useGuardContext(UsecaseContext);
42+
const { mutate: validateEmailCode, isPending } = useMutation({
43+
mutationFn: async ({ email, code }: { email: string; code: string }) => {
44+
return await authUsecase.validateEmailCode({ email, code });
45+
},
46+
onSuccess: (response) => {
47+
if (response.type === 'success') {
48+
onSuccess(response.data.verificationToken);
49+
return;
50+
}
51+
setCodeError(
52+
createErrorMessage(response.code, '인증 코드 확인에 실패했습니다.')
53+
);
54+
},
55+
});
56+
57+
return { validateEmailCode, isPending };
58+
};
59+
860
export const useSignIn = ({
961
setResponseMessage,
1062
}: {
@@ -43,8 +95,10 @@ export const useSignIn = ({
4395

4496
export const useSignUp = ({
4597
setResponseMessage,
98+
onDuplicateEmail,
4699
}: {
47100
setResponseMessage: (message: string) => void;
101+
onDuplicateEmail: () => void;
48102
}) => {
49103
const { authUsecase } = useGuardContext(UsecaseContext);
50104
const { toMain } = useRouteNavigation();
@@ -60,18 +114,21 @@ export const useSignUp = ({
60114
email: string;
61115
password: string;
62116
}) => {
63-
return await authUsecase.signUp({
64-
role,
65-
username,
66-
email,
67-
password,
68-
});
117+
const checkResult = await authUsecase.checkEmailDuplicate({ email });
118+
if (checkResult.type === 'error') {
119+
return checkResult;
120+
}
121+
return await authUsecase.signUp({ role, username, email, password });
69122
},
70123
onSuccess: (response) => {
71124
if (response.type === 'success') {
72125
toMain();
73126
return;
74127
}
128+
if (response.code === 'USER_001') {
129+
onDuplicateEmail();
130+
return;
131+
}
75132
setResponseMessage(
76133
createErrorMessage(
77134
response.code,

src/feature/auth/usecase/auth-usecase.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
import type { UserRole } from '@/feature/auth/domain/user-role';
22
import type { UsecaseResponse } from '@/feature/shared/response';
33
import type { Apis } from '@/infrastructure/api';
4+
import type { VerificationTokenResponse } from '@/infrastructure/api/apis/local-server/schemas';
45
import type { UserWithAccessTokenResponse } from '@/mocks/auth/schemas';
56

67
export type AuthUsecase = {
8+
checkEmailDuplicate: ({ email }: { email: string }) => UsecaseResponse<null>;
9+
sendVerificationEmail: ({
10+
email,
11+
}: {
12+
email: string;
13+
}) => UsecaseResponse<null>;
14+
validateEmailCode: ({
15+
email,
16+
code,
17+
}: {
18+
email: string;
19+
code: string;
20+
}) => UsecaseResponse<VerificationTokenResponse>;
721
signUp: ({
822
role,
923
username,
@@ -27,6 +41,36 @@ export type AuthUsecase = {
2741
};
2842

2943
export const implAuthUsecase = ({ api }: { api: Apis }): AuthUsecase => ({
44+
checkEmailDuplicate: async ({ email }) => {
45+
const { status, data } = await api['POST /api/auth/email']({
46+
body: { email },
47+
});
48+
if (status === 200) {
49+
return { type: 'success', data: null };
50+
}
51+
return { type: 'error', code: data.code, message: data.message };
52+
},
53+
54+
sendVerificationEmail: async ({ email }) => {
55+
const { status, data } = await api['POST /api/auth/email/verify']({
56+
body: { email },
57+
});
58+
if (status === 200) {
59+
return { type: 'success', data: null };
60+
}
61+
return { type: 'error', code: data.code, message: data.message };
62+
},
63+
64+
validateEmailCode: async ({ email, code }) => {
65+
const { status, data } = await api['POST /api/auth/email/validate']({
66+
body: { email, code },
67+
});
68+
if (status === 200) {
69+
return { type: 'success', data: data as VerificationTokenResponse };
70+
}
71+
return { type: 'error', code: data.code, message: data.message };
72+
},
73+
3074
signUp: async ({ role, username, email, password }) => {
3175
const { status, data } = await api['POST /api/auth/user']({
3276
body: { role, username, email, password },

src/feature/shared/error/create-error-message.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const ERROR_MESSAGES: Record<string, string> = {
3131
DEPOSIT_002: '예치금 정보를 찾을 수 없습니다.',
3232
// S3
3333
S3_001: '파일 업로드 URL 생성에 실패했습니다.',
34+
// Mail
35+
MAIL_001: '이메일 발송에 실패했습니다. 잠시 후 다시 시도해 주세요.',
3436
};
3537

3638
const FALLBACK_MESSAGE = '오류가 발생했습니다. 다시 시도해 주세요.';

src/infrastructure/api/apis/local-server/apis.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import type {
55
SuccessResponse,
66
} from '../../domain';
77
import type {
8+
EmailRequest,
9+
EmailValidateRequest,
810
SignInRequest,
911
SignUpRequest,
1012
UserWithAccessTokenResponse,
13+
VerificationTokenResponse,
1114
} from './schemas';
1215

1316
type GetApisProps = {
@@ -31,6 +34,28 @@ type Api = (_: {
3134

3235
export const getLocalServerApis = ({ callWithoutToken }: GetApisProps) =>
3336
({
37+
'POST /api/auth/email': ({ body }: { body: EmailRequest }) =>
38+
callWithoutToken<SuccessResponse<null>>({
39+
method: 'POST',
40+
path: 'api/auth/email',
41+
body,
42+
}),
43+
'POST /api/auth/email/verify': ({ body }: { body: EmailRequest }) =>
44+
callWithoutToken<SuccessResponse<null>>({
45+
method: 'POST',
46+
path: 'api/auth/email/verify',
47+
body,
48+
}),
49+
'POST /api/auth/email/validate': ({
50+
body,
51+
}: {
52+
body: EmailValidateRequest;
53+
}) =>
54+
callWithoutToken<SuccessResponse<VerificationTokenResponse>>({
55+
method: 'POST',
56+
path: 'api/auth/email/validate',
57+
body,
58+
}),
3459
'POST /api/auth/user': ({ body }: { body: SignUpRequest }) =>
3560
callWithoutToken<SuccessResponse<UserWithAccessTokenResponse>>({
3661
method: 'POST',

src/infrastructure/api/apis/local-server/schemas.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,16 @@ export type UserWithAccessTokenResponse = {
2020
user: UserBriefDTO;
2121
token: string;
2222
};
23+
24+
export type EmailRequest = {
25+
email: string;
26+
};
27+
28+
export type EmailValidateRequest = {
29+
email: string;
30+
code: string;
31+
};
32+
33+
export type VerificationTokenResponse = {
34+
verificationToken: string;
35+
};

src/mocks/README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,81 @@
1010

1111
---
1212

13+
# 메일 중복 확인
14+
15+
## POST `/api/auth/email`
16+
17+
| 케이스 | 조건 |
18+
|---|---|
19+
| `200` 성공 (중복 없음) | 정상 body |
20+
| `400` 필드 누락 | `email` 누락 |
21+
| `409` 중복 이메일 | `email: "duplicate@example.com"` |
22+
23+
---
24+
25+
# 메일 인증 이메일 발송
26+
27+
## POST `/api/auth/email/verify`
28+
29+
| 케이스 | 조건 |
30+
|---|---|
31+
| `200` 성공 | 정상 body |
32+
| `400` 필드 누락 | `email` 누락 |
33+
| `500` 발송 실패 | `email: "fail-send@example.com"` |
34+
35+
---
36+
37+
# 메일 인증 코드 확인
38+
39+
## POST `/api/auth/email/validate`
40+
41+
성공 시 `{ verificationToken: "mock-verification-token-uuid" }` 반환.
42+
43+
| 케이스 | 조건 |
44+
|---|---|
45+
| `200` 성공 | 정상 body |
46+
| `400` 필드 누락 | `email` 또는 `code` 누락 |
47+
| `400` 잘못된 코드 | `code: "000000"` |
48+
49+
---
50+
51+
# 로그아웃
52+
53+
## DELETE `/api/auth/user/session`
54+
55+
| 케이스 | 조건 |
56+
|---|---|
57+
| `200` 성공 | owner 또는 reviewer 토큰 |
58+
| `401` 인증 실패 | 토큰 없음 또는 유효하지 않은 토큰 |
59+
60+
---
61+
62+
# 비밀번호 재설정
63+
64+
## POST `/api/auth/password`
65+
66+
| 케이스 | 조건 |
67+
|---|---|
68+
| `200` 성공 | 정상 body |
69+
| `400` 필드 누락 | `email` 누락 |
70+
| `404` 사용자 없음 | `email: "notfound@example.com"` |
71+
| `500` 발송 실패 | `email: "fail-send@example.com"` |
72+
73+
---
74+
75+
# 비밀번호 변경
76+
77+
## PATCH `/api/auth/password`
78+
79+
| 케이스 | 조건 |
80+
|---|---|
81+
| `200` 성공 | 로그인 토큰 + 정상 body |
82+
| `400` 필드 누락 | `currentPassword` 또는 `newPassword` 누락 |
83+
| `401` 인증 실패 | 토큰 없음 또는 유효하지 않은 토큰 |
84+
| `401` 기존 비밀번호 불일치 | `currentPassword: "wrongpassword"` |
85+
86+
---
87+
1388
# 회원가입
1489

1590
## POST `/api/auth/user`

src/mocks/auth/data.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import type { UserWithAccessTokenResponse } from './schemas';
1+
import type {
2+
UserWithAccessTokenResponse,
3+
VerificationTokenResponse,
4+
} from './schemas';
25

36
export const mockOwnerUser: UserWithAccessTokenResponse = {
47
user: { id: 'owner-001', userRole: 'OWNER' },
@@ -9,3 +12,7 @@ export const mockReviewerUser: UserWithAccessTokenResponse = {
912
user: { id: 'reviewer-001', userRole: 'REVIEWER' },
1013
token: 'mock-reviewer-token',
1114
};
15+
16+
export const mockVerificationToken: VerificationTokenResponse = {
17+
verificationToken: 'mock-verification-token-uuid',
18+
};

src/mocks/auth/handler.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ import { http } from 'msw';
33
import { authResolver } from './resolvers';
44

55
export const authHandlers = [
6+
http.post('*/api/auth/email', authResolver.checkEmailDuplicate),
7+
http.post('*/api/auth/email/verify', authResolver.sendVerificationEmail),
8+
http.post('*/api/auth/email/validate', authResolver.validateEmailCode),
69
http.post('*/api/auth/user', authResolver.signUp),
710
http.post('*/api/auth/user/session', authResolver.signIn),
11+
http.delete('*/api/auth/user/session', authResolver.signOut),
12+
http.post('*/api/auth/password', authResolver.resetPassword),
13+
http.patch('*/api/auth/password', authResolver.changePassword),
814
];

0 commit comments

Comments
 (0)