-
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 (
+
+
+ }
+ onClick={handleEmailStart}
+ className="font-heading3 shadow-md hover:shadow-lg transition-all"
+ >
+ 이메일로 시작하기
+
+
+ }
+ onClick={() => {}}
+ className="bg-white border border-gray-100 text-text-main font-heading3 shadow-sm hover:bg-gray-50"
+ >
+ 구글 로그인
+
+
+ }
+ onClick={() => {}}
+ className="bg-social-kakao text-text-main font-heading3 shadow-sm hover:opacity-90"
+ >
+ 카카오 로그인
+
+
+ }
+ onClick={() => {}}
+ className="bg-social-naver text-white font-heading3 shadow-sm hover:opacity-90"
+ >
+ 네이버 로그인
+
+
+
+
+ 이미 사용자 계정이 있다면?
+
+ 로그인하기
+
+
+
+ );
}
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,
});