Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useEffect, useState } from "react";

import IntroSlide1 from "./intro/IntroSlide1";
import IntroSlide2 from "./intro/IntroSlide2";
import IntroSlide3 from "./intro/IntroSlide3";
import IntroSlide1 from "../intro/IntroSlide1";
import IntroSlide2 from "../intro/IntroSlide2";
import IntroSlide3 from "../intro/IntroSlide3";

export default function OnboardingIntro() {
const [currentSlide, setCurrentSlide] = useState(0);
Expand Down
145 changes: 145 additions & 0 deletions src/components/auth/findEmailStep/Step01Phone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { useEffect, useState } from "react";
import { type SubmitHandler, useForm, useWatch } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { zodResolver } from "@hookform/resolvers/zod";
import { toast } from "sonner";
import type { z } from "zod";

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

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

import useAuthStore from "@/store/useAuthStore";

interface IStep01PhoneProps {
onNext: () => void;
}

type TStep01FormValues = z.infer<typeof findEmailStep01Schema>;

export default function Step01Phone({ onNext }: IStep01PhoneProps) {
const navigate = useNavigate();
const { setEmail } = useAuthStore();

const [sendCode, setSendCode] = useState(false);
const [, setCodeVerify] = useState(false);
const [codeError, setCodeError] = useState("");

const {
register,
handleSubmit,
control,
trigger,
formState: { errors, isValid },
} = useForm<TStep01FormValues>({
mode: "onBlur",
resolver: zodResolver(findEmailStep01Schema),
});

const watchedPhone = useWatch({ control, name: "phoneNum" });
const watchedCode = useWatch({ control, name: "code" });

const postSendCode = async () => {
setCodeVerify(false);
const isPhoneValid = await trigger("phoneNum");
if (isPhoneValid && watchedPhone) {
setSendCode(true);
toast.success("인증번호가 발송되었습니다.", {
description: "테스트용: 아무 번호나 입력하세요",
});
}
};

const onSubmit: SubmitHandler<TStep01FormValues> = async () => {
// 임시 이메일
setEmail("smu2021@naver.com");
onNext();
};

useEffect(() => {
setCodeVerify(false);
setCodeError("");
}, [watchedCode, watchedPhone]);

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

return (
<div className="w-full min-h-screen bg-white flex items-center justify-center">
<div className="w-full max-w-130 px-6 pb-12">
<h1 className="text-start font-heading2 text-text-main mb-10">
<p>이메일 찾기를 위해</p>
<p>휴대폰 인증을 진행할게요</p>
</h1>

<div className="flex flex-col gap-6">
{!sendCode ? (
<div className="flex gap-2 w-full items-start">
<div className="flex-1">
<CommonAuthInput
placeholder="전화번호를 입력하세요"
type="phoneNum"
{...register("phoneNum")}
error={!!errors.phoneNum}
errorMessage={errors.phoneNum?.message}
/>
</div>
<Button
variant="custom"
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"
>
인증번호 받기
</Button>
</div>
) : (
<CommonAuthInput
type="text"
value={watchedPhone || ""}
readOnly
className="w-full h-13.5 px-5 border rounding-15 text-body1 text-text-main bg-white border-brand-400 focus:outline-none focus:border-brand-400"
/>
)}

<CommonAuthInput
placeholder={
sendCode
? "문자로 발송된 6자리 인증번호"
: "인증번호를 입력하세요"
}
type="text"
timer={sendCode ? "03:00" : undefined}
{...register("code")}
error={!!errors.code || !!codeError}
errorMessage={errors.code?.message || codeError}
/>
</div>

<div className="mt-10">
<Button
size="big"
fullWidth
onClick={handleSubmit(onSubmit)}
variant="gradient"
disabled={!isValid}
>
다음으로
</Button>
</div>

<div className="mt-10 flex justify-center">
<button
type="button"
className="font-body2 text-text-placeholder underline underline-offset-4 hover:text-text-auth-sub"
onClick={() => navigate("/auth/find-pw")}
>
비밀번호 찾기
</button>
</div>
</div>
</div>
);
}
50 changes: 50 additions & 0 deletions src/components/auth/findEmailStep/Step02Email.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useNavigate } from "react-router-dom";

import { maskEmail } from "@/utils/maskEmail";

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

import useAuthStore from "@/store/useAuthStore";

export default function Step02Email() {
const navigate = useNavigate();
const { email } = useAuthStore();

return (
<div className="w-full min-h-screen bg-white flex items-center justify-center">
<div className="w-full max-w-130 px-6 pb-12">
<h1 className="text-start font-heading2 text-text-main mb-10">
<p>입력하신 정보로</p>
<p>WYA에 가입된 계정을 찾았어요</p>
</h1>

<div className="w-full h-24 bg-gray-50 rounded-2xl flex items-center justify-between px-5 mb-10">
<div className="flex items-center gap-3 min-w-0">
<span className="font-caption text-text-placeholder shrink-0">
이메일 ID
</span>
<span className="font-body1 text-text-main break-all">
{maskEmail(email)}
</span>
</div>
<button
type="button"
className="shrink-0 h-8 px-2 border border-gray-200 bg-white rounded-lg text-xs text-text-sub hover:bg-gray-50 transition-colors"
onClick={() => navigate("/auth/find-pw")}
>
비밀번호 재설정
</button>
</div>

<Button
size="big"
fullWidth
onClick={() => navigate("/auth/login")}
variant="gradient"
>
로그인으로 돌아가기
</Button>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion src/components/auth/signupStep/Step01Email.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { z } from "zod";

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

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

import useAuthStore from "@/store/useAuthStore";
Expand Down
2 changes: 1 addition & 1 deletion src/components/auth/signupStep/Step02Password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { z } from "zod";

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

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

import useAuthStore from "@/store/useAuthStore";
Expand Down
2 changes: 1 addition & 1 deletion src/components/auth/signupStep/Step03Profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { z } from "zod";

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

import CommonAuthInput from "@/components/auth/CommonAuthInput";
import CommonAuthInput from "@/components/auth/common/CommonAuthInput";
import Button from "@/components/common/Button";
import { MODAL_TYPES } from "@/components/modal/ModalProvider";

Expand Down
33 changes: 33 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,37 @@ button {
outline: 2px solid #0084fe;
outline-offset: 2px;
}

/* Float Animation */
@keyframes float {
0% {
transform: translateY(0px);
}
50% {
transform: translateY(-20px);
}
100% {
transform: translateY(0px);
}
}

.animate-float {
animation: float 6s ease-in-out infinite;
}

/* Fade In Up */
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.animate-fade-in-up {
animation: fade-in-up 0.8s ease-out forwards;
}
}
2 changes: 1 addition & 1 deletion src/layout/auth/AuthLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Outlet } from "react-router-dom";

import OnboardingIntro from "@/components/auth/OnboardingIntro";
import OnboardingIntro from "@/components/auth/common/OnboardingIntro";

export default function AuthLayout() {
return (
Expand Down
19 changes: 19 additions & 0 deletions src/pages/auth/FindEmail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useState } from "react";

import Step01Phone from "@/components/auth/findEmailStep/Step01Phone";
import Step02Email from "@/components/auth/findEmailStep/Step02Email";

export default function FindEmail() {
const [step, setStep] = useState<number>(1);

const handleNextStep = () => {
setStep((prev) => prev + 1);
};

return (
<>
{step === 1 && <Step01Phone onNext={handleNextStep} />}
{step === 2 && <Step02Email />}
</>
);
}
6 changes: 3 additions & 3 deletions src/pages/auth/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { z } from "zod";

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

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

import GoogleIcon from "@/assets/auth/social/google.svg?react";
Expand Down Expand Up @@ -36,7 +36,7 @@ export default function Login() {
return (
<div className="w-full min-h-screen bg-white flex items-center justify-center">
<div className="w-full max-w-130 px-6 pt-30 pb-12">
<h1 className="text-center font-heading1 text-3xl font-bold text-text-main mb-12">
<h1 className="text-center font-heading1 text-3xl font-bold text-text-main mb-10">
로그인
</h1>

Expand All @@ -59,7 +59,7 @@ export default function Login() {
/>

<Link
to="/auth/find-pw"
to="/auth/find-email"
className="block w-full text-center mt-3 font-caption text-text-sub underline underline-offset-4 hover:text-text-auth-sub"
>
이메일/비밀번호를 잊어버렸어요
Expand Down
5 changes: 5 additions & 0 deletions src/routes/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createBrowserRouter, Navigate } from "react-router-dom";
import AuthLayout from "@/layout/auth/AuthLayout";
import Layout from "@/layout/common/Layout";
import RootLayout from "@/layout/common/RootLayout";
import FindEmail from "@/pages/auth/FindEmail";
import FindPw from "@/pages/auth/FindPw";
import Login from "@/pages/auth/Login";
import Signup from "@/pages/auth/Signup";
Expand Down Expand Up @@ -68,6 +69,10 @@ const router = createBrowserRouter([
path: "login",
element: <Login />,
},
{
path: "find-email",
element: <FindEmail />,
},
{
path: "find-pw",
element: <FindPw />,
Expand Down
12 changes: 12 additions & 0 deletions src/utils/maskEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function maskEmail(email: string) {
if (!email) return "";

const [local, domain] = email.split("@");
if (!local || !domain) return email;

if (local.length <= 3) {
return `${local}****@${domain}`;
}

return `${local.slice(0, 3)}****@${domain}`;
}
8 changes: 8 additions & 0 deletions src/utils/validation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ export const signupSchema = z

export const termsSchema = z.boolean().refine((val) => val === true);

// 로그인
export const loginSchema = z.object({
email: emailSchema,
password: passwordSchema,
});

// 단계별 회원가입
export const step01Schema = z.object({
email: emailSchema,
code: codeSchema,
Expand All @@ -78,3 +80,9 @@ export const step03Schema = z.object({
terms: termsSchema,
marketing: z.boolean().optional(),
});

// 이메일 찾기1 - 휴대폰 번호 인증
export const findEmailStep01Schema = z.object({
phoneNum: phoneSchema,
code: codeSchema,
});