diff --git a/package.json b/package.json index 7102090..d53d7da 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "react-dom": "^19.2.0", "react-hook-form": "^7.71.1", "react-router-dom": "^7.12.0", + "sonner": "^2.0.7", "tailwindcss": "^4.1.18", "zod": "^4.3.5", "zustand": "^5.0.10" @@ -27,6 +28,7 @@ "@commitlint/cli": "^20.3.1", "@commitlint/config-conventional": "^20.3.1", "@eslint/js": "^9.39.2", + "@tailwindcss/forms": "^0.5.11", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c84db41..2d6d5db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: react-router-dom: specifier: ^7.12.0 version: 7.12.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tailwindcss: specifier: ^4.1.18 version: 4.1.18 @@ -51,6 +54,9 @@ importers: '@eslint/js': specifier: ^9.39.2 version: 9.39.2 + '@tailwindcss/forms': + specifier: ^0.5.11 + version: 0.5.11(tailwindcss@4.1.18) '@types/node': specifier: ^24.10.1 version: 24.10.8 @@ -786,6 +792,11 @@ packages: '@swc/types@0.1.25': resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} + '@tailwindcss/forms@0.5.11': + resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' + '@tailwindcss/node@4.1.18': resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} @@ -2031,6 +2042,10 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + mini-svg-data-uri@1.4.4: + resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==} + hasBin: true + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2347,6 +2362,12 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3236,6 +3257,11 @@ snapshots: dependencies: '@swc/counter': 0.1.3 + '@tailwindcss/forms@0.5.11(tailwindcss@4.1.18)': + dependencies: + mini-svg-data-uri: 1.4.4 + tailwindcss: 4.1.18 + '@tailwindcss/node@4.1.18': dependencies: '@jridgewell/remapping': 2.3.5 @@ -4605,6 +4631,8 @@ snapshots: mimic-function@5.0.1: {} + mini-svg-data-uri@1.4.4: {} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -4960,6 +4988,11 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 + sonner@2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + source-map-js@1.2.1: {} split2@4.2.0: {} diff --git a/src/App.tsx b/src/App.tsx index 2bf7268..c21968e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,17 @@ import "./App.css"; import { RouterProvider } from "react-router-dom"; +import { Toaster } from "sonner"; import router from "./routes/routes"; function App() { - return ; + return ( + <> + + + + ); } export default App; diff --git a/src/assets/auth/password/eye-off.svg b/src/assets/auth/password/eye-off.svg index 2402500..3c6d445 100644 --- a/src/assets/auth/password/eye-off.svg +++ b/src/assets/auth/password/eye-off.svg @@ -1,18 +1,14 @@ - - + - + - - - - - - - - + + + + + diff --git a/src/assets/auth/password/eye.svg b/src/assets/auth/password/eye.svg index eb39bc0..5c479ab 100644 --- a/src/assets/auth/password/eye.svg +++ b/src/assets/auth/password/eye.svg @@ -1,9 +1,9 @@ - - - - + + + + - - + + diff --git a/src/assets/icon/check-white.svg b/src/assets/icon/check-white.svg new file mode 100644 index 0000000..e8c20b7 --- /dev/null +++ b/src/assets/icon/check-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/auth/CommonAuthInput.tsx b/src/components/auth/CommonAuthInput.tsx index 2e97bc6..0b36be2 100644 --- a/src/components/auth/CommonAuthInput.tsx +++ b/src/components/auth/CommonAuthInput.tsx @@ -21,6 +21,7 @@ type TCommonAuthInputProps = { buttonText?: string; buttonOnclick?: () => void; short?: boolean; + timer?: string; } & InputHTMLAttributes; const CommonAuthInput = React.forwardRef< @@ -41,6 +42,7 @@ const CommonAuthInput = React.forwardRef< buttonOnclick, short, validationState, + timer, ...rest }: TCommonAuthInputProps, ref, @@ -56,7 +58,9 @@ const CommonAuthInput = React.forwardRef< return (
{title && ( -
{title}
+
+ {title} +
)}
@@ -65,44 +69,47 @@ const CommonAuthInput = React.forwardRef< type={inputType === "phoneNum" ? "text" : inputType} placeholder={placeholder} value={value} - className={`w-full h-[54px] px-5 bg-brand-200 border rounding-15 text-body1 text-brand-900 - placeholder:text-text-placeholder focus:outline-none transition-colors duration-200 + className={`w-full h-14 px-5 bg-gray-50 border-transparent rounded-2xl text-body1 text-brand-900 + placeholder:text-text-placeholder focus:outline-none focus:bg-white focus:ring-2 focus:ring-logo-1/30 transition-all duration-200 ${ error - ? "border-status-red caret-status-red" + ? "ring-2 ring-status-red bg-status-red/5" : validation - ? "border-brand-500" - : "border-brand-400 focus:border-status-blue focus:ring-1 focus:ring-status-blue" + ? "ring-2 ring-logo-1 bg-white" + : "hover:bg-gray-100" + } + ${ + button || short || validationState || timer + ? "pr-25" + : "pr-5" } - ${button || short || validationState ? "pr-[100px]" : "pr-5"} `} + {...rest} onChange={(e) => { const rawValue = e.target.value; const formatted = type === "phoneNum" ? formatInputNumber(rawValue) : rawValue; if (rest.onChange) { - rest.onChange({ - ...e, - target: { - ...e.target, - value: formatted, - }, - }); + e.target.value = formatted; + rest.onChange(e); } }} - {...rest} /> {type === "password" && ( )} - {(button || validationState) && ( + {(button || validationState || timer) && (
{button && (
)} {short && ( -
+
)}
{error && errorMessage && ( -
+
{errorMessage}
)} diff --git a/src/components/auth/OnboardingIntro.tsx b/src/components/auth/OnboardingIntro.tsx index 2153b3f..92d780b 100644 --- a/src/components/auth/OnboardingIntro.tsx +++ b/src/components/auth/OnboardingIntro.tsx @@ -30,12 +30,9 @@ export default function OnboardingIntro() { return (
- {/* 슬라이드 패널 */} - - {/* 인디케이터 */}
{[0, 1, 2].map((index) => ( - ); -} diff --git a/src/components/auth/intro/IntroSlide1.tsx b/src/components/auth/intro/IntroSlide1.tsx index 1f2233a..1d3d662 100644 --- a/src/components/auth/intro/IntroSlide1.tsx +++ b/src/components/auth/intro/IntroSlide1.tsx @@ -7,9 +7,11 @@ export default function IntroSlide1({ isActive }: { isActive: boolean }) { isActive ? "opacity-100 z-10" : "opacity-0 z-0" }`} > -
-
- +
+
+ +
+
diff --git a/src/components/auth/intro/IntroSlide2.tsx b/src/components/auth/intro/IntroSlide2.tsx index e8038e3..e3eb1e6 100644 --- a/src/components/auth/intro/IntroSlide2.tsx +++ b/src/components/auth/intro/IntroSlide2.tsx @@ -6,23 +6,23 @@ const PLATFORMS = [ { id: "kakao", icon: LogoKakao, - bgColor: "bg-[#FEE500]", - className: "text-[#3A1D1D]", - size: "w-45 h-45", + bgColor: "bg-social-kakao", + className: "text-social-text-kakao", + size: "w-32 h-32", }, { id: "google", icon: LogoGoogle, - bgColor: "bg-white", + bgColor: "bg-social-google", className: "", - size: "w-45 h-45", + size: "w-32 h-32", }, { id: "naver", icon: LogoNaver, - bgColor: "bg-[#03C75A]", + bgColor: "bg-social-naver", className: "text-white", - size: "w-45 h-45", + size: "w-32 h-32", }, ]; @@ -35,27 +35,30 @@ export default function IntroSlide2({ isActive }: { isActive: boolean }) { isActive ? "opacity-100 z-10" : "opacity-0 z-0" }`} > -
+
-
- 통합 광고 성과 관리 -
-
-

흩어진 광고 데이터를 하나로!

-

광고 현황을 통합적으로 관리하고

-

성과 변화를 직관적으로 확인하세요.

-
+ + 통합 관리 + +

+ 흩어진 광고 성과를{"\n"} + 한곳에서 관리하세요 +

+

+ 여러 매체의 광고 데이터를{"\n"} + 실시간으로 모아서 보여드려요 +

-
-
+
+
{CAROUSEL_ITEMS.map((platform, index) => (
-
+
-
- AI 성과 요약 -
-
-

광고 데이터를 자동으로 분석해

-

중요한 성과 변화와 핵심만 정리하고

-

빠르게 파악할 수 있도록 제공합니다.

-
+ + AI 성과 분석 + +

+ 복잡한 데이터 분석,{"\n"} + AI가 대신 해드릴게요 +

+

+ 광고 성과를 자동으로 분석해서{"\n"} + 중요한 인사이트만 쏙쏙 뽑아드려요 +

-
-
- {bars.map((bar, index) => ( -
- {bar.isBlue && ( -
- -
- )} -
- ))} +
+
+
+ {bars.map((bar, index) => ( +
+ {bar.isBlue && ( +
+
+ +
+
+ )} +
+ ))} +
diff --git a/src/components/auth/signupStep/Step01Email.tsx b/src/components/auth/signupStep/Step01Email.tsx new file mode 100644 index 0000000..b64674b --- /dev/null +++ b/src/components/auth/signupStep/Step01Email.tsx @@ -0,0 +1,144 @@ +import { useEffect, useState } from "react"; +import { type SubmitHandler, useForm, useWatch } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { toast } from "sonner"; +import type { z } from "zod"; + +import { step01Schema } from "@/utils/validation"; + +import CommonAuthInput from "@/components/auth/CommonAuthInput"; +import Button from "@/components/common/Button"; + +import useAuthStore from "@/store/useAuthStore"; + +interface IStep01EmailProps { + onNext: () => void; +} + +type TStep01FormValues = z.infer; + +export default function SignupEmail({ onNext }: IStep01EmailProps) { + 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({ + mode: "onBlur", + resolver: zodResolver(step01Schema), + }); + + const watchedEmail = useWatch({ control, name: "email" }); + const watchedCode = useWatch({ control, name: "code" }); + + const postSendCode = async () => { + setCodeVerify(false); + const isEmailValid = await trigger("email"); + if (isEmailValid && watchedEmail) { + setSendCode(true); + toast.success("인증번호가 발송되었습니다.", { + description: "테스트용: 아무 번호나 입력하세요", + }); + } + }; + + const onSubmit: SubmitHandler = async (data) => { + setEmail(data.email); + onNext(); + }; + + useEffect(() => { + setCodeVerify(false); + setCodeError(""); + }, [watchedCode, watchedEmail]); + + useEffect(() => { + setSendCode(false); + }, [watchedEmail]); + + return ( +
+
+

+

회원가입을 위해

+

이메일 인증을 진행할게요

+

+ +
+ {!sendCode ? ( +
+
+ +
+ +
+ ) : ( + + )} + + +
+ +
+ +
+ + {sendCode && ( +
+ +
+ )} +
+
+ ); +} diff --git a/src/components/auth/signupStep/Step02Password.tsx b/src/components/auth/signupStep/Step02Password.tsx new file mode 100644 index 0000000..4b15eea --- /dev/null +++ b/src/components/auth/signupStep/Step02Password.tsx @@ -0,0 +1,75 @@ +import { type SubmitHandler, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import type { z } from "zod"; + +import { step02Schema } from "@/utils/validation"; + +import CommonAuthInput from "@/components/auth/CommonAuthInput"; +import Button from "@/components/common/Button"; + +import useAuthStore from "@/store/useAuthStore"; + +interface IStep02PasswordProps { + onNext: () => void; +} + +type TStep02FormValues = z.infer; + +export default function SignupPassword({ onNext }: IStep02PasswordProps) { + const { setPassword } = useAuthStore(); + const { + register, + handleSubmit, + formState: { errors, isValid }, + } = useForm({ + mode: "onBlur", + resolver: zodResolver(step02Schema), + }); + + const onSubmit: SubmitHandler = (data) => { + setPassword(data.password); + onNext(); + }; + + return ( +
+
+

+

로그인에 사용할

+

비밀번호를 입력해 주세요

+

+ +
+ + + +
+ +
+ +
+
+ ); +} diff --git a/src/components/auth/signupStep/Step03Profile.tsx b/src/components/auth/signupStep/Step03Profile.tsx new file mode 100644 index 0000000..28ad77f --- /dev/null +++ b/src/components/auth/signupStep/Step03Profile.tsx @@ -0,0 +1,114 @@ +import { Controller, type SubmitHandler, useForm } 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 { step03Schema } from "@/utils/validation"; + +import CommonAuthInput from "@/components/auth/CommonAuthInput"; +import Button from "@/components/common/Button"; + +import useAuthStore from "@/store/useAuthStore"; + +type TStep03FormValues = z.infer; + +export default function SignupProfile() { + const navigate = useNavigate(); + const { email, password } = useAuthStore(); + + const { + register, + handleSubmit, + control, + formState: { errors, isValid }, + } = useForm({ + mode: "onChange", + resolver: zodResolver(step03Schema), + }); + + const onSubmit: SubmitHandler = (data) => { + // 최종 회원가입 데이터 (예시) + const finalSignupData = { + email, + password, + ...data, + }; + + console.log("Final Signup Data:", finalSignupData); + + toast.success("회원가입이 완료되었습니다!", { + description: `이름: ${data.name}, 환영합니다!`, + }); + navigate("/auth/login"); + }; + + return ( +
+
+

+

사용자의

+

기본 정보를 입력해 주세요

+

+ +
+ + ( + + )} + /> + +
+
+ + + (필수) + 개인정보 수집 및 이용 동의 + +
+ +
+ +
+ +
+ +
+
+ ); +} diff --git a/src/components/common/Button.tsx b/src/components/common/Button.tsx index fa9689b..8a63d2f 100644 --- a/src/components/common/Button.tsx +++ b/src/components/common/Button.tsx @@ -3,10 +3,9 @@ import cx from "clsx"; interface IButtonProps extends React.ButtonHTMLAttributes { size?: "big" | "small"; - variant?: "dark" | "custom"; + variant?: "dark" | "gradient" | "custom"; isLoading?: boolean; leftIcon?: React.ReactNode; - rightIcon?: React.ReactNode; fullWidth?: boolean; } @@ -16,19 +15,20 @@ export default function Button({ isLoading = false, disabled = false, leftIcon, - rightIcon, fullWidth = false, className, children, ...rest }: IButtonProps) { const sizeClasses = { - big: "h-[50px] px-6 rounding-15 font-heading3", - small: "h-[40px] px-4 rounding-15 font-body1", + big: "h-[55px] px-6 rounded-2xl font-heading3", + small: "h-[40px] px-4 rounded-2xl font-body1", }; const variantClasses = { - dark: "bg-brand-800 text-white hover:bg-brand-700 disabled:bg-gray-300", + dark: "bg-brand-800 text-white hover:bg-brand-700 disabled:bg-bg-disabled disabled:text-text-disabled disabled:hover:bg-bg-disabled", + gradient: + "bg-linear-to-r from-logo-1 to-logo-2 text-white hover:opacity-90 shadow-brand-500/30 disabled:bg-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:shadow-none disabled:opacity-100", custom: "", }; @@ -55,11 +55,6 @@ export default function Button({ )} {children} - {!isLoading && rightIcon && ( - - {rightIcon} - - )} ); } diff --git a/src/index.css b/src/index.css index ee8cb86..2087eb6 100644 --- a/src/index.css +++ b/src/index.css @@ -46,6 +46,7 @@ button { --color-chart-3: #1485ff; --color-chart-4: #00aeef; --color-chart-5: #4fc3f7; + --color-chart-inactive: #F2F4F6; /* Status */ --color-status-red: #ff2a4b; @@ -56,7 +57,14 @@ button { --color-text-auth-sub: #546171; --color-text-sub: #8b8b8f; --color-text-placeholder: #c3c3c3; - --color-text-disabled: #e6e6e6; + --color-text-disabled: #B0B8C1; + --color-bg-disabled: #E5E8EB; + + /* Social */ + --color-social-kakao: #FEE500; + --color-social-naver: #03C75A; + --color-social-google: #ffffff; + --color-social-text-kakao: #3A1D1D; } @font-face { @@ -74,7 +82,7 @@ button { } .font-heading1 { - font-size: 38px; + font-size: 30px; font-weight: 600; line-height: 130%; } @@ -86,7 +94,7 @@ button { } .font-heading3 { - font-size: 20px; + font-size: 18px; font-weight: 400; line-height: 130%; } @@ -109,6 +117,13 @@ button { line-height: 130%; } + /* CommonAuthInput 위 폰트 */ + .font-label { + font-size: 14px; + font-weight: 700; + line-height: 140%; + } + /* Rounding */ .rounding-15 { border-radius: 15px; @@ -159,4 +174,29 @@ button { animation: graph-up 0.7s ease-out forwards; height: 0%; } + + /* checkbox */ + .checkbox { + appearance: none; + width: 24px; + height: 24px; + border-radius: 50%; + background-color: #E5E8EB; + cursor: pointer; + position: relative; + transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out; + flex-shrink: 0; + } + .checkbox:checked { + background-color: #0084fe; + background-image: url("/src/assets/icon/check-white.svg"); + background-size: 60% 60%; + background-position: center; + background-repeat: no-repeat; + } + /* Tab 접근성 고려 */ + .checkbox:focus-visible { + outline: 2px solid #0084fe; + outline-offset: 2px; + } } diff --git a/src/pages/auth/Login.tsx b/src/pages/auth/Login.tsx index d07a977..6a630e9 100644 --- a/src/pages/auth/Login.tsx +++ b/src/pages/auth/Login.tsx @@ -1,3 +1,130 @@ +import { type SubmitHandler, useForm } from "react-hook-form"; +import { Link } from "react-router-dom"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { toast } from "sonner"; +import type { z } from "zod"; + +import { loginSchema } from "@/utils/validation"; + +import CommonAuthInput from "@/components/auth/CommonAuthInput"; +import Button from "@/components/common/Button"; + +import GoogleIcon from "@/assets/auth/social/google.svg?react"; +import KakaoIcon from "@/assets/auth/social/kakao.svg?react"; +import NaverIcon from "@/assets/auth/social/naver.svg?react"; + +type TLoginFormValues = z.infer; + export default function Login() { - return
Login
; + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + mode: "onBlur", + resolver: zodResolver(loginSchema), + }); + + const onSubmit: SubmitHandler = (data) => { + // 임시 로그인 처리 + console.log("Login submit:", data); + toast.success("로그인 성공!", { + description: `이메일: ${data.email}`, + }); + }; + + return ( +
+
+

+ 로그인 +

+ +
+ + + + + 이메일/비밀번호를 잊어버렸어요 + + +
+ +
+ + +
+
+ + + + + +
+ +
+
+ + 또는 + +
+
+ + + 이메일로 회원가입 + +
+
+
+ ); } diff --git a/src/pages/auth/Signup.tsx b/src/pages/auth/Signup.tsx index 9bb3734..101b94c 100644 --- a/src/pages/auth/Signup.tsx +++ b/src/pages/auth/Signup.tsx @@ -1,3 +1,95 @@ +import { useState } from "react"; +import { Link, useLocation } from "react-router-dom"; + +import Step01Email from "@/components/auth/signupStep/Step01Email"; +import Step02Password from "@/components/auth/signupStep/Step02Password"; +import Step03Profile from "@/components/auth/signupStep/Step03Profile"; +import Button from "@/components/common/Button"; + +import GoogleIcon from "@/assets/auth/social/google.svg?react"; +import KakaoIcon from "@/assets/auth/social/kakao.svg?react"; +import MailIcon from "@/assets/auth/social/mail.svg?react"; +import NaverIcon from "@/assets/auth/social/naver.svg?react"; + export default function Signup() { - return
Signup
; + const location = useLocation(); + const [step, setStep] = useState(location.state?.step || 0); + + const handleEmailStart = () => { + setStep(1); + }; + + const handleNext = () => { + setStep((prev) => prev + 1); + }; + + if (step === 1) { + return ; + } + if (step === 2) { + return ; + } + if (step === 3) { + return ; + } + + return ( +
+
+ + + + + + + +
+ +
+ 이미 사용자 계정이 있다면? + + 로그인하기 + +
+
+ ); } diff --git a/src/pages/dashboard/replay/ReplayDashboard.tsx b/src/pages/dashboard/replay/ReplayDashboard.tsx deleted file mode 100644 index 147772e..0000000 --- a/src/pages/dashboard/replay/ReplayDashboard.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function ReplayDashboard() { - return
ReplayDashboard
; -} diff --git a/src/pages/dashboard/timeline/Timeline.tsx b/src/pages/dashboard/timeline/Timeline.tsx new file mode 100644 index 0000000..7bddd52 --- /dev/null +++ b/src/pages/dashboard/timeline/Timeline.tsx @@ -0,0 +1,3 @@ +export default function Timeline() { + return
Timeline
; +} diff --git a/src/routes/routes.tsx b/src/routes/routes.tsx index a5f7e54..68b9571 100644 --- a/src/routes/routes.tsx +++ b/src/routes/routes.tsx @@ -9,7 +9,7 @@ import Signup from "@/pages/auth/Signup"; import Error from "@/pages/common/Error"; import OverviewDashboard from "@/pages/dashboard/overview/OverviewDashboard"; import PlatformDashboard from "@/pages/dashboard/platform/PlatformDashboard"; -import ReplayDashboard from "@/pages/dashboard/replay/ReplayDashboard"; +import Timeline from "@/pages/dashboard/timeline/Timeline"; import Setting from "@/pages/setting/Setting"; import Workspace from "@/pages/workspace/Workspace"; @@ -88,8 +88,8 @@ const router = createBrowserRouter([ element: , }, { - path: "replay", - element: , + path: "timeline", + element: , }, { path: "platform", diff --git a/src/store/useAuthStore.ts b/src/store/useAuthStore.ts new file mode 100644 index 0000000..5de35f4 --- /dev/null +++ b/src/store/useAuthStore.ts @@ -0,0 +1,23 @@ +import { create } from "zustand"; + +interface IAuthState { + email: string; + password: string; + socialId: number; + setEmail: (email: string) => void; + setPassword: (password: string) => void; + setSocialId: (socialId: number) => void; + resetAuth: () => void; +} + +const useAuthStore = create((set) => ({ + email: "", + password: "", + socialId: -1, + setEmail: (email) => set({ email }), + setPassword: (password) => set({ password }), + setSocialId: (socialId) => set({ socialId }), + resetAuth: () => set({ email: "", password: "" }), +})); + +export default useAuthStore; diff --git a/src/utils/validation.tsx b/src/utils/validation.tsx index 5cfca46..cbbaaa5 100644 --- a/src/utils/validation.tsx +++ b/src/utils/validation.tsx @@ -50,7 +50,30 @@ export const signupSchema = z message: "비밀번호와 비밀번호 확인이 일치하지 않아요.", }); +export const termsSchema = z.boolean().refine((val) => val === true); + export const loginSchema = z.object({ email: emailSchema, - password: z.string().min(1, "비밀번호는 필수입니다."), + password: passwordSchema, +}); + +export const step01Schema = z.object({ + email: emailSchema, + code: codeSchema, +}); + +export const step02Schema = z + .object({ + password: passwordSchema, + repassword: z.string().min(1, "비밀번호 확인은 필수입니다."), + }) + .refine((data) => data.password === data.repassword, { + path: ["repassword"], + message: "비밀번호가 일치하지 않습니다.", + }); + +export const step03Schema = z.object({ + name: nameSchema, + phoneNum: phoneSchema, + terms: termsSchema, });