Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@hookform/resolvers": "^5.2.2",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-query": "^5.90.17",
"axios": "^1.13.4",
"clsx": "^2.1.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
Expand Down
75 changes: 75 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions src/api/auth/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { ICommonResponse } from "@/types/common/common";

import type {
IEmailSendRequest,
IEmailSendResponse,
IEmailVerifyRequest,
} from "../../types/auth/auth";

import { axiosInstance } from "@/lib/axiosInstance";

// 이메일 인증 코드 전송
export const sendEmail = async ({
email,
}: IEmailSendRequest): Promise<ICommonResponse<IEmailSendResponse>> => {
const { data } = await axiosInstance.post("/api/users/email-send", { email });
return data;
};

// 이메일 인증 코드 검증
export const verifyEmail = async ({
email,
authCode,
}: IEmailVerifyRequest): Promise<ICommonResponse<string>> => {
const { data } = await axiosInstance.post("/api/users/email-verify", {
email,
authCode,
});
return data;
};
67 changes: 55 additions & 12 deletions src/components/auth/signupStep/Step01Email.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import type { z } from "zod";

import { step01Schema } from "@/utils/validation";

import { useAuth } from "@/hooks/auth/useAuth";
import { useTimer } from "@/hooks/useTimer";

import CommonAuthInput from "@/components/auth/common/CommonAuthInput";
import Button from "@/components/common/Button";

Expand All @@ -19,6 +22,7 @@ type TStep01FormValues = z.infer<typeof step01Schema>;

export default function SignupEmail({ onNext }: IStep01EmailProps) {
const { setEmail } = useAuthStore();
const { useSendCode, useCheckCode } = useAuth();

const [sendCode, setSendCode] = useState(false);
const [, setCodeVerify] = useState(false);
Expand All @@ -38,20 +42,49 @@ export default function SignupEmail({ onNext }: IStep01EmailProps) {
const watchedEmail = useWatch({ control, name: "email" });
const watchedCode = useWatch({ control, name: "code" });

const { formattedTime, restart, stop, isExpired } = useTimer(180, {
onExpire: () => {
toast.error("인증 시간이 만료되었습니다. 다시 시도해주세요.");
},
});

const postSendCode = async () => {
setCodeVerify(false);
const isEmailValid = await trigger("email");
if (isEmailValid && watchedEmail) {
setSendCode(true);
toast.success("인증번호가 발송되었습니다.", {
description: "테스트용: 아무 번호나 입력하세요",
});
useSendCode.mutate(
{ email: watchedEmail },
{
onSuccess: () => {
setSendCode(true);
toast.success("인증번호가 발송되었습니다.");
restart();
},
onError: (error) => {
toast.error(
error.response?.data?.message || "메일 발송에 실패했습니다.",
);
},
},
);
}
};

const onSubmit: SubmitHandler<TStep01FormValues> = async (data) => {
setEmail(data.email);
onNext();
useCheckCode.mutate(
{ email: data.email, authCode: data.code },
{
onSuccess: () => {
setEmail(data.email);
onNext();
},
onError: (error) => {
setCodeError(
error.response?.data?.message || "인증번호가 올바르지 않습니다.",
);
},
},
);
};

useEffect(() => {
Expand All @@ -61,7 +94,8 @@ export default function SignupEmail({ onNext }: IStep01EmailProps) {

useEffect(() => {
setSendCode(false);
}, [watchedEmail]);
stop();
}, [watchedEmail, stop]);

return (
<div className="w-full min-h-screen bg-white flex items-center justify-center">
Expand All @@ -88,6 +122,7 @@ export default function SignupEmail({ onNext }: IStep01EmailProps) {
className="shrink-0 h-13.5! border border-brand-400 text-status-blue bg-white hover:bg-gray-50 px-4 rounded-15 font-body2 whitespace-nowrap"
onClick={postSendCode}
type="button"
disabled={useSendCode.isPending}
>
인증번호 받기
</Button>
Expand All @@ -108,10 +143,15 @@ export default function SignupEmail({ onNext }: IStep01EmailProps) {
: "인증번호를 입력하세요"
}
type="text"
timer={sendCode ? "03:00" : undefined}
timer={sendCode ? formattedTime : undefined}
{...register("code")}
error={!!errors.code || !!codeError}
errorMessage={errors.code?.message || codeError}
disabled={isExpired}
error={!!errors.code || !!codeError || (isExpired && sendCode)}
errorMessage={
isExpired
? "인증 시간이 만료되었습니다."
: errors.code?.message || codeError
}
/>
</div>

Expand All @@ -121,7 +161,7 @@ export default function SignupEmail({ onNext }: IStep01EmailProps) {
fullWidth
onClick={handleSubmit(onSubmit)}
variant="gradient"
disabled={!isValid}
disabled={!isValid || useCheckCode.isPending || isExpired}
>
다음으로
</Button>
Expand All @@ -131,7 +171,10 @@ export default function SignupEmail({ onNext }: IStep01EmailProps) {
<div className="mt-6 flex justify-center">
<button
type="button"
onClick={() => setSendCode(false)}
onClick={() => {
setSendCode(false);
stop();
}}
className="font-body2 text-text-placeholder underline underline-offset-4 hover:text-text-auth-sub"
>
인증번호 다시 받기
Expand Down
2 changes: 1 addition & 1 deletion src/components/auth/skeleton/AuthFormSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Skeleton } from "@/components/common/skeleton/Skeleton";
export default function AuthFormSkeleton() {
return (
<div className="flex w-full flex-col items-center bg-white">
<div className="w-full max-w-[360px]">
<div className="w-full max-w-90">
<div className="mb-10 flex flex-col gap-2">
<Skeleton className="h-8 w-40" />
<Skeleton className="h-8 w-56" />
Expand Down
13 changes: 13 additions & 0 deletions src/hooks/auth/useAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useCoreMutation } from "@/hooks/customQuery";

import { sendEmail, verifyEmail } from "@/api/auth/auth";

export const useAuth = () => {
const useSendCode = useCoreMutation(sendEmail);
const useCheckCode = useCoreMutation(verifyEmail);

return {
useSendCode,
useCheckCode,
};
};
Loading