diff --git a/src/components/Banner/Banner.tsx b/src/components/Banner/Banner.tsx
new file mode 100644
index 00000000..b3a0020b
--- /dev/null
+++ b/src/components/Banner/Banner.tsx
@@ -0,0 +1,107 @@
+/** @jsxImportSource @emotion/react */
+import { ReactNode } from "react";
+import { useNavigate } from "react-router-dom";
+import Button from "@/components/ui/Button";
+import BannerStyle from "./BannerStyle";
+
+interface BannerInfoProps {
+ align: string;
+ children: ReactNode;
+}
+
+const BannerInfo = ({ children, align }: BannerInfoProps) => {
+ return
{children}
;
+};
+
+interface BannerButtonProps {
+ linkTo: string;
+ children: ReactNode;
+ ariaLabel?: string;
+}
+
+const BannerButton = ({
+ linkTo,
+ children = "구경하러 가기",
+ ariaLabel,
+}: BannerButtonProps) => {
+ const navigate = useNavigate();
+
+ return (
+
+ );
+};
+
+interface BannerImageProps {
+ imgSrc: string;
+ imgAlt: string;
+ lazyLoading?: boolean;
+ align?: string;
+}
+
+const BannerImage = ({
+ imgSrc,
+ imgAlt,
+ lazyLoading,
+ align,
+}: BannerImageProps) => {
+ return (
+
+ );
+};
+
+interface BannerProps {
+ title: string | ReactNode;
+ imgSrc: string;
+ imgAlt: string;
+ linkTo?: string;
+ ariaLabel?: string;
+ lazyLoading?: boolean;
+ infoAlign?: string;
+ imgAlign?: string;
+}
+
+const Banner = ({
+ title,
+ linkTo,
+ imgSrc,
+ imgAlt,
+ ariaLabel,
+ lazyLoading,
+ infoAlign = "left",
+ imgAlign = "right",
+}: BannerProps) => {
+ return (
+
+
+
+ {title}
+ {linkTo && 구경하러 가기}
+
+
+
+
+ );
+};
+
+Banner.Info = BannerInfo;
+Banner.Button = BannerButton;
+Banner.Image = BannerImage;
+
+export default Banner;
diff --git a/src/components/Banner/BannerStyle.ts b/src/components/Banner/BannerStyle.ts
new file mode 100644
index 00000000..580d2a5e
--- /dev/null
+++ b/src/components/Banner/BannerStyle.ts
@@ -0,0 +1,126 @@
+/** @jsxImportSource @emotion/react */
+import { css } from "@emotion/react";
+import { BREAKPOINTS } from "@/constants/responsive";
+import { COLORS } from "@/styles/colors";
+import { FONT_SIZES } from "@/styles/fontSizes";
+
+const BannerStyle = css`
+ min-height: 540px;
+ background: ${COLORS.background.blue};
+ color: ${COLORS.gray[700]};
+ text-align: center;
+
+ .banner-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ height: 100%;
+ min-height: 540px;
+ }
+
+ .banner-hero {
+ background: ${COLORS.background.lightBlue};
+ }
+
+ .banner-title {
+ margin-bottom: 18px;
+ word-break: keep-all;
+
+ @media (min-width: ${BREAKPOINTS.tablet}px) {
+ margin-bottom: 24px;
+ }
+
+ @media (min-width: ${BREAKPOINTS.desktop}px) {
+ margin-bottom: 32px;
+ }
+ }
+
+ .banner-info {
+ padding-top: 120px;
+ }
+
+ .banner-hero .banner-info {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 0 auto;
+ padding-top: 48px;
+ max-width: 240px;
+ }
+
+ .banner-info .btn-lg {
+ display: block;
+ width: 100%;
+ font-size: ${FONT_SIZES[18]};
+ line-height: 24px;
+ max-width: 356px;
+ }
+
+ .banner-img {
+ width: 100%;
+ max-width: 744px;
+ margin: 0 auto;
+ }
+
+ @media (min-width: 640px) {
+ .banner-hero .banner-info {
+ padding: 84px 0 210px;
+ max-width: none;
+ }
+ }
+
+ @media (min-width: ${BREAKPOINTS.tablet}px) {
+ .banner {
+ height: 926px;
+ }
+ .banner.banner-hero {
+ height: 770px;
+ }
+
+ .banner-info .btn-lg {
+ line-height: 32px;
+ font-size: ${FONT_SIZES[20]};
+ }
+ }
+
+ @media (min-width: ${BREAKPOINTS.desktop}px) {
+ .banner-container {
+ width: var(--container-width);
+ margin: 0 auto;
+ }
+
+ .banner,
+ .banner.banner-hero {
+ height: 540px;
+ }
+
+ .banner-hero .banner-info {
+ align-items: flex-start;
+ }
+
+ .banner-container {
+ flex-direction: row;
+ justify-content: center;
+ align-items: flex-end;
+ }
+
+ .banner-info {
+ padding-bottom: 10.75rem;
+ }
+ .banner-hero .banner-info {
+ padding-bottom: 6.25rem;
+ }
+
+ .banner-title {
+ text-align: left;
+ }
+ }
+
+ @supports (font-size: clamp(1rem, 2vw, 3rem)) {
+ .banner-title {
+ font-size: clamp(32px, 5vw, 40px);
+ }
+ }
+`;
+
+export default BannerStyle;
diff --git a/src/components/Form/LoginForm.tsx b/src/components/Form/LoginForm.tsx
new file mode 100644
index 00000000..9b2943f6
--- /dev/null
+++ b/src/components/Form/LoginForm.tsx
@@ -0,0 +1,89 @@
+/** @jsxImportSource @emotion/react */
+import FormStyle from "./FormStyle";
+import { Link } from "react-router-dom";
+import Button from "@/components/ui/Button";
+import SocialLogin from "@/components/SocialLogin/SocialLogin";
+import logoImg from "@/assets/images/logo.svg";
+import loginUser from "@/services/post/loginUser";
+import InputField from "../ui/Form/InputField";
+import PasswordField from "../ui/Form/PasswordField";
+import useAuthForm from "@/hooks/useAuthForm";
+import { renderButtonTextByState } from "@/utils/renderButtonText";
+
+const LoginForm = () => {
+ const {
+ formRef,
+ isSubmitting,
+ submitError,
+ handleBlur,
+ validateForm,
+ isFormValid,
+ fieldErrors,
+ handleSubmit,
+ } = useAuthForm({
+ onSubmit: async (userData) => {
+ await loginUser(userData);
+ },
+ });
+
+ return (
+
+ );
+};
+
+export default LoginForm;
diff --git a/src/components/Form/SignUpForm.tsx b/src/components/Form/SignUpForm.tsx
new file mode 100644
index 00000000..e9c8cfc4
--- /dev/null
+++ b/src/components/Form/SignUpForm.tsx
@@ -0,0 +1,107 @@
+/** @jsxImportSource @emotion/react */
+import { Link } from "react-router-dom";
+import logoImg from "@/assets/images/logo.svg";
+import Button from "@/components/ui/Button";
+import SocialLogin from "@/components/SocialLogin/SocialLogin";
+import InputField from "@/components/ui/Form/InputField";
+import PasswordField from "@/components/ui/Form/PasswordField";
+import FormStyle from "./FormStyle";
+import useAuthForm from "@/hooks/useAuthForm";
+import createUser from "@/services/post/createUser";
+import { renderButtonTextByState } from "@/utils/renderButtonText";
+
+const SignUpForm = () => {
+ const {
+ formRef,
+ isSubmitting,
+ submitError,
+ handleBlur,
+ validateForm,
+ isFormValid,
+ fieldErrors,
+ handleSubmit,
+ } = useAuthForm({
+ onSubmit: async (userData) => {
+ await createUser(userData);
+ },
+ });
+
+ return (
+
+ );
+};
+
+export default SignUpForm;
diff --git a/src/components/Section/MainSection.tsx b/src/components/Section/MainSection.tsx
new file mode 100644
index 00000000..4388e8c3
--- /dev/null
+++ b/src/components/Section/MainSection.tsx
@@ -0,0 +1,46 @@
+/** @jsxImportSource @emotion/react */
+import { ReactNode } from "react";
+import MainSectionStyle from "./MainSectionStyle";
+import { BREAKPOINTS } from "@/constants/responsive";
+
+interface MainSectionProps {
+ title: string | ReactNode;
+ label?: string;
+ ariaLabel?: string;
+ description?: string | ReactNode;
+ imgSrc: string;
+ imgMobileSrc?: string;
+ reverse?: boolean;
+}
+
+const MainSection = ({
+ title,
+ label,
+ ariaLabel,
+ description,
+ imgSrc,
+ imgMobileSrc,
+ reverse = false,
+}: MainSectionProps) => {
+ return (
+
+
+

+
+
{label}
+
{title}
+
{description}
+
+
+
+ );
+};
+
+export default MainSection;
diff --git a/src/components/Section/MainSectionStyle.ts b/src/components/Section/MainSectionStyle.ts
new file mode 100644
index 00000000..19bd3854
--- /dev/null
+++ b/src/components/Section/MainSectionStyle.ts
@@ -0,0 +1,103 @@
+/** @jsxImportSource @emotion/react */
+import { css } from "@emotion/react";
+import { BREAKPOINTS } from "@/constants/responsive";
+
+const MainSectionStyle = ({ reverse }: { reverse: boolean }) => css`
+ display: flex;
+ justify-content: center;
+ background: #fff;
+ margin-bottom: 40px;
+
+ .section-container {
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ flex-direction: column;
+ gap: 24px;
+ flex-grow: 1;
+ }
+
+ .section-img {
+ width: 100%;
+ }
+
+ .section-info {
+ color: var(--gray700);
+ }
+
+ .section-label {
+ margin-bottom: 8px;
+ font-size: var(--label-font-size);
+ font-weight: 700;
+ color: var(--primary-color);
+ }
+
+ .section-title {
+ margin-bottom: var(--section-margin-bottom);
+ font-size: var(--heading-font-size);
+ word-break: keep-all;
+ }
+
+ .section-desc {
+ font-size: var(--description-font-size);
+ }
+
+ ${reverse &&
+ css`
+ .section-container {
+ align-items: flex-end;
+ }
+ .section-info {
+ text-align: right;
+ }
+ `}
+
+ @media (min-width: 640px) {
+ :root {
+ --sections-padding: 24px 24px 56px;
+ --section-margin-bottom: 24px;
+ }
+ }
+
+ @media (min-width: ${BREAKPOINTS.tablet}px) {
+ .section-container {
+ flex-grow: 0;
+ }
+ }
+
+ @media (min-width: ${BREAKPOINTS.desktop}px) {
+ :root {
+ --sections-padding: 138px 24px;
+ --section-margin-bottom: 1.5rem;
+ }
+
+ padding: 0 24px 138px;
+
+ .section-container {
+ align-items: center;
+ flex-direction: row;
+ width: var(--container-width-small);
+ gap: 4rem;
+ background: var(--background-light);
+ border-radius: var(--border-radius-md);
+ overflow: hidden;
+ padding: 0 1.5rem;
+ }
+
+ ${reverse &&
+ css`
+ .section-container {
+ align-items: center;
+ }
+ .section-container img {
+ order: 2;
+ }
+ `}
+
+ .section-container img {
+ max-width: 50%;
+ }
+ }
+`;
+
+export default MainSectionStyle;
diff --git a/src/components/SocialLogin/SocialLogin.tsx b/src/components/SocialLogin/SocialLogin.tsx
new file mode 100644
index 00000000..f2e3484e
--- /dev/null
+++ b/src/components/SocialLogin/SocialLogin.tsx
@@ -0,0 +1,52 @@
+/** @jsxImportSource @emotion/react */
+import { css } from "@emotion/react";
+import googleIcon from "@/assets/images/ic_google.png";
+import kakaoIcon from "@/assets/images/ic_kakao.png";
+import SocialLoginButton from "./SocialLoginButton";
+
+const SocialLogin = () => {
+ return (
+
+ );
+};
+
+export default SocialLogin;
+
+const SocialLoginStyle = css`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin: 8px 0;
+ padding: 16px 24px;
+ border-radius: var(--border-radius-xs);
+ background: var(--background-blue-light);
+ font-size: 16px;
+ color: var(--gray800);
+
+ .social-login-icons {
+ display: flex;
+ gap: 1rem;
+ }
+
+ @media (min-width: 640px) {
+ margin: 0;
+ }
+`;
diff --git a/src/components/SocialLogin/SocialLoginButton.tsx b/src/components/SocialLogin/SocialLoginButton.tsx
new file mode 100644
index 00000000..9d0abaa9
--- /dev/null
+++ b/src/components/SocialLogin/SocialLoginButton.tsx
@@ -0,0 +1,35 @@
+/** @jsxImportSource @emotion/react */
+import { css } from "@emotion/react";
+
+interface SocialLoginButtonProps {
+ href: string;
+ title?: string;
+ ariaLabel?: string;
+ imgSrc: string;
+ imgAlt: string;
+}
+
+const SocialLoginButton = ({
+ href,
+ title,
+ ariaLabel,
+ imgSrc,
+ imgAlt,
+}: SocialLoginButtonProps) => {
+ return (
+
+
+
+ );
+};
+
+export default SocialLoginButton;
+
+const SocialLoginButtonStyle = css`
+ border-radius: 50%;
+`;
diff --git a/src/components/ui/Footer/FooterStyle.js b/src/components/ui/Footer/FooterStyle.js
index 1e7354f9..075b5312 100644
--- a/src/components/ui/Footer/FooterStyle.js
+++ b/src/components/ui/Footer/FooterStyle.js
@@ -8,6 +8,8 @@ const FooterStyle = css`
.footer-container {
display: flex;
justify-content: space-between;
+ max-width: var(--container-width);
+ margin: 0 auto;
padding: var(--footer-padding);
flex-wrap: wrap;
gap: 24px;
diff --git a/src/components/ui/Form/InputField.tsx b/src/components/ui/Form/InputField.tsx
new file mode 100644
index 00000000..582fdb99
--- /dev/null
+++ b/src/components/ui/Form/InputField.tsx
@@ -0,0 +1,39 @@
+/** @jsxImportSource @emotion/react */
+import Input from "@/components/ui/Input";
+import FormControl from "./FormControl";
+import { FormField } from "@/types/form";
+
+const InputField = ({
+ label,
+ inputId,
+ type,
+ name,
+ placeholder,
+ required,
+ onBlur,
+ fieldError,
+}: FormField) => {
+ return (
+
+
+
+
+ {fieldError}
+
+
+ );
+};
+
+export default InputField;
diff --git a/src/components/ui/Form/PasswordField.tsx b/src/components/ui/Form/PasswordField.tsx
new file mode 100644
index 00000000..6c01a42d
--- /dev/null
+++ b/src/components/ui/Form/PasswordField.tsx
@@ -0,0 +1,58 @@
+/** @jsxImportSource @emotion/react */
+import { useState } from "react";
+import Input from "@/components/ui/Input";
+import FormControl from "./FormControl";
+import { FormField } from "@/types/form";
+import IconButton from "@/components/ui/Button/IconButton";
+import eyeImg from "@/assets/images/ic_visibility_on.svg";
+import eyeCloseImg from "@/assets/images/ic_visibility_off.svg";
+
+const PasswordField = ({
+ label,
+ inputId,
+ name,
+ placeholder,
+ onBlur,
+ fieldError,
+}: FormField) => {
+ const [isVisible, setIsVisible] = useState(false);
+
+ return (
+
+
+
+
+
+ setIsVisible((prev) => !prev)}
+ />
+
+
{fieldError}
+
+
+ );
+};
+
+export default PasswordField;
diff --git a/src/hooks/useAuthForm.ts b/src/hooks/useAuthForm.ts
new file mode 100644
index 00000000..98801389
--- /dev/null
+++ b/src/hooks/useAuthForm.ts
@@ -0,0 +1,67 @@
+import { useState, useRef } from "react";
+import useSignIn from "./useSignIn";
+import useForm from "./useForm";
+import { ReqData } from "@/types/form";
+
+interface UseAuthFormProps {
+ onSubmit: (data: ReqData) => Promise;
+}
+
+const useAuthForm = ({ onSubmit }: UseAuthFormProps) => {
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [submitError, setSubmitError] = useState(null);
+
+ const signIn = useSignIn();
+
+ const formRef = useRef(null);
+
+ const { handleBlur, validateForm, isFormValid, fieldErrors } =
+ useForm(formRef);
+
+ const handleSubmit = async () => {
+ const form = formRef.current;
+ if (!form) return;
+
+ const formData = new FormData(form);
+ const userData: ReqData = {};
+
+ for (const [key, value] of formData.entries()) {
+ userData[key] = value;
+ }
+
+ try {
+ setIsSubmitting(true);
+ setSubmitError(null);
+ await onSubmit(userData);
+ signIn(); // 로그인 컨텍스트 처리 -> 상품 목록 페이지로 이동
+ } catch (err) {
+ if (err instanceof Error) {
+ setSubmitError(err);
+ } else {
+ setSubmitError(new Error("알 수 없는 오류가 발생했습니다."));
+ }
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return {
+ // 대상 폼
+ formRef,
+
+ // 로딩, 에러 상태
+ isSubmitting,
+ submitError,
+
+ // 유효성 검사
+ handleBlur,
+ validateForm,
+ isFormValid,
+ fieldErrors,
+
+ // 폼
+ handleSubmit,
+ };
+};
+
+export default useAuthForm;
diff --git a/src/pages/auth/AuthPageStyle.ts b/src/pages/auth/AuthPageStyle.ts
index 37608e1c..049bb03a 100644
--- a/src/pages/auth/AuthPageStyle.ts
+++ b/src/pages/auth/AuthPageStyle.ts
@@ -1,20 +1,22 @@
/** @jsxImportSource @emotion/react */
import { css } from "@emotion/react";
import { BREAKPOINTS } from "@/constants/responsive";
-import FormStyle from "@/components/Form/FormStyle";
const AuthPageStyle = css`
- ${FormStyle};
-
- /*================ 로그인, 회원가입 ================*/
.form {
max-width: 400px;
padding: 0 16px;
}
.form-logo {
- width: 198px;
+ display: flex;
+ justify-content: center;
+ width: 100%;
margin: 0 0 2.5rem;
+
+ img {
+ max-width: 198px;
+ }
}
.form-footer {
@@ -32,48 +34,24 @@ const AuthPageStyle = css`
font-size: 14px;
}
- /* 로그인 */
.login .form-container {
padding: 80px 0;
}
- /* 회원가입 */
.signup .form-container {
padding-top: var(--form-padding-top);
padding-bottom: 178px;
}
- /* 간편 로그인 */
- .easy-login {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin: 8px 0;
- padding: 16px 24px;
- border-radius: var(--border-radius-xs);
- background: var(--background-blue-light);
- font-size: 16px;
- color: var(--gray800);
- }
-
- .easy-login-icons {
- display: flex;
- gap: 1rem;
- }
-
- /*================ 반응형 ================*/
- /* Tablet */
@media (min-width: 640px) {
.form {
max-width: 640px;
}
.form-logo {
- width: 396px;
- }
-
- .easy-login {
- margin: 0;
+ img {
+ max-width: 396px;
+ }
}
}
diff --git a/src/pages/auth/LoginPage.tsx b/src/pages/auth/LoginPage.tsx
index add81370..008736af 100644
--- a/src/pages/auth/LoginPage.tsx
+++ b/src/pages/auth/LoginPage.tsx
@@ -1,184 +1,14 @@
/** @jsxImportSource @emotion/react */
-import { useRef, useState } from "react";
-import { Link } from "react-router-dom";
import AuthContent from "@/components/layout/AuthContent";
import AuthPageStyle from "./AuthPageStyle";
-import Button from "@/components/ui/Button";
-import googleIcon from "@/assets/images/ic_google.png";
-import kakaoIcon from "@/assets/images/ic_kakao.png";
-import logoImg from "@/assets/images/logo.svg";
-import useForm from "@/hooks/useForm";
-import Input from "@/components/ui/Input";
-import FormControl from "@/components/ui/Form/FormControl";
-import IconButton from "@/components/ui/Button/IconButton";
-import eyeImg from "@/assets/images/ic_visibility_on.svg";
-import eyeCloseImg from "@/assets/images/ic_visibility_off.svg";
-import { ReqData } from "@/types/form";
-import loginUser from "@/services/post/loginUser";
-import useSignIn from "@/hooks/useSignIn";
+import LoginForm from "@/components/Form/LoginForm";
const LoginPage = () => {
- const [isVisible, setIsVisible] = useState(false);
- const [isSigningIn, setIsSigningIn] = useState(false);
- const [isSignInError, setIsSignInError] = useState(null);
-
- const formRef = useRef(null);
-
- const signIn = useSignIn();
-
- const { handleBlur, validateForm, isFormValid, fieldErrors } =
- useForm(formRef);
-
- const handleLogin = async () => {
- const form = formRef.current;
- if (!form) return;
-
- const formData = new FormData(form);
- const userData: ReqData = {};
-
- for (const [key, value] of formData.entries()) {
- userData[key] = value;
- }
-
- try {
- setIsSigningIn(true);
- setIsSignInError(null);
- await loginUser(userData);
- signIn(); // 로그인 컨텍스트 처리 -> 상품 목록 페이지로 이동
- } catch (err) {
- if (err instanceof Error) {
- setIsSignInError(err);
- } else {
- setIsSignInError(new Error("알 수 없는 오류가 발생했습니다."));
- }
- } finally {
- setIsSigningIn(false);
- }
- };
-
return (
diff --git a/src/pages/auth/SignUpPage.tsx b/src/pages/auth/SignUpPage.tsx
index 36f18f4e..599111f0 100644
--- a/src/pages/auth/SignUpPage.tsx
+++ b/src/pages/auth/SignUpPage.tsx
@@ -1,255 +1,14 @@
/** @jsxImportSource @emotion/react */
-import { useRef, useState } from "react";
-import { Link } from "react-router-dom";
-import useForm from "@/hooks/useForm";
-import Input from "@/components/ui/Input";
-import FormControl from "@/components/ui/Form/FormControl";
-import logoImg from "@/assets/images/logo.svg";
-import Button from "@/components/ui/Button";
import AuthContent from "@/components/layout/AuthContent";
import AuthPageStyle from "./AuthPageStyle";
-import googleIcon from "@/assets/images/ic_google.png";
-import kakaoIcon from "@/assets/images/ic_kakao.png";
-import IconButton from "@/components/ui/Button/IconButton";
-import eyeImg from "@/assets/images/ic_visibility_on.svg";
-import eyeCloseImg from "@/assets/images/ic_visibility_off.svg";
-import createUser from "@/services/post/createUser";
-import { ReqData } from "@/types/form";
-import useSignIn from "@/hooks/useSignIn";
+import SignUpForm from "@/components/Form/SignUpForm";
const SignUpPage = () => {
- const [isVisible, setIsVisible] = useState(false);
- const [isSigningUp, setIsSigningUp] = useState(false);
- const [isSignUpError, setIsSignUpError] = useState(null);
-
- const formRef = useRef(null);
- const {
- handleBlur,
- isFormValid,
- validateForm,
- fieldErrors,
- // emailMsg,
- // passwordMsg,
- // passwordCheckMsg,
- // nicknameMsg,
- } = useForm(formRef);
-
- const signIn = useSignIn();
-
- const handleSignUp = async () => {
- const form = formRef.current;
- if (!form) return;
-
- const formData = new FormData(form);
- const userData: ReqData = {};
-
- for (const [key, value] of formData.entries()) {
- const mappedKey = key === "passwordCheck" ? "passwordConfirmation" : key;
- userData[mappedKey] = value;
- }
-
- try {
- setIsSigningUp(true);
- setIsSignUpError(null);
- await createUser(userData);
- signIn(); // 로그인 컨텍스트 처리 -> 상품 목록 페이지로 이동
- } catch (err) {
- if (err instanceof Error) {
- setIsSignUpError(err);
- } else {
- setIsSignUpError(new Error("알 수 없는 오류가 발생했습니다."));
- }
- } finally {
- setIsSigningUp(false);
- }
- };
-
return (
diff --git a/src/pages/main/MainPage.tsx b/src/pages/main/MainPage.tsx
index f789aeba..57b87bdc 100644
--- a/src/pages/main/MainPage.tsx
+++ b/src/pages/main/MainPage.tsx
@@ -1,8 +1,6 @@
/** @jsxImportSource @emotion/react */
-import { useNavigate } from "react-router-dom";
import MainPageStyle from "./MainPageStyle.ts";
import Footer from "@/components/ui/Footer";
-import Button from "@/components/ui/Button";
import HomeTopImg from "@/assets/images/Img_home_top.png";
import HomeBottomImg from "@/assets/images/Img_home_bottom.png";
import HomeImg1 from "@/assets/images/Img_home_01.png";
@@ -11,127 +9,83 @@ import HomeImg2 from "@/assets/images/Img_home_02.png";
import HomeImg2Small from "@/assets/images/Img_home_02_sm.png";
import HomeImg3 from "@/assets/images/Img_home_03.png";
import HomeImg3Small from "@/assets/images/Img_home_03_sm.png";
-import { BREAKPOINTS } from "@/constants/responsive";
+import Banner from "@/components/Banner/Banner.tsx";
+import MainSection from "@/components/Section/MainSection.tsx";
const MainPage = () => {
- const navigate = useNavigate();
-
return (
- <>
-
-
-
-
-
일상의 모든 물건을 거래해 보세요
-
-
-

-
-
-
-
-
-

-
-
Hot item
-
인기 상품을 확인해 보세요
-
- 가장 HOT한 중고거래 물품을
-
- 판다 마켓에서 확인해 보세요
-
-
-
-
-
-
-

-
-
Search
-
- 구매를 원하는 상품을 검색하세요
-
-
- 구매하고 싶은 물품은 검색해서
-
- 쉽게 찾아보세요
-
-
-
-
-
-
-

-
-
Register
-
- 판매를 원하는 상품을 등록하세요
-
-
- 어떤 물건이든 판매하고 싶은 상품을
-
- 쉽게 등록하세요
-
-
-
-
-
-
-
-
-
- 믿을 수 있는
-
- 판다마켓 중고 거래
-
-
-

-
-
-
+
+
+
+
+
+ 가장 HOT한 중고거래 물품을
+
+ 판다 마켓에서 확인해 보세요
+ >
+ }
+ imgSrc={HomeImg1}
+ imgMobileSrc={HomeImg1Small}
+ />
+
+
+ 구매하고 싶은 물품은 검색해서
+
+ 쉽게 찾아보세요
+ >
+ }
+ imgSrc={HomeImg2}
+ imgMobileSrc={HomeImg2Small}
+ />
+
+
+ 어떤 물건이든 판매하고 싶은 상품을
+
+ 쉽게 등록하세요
+ >
+ }
+ imgSrc={HomeImg3}
+ imgMobileSrc={HomeImg3Small}
+ />
+
+
+
+ 믿을 수 있는
+
+ 판다마켓 중고 거래
+ >
+ }
+ imgSrc={HomeBottomImg}
+ imgAlt="팬더 두 마리가 파란 장바구니를 메고 서로 상품 후기를 주고받는 일러스트"
+ ariaLabel="하단 배너"
+ lazyLoading={true}
+ />
- >
+
);
};
diff --git a/src/pages/main/MainPageStyle.ts b/src/pages/main/MainPageStyle.ts
index e1fe47c7..eb7b3c25 100644
--- a/src/pages/main/MainPageStyle.ts
+++ b/src/pages/main/MainPageStyle.ts
@@ -1,230 +1,11 @@
/** @jsxImportSource @emotion/react */
-import { BREAKPOINTS } from "@/constants/responsive";
import { css } from "@emotion/react";
const MainPageStyle = css`
- /* section */
.sections {
padding: var(--sections-padding);
background: #fff;
}
-
- .section {
- display: flex;
- justify-content: center;
- background: #fff;
- margin-bottom: 40px;
- }
-
- .section-container {
- display: flex;
- justify-content: center;
- align-items: flex-start;
- flex-direction: column;
- gap: 24px;
- flex-grow: 1;
- }
- .section-search .section-container {
- align-items: flex-end;
- }
-
- .section-img {
- width: 100%;
- }
-
- .section-info {
- color: var(--gray700);
- }
-
- .section-search .section-info {
- text-align: right;
- }
-
- .section-label {
- margin-bottom: 8px;
- font-size: var(--label-font-size);
- font-weight: 700;
- color: var(--primary-color);
- }
-
- .section-title {
- margin-bottom: var(--section-margin-bottom);
- font-size: var(--heading-font-size);
- word-break: keep-all;
- }
-
- .section-desc {
- font-size: var(--description-font-size);
- }
-
- /* banner */
- .banner {
- min-height: 540px;
- background: var(--background-blue);
- color: var(--gray700);
- text-align: center;
- }
-
- .banner-container {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- height: 100%;
- min-height: 540px;
- }
-
- .banner-hero {
- background: var(--background-blue-light);
- }
-
- .banner-title {
- font-size: var(--banner-font-size);
- margin-bottom: 18px;
- word-break: keep-all;
-
- @media (min-width: ${BREAKPOINTS.tablet}px) {
- margin-bottom: 24px;
- }
-
- @media (min-width: ${BREAKPOINTS.desktop}px) {
- margin-bottom: 32px;
- }
- }
-
- .banner-info {
- padding-top: 120px;
- }
-
- .banner-hero .banner-info {
- display: flex;
- flex-direction: column;
- align-items: center;
- margin: 0 auto;
- padding-top: 48px;
- max-width: 240px;
- }
-
- .banner-info .btn-lg {
- display: block;
- width: 100%;
- font-size: var(--banner-btn-font-size);
- line-height: 24px;
- max-width: 356px;
- }
-
- .banner-img {
- width: 100%;
- max-width: 744px;
- margin: 0 auto;
- }
-
- /*================ 반응형 ================*/
- /* Tablet */
- @media (min-width: 640px) {
- :root {
- --sections-padding: 24px 24px 56px;
- --section-margin-bottom: 24px;
- }
-
- .banner-hero .banner-info {
- padding: 84px 0 210px;
- max-width: none;
- }
- }
-
- @media (min-width: ${BREAKPOINTS.tablet}px) {
- :root {
- --banner-btn-font-size: 20px;
- }
-
- .banner {
- height: 926px;
- }
- .banner.banner-hero {
- height: 770px;
- }
-
- .banner-info .btn-lg {
- line-height: 32px;
- }
-
- .section-container {
- flex-grow: 0;
- }
- }
-
- /* PC */
- @media (min-width: ${BREAKPOINTS.desktop}px) {
- :root {
- --sections-padding: 138px 24px;
- --section-margin-bottom: 1.5rem;
- }
-
- .banner-container {
- width: var(--container-width);
- margin: 0 auto;
- }
-
- .banner,
- .banner.banner-hero {
- height: 540px;
- }
-
- .banner-hero .banner-info {
- align-items: flex-start;
- }
-
- .banner-container {
- flex-direction: row;
- justify-content: center;
- align-items: flex-end;
- }
-
- .banner-info {
- padding-bottom: 10.75rem;
- }
- .banner-hero .banner-info {
- padding-bottom: 6.25rem;
- }
-
- .banner-title {
- text-align: left;
- }
-
- .section {
- padding: 0 24px 138px;
- }
-
- .section-container {
- align-items: center;
- flex-direction: row;
- width: var(--container-width-small);
- gap: 4rem;
- background: var(--background-light);
- border-radius: var(--border-radius-md);
- overflow: hidden;
- padding: 0 1.5rem;
- }
-
- .section-search .section-container {
- align-items: center;
- }
-
- .section-container img {
- max-width: 50%;
- }
-
- .section-search .section-container img {
- order: 2;
- }
- }
-
- /* 반응형 요소에 clamp() 적용 */
- @supports (font-size: clamp(1rem, 2vw, 3rem)) {
- :root {
- --banner-font-size: clamp(32px, 5vw, 40px);
- }
- }
`;
export default MainPageStyle;
diff --git a/src/styles/colors.ts b/src/styles/colors.ts
new file mode 100644
index 00000000..e62fc4f1
--- /dev/null
+++ b/src/styles/colors.ts
@@ -0,0 +1,38 @@
+type GrayScale = Record<
+ 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900,
+ string
+>;
+
+const gray: GrayScale = {
+ 100: "#f9fafb",
+ 200: "#f3f4f6",
+ 300: "#e5e7eb",
+ 400: "#9ca3af",
+ 500: "#6b7280",
+ 600: "#4b5563",
+ 700: "#374151",
+ 800: "#1f2937",
+ 900: "#111827",
+};
+
+export const COLORS = {
+ gray,
+ primary: {
+ DEFAULT: "#3692ff",
+ hover: "#1967d6",
+ click: "#1251aa",
+ },
+ secondary: {
+ DEFAULT: gray[800],
+ },
+ text: {
+ DEFAULT: gray[600],
+ },
+ background: {
+ lightGray: "#fcfcfc",
+ blue: "#cfe5ff",
+ lightBlue: "#e6f2ff",
+ },
+ error: "#f74747",
+ border: gray[200],
+};
diff --git a/src/styles/fontSizes.ts b/src/styles/fontSizes.ts
new file mode 100644
index 00000000..c046420a
--- /dev/null
+++ b/src/styles/fontSizes.ts
@@ -0,0 +1,7 @@
+type FontScale = Record<16 | 18 | 20, string>;
+
+export const FONT_SIZES: FontScale = {
+ 16: "16px",
+ 18: "18px",
+ 20: "20px",
+};
diff --git a/src/styles/variables.css b/src/styles/variables.css
index 0cbe3592..e1caf194 100644
--- a/src/styles/variables.css
+++ b/src/styles/variables.css
@@ -31,7 +31,7 @@
--page-content-width: 75rem;
--sections-padding: 24px 16px 84px;
--section-margin-bottom: 16px;
- --footer-padding: 32px;
+ --footer-padding: 32px 16px;
--list-header-gap: 12px;
/* form */
@@ -62,6 +62,8 @@
:root {
--sections-padding: 24px 24px 56px;
--section-margin-bottom: 24px;
+
+ --footer-padding: 32px 24px;
}
.banner-hero .banner-info {
@@ -81,6 +83,8 @@
:root {
--sections-padding: 138px 24px;
--section-margin-bottom: 1.5rem;
+
+ --footer-padding: 32px 0;
}
}
diff --git a/src/types/form.ts b/src/types/form.ts
index ac96d1c1..3d9e553e 100644
--- a/src/types/form.ts
+++ b/src/types/form.ts
@@ -1,3 +1,16 @@
-export type ReqData = {
+import { ChangeEvent } from "react";
+
+export interface FormField {
+ label: string;
+ inputId?: string;
+ type?: string;
+ name: string;
+ placeholder: string;
+ required?: boolean;
+ onBlur?: (e: ChangeEvent) => void;
+ fieldError?: string;
+}
+
+export interface ReqData {
[key: string]: FormDataEntryValue;
-};
+}
diff --git a/src/utils/renderButtonText.ts b/src/utils/renderButtonText.ts
new file mode 100644
index 00000000..363884e1
--- /dev/null
+++ b/src/utils/renderButtonText.ts
@@ -0,0 +1,26 @@
+type LoadingText = `${T}중...`;
+
+type ButtonState = {
+ isSubmitting: boolean;
+ defaultText: T;
+ loadingText: LoadingText;
+};
+
+const renderButtonText = (state: ButtonState) => {
+ const { isSubmitting, defaultText, loadingText } = state;
+
+ return isSubmitting ? loadingText : defaultText;
+};
+
+export const renderButtonTextByState = (
+ isSubmitting: boolean,
+ defaultText: T
+) => {
+ const loadingText: LoadingText = `${defaultText}중...` as LoadingText; // 템플릿 리터럴 타입 적용 보장
+
+ return renderButtonText({
+ isSubmitting,
+ defaultText,
+ loadingText,
+ });
+};